moved qdb here because matt is lazy
[public/www-new.git] / pub / qdb / src / modules / Chirpy / UI.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:: UI.pm 291 2007-02-05 21:24:46Z ceetee                               $ #\r
22 ###############################################################################\r
23 \r
24 =head1 NAME\r
25 \r
26 Chirpy::UI - Abstract user interface class\r
27 \r
28 =head1 TODO\r
29 \r
30 A detailed description of this module's API will be available in a future\r
31 release. If you want to write your own user interface implementation, you could\r
32 try analyzing the source code of this module and its only implementation so\r
33 far, L<Chirpy::UI::WebApp>. I apologize for the inconvenience.\r
34 \r
35 =head1 AUTHOR\r
36 \r
37 Tim De Pauw E<lt>ceetee@users.sourceforge.netE<gt>\r
38 \r
39 =head1 SEE ALSO\r
40 \r
41 L<Chirpy::UI::WebApp>, L<Chirpy>, L<http://chirpy.sourceforge.net/>\r
42 \r
43 =head1 COPYRIGHT\r
44 \r
45 Copyright 2005-2007 Tim De Pauw. All rights reserved.\r
46 \r
47 This program is free software; you can redistribute it and/or modify it under\r
48 the terms of the GNU General Public License as published by the Free Software\r
49 Foundation; either version 2 of the License, or (at your option) any later\r
50 version.\r
51 \r
52 This program is distributed in the hope that it will be useful, but WITHOUT ANY\r
53 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A\r
54 PARTICULAR PURPOSE.  See the GNU General Public License for more details.\r
55 \r
56 =cut\r
57 \r
58 package Chirpy::UI;\r
59 \r
60 use strict;\r
61 use warnings;\r
62 \r
63 use vars qw($VERSION);\r
64 \r
65 $VERSION = '0.3';\r
66 \r
67 use Chirpy 0.3;\r
68 use Chirpy::Util 0.3;\r
69 use Chirpy::UpdateChecker 0.3;\r
70 \r
71 use constant START_PAGE               =>  1;\r
72 use constant QUOTE_BROWSER            =>  2;\r
73 use constant SINGLE_QUOTE             =>  3;\r
74 use constant RANDOM_QUOTES            =>  4;\r
75 use constant TOP_QUOTES               =>  5;\r
76 use constant BOTTOM_QUOTES            =>  6;\r
77 use constant QUOTES_OF_THE_WEEK       =>  7;\r
78 use constant QUOTE_SEARCH             =>  8;\r
79 use constant TAG_CLOUD                =>  9;\r
80 use constant STATISTICS               => 10;\r
81 use constant SUBMIT_QUOTE             => 11;\r
82 use constant QUOTE_RATING_UP          => 12;\r
83 use constant QUOTE_RATING_DOWN        => 13;\r
84 use constant REPORT_QUOTE             => 14;\r
85 use constant LOGIN                    => 15;\r
86 use constant LOGOUT                   => 16;\r
87 use constant ADMINISTRATION           => 17;\r
88 use constant MODERATION_QUEUE         => 18;\r
89 \r
90 use constant CHANGE_PASSWORD          => 100;\r
91 use constant MANAGE_UNAPPROVED_QUOTES => 110;\r
92 use constant MANAGE_FLAGGED_QUOTES    => 120;\r
93 use constant EDIT_QUOTE               => 130;\r
94 use constant REMOVE_QUOTE             => 131;\r
95 use constant ADD_NEWS                 => 140;\r
96 use constant EDIT_NEWS                => 141;\r
97 use constant REMOVE_NEWS              => 142;\r
98 use constant ADD_ACCOUNT              => 150;\r
99 use constant EDIT_ACCOUNT             => 151;\r
100 use constant REMOVE_ACCOUNT           => 152;\r
101 use constant CHECK_FOR_UPDATE         => 160;\r
102 use constant VIEW_EVENT_LOG           => 170;\r
103 \r
104 use constant CURRENT_PASSWORD_INVALID => -1;\r
105 use constant NEW_PASSWORD_INVALID     => -2;\r
106 use constant PASSWORDS_DIFFER         => -3;\r
107 \r
108 # TODO: make this easily configurable one day\r
109 use constant ADMIN_PERMISSIONS => {\r
110         MANAGE_UNAPPROVED_QUOTES() => {\r
111                 Chirpy::Account::USER_LEVEL_3 => 1,\r
112                 Chirpy::Account::USER_LEVEL_6 => 1,\r
113                 Chirpy::Account::USER_LEVEL_9 => 1\r
114         },\r
115         MANAGE_FLAGGED_QUOTES() => {\r
116                 Chirpy::Account::USER_LEVEL_6 => 1,\r
117                 Chirpy::Account::USER_LEVEL_9 => 1\r
118         },\r
119         EDIT_QUOTE() => {\r
120                 Chirpy::Account::USER_LEVEL_6 => 1,\r
121                 Chirpy::Account::USER_LEVEL_9 => 1\r
122         },\r
123         REMOVE_QUOTE() => {\r
124                 Chirpy::Account::USER_LEVEL_6 => 1,\r
125                 Chirpy::Account::USER_LEVEL_9 => 1\r
126         },\r
127         ADD_NEWS() => {\r
128                 Chirpy::Account::USER_LEVEL_6 => 1,\r
129                 Chirpy::Account::USER_LEVEL_9 => 1\r
130         },\r
131         EDIT_NEWS() => {\r
132                 Chirpy::Account::USER_LEVEL_6 => 1,\r
133                 Chirpy::Account::USER_LEVEL_9 => 1\r
134         },\r
135         REMOVE_NEWS() => {\r
136                 Chirpy::Account::USER_LEVEL_6 => 1,\r
137                 Chirpy::Account::USER_LEVEL_9 => 1\r
138         },\r
139         VIEW_EVENT_LOG() => {\r
140                 Chirpy::Account::USER_LEVEL_9 => 1\r
141         },\r
142         ADD_ACCOUNT() => {\r
143                 Chirpy::Account::USER_LEVEL_9 => 1\r
144         },\r
145         EDIT_ACCOUNT() => {\r
146                 Chirpy::Account::USER_LEVEL_9 => 1\r
147         },\r
148         REMOVE_ACCOUNT() => {\r
149                 Chirpy::Account::USER_LEVEL_9 => 1\r
150         },\r
151         CHECK_FOR_UPDATE() => {\r
152                 Chirpy::Account::USER_LEVEL_9 => 1\r
153         }\r
154 };\r
155 \r
156 use constant STATISTICS_UPDATE_INTERVAL => 60 * 60;\r
157 use constant UPDATE_CHECK_INTERVAL => 7 * 24 * 60 * 60;\r
158 \r
159 sub new {\r
160         my ($class, $parent, $params) = @_;\r
161         return bless {\r
162                 'parent' => $parent,\r
163                 'params' => $params\r
164         }, $class;\r
165 }\r
166 \r
167 sub run {\r
168         my $self = shift;\r
169         my $page = $self->get_current_page();\r
170         if ($page == START_PAGE) {\r
171                 $self->welcome_user(\r
172                         $self->parent()->get_latest_news_items()\r
173                 );\r
174         }\r
175         elsif ($page == QUOTE_BROWSER) {\r
176                 my $start = $self->get_first_quote_index();\r
177                 $self->_browse_quotes_segmented(\r
178                         $page,\r
179                         $start,\r
180                         $self->parent()->get_quotes($start)\r
181                 );\r
182         }\r
183         elsif ($page == QUOTES_OF_THE_WEEK) {\r
184                 my $start = $self->get_first_quote_index();\r
185                 $self->_browse_quotes_segmented(\r
186                         $page,\r
187                         $start,\r
188                         $self->parent()->get_quotes_of_the_week($start),\r
189                 );\r
190         }\r
191         elsif ($page == QUOTE_SEARCH) {\r
192                 my $start = $self->get_first_quote_index();\r
193                 my ($queries, $tags) = $self->get_search_instruction();\r
194                 if (@$queries || @$tags) {\r
195                         $self->_browse_quotes_segmented(\r
196                                 $page,\r
197                                 $start,\r
198                                 $self->parent()->get_matching_quotes($start, $queries, $tags)\r
199                         );\r
200                 }\r
201                 else {\r
202                         $self->provide_quote_search_interface();\r
203                 }\r
204         }\r
205         elsif ($page == SINGLE_QUOTE) {\r
206                 my $quote = $self->parent()->get_quote(\r
207                         $self->get_selected_quote_id());\r
208                 if (defined $quote && ($quote->is_approved() || $self->moderation_queue_is_public())) {\r
209                         $self->browse_quotes([ $quote ], $page);\r
210                 }\r
211                 else {\r
212                         $self->report_inexistent_quote();\r
213                 }\r
214         }\r
215         elsif ($page == RANDOM_QUOTES) {\r
216                 my $quotes = $self->parent()->get_random_quotes();\r
217                 (defined $quotes\r
218                         ? $self->browse_quotes($quotes, $page)\r
219                         : $self->report_no_quotes_to_display($page));\r
220         }\r
221         elsif ($page == TOP_QUOTES) {\r
222                 my $start = $self->get_first_quote_index();\r
223                 $self->_browse_quotes_segmented(\r
224                         $page,\r
225                         $start,\r
226                         $self->parent()->get_top_quotes($start),\r
227                 );\r
228         }\r
229         elsif ($page == BOTTOM_QUOTES) {\r
230                 my $start = $self->get_first_quote_index();\r
231                 $self->_browse_quotes_segmented(\r
232                         $page,\r
233                         $start,\r
234                         $self->parent()->get_bottom_quotes($start),\r
235                 );\r
236         }\r
237         elsif ($page == MODERATION_QUEUE) {\r
238                 if ($self->moderation_queue_is_public()) {\r
239                         my $start = $self->get_first_quote_index();\r
240                         $self->_browse_quotes_segmented(\r
241                                 $page,\r
242                                 $start,\r
243                                 $self->parent()->get_unapproved_quotes($start),\r
244                         );\r
245                 }\r
246                 else {\r
247                         $self->report_unknown_action();\r
248                 }\r
249         }\r
250         elsif ($page == SUBMIT_QUOTE) {\r
251                 my ($body, $notes, $tags) = $self->get_submitted_quote();\r
252                 if (defined $body && $body) {\r
253                         my $approved\r
254                                 = $self->administration_allowed(Chirpy::UI::MANAGE_UNAPPROVED_QUOTES);\r
255                         $body = Chirpy::Util::clean_up_submission($body);\r
256                         $notes = (defined $notes\r
257                                 ? Chirpy::Util::clean_up_submission($notes)\r
258                                 : undef);\r
259                         $tags = Chirpy::Util::parse_tags($tags);\r
260                         my $quote = $self->parent()->add_quote(\r
261                                 $body,\r
262                                 $notes,\r
263                                 $approved,\r
264                                 $tags\r
265                         );\r
266                         $self->confirm_quote_submission($approved);\r
267                         my $id = $quote->get_id();\r
268                         $self->_log_event(Chirpy::Event::ADD_QUOTE, {\r
269                                 'id' => $id,\r
270                                 'body' => $body,\r
271                                 (defined $notes && length($notes) ? ('notes' => $notes) : ()),\r
272                                 (@$tags ? ('tags' => join(' ', sort @$tags)) : ())\r
273                         });\r
274                         if ($approved) {\r
275                                 $self->_log_event(Chirpy::Event::APPROVE_QUOTE,\r
276                                         { 'id' => $id });\r
277                         }\r
278                 }\r
279                 else {\r
280                         $self->provide_quote_submission_interface();\r
281                 }\r
282         }\r
283         elsif ($page == QUOTE_RATING_UP || $page == QUOTE_RATING_DOWN) {\r
284                 my $up = ($page == QUOTE_RATING_UP);\r
285                 my $id = $self->get_selected_quote_id();\r
286                 my $quote = $self->parent()->get_quote($id);\r
287                 if (defined $quote\r
288                 && ($quote->is_approved() || $self->moderation_queue_is_public())) {\r
289                         my $last_rating = $self->_last_rating($id);\r
290                         if (($page == QUOTE_RATING_UP && $last_rating > 0)\r
291                         || ($page == QUOTE_RATING_DOWN && $last_rating < 0)) {\r
292                                 $self->report_quote_already_rated($id);\r
293                         }\r
294                         else {\r
295                                 my ($history, $full) = $self->_rating_history();\r
296                                 if (!$full) {\r
297                                         if ($self->quote_rating_confirmed()) {\r
298                                                 $self->_rate_quote($id, $up, abs($last_rating), $history);\r
299                                         }\r
300                                         else {\r
301                                                 $self->request_quote_rating_confirmation(\r
302                                                         $quote, $up, abs($last_rating));\r
303                                         }\r
304                                 }\r
305                                 else {\r
306                                         $self->report_quote_rating_limit_excess();\r
307                                 }\r
308                         }\r
309                 }\r
310                 else {\r
311                         $self->report_rated_quote_not_found();\r
312                 }\r
313         }\r
314         elsif ($page == REPORT_QUOTE) {\r
315                 my $id = $self->get_selected_quote_id();\r
316                 my $quote = $self->parent()->get_quote($id);\r
317                 if (defined $quote && $quote->is_approved()) {\r
318                         if ($self->quote_report_confirmed()) {\r
319                                 $self->parent()->flag_quotes($id);\r
320                                 $self->confirm_quote_report($id);\r
321                                 $self->_log_event(Chirpy::Event::REPORT_QUOTE, { 'id' => $id });\r
322                         }\r
323                         else {\r
324                                 $self->request_quote_report_confirmation($quote);\r
325                         }\r
326                 }\r
327                 else {\r
328                         $self->report_reported_quote_not_found();\r
329                 }\r
330         }\r
331         elsif ($page eq TAG_CLOUD) {\r
332                 my $tag_counts = $self->parent()->get_tag_use_counts();\r
333                 if (%$tag_counts) {\r
334                         $self->_provide_tag_cloud($tag_counts);\r
335                 }\r
336                 else {\r
337                         $self->report_no_tagged_quotes();\r
338                 }\r
339         }\r
340         elsif ($page eq STATISTICS) {\r
341                 $self->_provide_statistics();\r
342         }\r
343         elsif ($page == LOGIN) {\r
344                 if ($self->attempting_login()) {\r
345                         my ($username, $password)\r
346                                 = $self->get_supplied_username_and_password();\r
347                         my $account = $self->parent()\r
348                                 ->attempt_login($username, $password);\r
349                         if (defined $account) {\r
350                                 $self->set_logged_in_user($account->get_id());\r
351                                 $self->confirm_login();\r
352                                 $self->_log_event(Chirpy::Event::LOGIN_SUCCESS);\r
353                         }\r
354                         else {\r
355                                 $self->report_invalid_login();\r
356                                 $self->_log_event(Chirpy::Event::LOGIN_FAILURE,\r
357                                         { 'username' => $username });\r
358                         }\r
359                 }\r
360                 else {\r
361                         $self->provide_login_interface();\r
362                 }\r
363         }\r
364         elsif ($page == LOGOUT) {\r
365                 $self->set_logged_in_user(undef);\r
366                 $self->confirm_logout();\r
367         }\r
368         elsif ($page == ADMINISTRATION) {\r
369                 if (defined $self->get_logged_in_user_account()) {\r
370                         $self->_provide_administration_interface()\r
371                 }\r
372                 else {\r
373                         $self->provide_login_interface();\r
374                 }\r
375         }\r
376         else {\r
377                 $self->report_unknown_action();\r
378         }\r
379 }\r
380 \r
381 sub statistics_update_allowed {\r
382         return 1;\r
383 }\r
384 \r
385 sub _provide_administration_interface {\r
386         my $self = shift;\r
387         if ($self->configuration()->get('general', 'update_check')\r
388         && $self->administration_allowed(CHECK_FOR_UPDATE)) {\r
389                 $self->_maybe_check_for_update();\r
390         }\r
391         my $page = $self->get_current_administration_page() || 0;\r
392         if ($page == CHANGE_PASSWORD) {\r
393                 my $user = $self->get_logged_in_user_account();\r
394                 if ($self->attempting_password_change()) {\r
395                         my ($current_password, $new_password, $repeat_password)\r
396                                 = $self->get_supplied_passwords();\r
397                         if (!defined $self->parent()\r
398                                 ->attempt_login(\r
399                                         $user->get_username(), $current_password)) {\r
400                                                 $self->provide_password_change_interface(\r
401                                                         CURRENT_PASSWORD_INVALID);\r
402                         }\r
403                         elsif (!Chirpy::Util::valid_password($new_password)) {\r
404                                 $self->provide_password_change_interface(\r
405                                         NEW_PASSWORD_INVALID);\r
406                         }\r
407                         elsif ($new_password ne $repeat_password) {\r
408                                 $self->provide_password_change_interface(\r
409                                         PASSWORDS_DIFFER);\r
410                         }\r
411                         else {\r
412                                 $self->parent()->modify_account(\r
413                                         $user, undef, $new_password);\r
414                                 $self->confirm_password_change();\r
415                                 $self->_log_event(Chirpy::Event::CHANGE_PASSWORD);\r
416                         }\r
417                 }\r
418                 else {\r
419                         $self->provide_password_change_interface();\r
420                 }\r
421         }\r
422         elsif ($page == MANAGE_UNAPPROVED_QUOTES) {\r
423                 if (!$self->administration_allowed($page)) {\r
424                         $self->report_administration_user_level_insufficient($page);\r
425                 }\r
426                 else {\r
427                         my ($approve, $remove, $edited)\r
428                                 = $self->get_quote_approval_result();\r
429                         my @approve = (defined $approve && ref $approve eq 'ARRAY'\r
430                                 ? @$approve : ());\r
431                         my @remove = (defined $remove && ref $remove eq 'ARRAY'\r
432                                 ? @$remove : ());\r
433                         if (@approve) {\r
434                                 $self->parent()->approve_quotes(@approve);\r
435                                 foreach my $id (@approve) {\r
436                                         if (exists $edited->{$id}) {\r
437                                                 my $quote = $self->parent()->get_quote($id);\r
438                                                 if (defined $quote) {\r
439                                                         my $body = $edited->{$id}->{'body'};\r
440                                                         my $notes = $edited->{$id}->{'notes'};\r
441                                                         my $tags = $edited->{$id}->{'tags'};\r
442                                                         if ($body) {\r
443                                                                 $self->_modify_quote(\r
444                                                                         $quote, $body, $notes, $tags);\r
445                                                         }\r
446                                                 }\r
447                                         }\r
448                                         $self->_log_event(Chirpy::Event::APPROVE_QUOTE,\r
449                                                 { 'id' => $id });\r
450                                 }\r
451                         }\r
452                         if (@remove) {\r
453                                 $self->parent()->remove_quotes(@remove);\r
454                                 foreach my $id (@remove) {\r
455                                         $self->_log_event(Chirpy::Event::REMOVE_QUOTE,\r
456                                                 { 'id' => $id });\r
457                                 }\r
458                         }\r
459                         $self->provide_quote_approval_interface(\r
460                                 @approve ? \@approve : undef,\r
461                                 @remove ? \@remove : undef);\r
462                 }\r
463         }\r
464         elsif ($page == MANAGE_FLAGGED_QUOTES) {\r
465                 if (!$self->administration_allowed($page)) {\r
466                         $self->report_administration_user_level_insufficient($page);\r
467                 }\r
468                 else {\r
469                         my ($unflag, $remove) = $self->get_quote_flag_management_result();\r
470                         my @unflag = (defined $unflag && ref $unflag eq 'ARRAY'\r
471                                 ? @$unflag : ());\r
472                         my @remove = (defined $remove && ref $remove eq 'ARRAY'\r
473                                 ? @$remove : ());\r
474                         if (@unflag) {\r
475                                 $self->parent()->unflag_quotes(@unflag);\r
476                                 foreach my $id (@unflag) {\r
477                                         $self->_log_event(Chirpy::Event::UNFLAG_QUOTE,\r
478                                                 { 'id' => $id });\r
479                                 }\r
480                         }\r
481                         if (@remove) {\r
482                                 $self->parent()->remove_quotes(@remove);\r
483                                 foreach my $id (@remove) {\r
484                                         $self->_log_event(Chirpy::Event::REMOVE_QUOTE,\r
485                                                 { 'id' => $id });\r
486                                 }\r
487                         }\r
488                         $self->provide_quote_flag_management_interface(\r
489                                 @unflag ? \@unflag : undef,\r
490                                 @remove ? \@remove : undef);\r
491                 }\r
492         }\r
493         elsif ($page == EDIT_QUOTE) {\r
494                 if (!$self->administration_allowed($page)) {\r
495                         $self->report_administration_user_level_insufficient($page);\r
496                 }\r
497                 else {\r
498                         my $id = $self->get_quote_to_edit();\r
499                         if ($id) {\r
500                                 my $quote = $self->parent()->get_quote($id);\r
501                                 if (defined $quote && $quote->is_approved()) {\r
502                                         my ($body, $notes, $tags)\r
503                                                 = $self->get_modified_quote_information();\r
504                                         if ($body) {\r
505                                                 $self->_modify_quote($quote, $body, $notes, $tags);\r
506                                                 $self->confirm_quote_modification($quote);\r
507                                         }\r
508                                         else {\r
509                                                 $self->provide_quote_editing_interface($quote);\r
510                                         }\r
511                                 }\r
512                                 else {\r
513                                         $self->report_quote_to_edit_not_found();\r
514                                 }\r
515                         }\r
516                         else {\r
517                                 $self->provide_quote_selection_for_modification_interface();\r
518                         }\r
519                 }\r
520         }\r
521         elsif ($page == REMOVE_QUOTE) {\r
522                 if (!$self->administration_allowed($page)) {\r
523                         $self->report_administration_user_level_insufficient($page);\r
524                 }\r
525                 else {\r
526                         my $id = $self->get_quote_to_remove();\r
527                         if ($id) {\r
528                                 my $quote = $self->parent()->get_quote($id);\r
529                                 if (defined $quote && $quote->is_approved()) {\r
530                                         if ($self->quote_removal_confirmed()) {\r
531                                                 $self->parent()->remove_quotes($id);\r
532                                                 $self->confirm_quote_removal();\r
533                                                 $self->_log_event(Chirpy::Event::REMOVE_QUOTE,\r
534                                                         { 'id' => $id });\r
535                                         }\r
536                                         else {\r
537                                                 $self->request_quote_removal_confirmation($quote);\r
538                                         }\r
539                                 }\r
540                                 else {\r
541                                         $self->report_quote_to_remove_not_found();\r
542                                 }\r
543                         }\r
544                         else {\r
545                                 $self->provide_quote_selection_for_removal_interface();\r
546                         }\r
547                 }\r
548         }\r
549         elsif ($page == ADD_NEWS) {\r
550                 if (!$self->administration_allowed($page)) {\r
551                         $self->report_administration_user_level_insufficient($page);\r
552                 }\r
553                 else {\r
554                         if (my $news = $self->get_news_item_to_add()) {\r
555                                 my $news = Chirpy::Util::clean_up_submission($news);\r
556                                 my $item = $self->parent()->add_news_item(\r
557                                         $news,\r
558                                         $self->get_logged_in_user_account()\r
559                                 );\r
560                                 $self->confirm_news_submission($news);\r
561                                 $self->_log_event(Chirpy::Event::ADD_NEWS, {\r
562                                         'id' => $item->get_id(), 'body' => $news\r
563                                 });\r
564                         }\r
565                         else {\r
566                                 $self->provide_news_submission_interface();\r
567                         }\r
568                 }\r
569         }\r
570         elsif ($page == EDIT_NEWS) {\r
571                 if (!$self->administration_allowed($page)) {\r
572                         $self->report_administration_user_level_insufficient($page);\r
573                 }\r
574                 else {\r
575                         my $id = $self->get_news_item_to_edit();\r
576                         if ($id) {\r
577                                 my $item = $self->parent()->get_news_item($id);\r
578                                 if (defined $item) {\r
579                                         my ($text, $poster_id) = $self->get_modified_news_item();\r
580                                         if ($text) {\r
581                                                 my $old_body = $item->get_body();\r
582                                                 my $old_poster = $item->get_poster();\r
583                                                 $text = Chirpy::Util::clean_up_submission($text);\r
584                                                 $self->parent()->modify_news_item(\r
585                                                         $item,\r
586                                                         $text,\r
587                                                         $self->parent()->get_account_by_id($poster_id)\r
588                                                 );\r
589                                                 $self->confirm_news_item_modification();\r
590                                                 $self->_log_event(Chirpy::Event::EDIT_NEWS, {\r
591                                                         'id' => $id,\r
592                                                         ($old_body ne $text ? ('new_body' => $text) : ()),\r
593                                                         (!defined $old_poster\r
594                                                                 || $old_poster->get_id() != $poster_id\r
595                                                                 ? ('new_poster' => $poster_id) : ())\r
596                                                 });\r
597                                         }\r
598                                         else {\r
599                                                 $self->provide_news_item_editing_interface($item);\r
600                                         }\r
601                                 }\r
602                                 else {\r
603                                         $self->report_news_item_to_edit_not_found();\r
604                                 }\r
605                         }\r
606                         else {\r
607                                 $self->provide_news_item_selection_for_modification_interface();\r
608                         }\r
609                 }\r
610         }\r
611         elsif ($page == REMOVE_NEWS) {\r
612                 if (!$self->administration_allowed($page)) {\r
613                         $self->report_administration_user_level_insufficient($page);\r
614                 }\r
615                 else {\r
616                         my $id = $self->get_news_item_to_remove();\r
617                         if ($id) {\r
618                                 my $item = $self->parent()->get_news_item($id);\r
619                                 if (defined $item) {\r
620                                         $self->parent()->remove_news_items($id);\r
621                                         $self->confirm_news_item_removal();\r
622                                         $self->_log_event(Chirpy::Event::REMOVE_NEWS,\r
623                                                 { 'id' => $id });\r
624                                 }\r
625                                 else {\r
626                                         $self->report_news_item_to_remove_not_found();\r
627                                 }\r
628                         }\r
629                         else {\r
630                                 $self->provide_news_item_selection_for_removal_interface();\r
631                         }\r
632                 }\r
633         }\r
634         elsif ($page == ADD_ACCOUNT) {\r
635                 if (!$self->administration_allowed($page)) {\r
636                         $self->report_administration_user_level_insufficient($page);\r
637                 }\r
638                 else {\r
639                         my ($username, $password, $repeat_password, $level)\r
640                                 = $self->get_account_information_to_add();\r
641                         if (defined $username) {\r
642                                 if (!Chirpy::Util::valid_username($username)) {\r
643                                         $self->report_invalid_new_username();\r
644                                 }\r
645                                 elsif ($self->parent()->username_exists($username)) {\r
646                                         $self->report_new_username_exists();\r
647                                 }\r
648                                 elsif (!Chirpy::Util::valid_password($password)) {\r
649                                         $self->report_invalid_new_password();\r
650                                 }\r
651                                 elsif ($password ne $repeat_password) {\r
652                                         $self->report_different_new_passwords();\r
653                                 }\r
654                                 elsif ($level <= 0) {\r
655                                         $self->report_invalid_new_user_level();\r
656                                 }\r
657                                 else {\r
658                                         my $account = $self->parent()->add_account(\r
659                                                 $username, $password, $level);\r
660                                         $self->confirm_account_creation();\r
661                                         $self->_log_event(Chirpy::Event::ADD_ACCOUNT, {\r
662                                                 'id' => $account->get_id(),\r
663                                                 'username' => $username,\r
664                                                 'level' => $level\r
665                                         });\r
666                                 }\r
667                         }\r
668                         else {\r
669                                 $self->provide_account_creation_interface();\r
670                         }\r
671                 }\r
672         }\r
673         elsif ($page == EDIT_ACCOUNT) {\r
674                 if (!$self->administration_allowed($page)) {\r
675                         $self->report_administration_user_level_insufficient($page);\r
676                 }\r
677                 else {\r
678                         my $id = $self->get_account_to_modify();\r
679                         if (defined $id) {\r
680                                 my $account = $self->parent()->get_account_by_id($id);\r
681                                 if (defined $account) {\r
682                                         my ($username, $password, $repeat_password, $level)\r
683                                                 = $self->get_modified_account_information();\r
684                                         if (defined $username\r
685                                                 && !Chirpy::Util::valid_username($username)) {\r
686                                                         $self->report_invalid_modified_username();\r
687                                         }\r
688                                         elsif ($self->parent()->username_exists($username)) {\r
689                                                 $self->report_modified_username_exists();\r
690                                         }\r
691                                         elsif (defined $password\r
692                                                 && !Chirpy::Util::valid_password($password)) {\r
693                                                         $self->report_invalid_modified_password();\r
694                                         }\r
695                                         elsif (defined $password\r
696                                                 && $password ne $repeat_password) {\r
697                                                         $self->report_different_modified_passwords();\r
698                                         }\r
699                                         elsif (defined $level && $level <= 0) {\r
700                                                 $self->report_invalid_modified_user_level();\r
701                                         }\r
702                                         elsif (!defined $username && !defined $password\r
703                                                 && !defined $level) {\r
704                                                         $self->report_modified_account_information_required();\r
705                                         }\r
706                                         else {\r
707                                                 my $old_username = $account->get_username();\r
708                                                 my $old_password = $account->get_password();\r
709                                                 my $old_level = $account->get_level();\r
710                                                 $self->parent()->modify_account(\r
711                                                         $account, $username, $password, $level);\r
712                                                 $self->confirm_account_modification();\r
713                                                 $self->_log_event(Chirpy::Event::EDIT_ACCOUNT, {\r
714                                                         'id' => $id,\r
715                                                         (defined $username && $old_username ne $username\r
716                                                                 ? ('new_username' => $username) : ()),\r
717                                                         (defined $level && $old_level != $level\r
718                                                                 ? ('new_level' => $level) : ()),\r
719                                                         (defined $password\r
720                                                                 ? ('password_changed' => 1) : ())\r
721                                                 });\r
722                                         }\r
723                                 }\r
724                                 else {\r
725                                         $self->report_account_to_modify_not_found();\r
726                                 }\r
727                         }\r
728                         else {\r
729                                 $self->provide_account_selection_for_modification_interface();\r
730                         }\r
731                 }\r
732         }\r
733         elsif ($page == REMOVE_ACCOUNT) {\r
734                 if (!$self->administration_allowed($page)) {\r
735                         $self->report_administration_user_level_insufficient($page);\r
736                 }\r
737                 else {\r
738                         my $parent = $self->parent();\r
739                         if ($parent->account_count() > 1) {\r
740                                 my $id = $self->get_account_to_remove();\r
741                                 if (defined $id) {\r
742                                         my $account = $self->parent()->get_account_by_id($id);\r
743                                         my $level = Chirpy::Account::USER_LEVEL_9;\r
744                                         if ($account->get_level() == $level\r
745                                                 && $parent->account_count_by_level($level) <= 1) {\r
746                                                         $self->report_last_owner_account_removal_error();\r
747                                         }\r
748                                         elsif (defined $account) {\r
749                                                 if ($account->get_id()\r
750                                                         == $self->get_logged_in_user_account()->get_id()) {\r
751                                                                 $self->set_logged_in_user(undef);\r
752                                                 }\r
753                                                 my $username = $account->get_username();\r
754                                                 my $level = $account->get_level();\r
755                                                 $parent->remove_accounts($id);\r
756                                                 $self->confirm_account_removal();\r
757                                                 $self->_log_event(Chirpy::Event::REMOVE_ACCOUNT,\r
758                                                         { 'id' => $id });\r
759                                         }\r
760                                         else {\r
761                                                 $self->report_account_to_remove_not_found();\r
762                                         }\r
763                                 }\r
764                                 else {\r
765                                         $self->provide_account_selection_for_removal_interface();\r
766                                 }\r
767                         }\r
768                         else {\r
769                                 $self->report_last_owner_account_removal_error();\r
770                         }\r
771                 }\r
772         }\r
773         else {\r
774                 $self->welcome_administrator();\r
775         }\r
776 }\r
777 \r
778 sub _maybe_check_for_update {\r
779         my $self = shift;\r
780         my $last_check = $self->get_parameter('last_update_check');\r
781         my $now = time();\r
782         my @update_info;\r
783         my $update_check_error;\r
784         if (!defined $last_check\r
785         || $last_check + UPDATE_CHECK_INTERVAL < $now) {\r
786                 $self->set_parameter('last_update_check', $now);\r
787                 my $upd_status = $self->_check_for_update();\r
788                 if (defined $upd_status) {\r
789                         if (ref $upd_status eq 'ARRAY') {\r
790                                 $self->set_parameter('update_version', $upd_status->[0]);\r
791                                 $self->set_parameter('update_released', $upd_status->[1]);\r
792                                 $self->set_parameter('update_url', $upd_status->[2]);\r
793                                 $self->set_parameter('update_version_current',\r
794                                         $Chirpy::VERSION);\r
795                                 @update_info = @$upd_status; \r
796                         }\r
797                         else {\r
798                                 $update_check_error = $upd_status;\r
799                         }\r
800                 }\r
801         }\r
802         else {\r
803                 my $version = $self->get_parameter('update_version');\r
804                 if ($version) {\r
805                         my $version_at_check\r
806                                 = $self->get_parameter('update_version_current');\r
807                         if ($version_at_check == $Chirpy::VERSION) {\r
808                                 my $date = $self->get_parameter('update_released');\r
809                                 my $url = $self->get_parameter('update_url');\r
810                                 @update_info = ($version, $date, $url);\r
811                         }\r
812                         else {\r
813                                 $self->set_parameter('update_version', undef);\r
814                                 $self->set_parameter('update_released', undef);\r
815                                 $self->set_parameter('update_url', undef);\r
816                                 $self->set_parameter('update_version_current', undef);\r
817                         }\r
818                 }\r
819         }\r
820         if (defined $update_check_error) {\r
821                 $self->update_check_error($update_check_error);\r
822         }\r
823         elsif (@update_info) {\r
824                 $self->update_available(@update_info);\r
825         }\r
826 }\r
827 \r
828 sub _check_for_update {\r
829         my $self = shift;\r
830         my $uc = new Chirpy::UpdateChecker($self);\r
831         my $result = $uc->check_for_updates();\r
832         return undef unless ($result);\r
833         return $uc->get_error_message() if (ref $result ne 'ARRAY');\r
834         return $result;\r
835 }\r
836 \r
837 sub _browse_quotes_segmented {\r
838         my ($self, $page, $start, $quotes, $leading, $trailing) = @_;\r
839         if (defined $quotes) {\r
840                 my $per_page = $self->parent()->quotes_per_page();\r
841                 my ($previous, $next);\r
842                 if ($leading) {\r
843                         $previous = $start - $per_page;\r
844                         $previous = 0 if ($previous < 0);\r
845                 }\r
846                 if ($trailing) {\r
847                         $next = $start + $per_page;\r
848                 }\r
849                 $self->browse_quotes($quotes, $page, $previous, $next);\r
850         }\r
851         elsif ($page == QUOTE_SEARCH) {\r
852                 $self->report_no_search_results();\r
853         }\r
854         else {\r
855                 $self->report_no_quotes_to_display($page);\r
856         }\r
857 }\r
858 \r
859 sub _provide_tag_cloud {\r
860         my ($self, $tag_counts) = @_;\r
861         my $highest = 0;\r
862         my $lowest = undef;\r
863         foreach my $cnt (values %$tag_counts) {\r
864                 $lowest = $cnt if (!defined $lowest || $cnt < $lowest);\r
865                 $highest = $cnt if ($cnt > $highest);\r
866         }\r
867         my $difference = $highest - $lowest;\r
868         my @tag_info = ();\r
869         my $conf = $self->configuration();\r
870         my @tags = $conf->get('ui', 'randomize_tag_cloud')\r
871                 ? Chirpy::Util::shuffle_array(keys %$tag_counts)\r
872                 : sort keys %$tag_counts;\r
873         my @tag_info_list;\r
874         my $factor = $conf->get('ui', 'tag_cloud_percentage_delta') || 100;\r
875         my $logarithmic = ($difference > 1\r
876                 && $conf->get('ui', 'tag_cloud_logarithmic')); \r
877         $factor /= log($difference) if ($logarithmic);\r
878         foreach my $tag (@tags) {\r
879                 my $cnt = $tag_counts->{$tag};\r
880                 my $delta;\r
881                 if (!$difference || $cnt == $lowest) {\r
882                         $delta = 0;\r
883                 }\r
884                 elsif ($logarithmic) {\r
885                         $delta = $factor * log($cnt - $lowest);\r
886                 }\r
887                 else {\r
888                         $delta = $factor * ($cnt - $lowest) / $difference;\r
889                 }\r
890                 $tag = [\r
891                         $tag,\r
892                         $cnt,\r
893                         sprintf('%.0f', 100 + $delta)\r
894                 ];\r
895         }\r
896         $self->provide_tag_cloud(\@tags);\r
897 }\r
898 \r
899 sub _provide_statistics {\r
900         my $self = shift;\r
901         my $stats;\r
902         require Storable;\r
903         my $file = $self->_statistics_cache_file();\r
904         my $exists = (-e $file);\r
905         my $tag_file = $file . '.tag';\r
906         if ((!$exists || (\r
907         $self->statistics_update_allowed()\r
908         && (stat($file))[9] + STATISTICS_UPDATE_INTERVAL < time\r
909         )) && (!-e $tag_file\r
910         # If the update seems to be taking longer than the update interval itself,\r
911         # something probably went wrong. In this case, we just try again.\r
912         || (stat($tag_file))[9] + STATISTICS_UPDATE_INTERVAL < time)) {\r
913                 local *TAG;\r
914                 open(TAG, '>', $tag_file) and close(TAG);\r
915                 $stats = $self->_compute_statistics();\r
916                 if (defined $stats) {\r
917                         Storable::store($stats, $file);\r
918                 }\r
919                 unlink $tag_file;\r
920         }\r
921         elsif ($exists) {\r
922                 $stats = Storable::retrieve($file);\r
923         }\r
924         if (defined $stats) {\r
925                 $self->provide_statistics(@$stats);\r
926         }\r
927         else {\r
928                 $self->report_statistics_unavailable();\r
929         }\r
930 }\r
931 \r
932 sub _compute_statistics {\r
933         my $self = shift;\r
934         my $quotes = $self->parent()->get_quotes(0, 0, [ [ 'submitted', 0 ] ]);\r
935         return undef unless (@$quotes);\r
936         my $by_date = [];\r
937         my $by_hour = &_init_array(0, 24);\r
938         my $by_month = &_init_array(0, 12);\r
939         my $by_day = &_init_array(0, 31);\r
940         my $by_weekday = &_init_array(0, 7);\r
941         my $by_rating = {};\r
942         my $by_votes = {};\r
943         my $votes_by_rating = [ 0, 0 ];\r
944         my ($date, $weekday, $year, $month, $day, $prev_time);\r
945         foreach my $quote (@$quotes) {\r
946                 my $time = $quote->get_date_submitted();\r
947                 my $d = $self->format_date($time);\r
948                 my @time = $self->get_time($time);\r
949                 if (!defined($date) || $d ne $date) {\r
950                         if (defined $prev_time) {\r
951                                 $self->_pad_statistics($prev_time, $date, $d, $by_date);\r
952                         }\r
953                         $prev_time = $time;\r
954                         $date = $d;\r
955                         $day = $time[3] - 1;\r
956                         $month = $time[4];\r
957                         $year = $time[5];\r
958                         $weekday = $time[6];\r
959                 }\r
960                 &_add_statistic($date, 1, $by_date);\r
961                 $by_weekday->[$weekday]++;\r
962                 $by_day->[$day]++;\r
963                 $by_month->[$month]++;\r
964                 $by_hour->[$time[2]]++;\r
965                 my $rating = $quote->get_rating();\r
966                 my $votes = $quote->get_vote_count();\r
967                 $by_rating->{$rating}++;\r
968                 $by_votes->{$votes}++;\r
969                 my $votes_down = ($votes - $rating) / 2;\r
970                 my $votes_up = $votes - $votes_down;\r
971                 $votes_by_rating->[0] += $votes_up;\r
972                 $votes_by_rating->[1] += $votes_down;\r
973         }\r
974         return [\r
975                 $by_date, $by_hour, $by_weekday, $by_day, $by_month,\r
976                 &_to_sorted_array($by_rating, 1), &_to_sorted_array($by_votes, 0),\r
977                 $votes_by_rating\r
978         ];\r
979 }\r
980 \r
981 sub _to_sorted_array {\r
982         my ($hashref, $negative) = @_;\r
983         my @keys = keys %$hashref;\r
984         my $highest = shift @keys;\r
985         $highest = abs $highest if ($negative);\r
986         foreach my $k (@keys) {\r
987                 my $a = ($negative ? abs $k : $k);\r
988                 if ($a > $highest) {\r
989                         $highest = $a;\r
990                 }\r
991         }\r
992         my @res = ();\r
993         for (my $i = ($negative ? - $highest : 0); $i <= $highest; $i++) {\r
994                 push @res, [ $i, $hashref->{$i} || 0 ];\r
995         }\r
996         return \@res;\r
997 }\r
998 \r
999 sub _init_array {\r
1000         my ($value, $length) = @_;\r
1001         my @array = ();\r
1002         foreach (1..$length) {\r
1003                 push @array, $value;\r
1004         }\r
1005         return \@array;\r
1006 }\r
1007 \r
1008 sub _add_statistic {\r
1009         my ($name, $value, $aref) = @_;\r
1010         if (@$aref && $aref->[scalar(@$aref) - 1]->[0] eq $name) {\r
1011                 $aref->[scalar(@$aref) - 1]->[1] += $value;\r
1012         }\r
1013         else {\r
1014                 push @$aref, [$name, $value];\r
1015         }\r
1016 }\r
1017 \r
1018 sub _pad_statistics {\r
1019         my ($self, $from, $from_date, $to, $by_date) = @_;\r
1020         while (1) {\r
1021                 my ($next_date, $next_date_time)\r
1022                         = $self->_next_date($from, $from_date);\r
1023                 last if ($next_date eq $to);\r
1024                 &_add_statistic($next_date, 0, $by_date);\r
1025                 my @time = $self->get_time($next_date_time);\r
1026                 $from = $next_date_time;\r
1027                 $from_date = $next_date;\r
1028         }\r
1029 }\r
1030 \r
1031 sub _next_date {\r
1032         my ($self, $time, $date) = @_;\r
1033         while (1) {\r
1034                 $time += 23 * 60 * 60;\r
1035                 my $d = $self->format_date($time);\r
1036                 next if ($d eq $date);\r
1037                 return ($d, $time);\r
1038         }\r
1039 }\r
1040 \r
1041 sub _last_rating {\r
1042         my ($self, $id) = @_;\r
1043         my @list = $self->get_rated_quotes();\r
1044         foreach my $i (@list) {\r
1045                 next unless (abs($i) == $id);\r
1046                 return ($i < 0 ? -1 : 1);\r
1047         }\r
1048         return 0;\r
1049 }\r
1050 \r
1051 sub _rate_quote {\r
1052         my ($self, $id, $up, $revert, $history) = @_;\r
1053         my $parent = $self->parent();\r
1054         my ($new_rating, $new_vote_count);\r
1055         my @rated = $self->get_rated_quotes();\r
1056         if ($revert) {\r
1057                 ($new_rating, $new_vote_count) = ($up\r
1058                         ? $parent->increase_quote_rating($id, 1)\r
1059                         : $parent->decrease_quote_rating($id, 1));\r
1060                 @rated = grep { abs($_) != $id } @rated;\r
1061         }\r
1062         else {\r
1063                 ($new_rating, $new_vote_count) = ($up\r
1064                         ? $parent->increase_quote_rating($id)\r
1065                         : $parent->decrease_quote_rating($id));\r
1066                 $self->set_rating_history(@$history, time);\r
1067         }\r
1068         $self->set_rated_quotes(@rated, ($up ? $id : -$id));\r
1069         $self->confirm_quote_rating($id, $up, $new_rating, $new_vote_count);\r
1070         $self->_log_event(($up\r
1071                         ? Chirpy::Event::QUOTE_RATING_UP\r
1072                         : Chirpy::Event::QUOTE_RATING_DOWN),\r
1073                 { 'id' => $id, 'new_rating' => $new_rating,\r
1074                         ($revert ? ('revert' => 1) : ()) });\r
1075 }\r
1076 \r
1077 sub _rating_history {\r
1078         my $self = shift;\r
1079         my $conf = $self->parent()->configuration();\r
1080         my $max_time = $conf->get('general', 'rating_limit_time');\r
1081         my $max_size = $conf->get('general', 'rating_limit_count');\r
1082         return undef if ($max_time <= 0 || $max_size <= 0);\r
1083         my $time = time;\r
1084         my $since = $time - $max_time;\r
1085         my @history = $self->get_rating_history();\r
1086         shift @history\r
1087                 while (@history && $history[0] < $since);\r
1088         return (\@history, @history >= $max_size);\r
1089 }\r
1090 \r
1091 sub _modify_quote {\r
1092         my ($self, $quote, $body, $notes, $tags) = @_;\r
1093         my $id = $quote->get_id();\r
1094         $body = Chirpy::Util::clean_up_submission($body);\r
1095         $notes = Chirpy::Util::clean_up_submission($notes);\r
1096         $tags = Chirpy::Util::parse_tags($tags);\r
1097         my $old_body = $quote->get_body();\r
1098         my $old_notes = $quote->get_notes();\r
1099         my $old_tags = join('', sort @{$quote->get_tags()});\r
1100         $self->parent()->modify_quote($quote, $body, $notes, $tags);\r
1101         $tags = join(' ', sort @$tags);\r
1102         $self->_log_event(Chirpy::Event::EDIT_QUOTE, {\r
1103                 'id' => $id,\r
1104                 ($old_body ne $body ? ('new_body' => $body) : ()),\r
1105                 ($old_notes ne $notes ? ('new_notes' => $notes) : ()),\r
1106                 ($old_tags ne $tags ? ('new_tags' => $tags) : ())\r
1107         });\r
1108 }\r
1109 \r
1110 sub _log_event {\r
1111         my ($self, $code, $params) = @_;\r
1112         $params = {} unless (defined $params);\r
1113         my $info = $self->get_user_information();\r
1114         while (my ($k, $v) = each %$info) {\r
1115                 $params->{'user:' . $k} = $v;\r
1116         }\r
1117         $self->parent()->log_event(\r
1118                 $code,\r
1119                 $self->get_logged_in_user_account(),\r
1120                 $params\r
1121         );\r
1122 }\r
1123 \r
1124 sub _statistics_cache_file {\r
1125         my $self = shift;\r
1126         return $self->configuration()->get('general', 'base_path')\r
1127                 . '/cache/statistics';\r
1128 }\r
1129 \r
1130 sub moderation_queue_is_public {\r
1131         my $self = shift;\r
1132         return $self->configuration()->get('ui', 'moderation_queue_public');\r
1133 }\r
1134 \r
1135 sub get_news_posters {\r
1136         my $self = shift;\r
1137         return $self->parent()->get_accounts_by_level(\r
1138                 keys %{ADMIN_PERMISSIONS->{ADD_NEWS()}});\r
1139 }\r
1140 \r
1141 sub get_logged_in_user_account {\r
1142         my $self = shift;\r
1143         my $id = $self->get_logged_in_user();\r
1144         return (defined $id\r
1145                 ? $self->parent()->get_account_by_id($id)\r
1146                 : undef);\r
1147 }\r
1148 \r
1149 sub administration_allowed {\r
1150         my ($self, $action) = @_;\r
1151         return 0 unless exists ADMIN_PERMISSIONS->{$action};\r
1152         my $user = $self->get_logged_in_user_account();\r
1153         return 0 unless defined $user;\r
1154         my $level = $user->get_level();\r
1155         return ADMIN_PERMISSIONS->{$action}{$level};\r
1156 }\r
1157 \r
1158 sub get_parameter {\r
1159         my ($self, $name) = @_;\r
1160         return $self->parent()->get_parameter($name);\r
1161 }\r
1162 \r
1163 sub set_parameter {\r
1164         my ($self, $name, $value) = @_;\r
1165         $self->parent()->set_parameter($name, $value);\r
1166 }\r
1167 \r
1168 sub format_date_time {\r
1169         my ($self, $timestamp) = @_;\r
1170         return Chirpy::Util::format_date_time($timestamp,\r
1171                 $self->configuration()->get('ui', 'date_time_format'),\r
1172                 $self->configuration()->get('ui', 'use_gmt'));\r
1173 }\r
1174 \r
1175 sub format_date {\r
1176         my ($self, $timestamp) = @_;\r
1177         return Chirpy::Util::format_date_time($timestamp,\r
1178                 $self->configuration()->get('ui', 'date_format'),\r
1179                 $self->configuration()->get('ui', 'use_gmt'));\r
1180 }\r
1181 \r
1182 sub format_time {\r
1183         my ($self, $timestamp) = @_;\r
1184         return Chirpy::Util::format_date_time($timestamp,\r
1185                 $self->configuration()->get('ui', 'time_format'),\r
1186                 $self->configuration()->get('ui', 'use_gmt'));\r
1187 }\r
1188 \r
1189 sub format_month {\r
1190         my ($self, $month_id) = @_;\r
1191         my @months = qw(january february march april may june\r
1192                 july august september october november december);\r
1193         my $l = $self->locale();\r
1194         my $month = $month_id % 100;\r
1195         my $year = $month_id - $month;\r
1196         my $m = $months[$month];\r
1197         my $suffix = ($year ? ' ' . (1900 + $year / 100) : '');\r
1198         my $long = $l->get_string($m) . $suffix;\r
1199         my $short = $l->get_string($m . '_short') . $suffix;\r
1200         return ($short, $long);\r
1201 }\r
1202 \r
1203 sub get_time {\r
1204         my ($self, $timestamp) = @_;\r
1205         return ($self->configuration()->get('ui', 'use_gmt')\r
1206                 ? gmtime($timestamp) : localtime($timestamp));\r
1207 }\r
1208 \r
1209 sub locale {\r
1210         my $self = shift;\r
1211         return $self->parent()->locale();\r
1212 }\r
1213 \r
1214 sub configuration {\r
1215         my $self = shift;\r
1216         return $self->parent()->configuration();\r
1217 }\r
1218 \r
1219 sub parent {\r
1220         my $self = shift;\r
1221         return $self->{'parent'};\r
1222 }\r
1223 \r
1224 sub param {\r
1225         my ($self, $name) = @_;\r
1226         return defined $self->{'params'} ? $self->{'params'}{$name} : undef;\r
1227 }\r
1228 \r
1229 *get_target_version = \&Chirpy::Util::abstract_method;\r
1230 \r
1231 *get_current_page = \&Chirpy::Util::abstract_method;\r
1232 \r
1233 *get_selected_quote_id = \&Chirpy::Util::abstract_method;\r
1234 \r
1235 *get_first_quote_index = \&Chirpy::Util::abstract_method;\r
1236 \r
1237 *get_search_instruction = \&Chirpy::Util::abstract_method;\r
1238 \r
1239 *get_submitted_quote = \&Chirpy::Util::abstract_method;\r
1240 \r
1241 *attempting_login = \&Chirpy::Util::abstract_method;\r
1242 \r
1243 *get_supplied_username_and_password = \&Chirpy::Util::abstract_method;\r
1244 \r
1245 *get_rating_history = \&Chirpy::Util::abstract_method;\r
1246 \r
1247 *set_rating_history = \&Chirpy::Util::abstract_method;\r
1248 \r
1249 *get_rated_quotes = \&Chirpy::Util::abstract_method;\r
1250 \r
1251 *set_rated_quotes = \&Chirpy::Util::abstract_method;\r
1252 \r
1253 *get_logged_in_user = \&Chirpy::Util::abstract_method;\r
1254 \r
1255 *set_logged_in_user = \&Chirpy::Util::abstract_method;\r
1256 \r
1257 *report_no_quotes_to_display = \&Chirpy::Util::abstract_method;\r
1258 \r
1259 *report_unknown_action = \&Chirpy::Util::abstract_method;\r
1260 \r
1261 *welcome_user = \&Chirpy::Util::abstract_method;\r
1262 \r
1263 *browse_quotes = \&Chirpy::Util::abstract_method;\r
1264 \r
1265 *provide_quote_search_interface = \&Chirpy::Util::abstract_method;\r
1266 \r
1267 *provide_tag_cloud = \&Chirpy::Util::abstract_method;\r
1268 \r
1269 *report_no_tagged_quotes = \&Chirpy::Util::abstract_method;\r
1270 \r
1271 *provide_statistics = \&Chirpy::Util::abstract_method;\r
1272 \r
1273 *report_statistics_unavailable = \&Chirpy::Util::abstract_method;\r
1274 \r
1275 *report_no_search_results = \&Chirpy::Util::abstract_method;\r
1276 \r
1277 *report_inexistent_quote = \&Chirpy::Util::abstract_method;\r
1278 \r
1279 *provide_quote_submission_interface = \&Chirpy::Util::abstract_method;\r
1280 \r
1281 *confirm_quote_submission = \&Chirpy::Util::abstract_method;\r
1282 \r
1283 *confirm_quote_rating = \&Chirpy::Util::abstract_method;\r
1284 \r
1285 *quote_rating_confirmed = \&Chirpy::Util::abstract_method;\r
1286 \r
1287 *request_quote_rating_confirmation = \&Chirpy::Util::abstract_method;\r
1288 \r
1289 *report_rated_quote_not_found = \&Chirpy::Util::abstract_method;\r
1290 \r
1291 *report_quote_already_rated = \&Chirpy::Util::abstract_method;\r
1292 \r
1293 *report_quote_rating_limit_excess = \&Chirpy::Util::abstract_method;\r
1294 \r
1295 *confirm_quote_report = \&Chirpy::Util::abstract_method;\r
1296 \r
1297 *quote_report_confirmed = \&Chirpy::Util::abstract_method;\r
1298 \r
1299 *request_quote_report_confirmation = \&Chirpy::Util::abstract_method;\r
1300 \r
1301 *report_reported_quote_not_found = \&Chirpy::Util::abstract_method;\r
1302 \r
1303 *provide_login_interface = \&Chirpy::Util::abstract_method;\r
1304 \r
1305 *report_invalid_login = \&Chirpy::Util::abstract_method;\r
1306 \r
1307 *attempting_password_change = \&Chirpy::Util::abstract_method;\r
1308 \r
1309 *get_supplied_passwords = \&Chirpy::Util::abstract_method;\r
1310 \r
1311 *provide_password_change_interface = \&Chirpy::Util::abstract_method;\r
1312 \r
1313 *confirm_password_change = \&Chirpy::Util::abstract_method;\r
1314 \r
1315 *confirm_login = \&Chirpy::Util::abstract_method;\r
1316 \r
1317 *confirm_logout = \&Chirpy::Util::abstract_method;\r
1318 \r
1319 *get_current_administration_page = \&Chirpy::Util::abstract_method;\r
1320 \r
1321 *welcome_administrator = \&Chirpy::Util::abstract_method;\r
1322 \r
1323 *get_quote_to_remove = \&Chirpy::Util::abstract_method;\r
1324 \r
1325 *confirm_quote_removal = \&Chirpy::Util::abstract_method;\r
1326 \r
1327 *quote_removal_confirmed = \&Chirpy::Util::abstract_method;\r
1328 \r
1329 *request_quote_removal_confirmation = \&Chirpy::Util::abstract_method;\r
1330 \r
1331 *report_quote_to_remove_not_found = \&Chirpy::Util::abstract_method;\r
1332 \r
1333 *provide_quote_selection_for_removal_interface\r
1334         = \&Chirpy::Util::abstract_method;\r
1335 \r
1336 *get_quote_to_edit = \&Chirpy::Util::abstract_method;\r
1337 \r
1338 *get_modified_quote_information = \&Chirpy::Util::abstract_method;\r
1339 \r
1340 *confirm_quote_modification = \&Chirpy::Util::abstract_method;\r
1341 \r
1342 *provide_quote_editing_interface = \&Chirpy::Util::abstract_method;\r
1343 \r
1344 *report_quote_to_edit_not_found = \&Chirpy::Util::abstract_method;\r
1345 \r
1346 *provide_quote_selection_for_modification_interface\r
1347         = \&Chirpy::Util::abstract_method;\r
1348 \r
1349 *provide_quote_approval_interface = \&Chirpy::Util::abstract_method;\r
1350 \r
1351 *get_quote_approval_result = \&Chirpy::Util::abstract_method;\r
1352 \r
1353 *provide_quote_flag_management_interface = \&Chirpy::Util::abstract_method;\r
1354 \r
1355 *get_quote_flag_management_result = \&Chirpy::Util::abstract_method;\r
1356 \r
1357 *get_news_item_to_add = \&Chirpy::Util::abstract_method;\r
1358 \r
1359 *confirm_news_submission = \&Chirpy::Util::abstract_method;\r
1360 \r
1361 *provide_news_submission_interface = \&Chirpy::Util::abstract_method;\r
1362 \r
1363 *get_news_item_to_edit = \&Chirpy::Util::abstract_method;\r
1364 \r
1365 *get_modified_news_item = \&Chirpy::Util::abstract_method;\r
1366 \r
1367 *confirm_news_item_modification = \&Chirpy::Util::abstract_method;\r
1368 \r
1369 *report_news_item_to_edit_not_found = \&Chirpy::Util::abstract_method;\r
1370 \r
1371 *provide_news_item_editing_interface = \&Chirpy::Util::abstract_method;\r
1372 \r
1373 *provide_news_item_selection_for_modification_interface\r
1374         = \&Chirpy::Util::abstract_method;\r
1375 \r
1376 *get_news_item_to_remove = \&Chirpy::Util::abstract_method;\r
1377 \r
1378 *confirm_news_item_removal = \&Chirpy::Util::abstract_method;\r
1379 \r
1380 *report_news_item_to_remove_not_found = \&Chirpy::Util::abstract_method;\r
1381 \r
1382 *provide_quote_selection_for_removal_interface\r
1383         = \&Chirpy::Util::abstract_method;\r
1384 \r
1385 *get_account_information_to_add = \&Chirpy::Util::abstract_method;\r
1386 \r
1387 *report_invalid_new_username = \&Chirpy::Util::abstract_method;\r
1388 \r
1389 *report_new_username_exists = \&Chirpy::Util::abstract_method;\r
1390 \r
1391 *report_invalid_new_password = \&Chirpy::Util::abstract_method;\r
1392 \r
1393 *report_different_new_passwords = \&Chirpy::Util::abstract_method;\r
1394 \r
1395 *report_invalid_new_user_level = \&Chirpy::Util::abstract_method;\r
1396 \r
1397 *confirm_account_creation = \&Chirpy::Util::abstract_method;\r
1398 \r
1399 *provide_account_creation_interface = \&Chirpy::Util::abstract_method;\r
1400 \r
1401 *get_account_to_modify = \&Chirpy::Util::abstract_method;\r
1402 \r
1403 *get_modified_account_information = \&Chirpy::Util::abstract_method;\r
1404 \r
1405 *report_invalid_modified_username = \&Chirpy::Util::abstract_method;\r
1406 \r
1407 *report_modified_username_exists = \&Chirpy::Util::abstract_method;\r
1408 \r
1409 *report_invalid_modified_password = \&Chirpy::Util::abstract_method;\r
1410 \r
1411 *report_different_modified_passwords = \&Chirpy::Util::abstract_method;\r
1412 \r
1413 *report_invalid_modified_user_level = \&Chirpy::Util::abstract_method;\r
1414 \r
1415 *confirm_account_modification = \&Chirpy::Util::abstract_method;\r
1416 \r
1417 *report_account_to_modify_not_found = \&Chirpy::Util::abstract_method;\r
1418 \r
1419 *report_modified_account_information_required\r
1420         = \&Chirpy::Util::abstract_method;\r
1421 \r
1422 *provide_account_selection_for_modification_interface\r
1423         = \&Chirpy::Util::abstract_method;\r
1424 \r
1425 *get_account_to_remove = \&Chirpy::Util::abstract_method;\r
1426 \r
1427 *confirm_account_removal = \&Chirpy::Util::abstract_method;\r
1428 \r
1429 *report_account_to_remove_not_found = \&Chirpy::Util::abstract_method;\r
1430 \r
1431 *provide_account_selection_for_removal_interface\r
1432         = \&Chirpy::Util::abstract_method;\r
1433 \r
1434 *report_last_owner_account_removal_error = \&Chirpy::Util::abstract_method;\r
1435 \r
1436 *get_user_information = \&Chirpy::Util::abstract_method;\r
1437 \r
1438 *update_available = \&Chirpy::Util::abstract_method;\r
1439 \r
1440 *update_check_error = \&Chirpy::Util::abstract_method;\r
1441 \r
1442 1;\r
1443 \r
1444 ###############################################################################