moved qdb here because matt is lazy
[public/www-new.git] / pub / qdb / src / modules / Chirpy / DataManager / MySQL.pm
1 ###############################################################################\r
2 # Chirpy! 0.3, a quote management system                                      #\r
3 # Copyright (C) 2005-2007 Tim De Pauw <ceetee@users.sourceforge.net>          #\r
4 ###############################################################################\r
5 # This program is free software; you can redistribute it and/or modify it     #\r
6 # under the terms of the GNU General Public License as published by the Free  #\r
7 # Software Foundation; either version 2 of the License, or (at your option)   #\r
8 # any later version.                                                          #\r
9 #                                                                             #\r
10 # This program is distributed in the hope that it will be useful, but WITHOUT #\r
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #\r
12 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for   #\r
13 # more details.                                                               #\r
14 #                                                                             #\r
15 # You should have received a copy of the GNU General Public License along     #\r
16 # with this program; if not, write to the Free Software Foundation, Inc., 51  #\r
17 # Franklin St, Fifth Floor, Boston, MA  02110-1301  USA                       #\r
18 ###############################################################################\r
19 \r
20 ###############################################################################\r
21 # $Id:: MySQL.pm 304 2007-02-09 01:06:15Z ceetee                            $ #\r
22 ###############################################################################\r
23 \r
24 =head1 NAME\r
25 \r
26 Chirpy::DataManager::MySQL - Data manager class, based on a MySQL backend\r
27 \r
28 =head1 REQUIREMENTS\r
29 \r
30 Apart from a proper Chirpy! installation, this module requires the following\r
31 Perl modules:\r
32 \r
33  Data::Dumper\r
34  DBD::mysql\r
35  DBI\r
36 \r
37 In addition, it needs access to a server running MySQL version 4.1 or higher.\r
38 \r
39 =head1 CONFIGURATION\r
40 \r
41 This module uses the following values from your configuration file:\r
42 \r
43 =over 4\r
44 \r
45 =item mysql.hostname\r
46 \r
47 The name of the server to use. In most cases, this will be C<localhost>.\r
48 \r
49 =item mysql.port\r
50 \r
51 The port on which the MySQL server accepts connections. The default is C<3306>.\r
52 \r
53 =item mysql.username\r
54 \r
55 The username to use for MySQL connections.\r
56 \r
57 =item mysql.password\r
58 \r
59 The password to use for MySQL connections. Note that this password is readable\r
60 if the user gains access to the file, and while Chirpy! takes precautions to\r
61 make the configuration file inaccessible, it is also your responsibility to\r
62 make sure that no one finds your password.\r
63 \r
64 =item mysql.database\r
65 \r
66 The name of the MySQL database where Chirpy! will store its data. \r
67 \r
68 =item mysql.prefix\r
69 \r
70 If your host only allows one database, Chirpy! can prefix its table names with\r
71 a custom prefix, so you can easily distinguish them. For instance, if you set\r
72 this value to C<chirpy_>, the tables will be called C<chirpy_quotes>,\r
73 C<chirpy_news>, etc.\r
74 \r
75 =back\r
76 \r
77 =head1 SESSIONS\r
78 \r
79 This class fully implements L<Chirpy::UI::WebApp::Session::DataManager>, which\r
80 allows L<Chirpy::UI::WebApp> to use sessions.\r
81 \r
82 =head1 AUTHOR\r
83 \r
84 Tim De Pauw E<lt>ceetee@users.sourceforge.netE<gt>\r
85 \r
86 =head1 SEE ALSO\r
87 \r
88 L<Chirpy::DataManager>, L<Chirpy::UI::WebApp::Session::DataManager>, L<Chirpy>,\r
89 L<http://chirpy.sourceforge.net/>\r
90 \r
91 =head1 COPYRIGHT\r
92 \r
93 Copyright 2005-2007 Tim De Pauw. All rights reserved.\r
94 \r
95 This program is free software; you can redistribute it and/or modify it under\r
96 the terms of the GNU General Public License as published by the Free Software\r
97 Foundation; either version 2 of the License, or (at your option) any later\r
98 version.\r
99 \r
100 This program is distributed in the hope that it will be useful, but WITHOUT ANY\r
101 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A\r
102 PARTICULAR PURPOSE.  See the GNU General Public License for more details.\r
103 \r
104 =cut\r
105 \r
106 package Chirpy::DataManager::MySQL;\r
107 \r
108 use strict;\r
109 use warnings;\r
110 \r
111 use vars qw($VERSION $TARGET_VERSION @ISA $SCORE_EXPR);\r
112 \r
113 $VERSION = '0.3';\r
114 @ISA = qw(Chirpy::DataManager Chirpy::UI::WebApp::Session::DataManager);\r
115 \r
116 $TARGET_VERSION = '0.3';\r
117 \r
118 $SCORE_EXPR = '`score` = ((`votes` + `rating`) / 2 + 1)'\r
119         . ' / ((`votes` - `rating`) / 2 + 1)';\r
120 \r
121 use Chirpy 0.3;\r
122 \r
123 use Chirpy::DataManager 0.3;\r
124 use Chirpy::UI::WebApp::Session::DataManager 0.3;\r
125 \r
126 use Chirpy::Quote 0.3;\r
127 use Chirpy::Account 0.3;\r
128 use Chirpy::NewsItem 0.3;\r
129 use Chirpy::Event 0.3;\r
130 \r
131 use DBI;\r
132 \r
133 use Data::Dumper;\r
134 \r
135 sub new {\r
136         my $class = shift;\r
137         my $self = $class->SUPER::new(@_);\r
138         my $dbh = DBI->connect('DBI:mysql:database=' . $self->param('database')\r
139                 . ($self->param('hostname')\r
140                         ? ';host=' . $self->param('hostname')\r
141                         : '')\r
142                 . ($self->param('port') ? ';port=' . $self->param('port') : ''),\r
143                 $self->param('username'), $self->param('password'));\r
144         Chirpy::die('Failed to connect to database: ' . DBI->errstr())\r
145                 unless (defined $dbh);\r
146         $dbh->do('SET NAMES utf8');\r
147         $self->{'dbh'} = $dbh;\r
148         $self->{'prefix'} = $self->param('prefix');\r
149         return $self;\r
150 }\r
151 \r
152 sub get_target_version {\r
153         return $TARGET_VERSION;\r
154 }\r
155 \r
156 sub DESTROY {\r
157         my $self = shift;\r
158         my $handle = $self->handle();\r
159         return unless (defined $handle);\r
160         $handle->disconnect();\r
161 }\r
162 \r
163 sub set_up {\r
164         my ($self, $accounts, $news, $quotes) = @_;\r
165         my $prefix = $self->table_name_prefix();\r
166         my $handle = $self->handle();\r
167         my $table = $prefix . 'accounts';\r
168         unless ($self->_table_exists($table)) {\r
169                 $handle->do(q|\r
170                         CREATE TABLE `| . $table . q|` (\r
171                                 `id` int unsigned NOT NULL auto_increment,\r
172                                 `username` varchar(32) NOT NULL,\r
173                                 `password` varchar(32) NOT NULL,\r
174                                 `level` tinyint(1) unsigned NOT NULL,\r
175                                 PRIMARY KEY (`id`),\r
176                                 UNIQUE KEY `username` (`username`)\r
177                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
178                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
179         }\r
180         $table = $prefix . 'news';\r
181         unless ($self->_table_exists($table)) {\r
182                 $handle->do(q|\r
183                         CREATE TABLE `| . $table . q|` (\r
184                                 `id` int unsigned NOT NULL auto_increment,\r
185                                 `body` text NOT NULL,\r
186                                 `poster` int unsigned default NULL,\r
187                                 `date` timestamp NOT NULL default CURRENT_TIMESTAMP,\r
188                                 PRIMARY KEY (`id`)\r
189                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
190                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
191         }\r
192         $table = $prefix . 'quotes';\r
193         my $determine_votes = 0;\r
194         my $determine_scores = 0;\r
195         if ($self->_table_exists($table)) {\r
196                 unless ($self->_column_exists($table, 'votes')) {\r
197                         $handle->do(q|\r
198                                 ALTER TABLE `| . $table . q|`\r
199                                         ADD `votes` int unsigned NOT NULL default 0 AFTER `rating`\r
200                         |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());\r
201                         $determine_votes = 1;\r
202                 }\r
203                 unless ($self->_column_exists($table, 'score')) {\r
204                         $handle->do(q|\r
205                                 ALTER TABLE `| . $table . q|`\r
206                                         ADD `score` double unsigned NOT NULL default 1\r
207                         |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());\r
208                         $determine_scores = 1;\r
209                 }\r
210         }\r
211         else {\r
212                 $handle->do(q|\r
213                         CREATE TABLE `| . $table . q|` (\r
214                                 `id` int unsigned NOT NULL auto_increment,\r
215                                 `body` text NOT NULL,\r
216                                 `notes` text,\r
217                                 `rating` int NOT NULL default 0,\r
218                                 `votes` int unsigned NOT NULL default 0,\r
219                                 `submitted` timestamp NOT NULL default CURRENT_TIMESTAMP,\r
220                                 `approved` tinyint(1) unsigned NOT NULL default 0,\r
221                                 `flagged` tinyint(1) unsigned NOT NULL default 0,\r
222                                 `score` double unsigned NOT NULL default 1,\r
223                                 PRIMARY KEY (`id`)\r
224                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
225                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
226         }\r
227         $table = $prefix . 'tags';\r
228         unless ($self->_table_exists($table)) {\r
229                 $handle->do(q|\r
230                         CREATE TABLE `| . $table . q|` (\r
231                                 `id` int unsigned NOT NULL auto_increment,\r
232                                 `tag` varchar(255) NOT NULL,\r
233                                 PRIMARY KEY (`id`),\r
234                                 INDEX (`tag`)\r
235                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
236                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
237         }\r
238         $table = $prefix . 'quote_tag';\r
239         unless ($self->_table_exists($table)) {\r
240                 $handle->do(q|\r
241                         CREATE TABLE `| . $table . q|` (\r
242                                 `quote_id` int unsigned NOT NULL,\r
243                                 `tag_id` int unsigned NOT NULL,\r
244                                 INDEX (`quote_id`),\r
245                                 INDEX (`tag_id`)\r
246                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
247                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
248         }\r
249         $table = $prefix . 'events';\r
250         unless ($self->_table_exists($table)) {\r
251                 $handle->do(q|\r
252                         CREATE TABLE `| . $table . q|` (\r
253                                 `id` int unsigned NOT NULL auto_increment,\r
254                                 `date` timestamp NOT NULL default CURRENT_TIMESTAMP,\r
255                                 `code` int unsigned NOT NULL,\r
256                                 `user` int unsigned,\r
257                                 PRIMARY KEY (`id`)\r
258                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
259                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
260         }\r
261         $table = $prefix . 'event_metadata';\r
262         unless ($self->_table_exists($table)) {\r
263                 $handle->do(q|\r
264                         CREATE TABLE `| . $table . q|` (\r
265                             `id` int unsigned NOT NULL,\r
266                                 `name` varchar(32) NOT NULL,\r
267                                 `value` text NOT NULL,\r
268                                 INDEX (`id`),\r
269                                 INDEX (`name`)\r
270                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
271                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
272         }\r
273         if ($self->_table_exists($prefix . 'log')) {\r
274                 $self->_migrate_log();\r
275                 $self->_remove_table($prefix . 'log');\r
276         }\r
277         $self->_determine_votes() if ($determine_votes);\r
278         $self->_determine_scores() if ($determine_scores);\r
279         $table = $prefix . 'sessions';\r
280         if ($self->_table_exists($table)) {\r
281                 unless ($self->_column_exists($table, 'expires')) {\r
282                         # XXX: Maybe unserialize session data to get the right expiry time.\r
283                         # It's not really worth the effort though.\r
284                         my $exp = time() + 3 * 24 * 60 * 60;\r
285                         $handle->do(q|\r
286                                 ALTER TABLE `| . $table . q|`\r
287                                         ADD `expires` int unsigned NOT NULL AFTER `id`\r
288                         |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());\r
289                         $handle->do(q|\r
290                                 ALTER TABLE `| . $table . q|` ADD INDEX (`expires`)\r
291                         |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());\r
292                         $handle->do(q|\r
293                                 UPDATE `| . $table . q|`SET `expires` = | . $exp\r
294                         ) or Chirpy::die('Cannot update ' . $table . ': ' . DBI->errstr());\r
295                 }\r
296         }\r
297         else {\r
298                 $handle->do(q|\r
299                         CREATE TABLE `| . $table . q|` (\r
300                                 `id` varchar(32) NOT NULL,\r
301                                 `expires` int unsigned NOT NULL,\r
302                                 `data` text NOT NULL,\r
303                                 UNIQUE KEY `id` (`id`)\r
304                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
305                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
306         }\r
307         $table = $prefix . 'vars';\r
308         unless ($self->_table_exists($table)) {\r
309                 $handle->do(q|\r
310                         CREATE TABLE `| . $table . q|` (\r
311                                 `name` varchar(32) NOT NULL,\r
312                                 `value` varchar(255) NOT NULL,\r
313                                 PRIMARY KEY (`name`)\r
314                         ) TYPE=MyISAM DEFAULT CHARSET=utf8\r
315                 |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());\r
316         }\r
317         if (defined $accounts) {\r
318                 foreach my $account (@$accounts) {\r
319                         $self->add_account($account);\r
320                 }\r
321         }\r
322         if (defined $news) {\r
323                 foreach my $item (@$news) {\r
324                         $self->add_news_item($item);\r
325                 }\r
326         }\r
327         if (defined $quotes) {\r
328                 foreach my $quote (@$quotes) {\r
329                         $self->add_quote($quote);\r
330                 }\r
331         }\r
332 }\r
333 \r
334 sub remove {\r
335         my ($self, $accounts, $news, $quotes) = @_;\r
336         my @tables = qw/accounts news quotes tags quote_tag log sessions vars/;\r
337         foreach my $table (@tables) {\r
338                 $self->_remove_table($self->table_name_prefix() . $table);\r
339         }\r
340 }\r
341 \r
342 sub _migrate_log {\r
343         my $self = shift;\r
344         my $prefix = $self->table_name_prefix();\r
345         my $old_table = $prefix . 'log';\r
346         my $event_table = $prefix . 'events';\r
347         my $metadata_table = $prefix . 'event_metadata';\r
348         my $query = 'SELECT * FROM `' . $old_table . '`';\r
349         my $sth = $self->handle()->prepare($query);\r
350         $self->_db_error() unless (defined $sth);\r
351         my $rows = $sth->execute();\r
352         $self->_db_error() unless (defined $rows);\r
353         while (my $row = $sth->fetchrow_hashref()) {\r
354                 my $date = $row->{'date'};\r
355                 my $code = $row->{'code'};\r
356                 my $user = $row->{'user'};\r
357                 my $data = $row->{'data'};\r
358                 eval '$data = ' . $data;\r
359                 if ($data && ref $data eq 'HASH') {\r
360                         $self->_do('INSERT INTO `' . $event_table . '`'\r
361                                 . ' (`date`, `code`, `user`) VALUES (?, ?, ?)',\r
362                                 $date, $code, $user);\r
363                         my $id = $self->handle()->{'mysql_insertid'};\r
364                         my $params = (exists $data->{'parameters'}\r
365                                 ? $data->{'parameters'} : {});\r
366                         if (exists $data->{'user'}) {\r
367                                 while (my ($name, $value) = each %{$data->{'user'}}) {\r
368                                         $params->{'user:' . $name} = $value;\r
369                                 }\r
370                         }\r
371                         while (my ($name, $value) = each %$params) {\r
372                                 if (ref $value) {\r
373                                         if ($name eq 'old_tags' || $name eq 'new_tags') {\r
374                                                 # Tags were stored as an arrayref.\r
375                                                 $value = join(' ', @$value);\r
376                                         }\r
377                                         elsif ($name eq 'poster' || $name eq 'old_poster') {\r
378                                                 # Old versions used the object instead of its ID.\r
379                                                 $value = $value->{'id'};\r
380                                         }\r
381                                         else {\r
382                                                 # This should never happen.\r
383                                                 $value = &_serialize($value);\r
384                                         }\r
385                                 }\r
386                                 elsif ($name eq 'quote' && $value eq 'id') {\r
387                                         # Bug 1493589. The quote ID is lost, so we use 0.\r
388                                         $name = 'id';\r
389                                         $value = 0;\r
390                                 }\r
391                                 next unless (defined $value && $value ne '');\r
392                                 my $query = 'INSERT INTO `' . $metadata_table . '`'\r
393                                         . ' (`id`, `name`, `value`) VALUES (?, ?, ?)';\r
394                                 my @params = ($id, $name, $value);\r
395                                 # XXX: This causes Unicode breakage sometimes.\r
396                                 #$self->_do($query, @params);\r
397                                 my $success = $self->handle()->do($query, undef, @params);\r
398                                 unless ($success) {\r
399                                         $params[2] = 'ERROR';\r
400                                         $self->_do($query, @params);\r
401                                 }\r
402                         }\r
403                 }\r
404                 else {\r
405                         # TODO: Failed to unserialize--report.\r
406                 }\r
407         }\r
408 }\r
409 \r
410 sub _determine_votes {\r
411         my $self = shift;\r
412         my $prefix = $self->table_name_prefix();\r
413         my $query = 'SELECT `value`, COUNT(*)'\r
414                 . ' FROM `' . $prefix . 'events` AS ev'\r
415                 . ' LEFT JOIN `' . $prefix . 'event_metadata` AS md'\r
416                 . ' ON ev.id = md.id'\r
417                 . ' WHERE `code` = ' . Chirpy::Event::QUOTE_RATING_DOWN\r
418                 . ' AND `name` = "id"'\r
419                 . ' GROUP BY `value`';\r
420         my $sth = $self->handle()->prepare($query);\r
421         $self->_db_error() unless (defined $sth);\r
422         my $rows = $sth->execute();\r
423         $self->_db_error() unless (defined $rows);\r
424         my %neg = ();\r
425         while (my $row = $sth->fetchrow_arrayref()) {\r
426                 my ($id, $votes) = @$row;\r
427                 $neg{$id} = $votes;\r
428         }\r
429         $query = 'SELECT `id`, `rating` FROM `' . $prefix . 'quotes`';\r
430         $sth = $self->handle()->prepare($query);\r
431         $self->_db_error() unless (defined $sth);\r
432         $rows = $sth->execute();\r
433         $self->_db_error() unless (defined $rows);\r
434         $query = 'UPDATE `' . $prefix . 'quotes`'\r
435                 . ' SET `votes` = ? WHERE `id` = ? LIMIT 1';\r
436         my $s = $self->handle()->prepare($query);\r
437         while (my $row = $sth->fetchrow_hashref()) {\r
438                 my ($id, $rating) = ($row->{'id'}, $row->{'rating'});\r
439                 my $votes = $rating;\r
440                 $votes += $neg{$id} * 2 if (exists $neg{$id} && $neg{$id});\r
441                 # This is only meaningful if the log is inconsistent.\r
442                 $votes = abs($rating) if ($votes < 0);\r
443                 $rows = $s->execute($votes, $id);\r
444                 $self->_db_error() unless (defined $rows);\r
445         }\r
446 }\r
447 \r
448 sub _determine_scores {\r
449         my $self = shift;\r
450         my $prefix = $self->table_name_prefix();\r
451         my $query = 'UPDATE `' . $prefix . 'quotes`'\r
452                 . ' SET ' . $SCORE_EXPR;\r
453         $self->_do($query);\r
454 }\r
455 \r
456 sub _remove_table {\r
457         my ($self, $table) = @_;\r
458         return unless ($self->_table_exists($table));\r
459         defined $self->_do('DROP TABLE `' . $table . '`')\r
460                 or Chirpy::die('Cannot remove "' . $table . '": ' . DBI->errstr());\r
461 }\r
462 \r
463 sub _table_exists {\r
464         my ($self, $table) = @_;\r
465         my $query = "SHOW TABLES LIKE '$table'";\r
466         return defined $self->_execute_scalar($query);\r
467 }\r
468 \r
469 sub _column_exists {\r
470         my ($self, $table, $column) = @_;\r
471         my $query = "SHOW COLUMNS FROM $table";\r
472         my $sth = $self->handle()->prepare($query);\r
473         $self->_db_error() unless (defined $sth);\r
474         my $rows = $sth->execute();\r
475         $self->_db_error() unless (defined $rows);\r
476         while (my $row = $sth->fetchrow_hashref()) {\r
477                 return 1 if ($row->{'Field'} eq $column);\r
478         }\r
479         return 0;\r
480 }\r
481 \r
482 # TODO: Implement a ResultSet interface in order to make this more lightweight\r
483 sub get_quotes {\r
484         my ($self, $params) = @_;\r
485         $params = {} unless (ref $params eq 'HASH');\r
486         my $query = 'SELECT DISTINCT `q`.`id` AS `id`, `body`, `notes`,'\r
487                 . ' `rating`, `votes`,'\r
488                 . ' UNIX_TIMESTAMP(`submitted`) AS `submitted`, `approved`, `flagged`'\r
489                 . ' FROM `' . $self->table_name_prefix() . 'quotes` AS `q`';\r
490         my @par = ();\r
491         my @cond = ();\r
492         if (defined $params->{'flagged'}) {\r
493                 push @cond, '`flagged` '\r
494                         . ($params->{'flagged'} ? '<>' : '=') . ' 0';\r
495         }\r
496         if (defined $params->{'approved'}) {\r
497                 push @cond, '`approved` '\r
498                         . ($params->{'approved'} ? '<>' : '=') . ' 0';\r
499         }\r
500         if (defined $params->{'contains'} && @{$params->{'contains'}}) {\r
501                 foreach my $q (@{$params->{'contains'}}) {\r
502                         (my $query = $q) =~ s/(?<!\\)\*/%/g;\r
503                         $query =~ s/(?<!\\)\?/_/g;\r
504                         $query =~ s/\\([*?\\])/$1/g;\r
505                         push @cond, '(`body` LIKE ? OR `notes` LIKE ?)';\r
506                         push @par, $query, $query;\r
507                 }\r
508         }\r
509         if (defined $params->{'tags'} && @{$params->{'tags'}}) {\r
510                 $query .= ' JOIN `' . $self->table_name_prefix() . 'quote_tag` AS `qt`'\r
511                         . ' ON `q`.`id` = `qt`.`quote_id`'\r
512                         . ' JOIN `' . $self->table_name_prefix() . 'tags` AS `t`'\r
513                         . ' ON `qt`.`tag_id` = `t`.`id`';\r
514                 my @tags = @{$params->{'tags'}};\r
515                 push @cond, '`tag` IN (?' . (',?' x (scalar(@tags) - 1)) . ')';\r
516                 push @par, @tags;\r
517         }\r
518         if (defined $params->{'since'}) {\r
519                 push @cond, '`submitted` > FROM_UNIXTIME(?)';\r
520                 push @par, $params->{'since'};\r
521         }\r
522         if (defined $params->{'id'}) {\r
523                 push @cond, '`id` = ?';\r
524                 push @par, $params->{'id'};\r
525                 delete $params->{'first'};\r
526                 delete $params->{'sort'};\r
527         }\r
528         if (@cond) {\r
529                 $query .= ' WHERE ' . join(' AND ', @cond);\r
530         }\r
531         if ($params->{'random'}) {\r
532                 $query .= ' ORDER BY RAND()';\r
533         }\r
534         elsif (ref $params->{'sort'} eq 'ARRAY') {\r
535                 $query .= ' ORDER BY ' . join(', ', map {\r
536                                 '`q`.`' . $_->[0] . '`' . ($_->[1] ? ' DESC' : '')\r
537                         } @{$params->{'sort'}});\r
538         }\r
539         my $per_page;\r
540         my $leading;\r
541         if (defined $params->{'id'}) {\r
542                 $query .= ' LIMIT 1';\r
543         }\r
544         elsif ($params->{'count'}) {\r
545                 $query .= ' LIMIT ';\r
546                 if ($params->{'first'}) {\r
547                         $leading = 1;\r
548                         $query .= int($params->{'first'}) . ',';\r
549                 }\r
550                 $query .= int($params->{'count'}) + 1;\r
551                 $per_page = $params->{'count'};\r
552         }\r
553         my $sth = $self->handle()->prepare($query);\r
554         $self->_db_error() unless (defined $sth);\r
555         my $rows = $sth->execute(@par);\r
556         $self->_db_error() unless (defined $rows);\r
557         my $trailing;\r
558         my @result = ();\r
559         while (my $row = $sth->fetchrow_hashref()) {\r
560                 if ($per_page && @result >= $per_page) {\r
561                         $trailing = 1;\r
562                         last;\r
563                 }\r
564                 push @result, new Chirpy::Quote(\r
565                         $row->{'id'}, Chirpy::Util::decode_utf8($row->{'body'}),\r
566                         Chirpy::Util::decode_utf8($row->{'notes'}),\r
567                         $row->{'rating'}, $row->{'votes'}, $row->{'submitted'},\r
568                         $row->{'approved'}, $row->{'flagged'},\r
569                         $self->_quote_tags($row->{'id'})\r
570                 );\r
571         }\r
572         my $result = (@result ? \@result : undef);\r
573         return ($result, $leading, $trailing) if (wantarray);\r
574         return $result;\r
575 }\r
576 \r
577 sub quote_count {\r
578         my ($self, $params) = @_;\r
579         $params = {} unless (ref $params eq 'HASH');\r
580         my $appr = $params->{'approved'};\r
581         return $self->_execute_scalar('SELECT COUNT(*) FROM `'\r
582                 . $self->table_name_prefix() . 'quotes`'\r
583                 . (defined $appr ? ' WHERE `approved` = ' . ($appr ? '1' : '0') : ''));\r
584 }\r
585 \r
586 sub add_quote {\r
587         my ($self, $quote) = @_;\r
588         $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'quotes`'\r
589                 . ' (`body`, `notes`, `approved`)'\r
590                 . ' VALUES (?, ?, ?)',\r
591                 $quote->get_body(),\r
592                 $quote->get_notes(),\r
593                 $quote->is_approved() || 0);\r
594         my $id = $self->handle()->{'mysql_insertid'};\r
595         $quote->set_id($id);\r
596         $self->_tag($id, $quote->get_tags());\r
597         return 1;\r
598 }\r
599 \r
600 sub modify_quote {\r
601         my ($self, $quote) = @_;\r
602         Chirpy::die('Not a Chirpy::Quote')\r
603                 unless (ref $quote eq 'Chirpy::Quote');\r
604         my $result = $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'\r
605                 . ' SET `body` = ?, `notes` = ? WHERE `id` = ?',\r
606                 $quote->get_body(), $quote->get_notes(), $quote->get_id());\r
607         $self->_replace_tags($quote->get_id(), $quote->get_tags());\r
608         return $result;\r
609 }\r
610 \r
611 sub increase_quote_rating {\r
612         my ($self, $id, $revert) = @_;\r
613         my ($r, $v) = ($revert\r
614                 ? ('`rating` + 2', '`votes`') : ('`rating` + 1', '`votes` + 1'));\r
615         $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'\r
616                 . ' SET `rating` = ' . $r . ', `votes` = ' . $v . ','\r
617                 . ' ' . $SCORE_EXPR\r
618                 . ' WHERE `id` = ' . $id . ' LIMIT 1')\r
619                         or return undef;\r
620         return $self->_get_quote_rating_and_vote_count($id);\r
621 }\r
622 \r
623 sub decrease_quote_rating {\r
624         my ($self, $id, $revert) = @_;\r
625         my ($r, $v) = ($revert\r
626                 ? ('`rating` - 2', '`votes`') : ('`rating` - 1', '`votes` + 1'));\r
627         $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'\r
628                 . ' SET `rating` = ' . $r . ', `votes` = ' . $v . ','\r
629                 . ' ' . $SCORE_EXPR\r
630                 . ' WHERE `id` = ' . $id . ' LIMIT 1')\r
631                         or return undef;\r
632         return $self->_get_quote_rating_and_vote_count($id);\r
633 }\r
634 \r
635 sub get_tag_use_counts {\r
636         my $self = shift;\r
637         my $query = 'SELECT `tag`, COUNT(`tag_id`) AS `cnt`'\r
638                 . ' FROM `' . $self->table_name_prefix() . 'quote_tag` AS `qt`'\r
639                 . ' JOIN `' . $self->table_name_prefix() . 'tags` AS `t`'\r
640                         . ' ON `qt`.`tag_id` = `t`.`id`'\r
641                 . ' JOIN `' . $self->table_name_prefix() . 'quotes` AS `q`'\r
642                         . ' ON `qt`.`quote_id` = `q`.`id`'\r
643                 . ' WHERE `approved` = 1'\r
644                 . ' GROUP BY `tag`';\r
645         my $sth = $self->handle()->prepare($query);\r
646         $self->_db_error() unless (defined $sth);\r
647         my $rows = $sth->execute();\r
648         $self->_db_error() unless (defined $rows);\r
649         my %result = ();\r
650         while (my $row = $sth->fetchrow_hashref()) {\r
651                 $result{$row->{'tag'}} = $row->{'cnt'};\r
652         }\r
653         return \%result;\r
654 }\r
655 \r
656 sub approve_quotes {\r
657         my ($self, @ids) = @_;\r
658         return undef unless (@ids);\r
659         return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'\r
660                 . ' SET `approved` = 1 WHERE `id` IN ('\r
661                 . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids);\r
662 }\r
663 \r
664 sub flag_quotes {\r
665         my ($self, @ids) = @_;\r
666         return undef unless (@ids);\r
667         return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'\r
668                 . ' SET `flagged` = 1 WHERE `id` IN ('\r
669                 . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids);\r
670 }\r
671 \r
672 sub unflag_quotes {\r
673         my ($self, @ids) = @_;\r
674         return undef unless (@ids);\r
675         return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'\r
676                 . ' SET `flagged` = 0 WHERE `id` IN ('\r
677                 . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids);\r
678 }\r
679 \r
680 sub remove_quote {\r
681         my ($self, $quote) = @_;\r
682         Chirpy::die('Not a Chirpy::Quote')\r
683                 unless (ref $quote eq 'Chirpy::Quote');\r
684         my $id = quote->get_id();\r
685         $self->_untag_all($id);\r
686         return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quotes`'\r
687                 . ' WHERE `id` = ' . $id\r
688                 . ' LIMIT 1');\r
689 }\r
690 \r
691 sub remove_quotes {\r
692         my ($self, @ids) = @_;\r
693         return undef unless (@ids);\r
694         $self->_untag_all(@ids);\r
695         return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quotes`'\r
696                 . ' WHERE `id` IN (' . join(',', map { int } @ids) . ')'\r
697                 . ' LIMIT ' . scalar @ids);\r
698 }\r
699 \r
700 sub get_news_items {\r
701         my ($self, $params) = @_;\r
702         my $query = 'SELECT N.id, N.body, N.poster, '\r
703                 . 'UNIX_TIMESTAMP(N.date) AS `date`, A.username '\r
704                 . 'FROM `' . $self->table_name_prefix() . 'news` N'\r
705                 . ' LEFT JOIN `' . $self->table_name_prefix() . 'accounts` A'\r
706                 . ' ON N.poster = A.id';\r
707         my @par = ();\r
708         if ($params->{'id'}) {\r
709                 $query .= ' WHERE N.id = ?';\r
710                 push @par, $params->{'id'};\r
711                 $params->{'count'} = 1;\r
712         }\r
713         $query .= ' ORDER BY `date` DESC';\r
714         if ($params->{'count'}) {\r
715                 $query .= ' LIMIT ' . int $params->{'count'};\r
716         }\r
717         my $sth = $self->handle()->prepare($query);\r
718         $self->_db_error() unless (defined $sth);\r
719         my $rows = $sth->execute(@par);\r
720         $self->_db_error() unless (defined $rows);\r
721         my @result = ();\r
722         while (my $row = $sth->fetchrow_hashref()) {\r
723                 my $item = new Chirpy::NewsItem(\r
724                         $row->{'id'}, Chirpy::Util::decode_utf8($row->{'body'}),\r
725                         ($row->{'username'}\r
726                                 ? new Chirpy::Account($row->{'poster'}, $row->{'username'})\r
727                                 : undef),\r
728                         $row->{'date'}\r
729                 );\r
730                 push @result, $item;\r
731         }\r
732         return (@result ? \@result : undef);\r
733 }\r
734 \r
735 sub add_news_item {\r
736         my ($self, $news) = @_;\r
737         Chirpy::die('Not a Chirpy::NewsItem')\r
738                 unless (ref $news eq 'Chirpy::NewsItem');\r
739         my $poster = $news->get_poster();\r
740         $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'news`'\r
741                 . ' (`body`, `poster`)'\r
742                 . ' VALUES (?, ?)',\r
743                 $news->get_body(), (defined $poster ? $poster->get_id() : undef),\r
744                 $news->get_date());\r
745         my $id = $self->handle()->{'mysql_insertid'};\r
746         $news->set_id($id);\r
747         return 1;\r
748 }\r
749 \r
750 sub modify_news_item {\r
751         my ($self, $item) = @_;\r
752         Chirpy::die('Not a Chirpy::NewsItem')\r
753                 unless (ref $item eq 'Chirpy::NewsItem');\r
754         my $poster = $item->get_poster();\r
755         return $self->_do('UPDATE `' . $self->table_name_prefix() . 'news`'\r
756                 . ' SET `body` = ?, `poster` = ? WHERE `id` = ? LIMIT 1',\r
757                 $item->get_body(), (defined $poster ? $poster->get_id() : undef),\r
758                 $item->get_id());\r
759 }\r
760 \r
761 sub remove_news_item {\r
762         my ($self, $item) = @_;\r
763         Chirpy::die('Not a Chirpy::NewsItem')\r
764                 unless (ref $item eq 'Chirpy::NewsItem');\r
765         return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'news`'\r
766                 . ' WHERE `id` = ' . $item->get_id()\r
767                 . ' LIMIT 1');\r
768 }\r
769 \r
770 sub remove_news_items {\r
771         my ($self, @ids) = @_;\r
772         return undef unless (@ids);\r
773         return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'news`'\r
774                 . ' WHERE `id` IN (' . join(',', map { int } @ids) . ')'\r
775                 . ' LIMIT ' . scalar @ids);\r
776 }\r
777 \r
778 sub get_accounts {\r
779         my ($self, $params) = @_;\r
780         $params = {} unless (ref $params eq 'HASH');\r
781         my $query = 'SELECT * FROM `' . $self->table_name_prefix() . 'accounts`';\r
782         my @cond = ();\r
783         my @par = ();\r
784         if (ref $params->{'levels'} eq 'ARRAY') {\r
785                 push @cond, '`level` IN (' . join (',', @{$params->{'levels'}}) . ')';\r
786         }\r
787         if ($params->{'id'}) {\r
788                 push @cond, '`id` = ?';\r
789                 push @par, $params->{'id'};\r
790         }\r
791         if ($params->{'username'}) {\r
792                 push @cond, '`username` = ?';\r
793                 push @par, $params->{'username'};\r
794         }\r
795         if (@cond) {\r
796                 $query .= ' WHERE ' . join(' AND ', @cond);\r
797         }\r
798         $query .= ' ORDER BY `level` DESC, `username`';\r
799         my $sth = $self->handle()->prepare($query);\r
800         $self->_db_error() unless (defined $sth);\r
801         my $rows = $sth->execute(@par);\r
802         $self->_db_error() unless (defined $rows);\r
803         my @result = ();\r
804         while (my $row = $sth->fetchrow_hashref()) {\r
805                 push @result, new Chirpy::Account(\r
806                         $row->{'id'}, $row->{'username'}, $row->{'password'},\r
807                         $row->{'level'}\r
808                 );\r
809         }\r
810         return (@result ? \@result : undef);\r
811 }\r
812 \r
813 sub add_account {\r
814         my ($self, $user) = @_;\r
815         $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'accounts`'\r
816                 . ' (`username`, `password`, `level`) VALUES (?, ?, ?)',\r
817                 $user->get_username(), $user->get_password(), $user->get_level());\r
818         my $id = $self->handle()->{'mysql_insertid'};\r
819         $user->set_id($id);\r
820         return 1;\r
821 }\r
822 \r
823 sub modify_account {\r
824         my ($self, $account) = @_;\r
825         Chirpy::die('Not a Chirpy::Account')\r
826                 unless (ref $account eq 'Chirpy::Account');\r
827         return $self->_do('UPDATE `' . $self->table_name_prefix() . 'accounts`'\r
828                 . ' SET `username` = ?, `password` = ?, `level` = ?'\r
829                 . ' WHERE `id` = ? LIMIT 1',\r
830                 $account->get_username(), $account->get_password(),\r
831                 $account->get_level(), $account->get_id());\r
832 }\r
833 \r
834 sub remove_account {\r
835         my ($self, $account) = @_;\r
836         Chirpy::die('Not a Chirpy::Account')\r
837                 unless (ref $account eq 'Chirpy::Account');\r
838         $self->_do('UPDATE `' . $self->table_name_prefix()\r
839                 . 'news` SET `poster` = NULL WHERE `poster` = ' . $account->get_id()\r
840                 . ' LIMIT 1');\r
841         return $self->_do('DELETE FROM `' . $self->table_name_prefix()\r
842                 . 'accounts` WHERE `id` = ' . $account->get_id()\r
843                 . ' LIMIT 1');\r
844 }\r
845 \r
846 sub remove_accounts {\r
847         my ($self, @ids) = @_;\r
848         return undef unless (@ids);\r
849         my $ids = join(',', map { int } @ids);\r
850         my $num = scalar @ids;\r
851         $self->_do('UPDATE `' . $self->table_name_prefix()\r
852                 . 'news` SET `poster` = NULL WHERE `poster` IN (' . $ids . ')'\r
853                 . ' LIMIT ' . $num);\r
854         return $self->_do('DELETE FROM `' . $self->table_name_prefix()\r
855                 . 'accounts` WHERE `id` IN (' . $ids . ')'\r
856                 . ' LIMIT ' . $num);\r
857 }\r
858 \r
859 sub username_exists {\r
860         my ($self, $username) = @_;\r
861         return $self->_do('SELECT `id` FROM `' . $self->table_name_prefix()\r
862                 . 'accounts` WHERE `username` = ? LIMIT 1', $username) ? 1 : 0;\r
863 }\r
864 \r
865 sub account_count {\r
866         my ($self, $params) = @_;\r
867         $params = {} unless (ref $params eq 'HASH');\r
868         return $self->_execute_scalar('SELECT COUNT(*) FROM `'\r
869                 . $self->table_name_prefix() . 'accounts`'\r
870                 . (defined $params->{'levels'}\r
871                         ? ' WHERE `level` IN (' . join(',', @{$params->{'levels'}}) . ')'\r
872                         : ''));\r
873 }\r
874 \r
875 sub get_events {\r
876         my ($self, $params) = @_;\r
877         $params = {} unless (ref $params eq 'HASH');\r
878         my $query = 'SELECT DISTINCT `e`.`id` AS `id`,'\r
879                 . ' UNIX_TIMESTAMP(`date`) AS `date`, `code`, `user`'\r
880                 . ' FROM `' . $self->table_name_prefix() . 'events` AS `e`';\r
881         my @conditions = ();\r
882         my @param = ();\r
883         if (defined $params->{'data'} && %{$params->{'data'}}) {\r
884                 $query .= ' JOIN `' . $self->table_name_prefix()\r
885                         . 'event_metadata` AS `m`'\r
886                         . ' ON `e`.`id` = `m`.`id`';\r
887                 my @cond = ();\r
888                 while (my ($key, $value) = each %{$params->{'data'}}) {\r
889                         push @cond, '(`name` = ? AND `value` = ?)';\r
890                         push @param, $key, $value;\r
891                 }\r
892                 push @conditions, '(' . join(' OR ', @cond) . ')';\r
893         }\r
894         if (my $code = $params->{'code'}) {\r
895                 if (ref $code eq 'ARRAY') {\r
896                         my $count = scalar @$code;\r
897                         if ($count) {\r
898                                 push @conditions, '`code` IN (?' . (',?' x ($count - 1)) . ')';\r
899                                 push @param, @$code;\r
900                         }\r
901                 }\r
902                 else {\r
903                         push @conditions, '`code` = ?';\r
904                         push @param, $code;\r
905                 }\r
906         }\r
907         my $user = $params->{'user'};\r
908         if (defined $user) {\r
909                 if (ref $user eq 'ARRAY') {\r
910                         if (@$user) {\r
911                                 my @set = ();\r
912                                 my $guest = 0;\r
913                                 foreach my $u (@$user) {\r
914                                         if ($u) {\r
915                                                 push @set, $u;\r
916                                         }\r
917                                         else {\r
918                                                 $guest = 1;\r
919                                         }\r
920                                 }\r
921                                 my $cond;\r
922                                 if (@set) {\r
923                                         $cond = '`user` IN (?' . (',?' x (scalar(@set) - 1)) . ')';\r
924                                         push @param, @set;\r
925                                 }\r
926                                 if ($guest) {\r
927                                         $cond = (defined $cond\r
928                                                 ? '(' . $cond . ' OR `user` IS NULL)'\r
929                                                 : '`user` IS NULL');\r
930                                 }\r
931                                 push @conditions, $cond;\r
932                         }\r
933                 }\r
934                 elsif ($user) {\r
935                         push @conditions, '`user` = ?';\r
936                         push @param, $user;\r
937                 }\r
938                 else {\r
939                         push @conditions, '`user` IS NULL';\r
940                 }\r
941         }\r
942         if (@conditions) {\r
943                 $query .= ' WHERE ' . join(' AND ', @conditions);\r
944         }\r
945         $query .= ' ORDER BY `id` ' . ($params->{'reverse'} ? 'DESC' : 'ASC');\r
946         my $per_page;\r
947         my $leading;\r
948         if ($params->{'count'}) {\r
949                 $query .= ' LIMIT ';\r
950                 if ($params->{'first'}) {\r
951                         $leading = 1;\r
952                         $query .= int($params->{'first'}) . ',';\r
953                 }\r
954                 $query .= int($params->{'count'}) + 1;\r
955                 $per_page = $params->{'count'};\r
956         }\r
957         my $sth = $self->handle()->prepare($query);\r
958         $self->_db_error() unless (defined $sth);\r
959         my $rows = $sth->execute(@param);\r
960         $self->_db_error() unless (defined $rows);\r
961         my $trailing;\r
962         my @result = ();\r
963         while (my $row = $sth->fetchrow_hashref()) {\r
964                 if ($per_page && @result >= $per_page) {\r
965                         $trailing = 1;\r
966                         last;\r
967                 }\r
968                 my $id = $row->{'id'};\r
969                 my $data = $self->_get_event_metadata($id);\r
970                 push @result, new Chirpy::Event(\r
971                         $id, $row->{'date'}, $row->{'code'}, $row->{'user'}, $data\r
972                 );\r
973         }\r
974         my $result = (@result ? \@result : undef);\r
975         return ($result, $leading, $trailing) if (wantarray);\r
976         return $result;\r
977 }\r
978 \r
979 sub _get_event_metadata {\r
980         my ($self, $id) = @_;\r
981         my $query = 'SELECT `name`, `value`'\r
982                 . ' FROM `' . $self->table_name_prefix() . 'event_metadata`'\r
983                 . ' WHERE `id` = ' . $id;\r
984         my $sth = $self->handle()->prepare($query);\r
985         $self->_db_error() unless (defined $sth);\r
986         my $rows = $sth->execute();\r
987         $self->_db_error() unless (defined $rows);\r
988         my %result = ();\r
989         while (my $row = $sth->fetchrow_hashref()) {\r
990                 $result{$row->{'name'}} = $row->{'value'};\r
991         }\r
992         return \%result;\r
993 }\r
994 \r
995 sub log_event {\r
996         my ($self, $event) = @_;\r
997         Chirpy::die('Not a Chirpy::Event')\r
998                 unless (ref $event eq 'Chirpy::Event');\r
999         my $user = $event->get_user();\r
1000         my $prefix = $self->table_name_prefix();\r
1001         $self->_do('INSERT INTO `' . $prefix . 'events`'\r
1002                 . ' (`code`, `user`) VALUES (?, ?)',\r
1003                 $event->get_code(),\r
1004                 (defined $user ? $user->get_id() : undef));\r
1005         my $id = $self->handle()->{'mysql_insertid'};\r
1006         $event->set_id($id);\r
1007         while (my ($name, $value) = each %{$event->get_data()}) {\r
1008                 $self->_do('INSERT INTO `' . $prefix . 'event_metadata`'\r
1009                         . ' (`id`, `name`, `value`) VALUES (?, ?, ?)',\r
1010                         $id, $name, $value);\r
1011         }\r
1012         return $id;\r
1013 }\r
1014 \r
1015 sub add_session {\r
1016         my ($self, $id, $data) = @_;\r
1017         my $string = &_serialize($data);\r
1018         $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'sessions`'\r
1019                 . ' (`id`, `expires`, `data`) VALUES (?, ?, ?)', $id,\r
1020                 $data->{'_SESSION_ATIME'} + $data->{'_SESSION_ETIME'}, $string);\r
1021         return 1;\r
1022 }\r
1023 \r
1024 sub get_sessions {\r
1025         my ($self, @ids) = @_;\r
1026         my $query = 'SELECT `id`, `data` FROM `' . $self->table_name_prefix()\r
1027                 . 'sessions`';\r
1028         $query .= ' WHERE `id` IN (?' . (',?' x (scalar(@ids) - 1)) . ')'\r
1029                 . ' LIMIT ' . scalar(@ids) if (@ids);\r
1030         my $sth = $self->handle()->prepare($query);\r
1031         $self->_db_error() unless (defined $sth);\r
1032         my $rows = $sth->execute(@ids);\r
1033         $self->_db_error() unless (defined $rows);\r
1034         my @results = ();\r
1035         while (my $row = $sth->fetchrow_hashref()) {\r
1036                 push @results, &_unserialize($row->{'data'});\r
1037         }\r
1038         return @results;\r
1039 }\r
1040 \r
1041 sub modify_session {\r
1042         my ($self, $id, $data) = @_;\r
1043         my $string = &_serialize($data);\r
1044         my $sql = 'UPDATE `' . $self->table_name_prefix()\r
1045                 . 'sessions` SET `expires` = ?, `data` = ? WHERE `id` = ? LIMIT 1';\r
1046         $self->_do($sql,\r
1047                 $data->{'_SESSION_ATIME'} + $data->{'_SESSION_ETIME'}, $string, $id);\r
1048 }\r
1049 \r
1050 sub remove_sessions {\r
1051         my ($self, @ids) = @_;\r
1052         return unless (@ids);\r
1053         $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'sessions`'\r
1054                 . ' WHERE `id` IN (?' . (',?' x (scalar(@ids) - 1)) . ')'\r
1055                 . ' LIMIT ' . scalar(@ids), @ids);\r
1056 }\r
1057 \r
1058 # Overrides Chirpy::UI::WebApp::Session::DataManager's default implementation\r
1059 sub remove_expired_sessions {\r
1060         my $self = shift;\r
1061         $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'sessions`'\r
1062                 . ' WHERE `expires` < ' . time());\r
1063 }\r
1064 \r
1065 sub get_parameter {\r
1066         my ($self, $name) = @_;\r
1067         return $self->_execute_scalar('SELECT `value` FROM `'\r
1068                 . $self->table_name_prefix() . 'vars`'\r
1069                 . ' WHERE `name` = ?', $name);\r
1070 }\r
1071 \r
1072 sub set_parameter {\r
1073         my ($self, $name, $value) = @_;\r
1074         if (!defined $value) {\r
1075                 $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'vars`'\r
1076                         . ' WHERE `name` = ? LIMIT 1', $name);\r
1077                 return;\r
1078         }\r
1079         my $res = $self->_do('UPDATE `' . $self->table_name_prefix() . 'vars`'\r
1080                 . ' SET `value` = ?'\r
1081                 . ' WHERE `name` = ? LIMIT 1',\r
1082                         $value, $name);\r
1083         unless ($res) {\r
1084                 $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'vars`'\r
1085                         . ' (`name`, `value`) VALUES (?, ?)',\r
1086                         $name, $value);\r
1087         }\r
1088 }\r
1089 \r
1090 sub handle {\r
1091         my $self = shift;\r
1092         return $self->{'dbh'};\r
1093 }\r
1094 \r
1095 sub table_name_prefix {\r
1096         my $self = shift;\r
1097         return $self->{'prefix'};\r
1098 }\r
1099 \r
1100 sub _quote_tags {\r
1101         my ($self, $quote_id, $mode) = @_;\r
1102         $mode = 0 unless (defined $mode);\r
1103         my $cols = ($mode == 2 ? '`tag`, `id`' : ($mode == 1 ? '`id`' : '`tag`'));\r
1104         my $query = 'SELECT ' .$cols\r
1105                 . ' FROM `' . $self->table_name_prefix() . 'tags` AS `t`'\r
1106                 . ' JOIN `' . $self->table_name_prefix() . 'quote_tag` AS `qt`'\r
1107                         . ' ON `t`.`id` = `qt`.`tag_id`'\r
1108                 . ' WHERE `quote_id` = ?';\r
1109         my $sth = $self->handle()->prepare($query);\r
1110         $self->_db_error() unless (defined $sth);\r
1111         my $rows = $sth->execute($quote_id);\r
1112         $self->_db_error() unless (defined $rows);\r
1113         if ($mode == 2) {\r
1114                 my %result = ();\r
1115                 while (my $row = $sth->fetchrow_arrayref()) {\r
1116                         $result{$row->[0]} = $row->[1];\r
1117                 }\r
1118                 return \%result;\r
1119         }\r
1120         my @result = ();\r
1121         while (my $row = $sth->fetchrow_arrayref()) {\r
1122                 push @result, $row->[0];\r
1123         }\r
1124         return \@result;\r
1125 }\r
1126 \r
1127 sub _tag {\r
1128         my ($self, $quote_id, $tags) = @_;\r
1129         return unless (@$tags);\r
1130         foreach my $tag (@$tags) {\r
1131                 my $tag_id = $self->_tag_id($tag);\r
1132                 unless (defined $tag_id) {\r
1133                         $tag_id = $self->_create_tag($tag);\r
1134                 }\r
1135                 $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'quote_tag`'\r
1136                         . ' (`quote_id`, `tag_id`) VALUES (?, ?)', $quote_id, $tag_id);\r
1137         }\r
1138 }\r
1139 \r
1140 sub _untag {\r
1141         my ($self, $quote_id, $tag_ids) = @_;\r
1142         my $cnt = scalar @$tag_ids;\r
1143         return unless ($cnt);\r
1144         $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quote_tag`'\r
1145                 . ' WHERE `quote_id` = ? AND `tag_id` IN (?' . (',?' x ($cnt - 1)) . ')'\r
1146                 . ' LIMIT ' . $cnt, $quote_id, @$tag_ids);\r
1147 }\r
1148 \r
1149 sub _untag_all {\r
1150         my ($self, @quote_ids) = @_;\r
1151         my $cnt = scalar @quote_ids;\r
1152         return unless ($cnt);\r
1153         $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quote_tag`'\r
1154                 . ' WHERE `quote_id` IN (?' . (',?' x ($cnt - 1)) . ')', @quote_ids);\r
1155         $self->_clean_up_tags();\r
1156 }\r
1157 \r
1158 sub _tag_id {\r
1159         my ($self, $tag) = @_;\r
1160         return $self->_execute_scalar('SELECT `id` FROM `'\r
1161                 . $self->table_name_prefix() . 'tags` WHERE `tag` = ? LIMIT 1', $tag);\r
1162 }\r
1163 \r
1164 sub _create_tag {\r
1165         my ($self, $tag) = @_;\r
1166         $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'tags`'\r
1167                 . ' (`tag`) VALUES (?)', $tag);\r
1168         return $self->handle()->{'mysql_insertid'};\r
1169 }\r
1170 \r
1171 sub _replace_tags {\r
1172         my ($self, $quote_id, $new_tags) = @_;\r
1173         my $old_tags = $self->_quote_tags($quote_id, 2);\r
1174         my @add = ();\r
1175         foreach my $tag (@$new_tags) {\r
1176                 if (exists $old_tags->{$tag}) {\r
1177                         delete $old_tags->{$tag};\r
1178                 }\r
1179                 else {\r
1180                         push @add, $tag;\r
1181                 }\r
1182         }\r
1183         my @remove = values %$old_tags;\r
1184         $self->_untag($quote_id, \@remove);\r
1185         $self->_clean_up_tags();\r
1186         $self->_tag($quote_id, \@add);\r
1187 }\r
1188 \r
1189 sub _clean_up_tags {\r
1190         my $self = shift;\r
1191         $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'tags`'\r
1192                 . ' WHERE `id` NOT IN ('\r
1193                 . 'SELECT `tag_id` FROM `' . $self->table_name_prefix() . 'quote_tag`'\r
1194                 . ')');\r
1195 }\r
1196 \r
1197 sub _get_quote_rating_and_vote_count {\r
1198         my ($self, $id) = @_;\r
1199         my $sth = $self->handle()->prepare('SELECT `rating`, `votes`'\r
1200                 . ' FROM `' . $self->table_name_prefix() . 'quotes`'\r
1201                 . ' WHERE `id` = ' . $id . ' LIMIT 1');\r
1202         $self->_db_error() unless (defined $sth);\r
1203         my $rows = $sth->execute();\r
1204         $self->_db_error() unless (defined $rows);\r
1205         my @row = $sth->fetchrow_array();\r
1206         return ($row[0], $row[1]);\r
1207 }\r
1208 \r
1209 sub _get_quote_vote_count {\r
1210         my ($self, $id) = @_;\r
1211         my $sth = $self->handle()->prepare('SELECT `votes`'\r
1212                 . ' FROM `' . $self->table_name_prefix() . 'quotes`'\r
1213                 . ' WHERE `id` = ' . $id . ' LIMIT 1');\r
1214         $self->_db_error() unless (defined $sth);\r
1215         my $rows = $sth->execute();\r
1216         $self->_db_error() unless (defined $rows);\r
1217         my @row = $sth->fetchrow_array();\r
1218         return $row[0];\r
1219 }\r
1220 \r
1221 sub _do {\r
1222         my ($self, $query, @params) = @_;\r
1223         my $rows = $self->handle()->do($query, undef, @params);\r
1224         $self->_db_error() unless (defined $rows);\r
1225         return ($rows eq '0E0' ? 0 : $rows);\r
1226 }\r
1227 \r
1228 sub _execute_scalar {\r
1229         my ($self, $query, @params) = @_;\r
1230         my $sth = $self->handle()->prepare($query);\r
1231         $self->_db_error() unless (defined $sth);\r
1232         my $rows = $sth->execute(@params);\r
1233         $self->_db_error() unless (defined $rows);\r
1234         my @row = $sth->fetchrow_array();\r
1235         return (scalar(@row) ? $row[0] : undef);\r
1236 }\r
1237 \r
1238 sub _serialize {\r
1239         my $dumper = new Data::Dumper(\@_)->Terse(1)->Indent(0);\r
1240         return $dumper->Dump();\r
1241 }\r
1242 \r
1243 sub _unserialize {\r
1244         my $string = shift;\r
1245         return (defined $string ? eval $string : undef);\r
1246 }\r
1247 \r
1248 sub _db_error {\r
1249         my $self = shift;\r
1250         my $msg = $self->handle()->errstr();\r
1251         Chirpy::die($msg);\r
1252 }\r
1253 \r
1254 1;\r
1255 \r
1256 ###############################################################################