diff --git a/pub/qdb/.htaccess b/pub/qdb/.htaccess new file mode 100644 index 0000000..122045a --- /dev/null +++ b/pub/qdb/.htaccess @@ -0,0 +1,22 @@ + +RewriteEngine On +RewriteRule ^([0-9]+|0x[0-9A-Fa-f]+|0b[01]+)$ index.cgi?id=$1 [L] +RewriteRule ^(rss|atom)$ %{REQUEST_URI}/qotw [L,R=301] +RewriteRule ^(rss|atom)/([a-z_]+)$ index.cgi?action=$2&output=$1 [L] +RewriteRule ^ms/([a-z_]+)$ index.cgi?action=$1&output=ms [L] +RewriteRule ^search/(.+) index.cgi?action=search&query=$1 [L] +RewriteRule ^tags/([^\ ]+)$ index.cgi?action=search&query=tag:$1 [L] +RewriteRule ^([a-z_]+)(/([a-z_]*))?$ index.cgi?action=$1&admin_action=$3 [L,QSA] + + + + +order deny,allow +deny from all + + + +AuthType WebAuth +require valid-user + +AddHandler cgi-script .cgi diff --git a/pub/qdb/.install.txt.swp b/pub/qdb/.install.txt.swp new file mode 100644 index 0000000..f2b0fab Binary files /dev/null and b/pub/qdb/.install.txt.swp differ diff --git a/pub/qdb/changelog.txt b/pub/qdb/changelog.txt new file mode 100644 index 0000000..c5e0afc --- /dev/null +++ b/pub/qdb/changelog.txt @@ -0,0 +1,111 @@ + _____________________________________________________________________________ +/ \ +| Chirpy! v0.3 2007-02-09 | +\_____________________________________________________________________________/ + +* Added statistics page +* Users can now revert their vote if, for example, they accidentally clicked + on the wrong link +* Now keeping (and displaying) vote counts per quote +* Radically changed order of top and bottom quotes, by introducing quote + scores: score = (positive votes + 1) / (negative votes + 1), as suggested by + sp3000 at irc.mozilla.org #bs on 2006-10-28 +* Added metadata-based logging, including search +* Made rating and reporting links use POST instead of GET and added + intermediate confirmation page for non-AJAX users +* Added microsummaries +* You can now edit quotes before approving them +* Added wildcard searching +* Made tag cloud ignore unapproved quotes +* Tag cloud can now use logarithmic calculation, to make tag use distribution + appear more even +* Added dynamic tag cloud pruning based on number of quotes +* Administrative interface now only displays tabs that are available +* Improved and simplified captcha support; added preliminary support for + GD::SecurityImage as an alternative for Authen::Captcha; to enable captchas, + simply webapp.captcha_provider=Authen_Captcha should suffice +* Made pages point to their own feeds instead of QotW (where applicable) +* Added description field in chirpy.ini, for use in feeds +* Made quote titles in feeds more meaningful +* Put page title in feed title instead of subtitle +* Improved feed modification detection +* Made templates use HTML::Template's caching feature, which is pretty fast +* Template parameters are now global, which means you can include parameters + from outside a loop +* Renamed template parameters for search form; they are now always available + and the default theme includes a search form with search results +* Optimized template parsing a little +* Moved administration-related subs to a separate class, speeding up non-admin + pages somewhat +* Extended quick style linking method: <> is now omitted and link text can be + changed by separating it from URL by whitespace, e.g. + Surf to ! +* Simplified RSS feed: using HTML in and removing + and , making feedvalidator.org like it more +* Added tags as categories in feeds +* Made check for expired sessions a lot faster +* News body on start page is now divided into paragraphs; opening and closing + tag are included, so update your templates +* Bugfix: live rating no longer throws a JavaScript error in IE +* Bugfix: quote rating up log entry now includes quote ID (#1493589) +* Just for fun, quote IDs can now be in binary and hex notation too, as per + + + _____________________________________________________________________________ +/ \ +| Chirpy! v0.2 2006-05-02 | +\_____________________________________________________________________________/ + +* Fixed SQL injection vulnerabilities +* Fixed logging of author when editing or removing news items (#1289047) +* Added on-the-fly gzip compression to Chirpy::UI::WebApp--webapp.enable_gzip=1 + in your configuration file enables it +* Added optional captcha image to Chirpy::UI::WebApp's quote submission page +* Added quote tagging +* Made search query Google-style and added tag: prefix for searching for quotes + with a certain tag +* Made top and bottom quotes browsable +* Added periodic update check (site owners only) +* Made ui.quotes_per_page apply to random, top and bottom quotes instead of + individual setting per page type +* Added webapp.quotes_per_feed to set maximum number of quotes in feeds + individually +* Made Atom 1.0 feed valid by adding feed ID and webmaster name. Webmaster name + must be configured as webapp.webmaster_name +* Added quote_count method to Chirpy::DataManager and APPROVED_QUOTE_COUNT, + UNAPPROVED_QUOTE_COUNT, and TOTAL_QUOTE_COUNT to templates +* Added mass quote approval and unflagging to Chirpy::UI::WebApp's + administration section +* Changed Chirpy::DataManager's API so add_* methods set IDs +* Added option to automatically turn URLs and e-mail addresses in quotes into + hyperlinks +* Made Chirpy::UI::WebApp escape all e-mail addresses to prevent spam +* Made Chirpy::UI::WebApp replace sequences of whitespaces with   instead + of  , so the Atom feed remains valid +* Made quote reporting require session information to prevent false positives + from crawlers +* Fixed sub account_count in Chirpy::DataManager::MySQL; removing accounts now + works again +* Optimized fetching single quote in Chirpy::DataManager::MySQL +* Made Chirpy::UI::WebApp::Session automatically remove expired sessions every + 24 hours; util/remove_expired_sessions.pl is now obsolete +* Fixed US English (and Dutch) locale: quote_submission_thanks_administrator is + now quote_submission_thanks_no_approval +* Extended feed templates with a couple of variables and added rating and + report URLs as well as notes to the default templates +* Replaced feed templates' CSS with legacy HTML +* Cosmetic fixes to Chirpy::UI::WebApp's live rating system +* Mentioned Chirpy::UI::WebApp::Session::DataManager in Chirpy::DataManager's + documentation +* Cosmetic fix in Account Manager: space after New Account +* Replaced table for vertical split on start page with divs +* No longer overriding old onunload function in style switcher +* Added some debugging features +* Added changelog.txt + + _____________________________________________________________________________ +/ \ +| Chirpy! v0.1 2005-09-12 | +\_____________________________________________________________________________/ + +* First official release \ No newline at end of file diff --git a/pub/qdb/chirpy.ini b/pub/qdb/chirpy.ini new file mode 100644 index 0000000..1aedd56 --- /dev/null +++ b/pub/qdb/chirpy.ini @@ -0,0 +1,42 @@ +[general] +title=CSC Quote Database +description=Quotes from the CSC +base_path=./src +locale=en-US +rating_limit_count=60 +rating_limit_time=60 +update_check=1 + +[data] +type=MySQL +mysql.hostname=localhost +mysql.port=3306 +mysql.username=mimcpher +mysql.password=oq5VuqPtNfLTFRZozDsH +mysql.database=mimcpher +mysql.prefix=qdb_ + +[ui] +type=WebApp +date_time_format=%Y-%m-%d %H:%M GMT +date_format=%Y-%m-%d +time_format=%H:%M GMT +use_gmt=1 +quotes_per_page=30 +recent_news_item=3 +moderation_queue_public=1 +tac_cloud_logarithmic=1 +webapp.webmaster_name=Calum T. Dalek +webapp.webmaster_email=calum@csclub.uwaterloo.ca +webapp.site_url=https://csclub.uwaterloo.ca/~j3parker/pub/qdb/ +webapp.resources_url=https://csclub.uwaterloo.ca/~j3parker/pub/qdb/res +webapp.theme=default +webapp.welcome_text_file=welcome.html +webapp.cookie_domain=csclub.uwaterloo.ca +webapp.cookie_path=/~j3parker/pub/qdb +webapp.session_expiry=+3d +webapp.enable_short_urls=0 +webapp.enable_feeds=1 +webapp.quotes_per_feed=50 +webapp.enable_gzip=0 +webapp.enable_autolink=1 diff --git a/pub/qdb/index.cgi b/pub/qdb/index.cgi new file mode 100755 index 0000000..fc6b03d --- /dev/null +++ b/pub/qdb/index.cgi @@ -0,0 +1,60 @@ +#!/usr/bin/perl + +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# index.cgi # +# Initialization script # +############################################################################### +# $Id:: index.cgi 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +use strict; +use warnings; +use CGI::Carp qw(fatalsToBrowser set_message); + +BEGIN { + unshift @INC, 'src/modules'; + set_message(sub { + my $msg = shift; + print '', $/, + '', $/, + '', $/, + '', $/, + 'An Error Occurred', $/, + '', $/, + '', $/, + '

An Error Occurred

', $/, + '
', $msg, '
', $/, + '

Powered by Chirpy!

', $/, + '', $/, + ''; + exit; + }); +} + +use Chirpy 0.3; + +chirpy('./chirpy.ini'); + +############################################################################### diff --git a/pub/qdb/install.txt b/pub/qdb/install.txt new file mode 100644 index 0000000..99d7736 --- /dev/null +++ b/pub/qdb/install.txt @@ -0,0 +1,533 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# INSTALLATION INSTRUCTIONS # +############################################################################### + + +CONTENTS +======== + +1. INTRODUCTION + +2. REQUIREMENTS + +3. INSTALLATION +3.1. Copy files +3.2. Configure +3.2.1. The [general] Section +3.2.2. The [data] Section +3.2.3. The [ui] Section +3.3. Setup +3.4. Import + +4. UPGRADING + +5. RUN CHIRPY! + +6. NEXT STEPS +6.1. Your Own Theme +6.2. Your Own Locale +6.3. Your Own Data Manager +6.4. Your Own User Interface + + +1. INTRODUCTION +=============== + +Thank you for trying this Chirpy! beta version. Note that, while every feature +contained in this product is functional unless stated otherwise, you may very +well run into problems here and there. However, in most cases, there's probably +an easy fix. You may find the following URLs helpful: + +* Trackers (Bugs, Support, Feature Requests) + http://sourceforge.net/tracker/?group_id=147270 + +* Chirpy! web site + http://chirpy.sourceforge.net/ + +* API Specification (mainly for developers) + http://chirpy.sourceforge.net/pod/ + + +2. REQUIREMENTS +=============== + +To install Chirpy!, you'll need web space that gives you access to Perl 5.8 and +a SQL server running MySQL 4.1 or higher. Those version numbers are expected +to drop a little in an upcoming version. + +In addition, you'll need these Perl modules: + +Carp Data::Dumper HTML::Template +Digest::MD5 DBD::mysql HTTP::Date +Encode DBI URI::Escape +POSIX Storable + +It seems like a long list, but most of them are common and come with standard +distributions of Perl. Other modules will need to be installed separately; you +should probably ask your host about that. If any of these modules are missing, +Chirpy! will not work. + +In addition, Chirpy! uses the CGI::Carp module for verbose error reporting, +which makes problems easier to trace. While this module is also really common, +you can drop the dependency by removing statements containing "CGI::Carp" or +"set_message" from the scripts. + + +3. INSTALLATION +=============== + +If all the requirements are met, you are ready to begin the installation of +Chirpy!. Let's go! + +3.1. Copy files +--------------- + +Since you're reading this file, we'll assume that you've already extracted the +entire installation package. + +First, we'll take a quick look at the contents of the package. You should have +the following directories and files in the root: + +res/ The resources directory. Resources are public files that + are required by themes, such as images and style sheets. +src/ The source directory. Contains all the necessary files to + run Chirpy!, including its modules, locales, templates and + configuration file. +util/ Contains some utilities that you may find useful while + installing or using Chirpy!. +.htaccess Has some directives that enable short URLs. Covered later + in this document. +changelog.txt All the stuff that's changed between Chirpy! releases. +index.cgi The main script. Will be the only URL that is accessed + directly by visitors. +install.txt This document. +license.txt The GNU General Public License, under which Chirpy! is + distributed. + +Now, we're going the copy the files to a public path on the web server. This is +usually done using an FTP client. + +As for the files in the package root directory, the only one you really need is +index.cgi. Since this is a Perl script, it has to be inside a path where the +server allows execution of scripts. Most servers these days will allow that +anywhere, but yours might require that you put it inside a cgi-bin/ directory, +which may even be outside the document root on the FTP server. This is no +problem--just make sure it wants to execute it and not display the contents. + +The index.cgi file needs some attributes. This is done by issueing a SITE CHMOD +command on the FTP server. If you use a graphical FTP client, you can probably +right-click on the file and tick off the necessary attributes. They are: + + Owner READ WRITE EXECUTE + Group READ - EXECUTE + Others READ - EXECUTE + +This translates to the string representation "rwxr-xr-x" or, numerically, 755 +on UNIX systems. On most systems, this step is essential. You will get an error +page if you fail to change the file's attributes. Also, if you are using an FTP +client, make sure you have uploaded index.cgi in ASCII (text) mode, not image +(binary). + +You may want to copy the .htaccess file from the package root as well. Make +sure you don't confuse it with the one in the src/ directory! The one covered +here allows you to use short URLs, which are easy to remember, using the +mod_rewrite Apache module. To use that option, you will need to run Chirpy! on +an Apache web server that has the mod_rewrite module. Note that, while short +URLs will not work if mod_rewrite is not enabled, it should be safe to have the +.htaccess file there anyway, since it checks for the module first. Moreover, it +also attempts to block access to the configuration file, so you might want to +have it anyway; we will cover that configuration file in a moment. + +The res/ directory only holds static files and should reside in a public path +on the server, like . You can upload it +anywhere, even on a different server. We'll need the URL later for the +configuration. + +Next up: the src/ directory. This one does not have to be publicly accessible +and for security reasons, it really shouldn't be either. Chirpy! takes some +precautions to disallow access to it by placing a .htaccess file in it, that +holds code specific to the Apache web server. However, placing the directory +outside the document root (if possible) is a MUCH safer choice. +Alternatively, you can keep chirpy.ini, the configuration file, which we will +cover later in this document, outside the document root and the src/ directory. + +Inside the src/ directory, you will find a directory called cache/. Make sure +it is empty. Also, it needs to be world-writable, i.e. its attributes must be +set to "rwxrwxrwx" or 777. + +The util/ directory should NOT be uploaded. You are however likely to need the +setup.pl script inside it, but we'll get to that in a moment. + +That covers uploading the files--almost. We'll need to upload a configuration +file at the end of the next step. + +3.2. Configure +-------------- + +Chirpy! stores its configuration in a standard .INI file, a basic format which +is common on Windows systems. In this step, we'll create such a configuration +file. + +By default, Chirpy! looks for chirpy.ini in the working directory (which is +where you put index.cgi) and inside the src/ directory if it is directly inside +the working directory. Otherwise, you will have to edit index.cgi. As explained +above, that little bit of editing is recommended, so your configuration file +resides outside the public domain. After all, it will contain your MySQL server +password, and we don't want visitors to read that, now, do we? + +Right, let's create that chirpy.ini file now. Here is a basic example: + +------------------------------------------------------------------------------- +[general] +title=My Little QDB +description=A place for my quotes +base_path=./src +locale=en-US +rating_limit_count=2 +rating_limit_time=60 +update_check=1 + +[data] +type=MySQL +mysql.hostname=localhost +mysql.port=3306 +mysql.username=my_username +mysql.password=my_password +mysql.database=my_database +mysql.prefix=chirpy_ + +[ui] +type=WebApp +date_time_format=%Y-%m-%d %H:%M GMT +date_format=%Y-%m-%d +time_format=%H:%M GMT +use_gmt=1 +quotes_per_page=10 +recent_news_items=3 +moderation_queue_public=1 +tag_cloud_logarithmic=1 +webapp.webmaster_name=John Doe +webapp.webmaster_email=you@yourserver.com +webapp.site_url=http://www.yourserver.com/cgi-bin/chirpy +webapp.resources_url=http://www.yourserver.com/chirpy/res +webapp.theme=default +webapp.welcome_text_file=welcome.html +webapp.cookie_domain=yourserver.com +webapp.cookie_path=/cgi-bin/chirpy +webapp.session_expiry=+3d +webapp.enable_short_urls=0 +webapp.enable_feeds=0 +webapp.quotes_per_feed=50 +webapp.enable_gzip=0 +webapp.enable_autolink=0 +------------------------------------------------------------------------------- + +A lot of the values don't look very interesting right now, but we'll have to +change some of the others. + +3.2.1. The [general] Section +---------------------------- + +title Change this value to the title you want your QDB to have. +description Enter a brief description of the purpose of your QDB. +base_path Enter the absolute or relative path to the src/ directory + here. When using relative paths, this is again relative to + the directory where index.cgi is. +update_check Set this to 1 to tell Chirpy! to automatically check for + updates periodically. Only site owners will be informed of + available updates. This feature requires that libwww-perl + (LWP) be installed; if it is not, Chirpy! will just show + you an informative error message. + +3.2.2. The [data] Section +------------------------- + +mysql.hostname Enter the name of the MySQL server here. Usually, this will + be "localhost". +mysql.port Enter the port the MySQL server uses. The default is 3306. +mysql.username Enter your MySQL username. This is not necessarily the same + as your regular username. +mysql.password Enter your MySQL password. This is not necessarily the same + as your regular password. +mysql.database Enter the name of the MySQL database Chirpy! should use. If + it does not exist, you need to create it first. Do not + create any tables; Chirpy! will do that for you. +mysql.prefix If you only have one MySQL database, Chirpy! can make its + tables easy to find by prefixing their names with the text + you enter here. The default "chirpy_" is a wise choice. + +3.2.3. The [ui] Section +----------------------- + +webapp.webmaster_name + Your name. +webapp.webmaster_email + Your e-mail address. Don't worry about spam, Chirpy! will + use some fancy tricks to hide it. +webapp.site_url The URL where you put index.cgi. +webapp.resources_url + The URL where you put the res/ directory. +webapp.cookie_domain + Essentially the domain name (without the www prefix) from + your site's URL. This will be used to store cookies. +webapp.cookie_path The part that comes after the domain in the site URL. This + will also be used to store cookies. +webapp.enable_short_urls + Change this to 1 to enable the short URLs feature described + above. If you get "Not Found" errors while browsing the QDB + later, you should turn it off. +webapp.enable_feeds Chirpy! can offer an RSS 2.0 feed and an Atom 1.0 feed of + the Quotes of the Week, so visitors can syndicate them. If + you want to enable those, set this to 1. +webapp.quotes_per_feed + This is the maximum number of quotes to include in a feed. + In theory, Chirpy! can provide a content feed for any page, + and since feeds do not offer "Previous"/"Next" links, this + should be a sensible number. The default is 50. +webapp.enable_gzip Chirpy! can greatly decrease bandwidth usage by compressing + output on the fly if the browser supports it. Set this to 1 + to enable that. It requires the Compress::Zlib Perl module. +webapp.enable_autolink + Change this to 1 to automatically turn URLs and e-mail + addresses in quote bodies into hyperlinks. This feature is + still sort of experimental, but should work fine. +webapp.captcha_provider + If you wish to prevent malicious users from spamming the + quote submission page, you will want to use captcha images. + This parameter sets the captcha provider, which will be + Authen_Captcha in most cases. Then Authen_Captcha provider + relies on Authen::Captcha being installed. + +You may want to turn the captcha feature off at first, so you can test-drive +Chirpy!'s other features. Configuring the captcha feature should be easy in the +case of Authen_Captcha. The alternative is to use GD_SecurityImage. Support for +that one is preliminary for now. If you are interested in using it, please +consult the appropriate documentation. + +That covers the configuration file. Save it as chirpy.ini and, as stressed +before, try to store it at a location on the server which cannot be accessed +using a Web browser. + +Now, we'll have to modify index.cgi a little to tell it where to find the +configuration file. Again, it looks for chirpy.ini in the working directory and +inside src/, but hopefully, it won't be there. So we'll just open index.cgi in +a text editor and change the line + + chirpy; + +to the following: + + chirpy('/path/to/chirpy.ini'); + +Again, you can use either an absolute path or a path relative to the working +directory. You'll also need this path in the next step. + +While we're editing index.cgi, there are two more things you might have to +change. The first is the path to the modules/ directory inside src/. This path +is stored like: + + unshift @INC, 'src/modules'; + +If you have not placed the src/ directory in the same directory as index.cgi, +update the path so Perl can find the Chirpy! modules. + +The other thing you might have to change is the path to Perl itself. Most +servers have it at /usr/bin/perl, but if yours doesn't, change the first line +of index.cgi to "#!" followed by the exact path to Perl. + +3.3. Setup +---------- + +Now that all the files are there, we'll grab setup.pl from the util/ directory +and open it in a text editor. Look for the line that reads + + my $ch = new Chirpy(); + +and change that to + + my $ch = new Chirpy('/path/to/chirpy.ini'); + +using the same path you entered in index.cgi. In addition, you will have to +update the path to src/modules/ and Perl itself again, if you had to do so for +index.cgi. + +Now, upload setup.pl to the directory where index.cgi resides and change its +attributes so they are the same as index.cgi's; as described above, they should +be rwxr-xr-x (755). Again, since this is a Perl script, upload in ASCII mode! + +Now, we're going to call your Web browser into action. Open it and surf to the +URL where setup.pl should be now, e.g. + + http://www.yourserver.com/cgi-bin/chirpy/setup.pl + +That should give you a basic page, welcoming you to the setup procedure. If +not, let's go over a few common problems ... + +- If you get the source code of setup.pl or maybe a download window, the server + doesn't allow execution of Perl scripts in that directory. You'll probably + need to use the cgi-bin directory instead. + +- If you get a Forbidden error, you probably didn't change the script's + attributes properly, as described above. + +- If you get a fairly verbose error that has line numbers and lots of weird + characters and other stuff that confuses you in it, the server executed the + script, but it crashed somewhere along the way. Something may have gone wrong + with the upload of one or more files; upload them again. If that doesn't do + any good, paste the error message at the Support Tracker, to which you can + find the URL at the start of this document. + +- If you get a generic "Internal Server Error" page, the server most likely + failed to execute the script at an earlier stage. Some files may have gotten + corrupted in the upload process; try uploading them again. If that doesn't + help, go over the text above to see if you didn't miss anything. If problems + persist, your host can give you a copy of the server's error log, which may + tell you more. If you can get an error message from the log, you can post + that at the Support Tracker; without it, the error will be nearly impossible + to trace. + +Assuming you've reached the setup page now, you get to decide if you want to +keep your existing installation if any. If you're installing Chirpy! for the +first time, you should choose to keep data, since it's faster. That should give +you a page with a basic event log for the setup procedure. If it tells you the +setup procedure has been completed, you can go to the next step now. If not, +take a look at the error and see if you can fix things. If not, try the Support +Tracker for assistance. + +3.4. Import +----------- + +This step is optional. It only applies if you have the Rash Quote Management +System installed and you want to migrate its data to Chirpy!. If you have no +idea what this is about, just skip this step. + +To import Rash's data, grab chirpy_rqms_import.php from the util/ directory. +Open it in a text editor and edit the configuration values at the start of the +script. Then place it in the directory where you installed Rash, so it can find +config.php. Surf to chirpy_rqms_import.php and everything should be clear from +there. If anything goes wrong, try the Support Tracker. + + +4. UPGRADING +============ + +If you are upgrading from Chirpy! version 0.1 or 0.2, you should just move the +updated setup.pl script from the "util" directory into Chirpy!'s root directory +for a second, make it executable, and surf to it. Chirpy! will then ask you if +you want to perform a fresh installation or an upgrade. Now, DO *NOT* CLICK ON +"FRESH INSTALLATION," because you would lose all your quotes, accounts, etc. +Instead, click the "UPGRADE" button, wait for the page to load, watch Chirpy! +inform you of the successful upgrade, and remove setup.pl again. + +Additionally, as Chirpy! 0.3 offers quite a couple of new features, you will +probably want to play with those. Most of them just come down to adding a line +in the configuration file. For captchas, you will need to create a couple of +directories; just scroll back to section 3.2, where the configuration file is +explained--it's all there. Enjoy! + + +5. RUN CHIRPY! +============== + _____________________________________________________________________________ +| | +| !!! DON'T FORGET TO REMOVE THE SETUP SCRIPT FROM THE SERVER FIRST !!! | +|_____________________________________________________________________________| + +By now, the setup script should have directed you to your brand new Chirpy! +installation already. If not, append /index.cgi to your site URL and open that +URL. That should give you a cute little start page. You should be able to leave +off index.cgi in the URL now--try it. If it doesn't work, you should definitely +turn short URLs off. If something else goes wrong, you should try the Support +Tracker; the URL is at the beginning of this file. + +Now, you're pretty much finished. Except that you should change the default +password. Surf to the administration section, log in as "superuser" with the +password "password" (if you didn't poke at setup.pl already) and click on the +"Manage Accounts" option. Select the "superuser" account and modify it. + +That concludes the installation of Chirpy!. I hope you'll enjoy it. If you run +into a problem or you'd like to see a new feature in the next release, surf to +the Trackers. + +Congratulations on a successful installation! + + +6. NEXT STEPS +============= + +Now that you've got a working Chirpy! installation, you can tweak it to your +liking. Most of this is done from the configuration file. If you want to change +the welcome message, you just edit the file welcome.html in the src/ directory, +which is actually a template. + +As Chirpy! is a work in progress, you can expect a lot more documentation on +customizing it in future releases. In the mean time, if you want to do more +advanced tweaking, here are a few possible scenarios: + +6.1. Your Own Theme +------------------- + +The easiest way to do this is to copy the existing "default" theme and modify +it, after which you change webapp.theme in the configuration file to use the +new theme. A theme is nothing but two directories named after it: the one in +src/themes/ holds the templates for the theme, the one in res/ holds its +resources. The template filenames are predefined, so you need to keep those +intact. The templates are parsed by the HTML::Template Perl module, so you will +probably have to look at its documentation for a second--or you could just +learn to use it by looking at the default theme's source code. The manual page +for HTML::Template can be found at + + http://search.cpan.org/dist/HTML-Template/Template.pm + +6.2. Your Own Locale +-------------------- + +You might want Chirpy! in your own language, and you can have it too. If you +look in the src/locales/ directory, you'll see that locales are actually just +INI files. Each locale string, along with some basic instructions for locale +creation, is documented in the POD, located at the URL at the start of this +document. + +6.3. Your Own Data Manager +-------------------------- + +Chirpy! is extremely modular. If you'd rather have it store its data in your +choice of database, you are free to implement the Chirpy::DataManager class. +Its API is explained in the POD, which is available at the start of this +document or by typing "perldoc Chirpy::DataManager" from a console or command +prompt, if you have Perl installed. + +6.4. Your Own User Interface +---------------------------- + +Apart from creating a backend, you can also write your own frontend class. This +class should implement Chirpy::UI. Unfortunately, the API documentation for it +is not yet available. Note that if your UI would be a web site, you should +probably just create a Chirpy::UI::WebApp theme as described in 5.1. + +If you've created a theme, a locale, a data manager or a UI, please share your +work with the community! E-mail it to me at ceetee@users.sourceforge.net and it +might be included in the next Chirpy! release by default. Obviously, you would +get credit for it. + + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/license.txt b/pub/qdb/license.txt new file mode 100644 index 0000000..06e25c8 --- /dev/null +++ b/pub/qdb/license.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/pub/qdb/readme.txt b/pub/qdb/readme.txt new file mode 100644 index 0000000..40aab9f --- /dev/null +++ b/pub/qdb/readme.txt @@ -0,0 +1,34 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +Installation instructions can be found in the install.txt file, which came with +Chirpy!. For anything else, please consult http://chirpy.sourceforge.net/. + +Many thanks go out to the guys over at irc.mozilla.org #bs for helping me test +and giving me lots of useful feedback, to jX for hosting the irc.mozilla.org +QDB, the first real Chirpy! web site, and to F2O.org for providing their free +developer webspace, ideal for testing. Peace to you all. --ct + +Chirpy! includes the projects listed below. Please consult their documentation, +which is also supplied with Chirpy!. + +* ExplorerCanvas +* WebFX Slider + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/res/.htaccess b/pub/qdb/res/.htaccess new file mode 100644 index 0000000..6f83ce8 --- /dev/null +++ b/pub/qdb/res/.htaccess @@ -0,0 +1,39 @@ +############################################################################### +# This file adds support for compressing static text content, such as CSS and # +# JavaScript files, in order to reduce traffic. To use it, you must perform # +# the following steps: # +# # +# 1. Move gzip.pl from the util/ directory to the root directory. If you had # +# to move index.cgi, e.g. because it had to be inside a cgi-bin, then move # +# gzip.pl there too. # +# 2. Modify the line use constant CACHE_DIR => '...'; in gzip.pl, setting the # +# cache directory to a writable path, where it can keep compressed files. # +# By default, this is a directory called "gzip" in your already present # +# "cache" directory. However, the path must be relative to the directory # +# where you puth gzip.pl! # +# 3. Change gzip.pl's attributes to rwxr-xr-x (755), like index.cgi. # +# 4. Comment out the lines below by removing the # in front of them. # +# 5. If gzip.pl is not in the root directory, modify its path in the line # +# that calls it below. # +# 6. Test! Obtain a tool that allows you to view HTTP headers and look at the # +# HTTP headers for a .js or .css file inside the res/ directory. If the # +# headers contain the line "Content-Encoding: gzip," the installation was # +# successful. No luck? Here are some common explanations: # +# a. Got an Internal Server Error? Verify that you uploaded gzip.pl in # +# ASCII mode and that you set its attributes. If you did, obtain an # +# error log from your host and see what that tells you. # +# b. If the server redirected the request to the same URL, with "?nogzip" # +# appended to it, the gzip.pl script did run, but decided compression # +# was not possible because of an incompatibility. # +# c. If the server neither compressed the file, nor redirected, then # +# gzip.pl didn't get invoked at all. The server might not support the # +# Rewrite module, or had trouble interpreting the directives below. # +############################################################################### + +# +#RewriteEngine On +#RewriteCond %{HTTP:Accept-Encoding} \bgzip\b +#RewriteCond %{QUERY_STRING} ="" +#RewriteCond %{REQUEST_FILENAME} -s +#RewriteRule \.(css|js)$ ../gzip.pl?filename=%{REQUEST_FILENAME}&uri=%{REQUEST_URI} +# \ No newline at end of file diff --git a/pub/qdb/res/captcha/.htaccess b/pub/qdb/res/captcha/.htaccess new file mode 100644 index 0000000..45552cb --- /dev/null +++ b/pub/qdb/res/captcha/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/pub/qdb/res/themes/default/cite.ico b/pub/qdb/res/themes/default/cite.ico new file mode 100644 index 0000000..3cba558 Binary files /dev/null and b/pub/qdb/res/themes/default/cite.ico differ diff --git a/pub/qdb/res/themes/default/css/default.css b/pub/qdb/res/themes/default/css/default.css new file mode 100644 index 0000000..3fcd5d6 --- /dev/null +++ b/pub/qdb/res/themes/default/css/default.css @@ -0,0 +1,1104 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# default.css # +# The default theme, without color theme-specific style # +############################################################################### +# $Id:: default.css 294 2007-02-06 00:01:15Z ceetee $ # +############################################################################### +*/ + +html { + font-family: Verdana, Arial, Helvetica, sans-serif; + margin: 0; + padding: 2em 5em; + background: #FBFBFB; + color: black; + font-size: 12px; +} + +body { + margin: 0; + padding: 0; +} + +a:link, a:visited { + text-decoration: none; + color: #555; +} + +a:hover, a:active { + text-decoration: underline; +} + +.news-text strong { + font-weight: bold; + font-style: normal; + text-decoration: none; +} + +.news-text em { + font-weight: normal; + font-style: normal; + text-decoration: none; + border-bottom: 1px solid #999; +} + +h1, h2, h3 { + font-family: Trebuchet MS, Tahoma, Verdana, Arial, Helvetica, sans-serif; + font-weight: normal; + margin: 0 0 1em 0; +} + +h1 { + margin: 0; + padding: 0; + font-size: 275%; +} + +h1 a, h1 a:link, h1 a:visited { + -moz-border-radius: 1em; + border-radius: 1em; + margin: 0; + padding: 0.1em 0.5em; + border-top: 0.25em solid; + border-bottom: 0.25em solid; + color: black; + text-decoration: none; + display: block; +} + +h1 a:hover, h1 a:active { + text-decoration: underline; + color: black; +} + +h2 { + font-size: 175%; +} + +h3 { + font-size: 120%; +} + +p { + margin: 0 0 1em 0; +} + +form { + display: inline; +} + +table { + border-collapse: collapse; + margin: 0; + padding: 0; +} + +thead, tbody, tr, td { + margin: 0; + padding: 0; +} + +input, select, textarea { + border: 1px solid #CCC; + background: white; + color: #666; + margin: 0.1em; + padding: 0.25em; + -moz-border-radius: 0.5em; + border-radius: 0.5em; +} + +select option { + padding: 0.1em; + margin: 0.1em; + background: #F9F9F9; +} + +input[type=radio], input[type=checkbox] { + border: none; + vertical-align: middle; +} + +input, select { + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 12px; +} + +textarea { + font-family: Consolas, Monaco, Courier New, monospace; + font-size: 12px; + line-height: 1.5em; +} + +input:focus, select:focus, textarea:focus { + color: black; + border-color: #999; +} + +input[type=submit], input[type=reset], input[type=button] { + padding-left: 1em; + padding-right: 1em; +} + +input[type=submit]:hover, input[type=reset]:hover, input[type=button]:hover { + color: black; + cursor: pointer; +} + +label { + border-bottom: 1px dotted transparent; +} + +label:hover { + border-color: #CCC; + cursor: pointer; +} + +ul#navigation { + -moz-border-radius: 0 0 1em 1em; + border-radius: 0 0 1em 1em; + margin: 0 4em 3em 0; + padding: 0.2em 0; + list-style-type: none; + font-size: 90%; + text-align: right; + text-transform: uppercase; + white-space: nowrap; + letter-spacing: 0.1em; +} + +ul#navigation li { + display: inline; + margin: 0; + padding: 0 0 0 0.5em; +} + +ul#navigation li a, ul#navigation li a:visited { + -moz-border-radius: 0 0 1em 1em; + border-radius: 0 0 1em 1em; + background: #EEE; + margin: 0; + padding: 0.2em 1em 0.3em 1em; + color: black; + text-decoration: none; + border-bottom: 0.2em solid #E0E0E0; +} + +#footer { + clear: both; + float: right; + font-size: 75%; + text-align: right; + border-top: 1px solid black; + margin: 2.5em 0 0 0; + padding: 0.75em 1em 0 10em; + line-height: 1.75em; +} + +#content { + padding: 1.5em 2.5em; + -moz-border-radius: 2.5em; + border-radius: 2.5em; + background: white; + min-height: 15em; + line-height: 1.75em; +} + +ul.quote-list { + list-style-type: none; + margin: 0 0 1em 0; + padding: 0; +} + +ul.quote-list li { + margin: 0 0 1em 0; + padding: 0; +} + +ul.quote-list .quote-container { + background: #FCFCFC; + -moz-border-radius: 1em 1em 2em 0.5em; + border-radius: 1em 1em 2em 0.5em; +} + +ul.quote-list li h3.quote-header { + margin: 0; + padding: 0.25em 0.75em; + border: 1px #F4F4F4 solid; + background: #F7F7F7; + -moz-border-radius: 0.5em 0.5em 0 0; + border-radius: 0.5em 0.5em 0 0; +} + +ul.quote-list li h3.quote-header * { + margin: 0 0.125em; + padding: 0; +} + +ul.quote-list li h3.quote-header * * { + margin: 0; +} + +ul.quote-list li h3.quote-header a.casted-vote { + opacity: 0.2; + text-decoration: line-through; + cursor: default; +} + +/* opacity doesn't work in IE, so hide it altogether */ +* html ul.quote-list li h3.quote-header a.casted-vote, +*:first-child + html ul.quote-list li h3.quote-header a.casted-vote { + visibility: hidden; +} + +blockquote.quote-body { + margin: 0.5em 0.75em; + padding: 0; + font-family: Consolas, Monaco, Courier New, monospace; + font-size: 12px; +} + +blockquote.quote-body p { + margin: 0; + padding: 0; +} + +#confirmation-form blockquote.quote-body, +#quote-removal-confirmation-form blockquote.quote-body { + margin: 1em 0; + padding: 1em; + border: 1px dashed #EEE; +} + +ul.quote-list li .quote-footer { + margin: 0.25em 0.75em; + padding: 0; + border-top: 1px dashed #EEE; + line-height: 1.5em; +} + +ul.quote-list li .quote-footer, ul.quote-list li .quote-footer a { + color: #999; +} + +ul.quote-list li .quote-footer p { + font-size: 80%; + margin: 0; + padding: 0 0 0 2.5ex; + text-indent: -2.5ex; +} + +ul.quote-list li .quote-notes .quote-notes-title, +ul.quote-list li .quote-tags .quote-tags-title { + font-size: 80%; + text-transform: uppercase; + font-style: normal; + font-weight: normal; +} + +ul.quote-list h3.quote-header .quote-date, +ul.quote-list h3.quote-header .quote-edit, +ul.quote-list h3.quote-header .quote-remove, +ul.quote-list h3.quote-header .quote-unflag, +ul.quote-list h3.quote-header .quote-live-vote-result { + font-size: 80%; +} + +ul.quote-list h3.quote-header .quote-vote-count { + margin-left: 0; + font-size: 90%; +} + +ul.quote-list h3.quote-header .quote-live-vote-result { + float: right; + text-transform: uppercase; +} + +ul.quote-list li.flagged h3.quote-header { + border: 1px #E9E9E9 solid; + background: #ECECEC; +} + +ul.quote-list li.unapproved h3.quote-header { + border: 1px #F9E9E9 solid; + background: #FCF0F0; +} + +.flag-options label, .approval-options label { + margin-right: 0.25em; +} + +#welcome-message-container { + width: 50%; + float: left; +} + +#welcome-message { + padding-right: 1.25em; +} + +#news-display-container { + width: 50%; + float: right; +} + +#news-display { + padding-left: 1.25em; +} + +#start-page-end { + clear: both; +} + +ul#news-list { + list-style-type: none; + margin: 0; + padding: 0; +} + +ul#news-list .news-text { + margin-bottom: 0.5em; +} + +ul#news-list .news-footer { + font-weight: bold; + margin-bottom: 1.5em; +} + +ul#news-list .news-footer .news-edit, ul#news-list .news-footer .news-remove { + font-size: 80%; +} + +.quote-browser { + margin: 0; + padding: 0; +} + +#quote-browser-top { + float: right; + margin-top: 0.25em; +} + +#quote-browser-bottom { + text-align: right; + margin-bottom: 0.25em; +} + +.quote-browser * { + margin: 0 0 0 0.1em; + padding: 0.25em 0.5em; + background: #F9F9F9; + -moz-border-radius: 0 0.5em 2em 0.5em; + border-radius: 0 0.5em 2em 0.5em; +} + +.quote-browser .inactive { + color: #C9C9C9; +} + +#search-haiku { + font-family: Trebuchet MS, Tahoma, Verdana, Arial, Helvetica, sans-serif; + font-style: italic; + font-size: 14px; + color: #DDD; + letter-spacing: 0.5ex; + line-height: 2.25em; + text-align: right; + margin: -3em 0 1em 0; + padding: 0 2em; + cursor: default; +} + +#search-haiku del { + display: none; +} + +#search-haiku:hover { + letter-spacing: 0.4ex; +} + +/* IE hates cool CSS */ +* html #search-haiku, *:first-child + html #search-haiku { + display: none; +} + +#search-haiku:hover del { + display: inline; + text-decoration: none; + color: #AAA; +} + +#search-form { + white-space: nowrap; + margin-bottom: 1.5em; +} + +#search-form #query-container, #search-form #submit-container { + display: inline; +} + +#search-form #query-field { + width: 33%; +} + +#submit-form #quote-field, #edit-quote-form #quote-field, .quote-data .body-field, +#submit-form #notes-field, #edit-quote-form #notes-field, .quote-data .notes-field, +#edit-news-item-form #news-item-field { + width: 95%; + height: 12em; + display: block; +} + +#submit-form #notes-field, #edit-quote-form #notes-field, .quote-data .notes-field { + height: 6em; +} + +.quote-data .field-container { + margin: 0.5em 0.75em; +} + +#submit-form #quote-container, #submit-form #notes-container, +#submit-form #tags-container, #submit-form #captcha-container, +#edit-quote-form #quote-container, #edit-quote-form #notes-container, +#edit-quote-form #tags-container { + margin-bottom: 1em; +} + +#submit-form #tags-container label, +#edit-quote-form #tags-container label { + display: block; +} + +#submit-form #tags-container input, +#edit-quote-form #tags-container input, +.quote-data input { + width: 95%; +} + +#submit-form #captcha-image { + vertical-align: middle; + padding: 0; + margin: 0.1em 1ex 0.1em 0.1em; +} + +#submit-form #captcha-code-field { + width: 16ex; +} + +#jump-to-quote-form #jump-to-id-field { + width: 8ex; +} + +#jump-to-quote-form #jump-to-quote-submit-button { + padding: 0.1em 0.5em; +} + +ul.tag-cloud { + list-style: none; + margin: 0; + padding: 0; +} + +ul.tag-cloud li { + display: inline; + margin: 0; + padding: 0 1ex 0 0; +} + +ul.tag-cloud li a { + white-space: nowrap; +} + +#confirmation-form #confirm-button, #confirmation-form #cancel-button { + width: 16ex; +} + +ul.tabbed-pane-header { + list-style-type: none; + margin: 0 0 0 1.5em; + padding: 0; + line-height: 2em; + white-space: nowrap; +} + +ul.tabbed-pane-header li { + display: inline; + margin: 0 0.25em 0 0; + padding: 0; +} + +ul.tabbed-pane-header li a, ul.tabbed-pane-header li a:visited { + padding: 0.25em 0.75em 0.5em 0.75em; + background: #F0F0F0; + border-top: 0.2em solid #ECECEC; + color: black; + text-decoration: none; + -moz-border-radius: 0.5em 0.5em 0 0; + border-radius: 0.5em 0.5em 0 0; + cursor: pointer; +} + +ul.tabbed-pane-contents { + list-style-type: none; + margin: 0; + padding: 1em; + -moz-border-radius: 1em; + border-radius: 1em; +} + +ul.tabbed-pane-contents > li { + display: block; + margin: 0; + padding: 0; + min-height: 15em; +} + +.tabbed-pane-contents .tab-title { + display: none; +} + +#post-news-form { + margin-bottom: 1em; +} + +#post-news-form #news-field { + width: 95%; + height: 12em; + display: block; +} + +#quick-manage-quote-form #quick-manage-quote-id-container, +#quick-manage-quote-form #quick-manage-quote-options, +#quick-manage-quote-form #quick-manage-quote-submit { + display: inline; + margin-right: 0.5em; +} + +#quick-manage-quote-form #quick-manage-quote-id-field { + width: 8ex; +} + +#account-manager { + text-align: right; + padding: 0 1em; +} + +#account-manager #username-select-container { + width: 40%; + float: left; +} + +#account-manager #username-select { + width: 100%; + height: 20em; +} + +#account-manager #username-select #username-new-user { + font-weight: bold; +} + +#account-manager #username-select .user-level-9, +#account-manager #level-select .user-level-9, +#edit-news-item-form #news-item-poster-select .user-level-9 { + background: #FFF0F9; +} + +#account-manager #username-select .user-level-6, +#account-manager #level-select .user-level-6, +#edit-news-item-form #news-item-poster-select .user-level-6 { + background: #FFF9F0; +} + +#account-manager #username-select .user-level-3, +#account-manager #level-select .user-level-3, +#edit-news-item-form #news-item-poster-select .user-level-3 { + background: #F0FFE9; +} + +#account-manager #username-field { + margin-top: 2em; +} + +#account-manager #username-field, +#account-manager #password-field, +#account-manager #repeat-password-field { + width: 40%; +} + +#account-manager #username-select-container, +#account-manager #username-container, +#account-manager #repeat-password-container, +#account-manager #level-container { + margin-bottom: 0.75em; +} + +#account-manager #account-manager-result { + margin: 2em 0 0 0; + padding: 0.25em 0 0 0; + width: 45%; + float: right; + font-weight: bold; + border-top: 1px solid #EEE; +} + +#edit-news-item-form #news-item-poster-container, +#edit-news-item-form #edit-news-item-submit-container { + display: inline; +} + +#post-news-form #news-container, #edit-news-item-form #news-item-container { + margin-bottom: 0.75em; +} + +#login-form #user-name-container, +#login-form #password-container, +#login-form #submit-container, +#change-password-form #current-password-container, +#change-password-form #new-password-container, +#change-password-form #repeat-new-password-container, +#change-password-form #submit-container { + display: inline; + margin-right: 0.5em; + white-space: nowrap; +} + +#login-form #user-name-field, +#login-form #password-field { + width: 25ex; +} + +#change-password-form #current-password-field, +#change-password-form #new-password-field, +#change-password-form #repeat-new-password-field { + width: 20ex; +} + +#mass-approve-container, #mass-unflag-container { + display: inline; +} + +#update-information { + background: #FFF6E6; + border: 2px dashed #F90; + color: #E70; + margin: 2em 5%; + padding: 0; + font-weight: bold; + line-height: 1.25em; +} + +#update-information a, #update-information a:visited { + display: block; + color: #E70; + text-align: center; + margin: 0; + padding: 1em; +} + +#update-information a:hover { + text-decoration: none; + color: #E50; +} + +#update-information p { + text-align: center; + margin: 0.75em 1em 0.25em 1em; + padding: 0; +} + +#update-information blockquote { + text-align: center; + margin: 0 1em 0.75em 1em; + color: #666; + font-weight: normal; + font-style: italic; +} + +#event-log-table { + border-collapse: collapse; + width: 100%; + clear: both; +} + +.event-log-navigation { + text-align: center; +} + +#event-log-navigation-top { + margin-bottom: 0.75em; +} + +#event-log-navigation-bottom { + margin-top: 0.75em; +} + +.event-log-navigation * { + padding: 0.25em 0.5em; + background: #F9F9F9; + -moz-border-radius: 0 0.5em 2em 0.5em; + border-radius: 0 0.5em 2em 0.5em; + cursor: pointer; +} + +.event-log-navigation .back { + float: left; + padding-top: 0; + padding-bottom: 0; +} + +.event-log-navigation .forward { + float: right; + padding-top: 0; + padding-bottom: 0; +} + +.event-log-navigation .inactive { + color: #C9C9C9; + cursor: default; + text-decoration: none; +} + +#event-log-table tr { + margin: 0; + padding: 0; +} + +#event-log-table th, #event-log-table td { + margin: 0; + padding: 0.25ex 1ex; + text-align: left; + vertical-align: top; +} + +#event-log-table thead tr { + background: #F0F0F0; + border: 1px solid #E6E6E6; +} + +#event-log-table td { + border: 1px solid #F0F0F0; +} + +#event-log-table tbody { + border: 1px solid #E6E6E6; +} + +#event-log-table tr.odd { + background: #F9F9F9; +} + +#event-log-table tr.even { + background: white; +} + +#event-log-table td.id { + text-align: right; +} + +#event-log-table th.id { + text-align: center; +} + +#event-log-table th.id a { + display: block; + text-decoration: none; + color: black; +} + +#event-log-table th, #event-log-table td.date, #event-log-table td.event { + white-space: nowrap; +} + +#event-log-table th.event, #event-log-table td.event { + width: 100%; +} + +#event-log-table .guest { + font-style: italic; +} + +#event-log-table .removed-account { + font-style: italic; +} + +#event-log-table .filtered, #event-log-table .filtered * { + text-decoration: underline; +} + +#event-log-table .empty { + font-style: italic; +} + +#event-log-table .loading { + font-style: italic; + text-align: center; +} + +.statistics-section { + display: inline; + float: left; + margin-right: 1.5em; +} + +.statistics-section h3 { + margin-bottom: 0.75em; +} + +#after-statistics { + clear: both; +} + +.chart, .chart * { + cursor: default; +} + +.chart { + position: relative; + width: 720px; + height: 400px; + overflow: hidden; + border: 1px solid #CCC; + line-height: 1em; + font-size: 10px; + color: #666; + background: #FCFCFC; + margin: 0 0 1.5em 0; +} + +.bar-chart-graph, .ogive-graph { + position: absolute; + left: 50px; + top: 10px; + width: 660px; + height: 360px; + border-bottom: 1px solid #666; + border-left: 1px solid #666; +} + +.chart-line { + position: absolute; + left: 0; + width: 100%; + height: 0; + border-top: 1px dashed #E0E0E0; +} + +.chart-labels { + position: absolute; + left: 50px; + bottom: 10px; + width: 660px; + margin-left: 1px; +} + +.chart-values { + position: absolute; + left: 10px; + top: 10px; + width: 30px; + height: 360px; +} + +.chart-value { + position: absolute; + right: 0; + text-align: right; + margin-bottom: -5px; +} + +.chart-label { + position: absolute; + bottom: 0; +} + +.chart-inner-label { + position: absolute; + left: 0; + bottom: 0; + width: 100%; +} + +.bar-chart-column { + position: absolute; + bottom: 0; + height: 100%; +} + +.bar-chart-bar { + position: absolute; + left: 0; + bottom: 0; + width: 100%; +} + +.bar-chart-inner-bar { + margin: 0 5%; + min-width: 1px; + height: 100%; + background: #DCDCDC; + -moz-border-radius: 20% 20% 0 0; + border-radius: 20% 20% 0 0; +} + +.bar-chart-column:hover .bar-chart-inner-bar { + background: #D6D6D6; +} + +.bar-chart-average-container { + position: absolute; + left: 0; + width: 100%; + height: 0; + border-top: 1px solid #BCBCBC; + color: #999; +} + +.bar-chart-average-container div { + float: right; + padding: 2px 5px; +} + +.pie-chart-graph { + position: absolute; + left: 20px; + top: 20px; +} + +.pie-chart-legend { + position: absolute; + right: 20px; + top: 70px; + border: 1px solid #ACACAC; + background: #FEFEFE; + margin: 0; + padding: 8px 10px; + width: 280px; + white-space: nowrap; +} + +.pie-chart-legend dt, .pie-chart-legend dd { + margin: 0; + padding: 0; +} + +.pie-chart-legend dt { + float: left; + width: 21px; + height: 20px; +} + +.pie-chart-legend dt div { + width: 14px; + height: 14px; + border: 1px solid #ACACAC; + margin: 2px 0 0 0; +} + +.pie-chart-legend dd { + height: 15px; + padding: 5px 0 0 0; + text-align: right; +} + +.pie-chart-legend dd .label { + float: left; + text-align: left; + width: 180px; +} + +.pie-chart-legend dd .value, .pie-chart-legend dd .percentage { + color: #999; + width: 35px; +} + +.pie-chart-legend dd .value { + float: left; + text-align: right; +} + +.pie-chart-legend dd .percentage { + float: right; +} + +.ogive .regression-equation { + color: #999; +} + +#tag-cloud-slider-container { + float: right; + background: #FBFBFB; + padding: 1ex; +} + +#tag-usage-minimum { + float: left; + margin: 0 1ex 0 0; + font-style: italic; +} + +#tag-cloud-slider-container .dynamic-slider-control { + float: left; +} + +.dynamic-slider-control { + position: relative; + -moz-user-focus: normal; + -moz-user-select: none; + cursor: default; + width: 150px; + height: 1.75em; +} + +.dynamic-slider-control * { + font-size: 1px; +} + +.dynamic-slider-control input { + display: none; +} + +.dynamic-slider-control .handle { + position: absolute; + -moz-user-select: none; + cursor: default; + background: #D3D3D3; + border: 1px solid; + border-color: #F3F3F3 #B3B3B3 #B3B3B3 #F3F3F3; + width: 5px; +} + +.dynamic-slider-control .line { + behavior: url("../js/slider/boxsizing.htc"); + box-sizing: content-box; + -moz-box-sizing: content-box; + position: absolute; + overflow: hidden; + border: 1px solid; + border-color: #A3A3A3 #E3E3E3 #E3E3E3 #A3A3A3; + background: #C3C3C3; + height: 1px; +} + +.dynamic-slider-control .line div { + display: none; +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/css/styles/default.css b/pub/qdb/res/themes/default/css/styles/default.css new file mode 100644 index 0000000..2d652af --- /dev/null +++ b/pub/qdb/res/themes/default/css/styles/default.css @@ -0,0 +1,66 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# default.css # +# "Default" color theme for the default theme # +############################################################################### +# $Id:: default.css 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +a:hover, a:active { + color: #F63; +} + +h1 a, h1 a:link, h1 a:visited { + background: #CFB; + border-top-color: #D6FFC6; + border-bottom-color: #C9F9B9; +} + +ul#navigation li a:hover, ul#navigation li a:active, +ul.tabbed-pane-header li a:hover, ul.tabbed-pane-header li a:active { + background: #FDA; + border-color: #F0D0A0; +} + +ul.quote-list li:hover h3.quote-header { + background: #FEC; + border-color: #FFE9C9; +} + +ul.quote-list li:hover .quote-container { + background: #FFFCEC; +} + +ul.quote-list li:hover .quote-footer, ul.quote-list li:hover .quote-tags a { + color: #BA9; + border-color: #FFE9C9; +} + +ul.tabbed-pane-header li a.active-tab { + background: #ECF6EC; + border-top-color: #C0F0B0; +} + +ul.tabbed-pane-contents { + border: 1px solid #ECF6EC; +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/css/styles/fish_tank.css b/pub/qdb/res/themes/default/css/styles/fish_tank.css new file mode 100644 index 0000000..34c034a --- /dev/null +++ b/pub/qdb/res/themes/default/css/styles/fish_tank.css @@ -0,0 +1,66 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# fish_tank.css # +# "Fish Tank" color theme for the default theme # +############################################################################### +# $Id:: fish_tank.css 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +a:hover, a:active { + color: #3F6; +} + +h1 a, h1 a:link, h1 a:visited { + background: #BCF; + border-top-color: #C6D6FF; + border-bottom-color: #B9C9F9; +} + +ul#navigation li a:hover, ul#navigation li a:active, +ul.tabbed-pane-header li a:hover, ul.tabbed-pane-header li a:active { + background: #AFD; + border-color: #A0F0D0; +} + +ul.quote-list li:hover h3.quote-header { + background: #CFE; + border-color: #C9FFE9; +} + +ul.quote-list li:hover .quote-container { + background: #ECFFFC; +} + +ul.quote-list li:hover .quote-footer, ul.quote-list li:hover .quote-tags a { + color: #9BA; + border-color: #C9FFE9; +} + +ul.tabbed-pane-header li a.active-tab { + background: #ECECF6; + border-top-color: #B0C0F0; +} + +ul.tabbed-pane-contents { + border: 1px solid #ECECF6; +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/css/styles/grayscale.css b/pub/qdb/res/themes/default/css/styles/grayscale.css new file mode 100644 index 0000000..800cc08 --- /dev/null +++ b/pub/qdb/res/themes/default/css/styles/grayscale.css @@ -0,0 +1,66 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# grayscale.css # +# "Grayscale" color theme for the default theme # +############################################################################### +# $Id:: grayscale.css 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +a:hover, a:active { + color: #999; +} + +h1 a, h1 a:link, h1 a:visited { + background: #ECECEC; + border-top-color: #F3F3F3; + border-bottom-color: #E9E9E9; +} + +ul#navigation li a:hover, ul#navigation li a:active, +ul.tabbed-pane-header li a:hover, ul.tabbed-pane-header li a:active { + background: #F3F3F3; + border-color: #ECECEC; +} + +ul.quote-list li:hover h3.quote-header { + background: #F6F6F6; + border-color: #F0F0F0; +} + +ul.quote-list li:hover .quote-container { + background: #FCFCFC; +} + +ul.quote-list li:hover .quote-footer, ul.quote-list li:hover .quote-tags a { + color: #BBB; + border-color: #F0F0F0; +} + +ul.tabbed-pane-header li a.active-tab { + background: #E9E9E9; + border-top-color: #CCC; +} + +ul.tabbed-pane-contents { + border: 1px solid #E9E9E9; +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/css/styles/nineties.css b/pub/qdb/res/themes/default/css/styles/nineties.css new file mode 100644 index 0000000..89d4810 --- /dev/null +++ b/pub/qdb/res/themes/default/css/styles/nineties.css @@ -0,0 +1,66 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# nineties.css # +# "Nineties" color theme for the default theme # +############################################################################### +# $Id:: nineties.css 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +a:hover, a:active { + color: #69C; +} + +h1 a, h1 a:link, h1 a:visited { + background: #CEF; + border-top-color: #D6F6FF; + border-bottom-color: #C9E9F9; +} + +ul#navigation li a:hover, ul#navigation li a:active, +ul.tabbed-pane-header li a:hover, ul.tabbed-pane-header li a:active { + background: #FDA; + border-color: #F0D0A0; +} + +ul.quote-list li:hover h3.quote-header { + background: #CEF; + border-color: #C9E9FF; +} + +ul.quote-list li:hover .quote-container { + background: #ECFCFF; +} + +ul.quote-list li:hover .quote-footer, ul.quote-list li:hover .quote-tags a { + color: #9AB; + border-color: #C9E9FF; +} + +ul.tabbed-pane-header li a.active-tab { + background: #E9E9E9; + border-top-color: #CCC; +} + +ul.tabbed-pane-contents { + border: 1px solid #E9E9E9; +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/css/styles/spring.css b/pub/qdb/res/themes/default/css/styles/spring.css new file mode 100644 index 0000000..8f9e174 --- /dev/null +++ b/pub/qdb/res/themes/default/css/styles/spring.css @@ -0,0 +1,66 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# spring.css # +# "Spring" color theme for the default theme # +############################################################################### +# $Id:: spring.css 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +a:hover, a:active { + color: #36F; +} + +h1 a, h1 a:link, h1 a:visited { + background: #BFC; + border-top-color: #C6FFD6; + border-bottom-color: #B9F9C9; +} + +ul#navigation li a:hover, ul#navigation li a:active, +ul.tabbed-pane-header li a:hover, ul.tabbed-pane-header li a:active { + background: #ADF; + border-color: #A0D0F0; +} + +ul.quote-list li:hover h3.quote-header { + background: #CEF; + border-color: #C9E9FF; +} + +ul.quote-list li:hover .quote-container { + background: #ECFCFF; +} + +ul.quote-list li:hover .quote-footer, ul.quote-list li:hover .quote-tags a { + color: #9AB; + border-color: #C9E9FF; +} + +ul.tabbed-pane-header li a.active-tab { + background: #ECF6EC; + border-top-color: #B0F0C0; +} + +ul.tabbed-pane-contents { + border: 1px solid #ECF6EC; +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/administration.js b/pub/qdb/res/themes/default/js/administration.js new file mode 100644 index 0000000..00220c4 --- /dev/null +++ b/pub/qdb/res/themes/default/js/administration.js @@ -0,0 +1,483 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# administration.js # +# Functions specific to the administration interface # +############################################################################### +# $Id:: administration.js 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +var eventLogTable; +var eventLogTableBody; +var eventLogHeaders; +var eventLogRequest; +var eventLogPreviousLinks = new Array(); +var eventLogNextLinks = new Array(); +var eventLogCurrentLinks = new Array(); +var eventLogURLParam = new Array(); +eventLogURLParam["start"] = 0; +eventLogURLParam["count"] = 10; +eventLogURLParam["asc"] = 0; + +function editQuote (id) { + var editLink = document.getElementById("quote-edit-" + id); + editLink.parentNode.removeChild(editLink); + var body = getNodeText("quote-body-" + id); + var notes = getNodeText("quote-notes-" + id); + var tags = getNodeText("quote-tags-" + id); + var dataNode = document.getElementById("quote-data-" + id); + while (dataNode.firstChild) dataNode.removeChild(dataNode.firstChild); + var bodyArea = document.createElement("textarea"); + bodyArea.name = "body_" + id; + bodyArea.appendChild(document.createTextNode(body)); + bodyArea.className = "body-field"; + var notesArea = document.createElement("textarea"); + notesArea.name = "notes_" + id; + notesArea.appendChild(document.createTextNode(notes)); + notesArea.className = "notes-field"; + var tagsInput = document.createElement("input"); + tagsInput.name = "tags_" + id; + tagsInput.value = tags; + tagsInput.className = "tags-field"; + dataNode.appendChild(stickInDiv(bodyArea)); + dataNode.appendChild(stickInDiv(notesArea)); + dataNode.appendChild(stickInDiv(tagsInput)); + document.getElementById("a" + id + "-1").checked = true; +} + +function stickInDiv (node) { + var div = document.createElement("div"); + div.className = "field-container"; + div.appendChild(node); + return div; +} + +function getNodeText (id) { + var node = document.getElementById(id); + if (!node) return ""; + var text = ""; + for (var i = 0; i < node.childNodes.length; i++) { + var child = node.childNodes[i]; + if (child.nodeType == 3) { + text += child.nodeValue.replace(/ *[\r\n]/g, ""); + } + else if (child.nodeName && child.nodeName.toLowerCase() == "br") { + // \n breaks in IE + text += "\r"; + } + } + return text; +} + +function insertEventLog () { + var node = document.getElementById("event-log-placeholder"); + if (!node || !ajaxSupported()) return; + var div = document.createElement("div"); + var navTop = createEventLogNavigation(); + navTop.id = "event-log-navigation-top"; + var navBottom = createEventLogNavigation(); + navBottom.id = "event-log-navigation-bottom"; + div.appendChild(navTop); + var table = document.createElement("table"); + eventLogTable = table; + eventLogHeaders = new Array(); + table.id = "event-log-table"; + var thead = document.createElement("thead"); + var thr = document.createElement("tr"); + var cols = [ "id", "date", "username", "event" ]; + for (var i = 0; i < cols.length; i++) { + var col = cols[i]; + var th = document.createElement("th"); + th.className = col; + var text = eventLogLocale[col]; + var cont; + if (col == "id") { + cont = document.createElement("a"); + var href = document.createAttribute("href"); + var asc = (eventLogURLParam["asc"] > 0); + href.value = getEventLogURL(true, "asc", asc ? 0 : 1); + cont.setAttributeNode(href); + var up = String.fromCharCode(0x25B2); + var down = String.fromCharCode(0x25BC); + var tn = document.createTextNode(asc ? up : down); + cont.appendChild(tn); + cont.onclick = function () { + var asc = !(eventLogURLParam["asc"] > 0); + tn.nodeValue = (asc ? up : down); + href.value = getEventLogURL(true, "asc", asc ? 1 : 0); + eventLogURLParam["asc"] = (asc ? 1 : 0); + updateEventLog(true); + return false; + }; + } + else + cont = document.createTextNode(text); + th.appendChild(cont); + thr.appendChild(th); + eventLogHeaders[col] = th; + } + thead.appendChild(thr); + table.appendChild(thead); + eventLogTableBody = document.createElement("tbody"); + table.appendChild(eventLogTableBody); + div.appendChild(table); + div.appendChild(navBottom); + node.parentNode.replaceChild(div, node); + updateEventLog(); +} + +function createEventLogNavigation () { + var div = document.createElement("div"); + div.className = "event-log-navigation"; + var prev = document.createElement("a"); + var pHref = document.createAttribute("href"); + prev.setAttributeNode(pHref); + prev.appendChild(document.createTextNode( + String.fromCharCode(0x2190) + " " + eventLogLocale["previous"])); + prev.className = "back"; + var next = document.createElement("a"); + var nHref = document.createAttribute("href"); + next.setAttributeNode(nHref); + next.appendChild(document.createTextNode( + eventLogLocale["next"] + " " + String.fromCharCode(0x2192))); + next.className = "forward"; + var current = document.createElement("a"); + var cHref = document.createAttribute("href"); + current.setAttributeNode(cHref); + current.appendChild(document.createTextNode( + eventLogLocale["current"])); + current.className = "current"; + div.appendChild(prev); + div.appendChild(next); + div.appendChild(current); + eventLogPreviousLinks.push(prev); + eventLogNextLinks.push(next); + eventLogCurrentLinks.push(current); + return div; +} + +function updateEventLog (reset) { + clearEventLog(); + var tr = document.createElement("tr"); + var td = document.createElement("td"); + td.colSpan = 4; + td.className = "loading"; + td.appendChild(document.createTextNode(eventLogLocale["loading"])); + tr.appendChild(td); + eventLogTableBody.appendChild(tr); + eventLogHeaders["event"].className = "event" + (eventLogURLParam["code"] != null ? " filtered" : ""); + eventLogHeaders["username"].className = "username" + (eventLogURLParam["user"] != null ? " filtered" : ""); + if (eventLogRequest) eventLogRequest.abort(); + eventLogRequest = getAjaxObject(); + eventLogRequest.onreadystatechange = checkEventLogRequest; + var url = getEventLogURL(reset); + for (var i = 0; i < eventLogCurrentLinks.length; i++) { + var link = eventLogCurrentLinks[i]; + link.getAttributeNode("href").value = url; + } + url += "&output=xml"; + eventLogRequest.open("GET", url, true); + eventLogRequest.send(""); +} + +function checkEventLogRequest () { + if (eventLogRequest.readyState != 4 || eventLogRequest.status != 200) return; + var xml = eventLogRequest.responseXML; + eventLogRequest = null; + var data = parseEventLogXML(xml); + if (data == null) { + return; + } + fillEventLog(data); +} + +function clearEventLog () { + setEventLogNavigationEnabled(false, false); + setEventLogNavigationEnabled(true, false); + while (eventLogTableBody.firstChild) + eventLogTableBody.removeChild(eventLogTableBody.firstChild); +} + +function fillEventLog (tableData) { + clearEventLog(); + setEventLogNavigationEnabled(false, tableData["leading"]); + setEventLogNavigationEnabled(true, tableData["trailing"]); + var events = tableData["events"]; + var dataFilter; + if (eventLogURLParam["data"]) + dataFilter = eventLogURLParam["data"].replace(/=.*/, ""); + for (var i = 0; i < events.length; i++) { + var evt = events[i]; + var data = evt["data"]; + var evenOdd = (i % 2 == 0 ? "even" : "odd"); + var firstRow = document.createElement("tr"); + firstRow.className = evenOdd; + var idCell = document.createElement("td"); + idCell.className = "id"; + var rowSpan = 1; + for (name in data) rowSpan++; + idCell.rowSpan = rowSpan; + idCell.appendChild(document.createTextNode(evt["id"])); + firstRow.appendChild(idCell); + var dateCell = document.createElement("td"); + dateCell.className = "date"; + dateCell.appendChild(document.createTextNode(evt["date"])); + firstRow.appendChild(dateCell); + var userCell = document.createElement("td"); + userCell.className = "username"; + var userID = evt["userid"]; + var username; + if (userID == 0) { + userCell.className += " guest"; + username = eventLogLocale["guest"]; + } + else if ("username" in evt) { + username = evt["username"]; + } + else { + userCell.className += " removed-account"; + username = "#" + userID; + } + var userLink = createEventLogLink("user", userID); + userLink.appendChild(document.createTextNode(username)); + userCell.appendChild(userLink); + firstRow.appendChild(userCell); + var descCell = document.createElement("td"); + descCell.className = "event"; + var descLink = document.createElement("a"); + var descHref = document.createAttribute("href"); + var descLink = createEventLogLink("code", evt["code"]); + descLink.appendChild(document.createTextNode(evt["description"])); + descCell.appendChild(descLink); + firstRow.appendChild(descCell); + eventLogTableBody.appendChild(firstRow); + for (name in data) { + var value = data[name]; + var row = document.createElement("tr"); + row.className = evenOdd; + var nameCell = document.createElement("td"); + nameCell.className = "property-name"; + if (dataFilter && name == dataFilter) + nameCell.className += " filtered"; + nameCell.appendChild(document.createTextNode(name)); + var valueCell = document.createElement("td"); + valueCell.className = "property-value"; + valueCell.colSpan = 2; + if (value.length > 1) { + valueCell.appendChild(document.createTextNode(fixWhiteSpace(value[0]))); + for (var j = 1; j < value.length; j++) { + valueCell.appendChild(document.createElement("br")); + valueCell.appendChild(document.createTextNode(fixWhiteSpace(value[j]))); + } + } + else { + var empty = (value.length == 0); + var val = (!empty ? value[0].replace(/'/g, '\\\'') : ""); + var link = createEventLogLink("data", name + "=" + val); + var text; + if (empty) { + link.className = "empty"; + text = eventLogLocale["empty"]; + } + else + text = fixWhiteSpace(value[0]); + link.appendChild(document.createTextNode(text)); + valueCell.appendChild(link); + } + row.appendChild(nameCell); + row.appendChild(valueCell); + eventLogTableBody.appendChild(row); + } + } +} + +function setEventLogNavigationEnabled (next, enabled) { + var links, className; + if (next) { + links = eventLogNextLinks; + className = "forward"; + } + else { + links = eventLogPreviousLinks; + className = "back"; + } + for (var i = 0; i < links.length; i++) { + var link = links[i]; + var href = link.getAttributeNode("href"); + if (enabled) { + link.className = className; + var start = eventLogURLParam["start"]; + var count = eventLogURLParam["count"]; + if (!next) + start = (start >= count ? start - count : 0); + else + start += count; + link.onclick = function () { + eventLogURLParam["start"] = start; + updateEventLog(); + return false; + }; + href.value = getEventLogURL(false, "start", start); + } + else { + link.className = className + " inactive"; + link.onclick = function () { + return false; + }; + href.value = "#"; + } + } +} + +function parseEventLogXML (xml) { + if (!xml || !xml.childNodes) return null; + var leading = false; + var trailing = false; + var events = new Array(); + for (var i = 0; i < xml.childNodes.length; i++) { + var root = xml.childNodes[i]; + if (root.nodeType != 1) continue; + for (var j = 0; j < root.childNodes.length; j++) { + var child = root.childNodes[j]; + if (child.nodeType == 1) { + switch (child.nodeName) { + case "event": + events.push(parseLogEventNode(child)); + break; + case "leading": + leading = true; + break; + case "trailing": + trailing = true; + break; + } + } + } + } + var data = new Array(); + data["leading"] = leading; + data["trailing"] = trailing; + data["events"] = events; + return data; +} + +function parseLogEventNode (node) { + var event = new Array(); + event["data"] = new Array(); + for (var i = 0; i < node.childNodes.length; i++) { + var child = node.childNodes[i]; + if (child.nodeType != 1) continue; + if (child.nodeName == "data") { + var name, value; + for (var j = 0; j < child.childNodes.length; j++) { + var n = child.childNodes[j]; + if (n.nodeType != 1) continue; + switch (n.nodeName) { + case "name": + name = n.firstChild.nodeValue; + break; + case "value": + value = extractLogEventDataValue(n); + break; + } + } + event["data"][name] = value; + } + else { + event[child.nodeName] = child.firstChild.nodeValue; + } + } + return event; +} + +function extractLogEventDataValue (node) { + var value = new Array(); + for (var i = 0; i < node.childNodes.length; i++) { + var child = node.childNodes[i]; + if (child.nodeType != 3) continue; + value.push(child.nodeValue); + } + return value; +} + +function createEventLogLink (name, value) { + var anchor = document.createElement("a"); + var href = document.createAttribute("href"); + href.value = getEventLogURL(true, name, value); + anchor.setAttributeNode(href); + anchor.onclick = function () { + eventLogURLParam[name] + = (eventLogURLParam[name] == value ? null : value); + updateEventLog(true); + return false; + }; + return anchor; +} + +function getEventLogURL (reset, name, value) { + var pairs = new Array(); + var found = false; + for (key in eventLogURLParam) { + var val; + switch (key) { + case name: + val = (eventLogURLParam[key] == value ? null : value); + found = true; + break; + case "start": + val = (reset ? 0 : eventLogURLParam[key]); + break; + default: + val = eventLogURLParam[key]; + } + if (val != null) + pairs.push(escape(key) + "=" + escape(val)); + } + if (name && !found) { + pairs.push(escape(name) + "=" + escape(value)); + } + return eventLogURL + pairs.join("&"); +} + +function fixWhiteSpace (text) { + var nbsp = String.fromCharCode(160); + return text + .replace(/[\r\n]+/g, "") + .replace(/^\s/, nbsp) + .replace(/\s$/, nbsp) + .replace(/\s{2}/g, nbsp + nbsp); +} + +function addOnloadFunction (f) { + if (window.onload != null) { + var old = window.onload; + window.onload = function (e) { + old(e); + f(); + }; + } + else { + window.onload = f; + } +} + +addOnloadFunction(insertEventLog); \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/ajax.js b/pub/qdb/res/themes/default/js/ajax.js new file mode 100644 index 0000000..d31d654 --- /dev/null +++ b/pub/qdb/res/themes/default/js/ajax.js @@ -0,0 +1,53 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# ajax.js # +# Facilitates access to the browser's AJAX features, if any # +############################################################################### +# $Id:: ajax.js 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +var ajaxMethods = new Array( + function() { return new ActiveXObject("Msxml2.XMLHTTP") }, + function() { return new ActiveXObject("Microsoft.XMLHTTP") }, + function() { return new XMLHttpRequest() } +); + +var ajaxMethodIndex = -1; + +for (var i = 0; i < ajaxMethods.length; i++) { + try { + ajaxMethods[i](); + ajaxMethodIndex = i; + break; + } catch (e) { } +} + +function ajaxSupported () { + return (ajaxMethodIndex >= 0); +} + +function getAjaxObject () { + return (ajaxSupported() + ? ajaxMethods[ajaxMethodIndex]() + : null); +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/excanvas/AUTHORS b/pub/qdb/res/themes/default/js/excanvas/AUTHORS new file mode 100644 index 0000000..5813b92 --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/AUTHORS @@ -0,0 +1 @@ +opensource@google.com \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/excanvas/COPYING b/pub/qdb/res/themes/default/js/excanvas/COPYING new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pub/qdb/res/themes/default/js/excanvas/README b/pub/qdb/res/themes/default/js/excanvas/README new file mode 100644 index 0000000..eb7c42f --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/README @@ -0,0 +1,22 @@ +ExplorerCanvas +Copyright 2006 Google Inc. + +------------------------------------------------------------------------------- +DESCRIPTION + +Firefox, Safari and Opera 9 support the canvas tag to allow 2D command-based +drawing operations. ExplorerCanvas brings the same functionality to Internet +Explorer; web developers only need to include a single script tag in their +existing canvas webpages to enable this support. + + +------------------------------------------------------------------------------- +INSTALLATION + +Include the ExplorerCanvas tag in the same directory as your HTML files, and +add the following code to your page, preferably in the tag. + + + +If you run into trouble, please look at the included example code to see how +to best implement this \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/excanvas/examples/example1.html b/pub/qdb/res/themes/default/js/excanvas/examples/example1.html new file mode 100644 index 0000000..d67ebde --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/examples/example1.html @@ -0,0 +1,93 @@ + + + + ExplorerCanvas Example 1 + + + + + + + + \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/excanvas/examples/example2.html b/pub/qdb/res/themes/default/js/excanvas/examples/example2.html new file mode 100644 index 0000000..55f163b --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/examples/example2.html @@ -0,0 +1,513 @@ + + + + ExplorerCanvas Example 1 + + + + + + + + \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/excanvas/examples/example3.html b/pub/qdb/res/themes/default/js/excanvas/examples/example3.html new file mode 100644 index 0000000..ab95544 --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/examples/example3.html @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + +
+
+ +
+ + +
+ + + + + diff --git a/pub/qdb/res/themes/default/js/excanvas/examples/ff.jpg b/pub/qdb/res/themes/default/js/excanvas/examples/ff.jpg new file mode 100644 index 0000000..dc32d11 Binary files /dev/null and b/pub/qdb/res/themes/default/js/excanvas/examples/ff.jpg differ diff --git a/pub/qdb/res/themes/default/js/excanvas/excanvas.js b/pub/qdb/res/themes/default/js/excanvas/excanvas.js new file mode 100644 index 0000000..50542e1 --- /dev/null +++ b/pub/qdb/res/themes/default/js/excanvas/excanvas.js @@ -0,0 +1,730 @@ +// Copyright 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO: Patterns +// TODO: Radial gradient +// TODO: Clipping paths +// TODO: Coordsize (still need to support stretching) +// TODO: Painting mode +// TODO: Optimize +// TODO: canvas width/height sets content size in moz, border size in ie + +// only add this code if we do not already have a canvas implementation +if (!window.CanvasRenderingContext2D) { + +(function () { + + // alias some functions to make (compiled) code shorter + var m = Math; + var mr = m.round; + var ms = m.sin; + var mc = m.cos; + + var G_vmlCanvasManager_ = { + init: function (opt_doc) { + var doc = opt_doc || document; + if (/MSIE/.test(navigator.userAgent) && !window.opera) { + var self = this; + doc.attachEvent("onreadystatechange", function () { + self.init_(doc); + }); + } + }, + + init_: function (doc, e) { + if (doc.readyState == "complete") { + // create xmlns + if (!doc.namespaces["g_vml_"]) { + doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); + } + + // setup default css + var ss = doc.createStyleSheet(); + ss.cssText = "canvas{display:inline-block;overflow:hidden;" + + "text-align:left;}" + + "canvas *{behavior:url(#default#VML)}"; + + // find all canvas elements + var els = doc.getElementsByTagName("canvas"); + for (var i = 0; i < els.length; i++) { + if (!els[i].getContext) { + this.initElement(els[i]); + } + } + } + }, + + fixElement_: function (el) { + // in IE before version 5.5 we would need to add HTML: to the tag name + // but we do not care about IE before version 6 + var outerHTML = el.outerHTML; + var newEl = document.createElement(outerHTML); + // if the tag is still open IE has created the children as siblings and + // it has also created a tag with the name "/FOO" + if (outerHTML.slice(-2) != "/>") { + var tagName = "/" + el.tagName; + var ns; + // remove content + while ((ns = el.nextSibling) && ns.tagName != tagName) { + ns.removeNode(); + } + // remove the incorrect closing tag + if (ns) { + ns.removeNode(); + } + } + el.parentNode.replaceChild(newEl, el); + return newEl; + }, + + /** + * Public initializes a canvas element so that it can be used as canvas + * element from now on. This is called automatically before the page is + * loaded but if you are creating elements using createElement you need to + * make sure this is called on the element. + * @param {HTMLElement} el The canvas element to initialize. + * @return {HTMLElement} the element that was created. + */ + initElement: function (el) { + el = this.fixElement_(el); + el.getContext = function () { + if (this.context_) { + return this.context_; + } + return this.context_ = new CanvasRenderingContext2D_(this); + }; + + // do not use inline function because that will leak memory + // el.attachEvent('onpropertychange', onPropertyChange) + el.attachEvent('onresize', onResize); + + var attrs = el.attributes; + if (attrs.width && attrs.width.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setWidth_(attrs.width.nodeValue); + el.style.width = attrs.width.nodeValue + "px"; + } + if (attrs.height && attrs.height.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setHeight_(attrs.height.nodeValue); + el.style.height = attrs.height.nodeValue + "px"; + } + //el.getContext().setCoordsize_() + return el; + } + }; + + function onPropertyChange(e) { + // we need to watch changes to width and height + switch (e.propertyName) { + case 'width': + case 'height': + // TODO: coordsize and size + break; + } + } + + function onResize(e) { + var el = e.srcElement; + if (el.firstChild) { + el.firstChild.style.width = el.clientWidth + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + } + } + + G_vmlCanvasManager_.init(); + + // precompute "00" to "FF" + var dec2hex = []; + for (var i = 0; i < 16; i++) { + for (var j = 0; j < 16; j++) { + dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); + } + } + + function createMatrixIdentity() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ]; + } + + function matrixMultiply(m1, m2) { + var result = createMatrixIdentity(); + + for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var sum = 0; + + for (var z = 0; z < 3; z++) { + sum += m1[x][z] * m2[z][y]; + } + + result[x][y] = sum; + } + } + return result; + } + + function copyState(o1, o2) { + o2.fillStyle = o1.fillStyle; + o2.lineCap = o1.lineCap; + o2.lineJoin = o1.lineJoin; + o2.lineWidth = o1.lineWidth; + o2.miterLimit = o1.miterLimit; + o2.shadowBlur = o1.shadowBlur; + o2.shadowColor = o1.shadowColor; + o2.shadowOffsetX = o1.shadowOffsetX; + o2.shadowOffsetY = o1.shadowOffsetY; + o2.strokeStyle = o1.strokeStyle; + } + + function processStyle(styleString) { + var str, alpha = 1; + + styleString = String(styleString); + if (styleString.substring(0, 3) == "rgb") { + var start = styleString.indexOf("(", 3); + var end = styleString.indexOf(")", start + 1); + var guts = styleString.substring(start + 1, end).split(","); + + str = "#"; + for (var i = 0; i < 3; i++) { + str += dec2hex[parseInt(guts[i])]; + } + + if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) { + alpha = guts[3]; + } + } else { + str = styleString; + } + + return [str, alpha]; + } + + function processLineCap(lineCap) { + switch (lineCap) { + case "butt": + return "flat"; + case "round": + return "round"; + case "square": + default: + return "square"; + } + } + + /** + * This class implements CanvasRenderingContext2D interface as described by + * the WHATWG. + * @param {HTMLElement} surfaceElement The element that the 2D context should + * be associated with + */ + function CanvasRenderingContext2D_(surfaceElement) { + this.m_ = createMatrixIdentity(); + + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + + // Canvas context properties + this.strokeStyle = "#000"; + this.fillStyle = "#ccc"; + + this.lineWidth = 1; + this.lineJoin = "miter"; + this.lineCap = "butt"; + this.miterLimit = 10; + this.globalAlpha = 1; + + var el = document.createElement('div'); + el.style.width = surfaceElement.clientWidth + 'px'; + el.style.height = surfaceElement.clientHeight + 'px'; + el.style.overflow = 'hidden'; + el.style.position = 'absolute'; + surfaceElement.appendChild(el); + + this.element_ = el; + }; + + var contextPrototype = CanvasRenderingContext2D_.prototype; + contextPrototype.clearRect = function() { + this.element_.innerHTML = ""; + this.currentPath_ = []; + }; + + contextPrototype.beginPath = function() { + // TODO: Branch current matrix so that save/restore has no effect + // as per safari docs. + + this.currentPath_ = []; + }; + + contextPrototype.moveTo = function(aX, aY) { + this.currentPath_.push({type: "moveTo", x: aX, y: aY}); + }; + + contextPrototype.lineTo = function(aX, aY) { + this.currentPath_.push({type: "lineTo", x: aX, y: aY}); + }; + + contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, + aCP2x, aCP2y, + aX, aY) { + this.currentPath_.push({type: "bezierCurveTo", + cp1x: aCP1x, + cp1y: aCP1y, + cp2x: aCP2x, + cp2y: aCP2y, + x: aX, + y: aY}); + }; + + contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { + // VML's qb produces different output to Firefox's + // FF's behaviour seems to have changed in 1.5.0.1, check this + this.bezierCurveTo(aCPx, aCPy, aCPx, aCPy, aX, aY); + }; + + contextPrototype.arc = function(aX, aY, aRadius, + aStartAngle, aEndAngle, aClockwise) { + if (!aClockwise) { + var t = aStartAngle; + aStartAngle = aEndAngle; + aEndAngle = t; + } + + aRadius *= 10; + + var xStart = aX + (mc(aStartAngle) * aRadius) - 5; + var yStart = aY + (ms(aStartAngle) * aRadius) - 5; + + var xEnd = aX + (mc(aEndAngle) * aRadius) - 5; + var yEnd = aY + (ms(aEndAngle) * aRadius) - 5; + + this.currentPath_.push({type: "arc", + x: aX, + y: aY, + radius: aRadius, + xStart: xStart, + yStart: yStart, + xEnd: xEnd, + yEnd: yEnd}); + + }; + + contextPrototype.rect = function(aX, aY, aWidth, aHeight) { + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + }; + + contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { + // Will destroy any existing path (same as FF behaviour) + this.beginPath(); + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.stroke(); + }; + + contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { + // Will destroy any existing path (same as FF behaviour) + this.beginPath(); + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.fill(); + }; + + contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { + var gradient = new CanvasGradient_("gradient"); + return gradient; + }; + + contextPrototype.createRadialGradient = function(aX0, aY0, + aR0, aX1, + aY1, aR1) { + var gradient = new CanvasGradient_("gradientradial"); + gradient.radius1_ = aR0; + gradient.radius2_ = aR1; + gradient.focus_.x = aX0; + gradient.focus_.y = aY0; + return gradient; + }; + + contextPrototype.drawImage = function (image, var_args) { + var dx, dy, dw, dh, sx, sy, sw, sh; + var w = image.width; + var h = image.height; + + if (arguments.length == 3) { + dx = arguments[1]; + dy = arguments[2]; + sx = sy = 0; + sw = dw = w; + sh = dh = h; + } else if (arguments.length == 5) { + dx = arguments[1]; + dy = arguments[2]; + dw = arguments[3]; + dh = arguments[4]; + sx = sy = 0; + sw = w; + sh = h; + } else if (arguments.length == 9) { + sx = arguments[1]; + sy = arguments[2]; + sw = arguments[3]; + sh = arguments[4]; + dx = arguments[5]; + dy = arguments[6]; + dw = arguments[7]; + dh = arguments[8]; + } else { + throw "Invalid number of arguments"; + } + + var d = this.getCoords_(dx, dy); + + var w2 = (sw / 2); + var h2 = (sh / 2); + + var vmlStr = []; + + // For some reason that I've now forgotten, using divs didn't work + vmlStr.push(' ' , + '', + ''); + + this.element_.insertAdjacentHTML("BeforeEnd", + vmlStr.join("")); + }; + + contextPrototype.stroke = function(aFill) { + var lineStr = []; + var lineOpen = false; + var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); + var color = a[0]; + var opacity = a[1] * this.globalAlpha; + + lineStr.push(' max.x) { + max.x = c.x; + } + if (min.y == null || c.y < min.y) { + min.y = c.y; + } + if (max.y == null || c.y > max.y) { + max.y = c.y; + } + } + } + lineStr.push(' ">'); + + if (typeof this.fillStyle == "object") { + var focus = {x: "50%", y: "50%"}; + var width = (max.x - min.x); + var height = (max.y - min.y); + var dimension = (width > height) ? width : height; + + focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%"; + focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%"; + + var colors = []; + + // inside radius (%) + if (this.fillStyle.type_ == "gradientradial") { + var inside = (this.fillStyle.radius1_ / dimension * 100); + + // percentage that outside radius exceeds inside radius + var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside; + } else { + var inside = 0; + var expansion = 100; + } + + var insidecolor = {offset: null, color: null}; + var outsidecolor = {offset: null, color: null}; + + // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie + // won't interpret it correctly + this.fillStyle.colors_.sort(function (cs1, cs2) { + return cs1.offset - cs2.offset; + }); + + for (var i = 0; i < this.fillStyle.colors_.length; i++) { + var fs = this.fillStyle.colors_[i]; + + colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ","); + + if (fs.offset > insidecolor.offset || insidecolor.offset == null) { + insidecolor.offset = fs.offset; + insidecolor.color = fs.color; + } + + if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) { + outsidecolor.offset = fs.offset; + outsidecolor.color = fs.color; + } + } + colors.pop(); + + lineStr.push(''); + } else if (aFill) { + lineStr.push(''); + } else { + lineStr.push( + '' + ); + } + + lineStr.push(""); + + this.element_.insertAdjacentHTML("beforeEnd", lineStr.join("")); + + this.currentPath_ = []; + }; + + contextPrototype.fill = function() { + this.stroke(true); + } + + contextPrototype.closePath = function() { + this.currentPath_.push({type: "close"}); + }; + + /** + * @private + */ + contextPrototype.getCoords_ = function(aX, aY) { + return { + x: 10 * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - 5, + y: 10 * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - 5 + } + }; + + contextPrototype.save = function() { + var o = {}; + copyState(this, o); + this.aStack_.push(o); + this.mStack_.push(this.m_); + this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); + }; + + contextPrototype.restore = function() { + copyState(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop(); + }; + + contextPrototype.translate = function(aX, aY) { + var m1 = [ + [1, 0, 0], + [0, 1, 0], + [aX, aY, 1] + ]; + + this.m_ = matrixMultiply(m1, this.m_); + }; + + contextPrototype.rotate = function(aRot) { + var c = mc(aRot); + var s = ms(aRot); + + var m1 = [ + [c, s, 0], + [-s, c, 0], + [0, 0, 1] + ]; + + this.m_ = matrixMultiply(m1, this.m_); + }; + + contextPrototype.scale = function(aX, aY) { + var m1 = [ + [aX, 0, 0], + [0, aY, 0], + [0, 0, 1] + ]; + + this.m_ = matrixMultiply(m1, this.m_); + }; + + /******** STUBS ********/ + contextPrototype.clip = function() { + // TODO: Implement + }; + + contextPrototype.arcTo = function() { + // TODO: Implement + }; + + contextPrototype.createPattern = function() { + return new CanvasPattern_; + }; + + // Gradient / Pattern Stubs + function CanvasGradient_(aType) { + this.type_ = aType; + this.radius1_ = 0; + this.radius2_ = 0; + this.colors_ = []; + this.focus_ = {x: 0, y: 0}; + } + + CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { + aColor = processStyle(aColor); + this.colors_.push({offset: 1-aOffset, color: aColor}); + }; + + function CanvasPattern_() {} + + // set up externs + G_vmlCanvasManager = G_vmlCanvasManager_; + CanvasRenderingContext2D = CanvasRenderingContext2D_; + CanvasGradient = CanvasGradient_; + CanvasPattern = CanvasPattern_; + +})(); + +} // if diff --git a/pub/qdb/res/themes/default/js/graph.js b/pub/qdb/res/themes/default/js/graph.js new file mode 100644 index 0000000..0903618 --- /dev/null +++ b/pub/qdb/res/themes/default/js/graph.js @@ -0,0 +1,594 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# graph.js # +# Turns graph data into nice graphs # +############################################################################### +# $Id:: graph.js 308 2007-02-09 01:31:25Z ceetee $ # +############################################################################### +*/ + +var graphConfig = new Array(); +graphConfig["bar_chart_values"] = 5; +graphConfig["pie_chart_radius"] = 175; +graphConfig["pie_chart_extrusion"] = 5; +graphConfig["pie_chart_colors"] = new Array( + "#DCDCDC", "#CCCCCC", "#BCBCBC", "#ACACAC" +); +graphConfig["pie_chart_border_width"] = 1; +graphConfig["pie_chart_border_color"] = "#ACACAC"; +graphConfig["pie_chart_random_rotation"] = false; +graphConfig["ogive_values"] = 5; +graphConfig["ogive_chart_width"] = 660; +graphConfig["ogive_chart_height"] = 360; +graphConfig["ogive_chart_color"] = "#DCDCDC"; +graphConfig["ogive_average_color"] = "#BCBCBC"; +graphConfig["decimal_point_is_comma"] = false; +graphConfig["average_decimal_count"] = 2; + +function checkForGraphs () { + var dls = document.getElementsByTagName("dl"); + for (var i = dls.length - 1; i >= 0; i--) { + var dl = dls[i]; + var func; + if (hasClassName(dl, "bar-chart-data")) { + func = createBarChart; + } + else if (hasClassName(dl, "pie-chart-data")) { + func = createPieChart; + } + else if (hasClassName(dl, "ogive-data")) { + func = createOgive; + } + if (func) { + var chartData = extractChartData(dl); + var samples = extractChartLabelCount(dl); + var node = func(dl, chartData, samples); + node.id = dl.id; + } + } +} + +function createBarChart (sourceNode, chartData, samples) { + var div = document.createElement("div"); + div.className = "chart bar-chart"; + var graph = document.createElement("div"); + graph.className = "bar-chart-graph"; + div.appendChild(graph); + var max = 0; + for (var i = 0; i < chartData.length; i++) { + var value = chartData[i][1]; + if (value > max) { + max = value; + } + } + createChartPane(div, graph, chartData, samples, + graphConfig["bar_chart_values"], 0, max); + var barWidth = 100 / chartData.length; + var total = 0; + for (var i = 0; i < chartData.length; i++) { + var data = chartData[i]; + var text = data[0]; + var value = data[1]; + var column = document.createElement("div"); + column.className = "bar-chart-column"; + column.style.left = i * barWidth + "%"; + column.style.width = barWidth + "%"; + column.title = data[0] + " " + + String.fromCharCode(0x2192) + " " + data[1]; + if (value > 0) { + var bar = document.createElement("div"); + bar.className = "bar-chart-bar"; + bar.style.height = Math.round(100 * value / max) + "%"; + var innerBar = document.createElement("div"); + innerBar.className = "bar-chart-inner-bar"; + bar.appendChild(innerBar); + column.appendChild(bar); + } + graph.appendChild(column); + total += value; + } + var avg = total / chartData.length; + var avgDiv = document.createElement("div"); + avgDiv.className = "bar-chart-average-container"; + avgDiv.style.top = (100 - (100 * avg / max)) + "%"; + var stdDevTotal = 0; + for (var i = 0; i < chartData.length; i++) { + var temp = chartData[i][1] - avg; + stdDevTotal += temp * temp; + } + var stdDev = roundToDecimals( + Math.sqrt(stdDevTotal / chartData.length), + graphConfig["average_decimal_count"]); + var avgText = roundToDecimals(avg, graphConfig["average_decimal_count"]); + if (graphConfig["decimal_point_is_comma"]) { + avgText = ("" + avgText).replace(".", ","); + stdDev = ("" + stdDev).replace(".", ","); + } + var avgSpan = document.createElement("span"); + avgSpan.appendChild(document.createTextNode(avgText)); + avgSpan.className = "bar-chart-average"; + var stdDevSpan = document.createElement("span"); + stdDevSpan.appendChild( + document.createTextNode(String.fromCharCode(0x00B1) + " " + stdDev)); + stdDevSpan.className = "bar-chart-standard-deviation"; + var avgTextDiv = document.createElement("div"); + avgTextDiv.appendChild(avgSpan); + avgTextDiv.appendChild(document.createTextNode(" ")); + avgTextDiv.appendChild(stdDevSpan); + avgDiv.appendChild(avgTextDiv); + graph.appendChild(avgDiv); + sourceNode.parentNode.replaceChild(div, sourceNode); + return div; +} + +function createPieChart (sourceNode, chartData) { + var cnv = document.createElement("canvas"); + var rad = graphConfig["pie_chart_radius"]; + var ext = graphConfig["pie_chart_extrusion"]; + var side = (rad + ext) * 2; + cnv.width = cnv.height = side; + cnv.style.width = cnv.style.height = side + "px"; + var graph = document.createElement("div"); + graph.appendChild(cnv); + cnv = ensureCanvas(cnv); + if (!cnv) { + return createBarChart(sourceNode, chartData); + } + var div = document.createElement("div"); + div.className = "chart pie-chart"; + sourceNode.parentNode.replaceChild(div, sourceNode); + graph.className = "pie-chart-graph"; + var legend = document.createElement("dl"); + legend.className = "pie-chart-legend"; + div.appendChild(graph); + div.appendChild(legend); + drawPieChart(cnv, legend, chartData); + return div; +} + +function drawPieChart (canvas, legend, data) { + var ctx = canvas.getContext("2d"); + var total = 0; + for (var i = 0; i < data.length; i++) { + total += data[i][1]; + } + var runningTotal = 0; + var rad = graphConfig["pie_chart_radius"]; + var ext = graphConfig["pie_chart_extrusion"]; + var xCenter = rad + ext; + var yCenter = rad + ext; + var radius = rad; + var colors = graphConfig["pie_chart_colors"]; + var stroke; + if (graphConfig["pie_chart_border_width"]) { + ctx.strokeStyle = graphConfig["pie_chart_border_color"]; + var w = graphConfig["pie_chart_border_width"]; + ctx.lineWidth = w; + radius -= 2 * w; + stroke = true; + } + else { + stroke = false; + } + var initialAngle = (graphConfig["pie_chart_random_rotation"] + ? Math.round(2 * Math.PI * Math.random()) + : - Math.PI / 2); + for (var i = 0; i < data.length; i++) { + var name = data[i][0]; + var value = data[i][1]; + var color = colors[i % colors.length]; + if (value > 0) { + var startAngle = runningTotal / total * 2 * Math.PI + initialAngle; + runningTotal += value; + var endAngle = runningTotal / total * 2 * Math.PI + initialAngle; + var diff = (startAngle + endAngle) / 2; + var x = xCenter + Math.cos(diff) * ext; + var y = yCenter + Math.sin(diff) * ext; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.arc(x, y, radius, startAngle, endAngle, false); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.fill(); + if (stroke) { + // Safari doesn't like it when we don't recreate the path + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.arc(x, y, radius, startAngle, endAngle, false); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.stroke(); + } + } + var block = document.createElement("div"); + block.style.backgroundColor = color; + var dt = document.createElement("dt"); + dt.appendChild(block); + legend.appendChild(dt); + var dd = document.createElement("dd"); + var l = document.createElement("span"); + l.className = "label"; + l.appendChild(document.createTextNode(name)); + dd.appendChild(l); + var v = document.createElement("span"); + v.className = "value"; + v.appendChild(document.createTextNode(value)); + dd.appendChild(v); + var p = document.createElement("span"); + p.className = "percentage"; + p.appendChild(document.createTextNode( + (value == 0 ? 0 : Math.round(100 * value / total)) + "%")); + dd.appendChild(p); + legend.appendChild(dd); + } +} + +function createOgive (sourceNode, chartData, samples) { + var cnv = document.createElement("canvas"); + cnv.width = graphConfig["ogive_chart_width"]; + cnv.height = graphConfig["ogive_chart_height"]; + cnv.style.width = graphConfig["ogive_chart_width"] + "px"; + cnv.style.height = graphConfig["ogive_chart_height"] + "px"; + var graph = document.createElement("div"); + graph.appendChild(cnv); + cnv = ensureCanvas(cnv); + if (!cnv) { + var total = 0; + for (var i = 0; i < chartData.length; i++) { + var old = chartData[i][1]; + chartData[i][1] += total; + total += old; + } + return createBarChart(sourceNode, chartData, samples); + } + var div = document.createElement("div"); + div.className = "chart ogive"; + div.appendChild(graph); + sourceNode.parentNode.replaceChild(div, sourceNode); + graph.className = "ogive-graph"; + var ignoreFirst = extractOgiveIgnoreCount(sourceNode); + var totalIgnored = 0; + if (ignoreFirst) { + for (var i = 0; i < ignoreFirst; i++) { + totalIgnored += chartData[i][1]; + } + var newChartData = new Array(); + for (var i = ignoreFirst; i < chartData.length; i++) { + newChartData[i - ignoreFirst] = chartData[i]; + } + chartData = newChartData; + } + var chartCumulData = new Array(); + var total = 0; + for (var i = 0; i < chartData.length; i++) { + total += chartData[i][1]; + chartCumulData[i] = total; + } + var width = graphConfig["ogive_chart_width"]; + var max = chartCumulData[chartCumulData.length - 1]; + var xy = new Array(); + for (var i = 0; i < chartCumulData.length; i++) { + xy[i + 1] = chartCumulData[i]; + } + var regrParam = powerRegression(xy); + var a = regrParam[0]; + var b = regrParam[1]; + var drawAvg; + if (!isNaN(a) && !isNaN(b)) { + var chartAvgData = new Array(); + for (var x = 0; x < width; x++) { + var i = x / width * chartData.length; + chartAvgData[x] = a * Math.pow(i, b); + if (chartAvgData[x] > max) { + max = chartAvgData[x]; + } + } + drawAvg = true; + } + else { + drawAvg = false; + } + createChartPane(div, graph, chartData, samples, + graphConfig["ogive_values"], 0, max, totalIgnored); + var scale = graphConfig["ogive_chart_height"] / max; + drawOgive(cnv, chartCumulData, false, scale); + if (drawAvg) { + drawOgive(cnv, chartAvgData, true, scale); + var equation = document.createElement("div"); + equation.className = "regression-equation"; + equation.appendChild(document.createTextNode("y = ")); + var aRounded = roundToDecimals(a, 2); + var bRounded = roundToDecimals(b, 2); + if (graphConfig["decimal_point_is_comma"]) { + aRounded = ("" + aRounded).replace(".", ","); + bRounded = ("" + bRounded).replace(".", ","); + } + if (aRounded != 1) { + var aTxt = document.createElement("span"); + aTxt.appendChild(document.createTextNode(aRounded)); + equation.appendChild(aTxt); + } + equation.appendChild(document.createTextNode("x")); + if (bRounded != 1) { + var bTxt = document.createElement("sup"); + bTxt.appendChild(document.createTextNode(bRounded)); + equation.appendChild(bTxt); + } + equation.style.position = "absolute"; + var xPos = 3/4; + var xPad = 15; + var yBase = Math.round(chartAvgData[Math.round(xPos * chartAvgData.length)] + / max * graphConfig["ogive_chart_height"]); + /*if (b < 1) {*/ + equation.style.left = xPad + Math.round( + xPos * graphConfig["ogive_chart_width"]) + "px"; + equation.style.bottom = yBase + "px"; + /*} + else { + equation.style.right = xPad + Math.round( + (1 - xPos) * graphConfig["ogive_chart_width"]) + "px"; + equation.style.top = graphConfig["ogive_chart_height"] - yBase + "px"; + }*/ + div.appendChild(equation); + } + return div; +} + +function drawOgive (canvas, chartData, avg, yScale) { + var ctx = canvas.getContext("2d"); + var graphWidth = graphConfig["ogive_chart_width"]; + var graphHeight = graphConfig["ogive_chart_height"]; + if (avg) { + ctx.strokeStyle = graphConfig["ogive_average_color"]; + ctx.lineWidth = 1; + } + else { + ctx.fillStyle = graphConfig["ogive_chart_color"]; + } + var xScale = graphWidth / chartData.length; + var x0 = 0; + var y0 = graphHeight; + ctx.beginPath(); + var notFirst = false; + for (var i = 0; i < chartData.length; i++) { + if (chartData[i] == null) continue; + var x = Math.round(x0 + (i + 1) * xScale); + var y = Math.round(y0 - chartData[i] * yScale); + if (notFirst) { + ctx.lineTo(x, y); + } + else { + ctx.moveTo(x, y); + notFirst = true; + } + } + if (avg) { + ctx.stroke(); + } + else { + ctx.lineTo(graphWidth, graphHeight); + ctx.lineTo(x0, y0); + ctx.fill(); + } + return yScale; +} + +function extractChartData (dl) { + var name, label; + var data = new Array(); + for (var i = 0; i < dl.childNodes.length; i++) { + var child = dl.childNodes[i]; + var eln = child.nodeName.toLowerCase(); + switch (eln) { + case "dt": + name = child.firstChild.nodeValue; + label = (child.title ? child.title : null); + break; + case "dd": + var value = parseInt(child.firstChild.nodeValue); + data.push(new Array(name, value, label)); + break; + } + } + return data; +} + +function extractChartLabelCount (dl) { + var result = extractChartParameter(dl, "label-count"); + if (result == null) return 0; + return result; +} + +function extractOgiveIgnoreCount (dl) { + var result = extractChartParameter(dl, "ignore-first"); + if (result == null) return 0; + return result; +} + +function extractChartParameter (dl, name) { + name += "-"; + var classNames = dl.className.split(/\s+/); + for (var i = 0; i < classNames.length; i++) { + var className = classNames[i]; + if (className.indexOf(name) == 0) { + return parseInt(className.substring(name.length)); + } + } + return null; +} + +function createChartPane (div, graph, chartData, samples, values, min, max, delta) { + if (!delta) delta = 0; + div.appendChild(createChartValues(min + delta, max + delta, values, graph, delta)); + div.appendChild(createChartLabels(chartData, samples)); +} + +function createChartValues (min, max, count, graph, delta) { + var values = document.createElement("div"); + values.className = "chart-values"; + var top = createChartValue(max); + top.style.top = "0"; + top.style.marginTop = "0"; + var bottom = createChartValue(min); + bottom.style.bottom = "0"; + bottom.style.marginBottom = "0"; + values.appendChild(top); + var step = (max - min) / (count - 1); + for (var i = 1; i < count - 1; i++) { + var val = Math.round(step * i) + delta; + var perc = 100 / (count - 1) * i + "%"; + var v = createChartValue(val); + v.style.bottom = perc; + values.appendChild(v); + var line = document.createElement("div"); + line.className = "chart-line"; + line.style.top = perc; + graph.appendChild(line); + } + var line = document.createElement("div"); + line.className = "chart-line"; + line.style.top = "0"; + graph.appendChild(line); + values.appendChild(bottom); + return values; +} + +function createChartValue (value) { + var v = document.createElement("div"); + v.className = "chart-value"; + v.appendChild(document.createTextNode(Math.round(value))); + return v; +} + +function createChartLabels (chartData, samples) { + var labels = document.createElement("div"); + labels.className = "chart-labels"; + var sample = (samples > 0 && chartData.length > samples); + var barWidth = 100 / chartData.length; + var sampleInterval, labelWidth; + if (sample) { + if (chartData.length < samples * 2) { + samples = Math.ceil(chartData.length / 2); + } + sampleInterval = chartData.length / samples; + labelWidth = 100 / samples; + var firstLabel = createChartLabel( + chartData[0], 0, labelWidth + "%", + "left"); + labels.appendChild(firstLabel); + var lastLabel = createChartLabel( + chartData[chartData.length - 1], + (100 - labelWidth) + "%", labelWidth + "%", + "right"); + labels.appendChild(lastLabel); + } + else { + labelWidth = barWidth; + } + var labelCount = 0; + for (var i = 0; i < chartData.length; i++) { + var data = chartData[i]; + if (!sample || Math.floor((labelCount + 0.5) * sampleInterval) == i) { + if (sample && (++labelCount == 1 || labelCount == samples)) continue; + var left = ((i + 0.5) * barWidth - labelWidth / 2) + "%"; + var label = createChartLabel(data, left, labelWidth + "%", "center"); + labels.appendChild(label); + } + } + return labels; +} + +function createChartLabel (data, position, width, align) { + var label = document.createElement("div"); + label.className = "chart-label"; + label.style.left = position; + label.style.width = width; + var innerLabel = document.createElement("div"); + innerLabel.className = "chart-inner-label"; + var text = (data[2] != null ? data[2] : data[0]); + innerLabel.appendChild(document.createTextNode(text)); + innerLabel.style.textAlign = align; + label.appendChild(innerLabel); + return label; +} + +function ensureCanvas (canvas) { + if (!canvas.getContext && useExCanvas()) { + canvas = G_vmlCanvasManager.initElement(canvas); + } + return (canvas.getContext ? canvas : null); +} + +function useExCanvas () { + return (typeof G_vmlCanvasManager != "undefined"); +} + +function hasClassName (element, className) { + return element.className.match(new RegExp("\\b" + className + "\\b")); +} + +function powerRegression (data) { + var n = data.length; + var sumX = 0; + var sumY = 0; + var sumXX = 0; + var sumXY = 0; + for (i in data) { + var x = Math.log(i); + var y = Math.log(data[i]); + sumX += x; + sumY += y; + sumXX += x * x; + sumXY += x * y; + } + var sxx = sumXX - (sumX * sumX) / n; + var sxy = sumXY - (sumX * sumY) / n; + var xbar = sumX / n; + var ybar = sumY / n; + var b = sxy / sxx; + var a = Math.pow(Math.exp(1), ybar - b * xbar); + var result = new Array(); + result[0] = a; + result[1] = b; + return result; +} + +function roundToDecimals (number, decimals) { + var factor = Math.pow(10, decimals); + return Math.round(number * factor) / factor; +} + +function addOnloadFunction (f) { + if (window.onload != null) { + var old = window.onload; + window.onload = function (e) { + old(e); + f(); + }; + } + else { + window.onload = f; + } +} + +addOnloadFunction(checkForGraphs); diff --git a/pub/qdb/res/themes/default/js/live_rating.js b/pub/qdb/res/themes/default/js/live_rating.js new file mode 100644 index 0000000..a9a63fa --- /dev/null +++ b/pub/qdb/res/themes/default/js/live_rating.js @@ -0,0 +1,230 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# live_rating.js # +# Enables rating and reporting of quotes without leaving the page # +############################################################################### +# $Id:: live_rating.js 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +var useBlinking = true; +var confirmationTimeout = 3000; +var errorTimeout = 10000; +var connectionTimeout = 30000; +var pollingInterval = 1000; +var okText = (/MSIE/.test(navigator.userAgent) && !window.opera + ? "OK" : String.fromCharCode(0x2713)); + +var requests = new Array(); + +function QuoteActionRequest (url, id, isReport) { + var t = getTimestamp(); + var req = this; + this.id = id; + if (requests[this.id]) requests[this.id].verbose = false; + requests[this.id] = this; + this.url = url + "&output=xml&ignore=" + t; + this.isReport = isReport; + this.verbose = true; + this.resultField = document.getElementById('quote-live-vote-result-' + id); + this.setResult(locale["processing"], true); + this.ajax = getAjaxObject(); + this.ajax.onreadystatechange = function() { readyStateChanged(req); }; + this.startTime = t; + this.interval = setInterval( + function () { checkRequestTime(req); }, pollingInterval); + this.ajax.open("POST", this.url, true); + this.ajax.send(""); +} + +QuoteActionRequest.prototype.setResult = function (text, blink, timeout) { + if (!this.verbose) return; + if (this.fadeTimeout) { + clearTimeout(this.fadeTimeout); + this.fadeTimeout = null; + } + setText(this.resultField, text); + if (useBlinking) + this.resultField.style.textDecoration = blink ? "blink" : ""; + var field = this.resultField; + if (timeout) + this.fadeTimeout = setTimeout( + function () { setText(field, ""); }, timeout); +}; + +function sendRating (anchor, id) { + if (!ajaxSupported()) return true; + new QuoteActionRequest(anchor.href, id, false); + var other; + if (anchor.id.indexOf("down") >= 0) { + other = document.getElementById(anchor.id.replace("down", "up")); + } + else { + other = document.getElementById(anchor.id.replace("up", "down")); + } + addClassName(anchor, "casted-vote"); + removeClassName(other, "casted-vote"); + return false; +} + +function sendReport (anchor, id) { + if (!ajaxSupported()) return true; + new QuoteActionRequest(anchor.href, id, true); + return false; +} + +function readyStateChanged (req) { + if (req.ajax.readyState != 4) return; + if (req.ajax.status == 200) { + if (req.isReport) + reportRequestCompleted(req, processXML(req.ajax.responseXML)); + else + ratingRequestCompleted(req, processXML(req.ajax.responseXML)); + } + else + req.setResult(locale["error"], false, errorTimeout); + removeRequest(req); +} + +function ratingRequestCompleted (req, result) { + if (result) { + switch (result["status"]) { + case "1": + req.setResult(""); + setText(document.getElementById('quote-rating-' + req.id), + result["rating"]); + var vc = document.getElementById('quote-vote-count-' + req.id); + if (vc) setText(vc, result["votes"]); + req.setResult(okText, false, confirmationTimeout); + break; + case "2": + req.setResult(locale["error"], false, errorTimeout); + if (req.verbose) alert(locale["already_rated_text"]); + break; + case "3": + req.setResult(locale["error"], false, errorTimeout); + if (req.verbose) alert(locale["limit_exceeded_text"]); + break; + case "4": + req.setResult(locale["error"], false, errorTimeout); + if (req.verbose) alert(locale["quote_not_found_text"]); + break; + case "5": + req.setResult(locale["error"], false, errorTimeout); + if (req.verbose) alert(locale["session_required_text"]); + break; + default: + req.setResult(locale["error"], false, errorTimeout); + break; + } + } + else + req.setResult(locale["error"], false, errorTimeout); +} + +function reportRequestCompleted (req, result) { + if (result) { + switch (result["status"]) { + case "1": + req.setResult(""); + document.getElementById('quote-' + req.id).className + += " flagged"; + var rep = document.getElementById('quote-report-' + req.id); + var el = document.createElement("span"); + var at = document.createAttribute("class"); + at.value = "quote-flagged"; + el.setAttributeNode(at); + el.appendChild(document.createTextNode( + "[" + locale["flagged"] + "]")); + rep.parentNode.replaceChild(el, rep); + req.setResult(okText, false, confirmationTimeout); + break; + case "5": + req.setResult(locale["error"], false, errorTimeout); + if (req.verbose) alert(locale["session_required_text"]); + break; + default: + req.setResult(locale["error"], false, errorTimeout); + break; + } + } + else + req.setResult(locale["error"], false, errorTimeout); +} + +function removeRequest (req) { + requests[req.id] = null; + clearInterval(req.interval); +} + +function checkRequestTime (req) { + if (getTimestamp() - req.startTime >= connectionTimeout) { + clearInterval(req.interval); + req.setResult(locale["error"], false, errorTimeout); + if (req.verbose) alert(locale["timeout_text"]); + } +} + +function processXML (xml) { + if (!xml || !xml.childNodes + || xml.childNodes[xml.childNodes.length - 1].nodeName != "result") + return null; + var a = new Array(); + var root = xml.childNodes[xml.childNodes.length - 1]; + for (var i = 0; i < root.childNodes.length; i++) { + var node = root.childNodes[i]; + a[node.nodeName] = node.firstChild.nodeValue; + } + return a; +} + +function getTimestamp () { + return (new Date()).getTime(); +} + +function setText (element, text) { + if (element.firstChild) + element.firstChild.nodeValue = text; + else + element.appendChild(document.createTextNode(text)); +} + +function addClassName (element, className) { + element.className += " " + className; +} + +function removeClassName (element, className) { + var c = getClassNames(element); + if (c.length == 0) return; + var newCN = c[0]; + for (var i = 1; i < c.length; c++) { + if (c[i] == className) continue; + newCN += " " + c[i]; + } + element.className = newCN; +} + +function getClassNames (element) { + if (element.className == null || element.className.length == 0) + return new Array(); + return element.className.split(/ +/); +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/slider/boxsizing.htc b/pub/qdb/res/themes/default/js/slider/boxsizing.htc new file mode 100644 index 0000000..fbeaa56 --- /dev/null +++ b/pub/qdb/res/themes/default/js/slider/boxsizing.htc @@ -0,0 +1,157 @@ + + + + + \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/slider/range.js b/pub/qdb/res/themes/default/js/slider/range.js new file mode 100644 index 0000000..54f9a48 --- /dev/null +++ b/pub/qdb/res/themes/default/js/slider/range.js @@ -0,0 +1,132 @@ +/*----------------------------------------------------------------------------\ +| Range Class | +|-----------------------------------------------------------------------------| +| Created by Erik Arvidsson | +| (http://webfx.eae.net/contact.html#erik) | +| For WebFX (http://webfx.eae.net/) | +|-----------------------------------------------------------------------------| +| Used to model the data used when working with sliders, scrollbars and | +| progress bars. Based on the ideas of the javax.swing.BoundedRangeModel | +| interface defined by Sun for Java; http://java.sun.com/products/jfc/ | +| swingdoc-api-1.0.3/com/sun/java/swing/BoundedRangeModel.html | +|-----------------------------------------------------------------------------| +| Copyright (c) 2002, 2005, 2006 Erik Arvidsson | +|-----------------------------------------------------------------------------| +| Licensed under the Apache License, Version 2.0 (the "License"); you may not | +| use this file except in compliance with the License. You may obtain a copy | +| of the License at http://www.apache.org/licenses/LICENSE-2.0 | +| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | +| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | +| License for the specific language governing permissions and limitations | +| under the License. | +|-----------------------------------------------------------------------------| +| 2002-10-14 | Original version released | +| 2005-10-27 | Use Math.round instead of Math.floor | +| 2006-05-28 | Changed license to Apache Software License 2.0. | +|-----------------------------------------------------------------------------| +| Created 2002-10-14 | All changes are in the log above. | Updated 2006-05-28 | +\----------------------------------------------------------------------------*/ + + +function Range() { + this._value = 0; + this._minimum = 0; + this._maximum = 100; + this._extent = 0; + + this._isChanging = false; +} + +Range.prototype.setValue = function (value) { + value = Math.round(parseFloat(value)); + if (isNaN(value)) return; + if (this._value != value) { + if (value + this._extent > this._maximum) + this._value = this._maximum - this._extent; + else if (value < this._minimum) + this._value = this._minimum; + else + this._value = value; + if (!this._isChanging && typeof this.onchange == "function") + this.onchange(); + } +}; + +Range.prototype.getValue = function () { + return this._value; +}; + +Range.prototype.setExtent = function (extent) { + if (this._extent != extent) { + if (extent < 0) + this._extent = 0; + else if (this._value + extent > this._maximum) + this._extent = this._maximum - this._value; + else + this._extent = extent; + if (!this._isChanging && typeof this.onchange == "function") + this.onchange(); + } +}; + +Range.prototype.getExtent = function () { + return this._extent; +}; + +Range.prototype.setMinimum = function (minimum) { + if (this._minimum != minimum) { + var oldIsChanging = this._isChanging; + this._isChanging = true; + + this._minimum = minimum; + + if (minimum > this._value) + this.setValue(minimum); + if (minimum > this._maximum) { + this._extent = 0; + this.setMaximum(minimum); + this.setValue(minimum) + } + if (minimum + this._extent > this._maximum) + this._extent = this._maximum - this._minimum; + + this._isChanging = oldIsChanging; + if (!this._isChanging && typeof this.onchange == "function") + this.onchange(); + } +}; + +Range.prototype.getMinimum = function () { + return this._minimum; +}; + +Range.prototype.setMaximum = function (maximum) { + if (this._maximum != maximum) { + var oldIsChanging = this._isChanging; + this._isChanging = true; + + this._maximum = maximum; + + if (maximum < this._value) + this.setValue(maximum - this._extent); + if (maximum < this._minimum) { + this._extent = 0; + this.setMinimum(maximum); + this.setValue(this._maximum); + } + if (maximum < this._minimum + this._extent) + this._extent = this._maximum - this._minimum; + if (maximum < this._value + this._extent) + this._extent = this._maximum - this._value; + + this._isChanging = oldIsChanging; + if (!this._isChanging && typeof this.onchange == "function") + this.onchange(); + } +}; + +Range.prototype.getMaximum = function () { + return this._maximum; +}; diff --git a/pub/qdb/res/themes/default/js/slider/slider.js b/pub/qdb/res/themes/default/js/slider/slider.js new file mode 100644 index 0000000..865a625 --- /dev/null +++ b/pub/qdb/res/themes/default/js/slider/slider.js @@ -0,0 +1,489 @@ +/*----------------------------------------------------------------------------\ +| Slider 1.02 | +|-----------------------------------------------------------------------------| +| Created by Erik Arvidsson | +| (http://webfx.eae.net/contact.html#erik) | +| For WebFX (http://webfx.eae.net/) | +|-----------------------------------------------------------------------------| +| A slider control that degrades to an input control for non supported | +| browsers. | +|-----------------------------------------------------------------------------| +| Copyright (c) 2002, 2003, 2006 Erik Arvidsson | +|-----------------------------------------------------------------------------| +| Licensed under the Apache License, Version 2.0 (the "License"); you may not | +| use this file except in compliance with the License. You may obtain a copy | +| of the License at http://www.apache.org/licenses/LICENSE-2.0 | +| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | +| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | +| License for the specific language governing permissions and limitations | +| under the License. | +|-----------------------------------------------------------------------------| +| Dependencies: timer.js - an OO abstraction of timers | +| range.js - provides the data model for the slider | +| winclassic.css or any other css file describing the look | +|-----------------------------------------------------------------------------| +| 2002-10-14 | Original version released | +| 2003-03-27 | Added a test in the constructor for missing oElement arg | +| 2003-11-27 | Only use mousewheel when focused | +| 2006-05-28 | Changed license to Apache Software License 2.0. | +|-----------------------------------------------------------------------------| +| Created 2002-10-14 | All changes are in the log above. | Updated 2006-05-28 | +\----------------------------------------------------------------------------*/ + +Slider.isSupported = typeof document.createElement != "undefined" && + typeof document.documentElement != "undefined" && + typeof document.documentElement.offsetWidth == "number"; + + +function Slider(oElement, oInput, sOrientation) { + if (!oElement) return; + this._orientation = sOrientation || "horizontal"; + this._range = new Range(); + this._range.setExtent(0); + this._blockIncrement = 10; + this._unitIncrement = 1; + this._timer = new Timer(100); + + + if (Slider.isSupported && oElement) { + + this.document = oElement.ownerDocument || oElement.document; + + this.element = oElement; + this.element.slider = this; + this.element.unselectable = "on"; + + // add class name tag to class name + this.element.className = this._orientation + " " + this.classNameTag + " " + this.element.className; + + // create line + this.line = this.document.createElement("DIV"); + this.line.className = "line"; + this.line.unselectable = "on"; + this.line.appendChild(this.document.createElement("DIV")); + this.element.appendChild(this.line); + + // create handle + this.handle = this.document.createElement("DIV"); + this.handle.className = "handle"; + this.handle.unselectable = "on"; + this.handle.appendChild(this.document.createElement("DIV")); + this.handle.firstChild.appendChild( + this.document.createTextNode(String.fromCharCode(160))); + this.element.appendChild(this.handle); + } + + this.input = oInput; + + // events + var oThis = this; + this._range.onchange = function () { + oThis.recalculate(); + if (typeof oThis.onchange == "function") + oThis.onchange(); + }; + + if (Slider.isSupported && oElement) { + this.element.onfocus = Slider.eventHandlers.onfocus; + this.element.onblur = Slider.eventHandlers.onblur; + this.element.onmousedown = Slider.eventHandlers.onmousedown; + this.element.onmouseover = Slider.eventHandlers.onmouseover; + this.element.onmouseout = Slider.eventHandlers.onmouseout; + this.element.onkeydown = Slider.eventHandlers.onkeydown; + this.element.onkeypress = Slider.eventHandlers.onkeypress; + this.element.onmousewheel = Slider.eventHandlers.onmousewheel; + this.handle.onselectstart = + this.element.onselectstart = function () { return false; }; + + this._timer.ontimer = function () { + oThis.ontimer(); + }; + + // extra recalculate for ie + window.setTimeout(function() { + oThis.recalculate(); + }, 1); + } + else { + this.input.onchange = function (e) { + oThis.setValue(oThis.input.value); + }; + } +} + +Slider.eventHandlers = { + + // helpers to make events a bit easier + getEvent: function (e, el) { + if (!e) { + if (el) + e = el.document.parentWindow.event; + else + e = window.event; + } + if (!e.srcElement) { + var el = e.target; + while (el != null && el.nodeType != 1) + el = el.parentNode; + e.srcElement = el; + } + if (typeof e.offsetX == "undefined") { + e.offsetX = e.layerX; + e.offsetY = e.layerY; + } + + return e; + }, + + getDocument: function (e) { + if (e.target) + return e.target.ownerDocument; + return e.srcElement.document; + }, + + getSlider: function (e) { + var el = e.target || e.srcElement; + while (el != null && el.slider == null) { + el = el.parentNode; + } + if (el) + return el.slider; + return null; + }, + + getLine: function (e) { + var el = e.target || e.srcElement; + while (el != null && el.className != "line") { + el = el.parentNode; + } + return el; + }, + + getHandle: function (e) { + var el = e.target || e.srcElement; + var re = /handle/; + while (el != null && !re.test(el.className)) { + el = el.parentNode; + } + return el; + }, + // end helpers + + onfocus: function (e) { + var s = this.slider; + s._focused = true; + s.handle.className = "handle hover"; + }, + + onblur: function (e) { + var s = this.slider + s._focused = false; + s.handle.className = "handle"; + }, + + onmouseover: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + var s = this.slider; + if (e.srcElement == s.handle) + s.handle.className = "handle hover"; + }, + + onmouseout: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + var s = this.slider; + if (e.srcElement == s.handle && !s._focused) + s.handle.className = "handle"; + }, + + onmousedown: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + var s = this.slider; + if (s.element.focus) + s.element.focus(); + + Slider._currentInstance = s; + var doc = s.document; + + if (doc.addEventListener) { + doc.addEventListener("mousemove", Slider.eventHandlers.onmousemove, true); + doc.addEventListener("mouseup", Slider.eventHandlers.onmouseup, true); + } + else if (doc.attachEvent) { + doc.attachEvent("onmousemove", Slider.eventHandlers.onmousemove); + doc.attachEvent("onmouseup", Slider.eventHandlers.onmouseup); + doc.attachEvent("onlosecapture", Slider.eventHandlers.onmouseup); + s.element.setCapture(); + } + + if (Slider.eventHandlers.getHandle(e)) { // start drag + Slider._sliderDragData = { + screenX: e.screenX, + screenY: e.screenY, + dx: e.screenX - s.handle.offsetLeft, + dy: e.screenY - s.handle.offsetTop, + startValue: s.getValue(), + slider: s + }; + } + else { + var lineEl = Slider.eventHandlers.getLine(e); + s._mouseX = e.offsetX + (lineEl ? s.line.offsetLeft : 0); + s._mouseY = e.offsetY + (lineEl ? s.line.offsetTop : 0); + s._increasing = null; + s.ontimer(); + } + }, + + onmousemove: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + + if (Slider._sliderDragData) { // drag + var s = Slider._sliderDragData.slider; + + var boundSize = s.getMaximum() - s.getMinimum(); + var size, pos, reset; + + if (s._orientation == "horizontal") { + size = s.element.offsetWidth - s.handle.offsetWidth; + pos = e.screenX - Slider._sliderDragData.dx; + reset = Math.abs(e.screenY - Slider._sliderDragData.screenY) > 100; + } + else { + size = s.element.offsetHeight - s.handle.offsetHeight; + pos = s.element.offsetHeight - s.handle.offsetHeight - + (e.screenY - Slider._sliderDragData.dy); + reset = Math.abs(e.screenX - Slider._sliderDragData.screenX) > 100; + } + s.setValue(reset ? Slider._sliderDragData.startValue : + s.getMinimum() + boundSize * pos / size); + return false; + } + else { + var s = Slider._currentInstance; + if (s != null) { + var lineEl = Slider.eventHandlers.getLine(e); + s._mouseX = e.offsetX + (lineEl ? s.line.offsetLeft : 0); + s._mouseY = e.offsetY + (lineEl ? s.line.offsetTop : 0); + } + } + + }, + + onmouseup: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + var s = Slider._currentInstance; + var doc = s.document; + if (doc.removeEventListener) { + doc.removeEventListener("mousemove", Slider.eventHandlers.onmousemove, true); + doc.removeEventListener("mouseup", Slider.eventHandlers.onmouseup, true); + } + else if (doc.detachEvent) { + doc.detachEvent("onmousemove", Slider.eventHandlers.onmousemove); + doc.detachEvent("onmouseup", Slider.eventHandlers.onmouseup); + doc.detachEvent("onlosecapture", Slider.eventHandlers.onmouseup); + s.element.releaseCapture(); + } + + if (Slider._sliderDragData) { // end drag + Slider._sliderDragData = null; + } + else { + s._timer.stop(); + s._increasing = null; + } + Slider._currentInstance = null; + }, + + onkeydown: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + //var s = Slider.eventHandlers.getSlider(e); + var s = this.slider; + var kc = e.keyCode; + switch (kc) { + case 33: // page up + s.setValue(s.getValue() + s.getBlockIncrement()); + break; + case 34: // page down + s.setValue(s.getValue() - s.getBlockIncrement()); + break; + case 35: // end + s.setValue(s.getOrientation() == "horizontal" ? + s.getMaximum() : + s.getMinimum()); + break; + case 36: // home + s.setValue(s.getOrientation() == "horizontal" ? + s.getMinimum() : + s.getMaximum()); + break; + case 38: // up + case 39: // right + s.setValue(s.getValue() + s.getUnitIncrement()); + break; + + case 37: // left + case 40: // down + s.setValue(s.getValue() - s.getUnitIncrement()); + break; + } + + if (kc >= 33 && kc <= 40) { + return false; + } + }, + + onkeypress: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + var kc = e.keyCode; + if (kc >= 33 && kc <= 40) { + return false; + } + }, + + onmousewheel: function (e) { + e = Slider.eventHandlers.getEvent(e, this); + var s = this.slider; + if (s._focused) { + s.setValue(s.getValue() + e.wheelDelta / 120 * s.getUnitIncrement()); + // windows inverts this on horizontal sliders. That does not + // make sense to me + return false; + } + } +}; + + + +Slider.prototype.classNameTag = "dynamic-slider-control", + +Slider.prototype.setValue = function (v) { + this._range.setValue(v); + this.input.value = this.getValue(); +}; + +Slider.prototype.getValue = function () { + return this._range.getValue(); +}; + +Slider.prototype.setMinimum = function (v) { + this._range.setMinimum(v); + this.input.value = this.getValue(); +}; + +Slider.prototype.getMinimum = function () { + return this._range.getMinimum(); +}; + +Slider.prototype.setMaximum = function (v) { + this._range.setMaximum(v); + this.input.value = this.getValue(); +}; + +Slider.prototype.getMaximum = function () { + return this._range.getMaximum(); +}; + +Slider.prototype.setUnitIncrement = function (v) { + this._unitIncrement = v; +}; + +Slider.prototype.getUnitIncrement = function () { + return this._unitIncrement; +}; + +Slider.prototype.setBlockIncrement = function (v) { + this._blockIncrement = v; +}; + +Slider.prototype.getBlockIncrement = function () { + return this._blockIncrement; +}; + +Slider.prototype.getOrientation = function () { + return this._orientation; +}; + +Slider.prototype.setOrientation = function (sOrientation) { + if (sOrientation != this._orientation) { + if (Slider.isSupported && this.element) { + // add class name tag to class name + this.element.className = this.element.className.replace(this._orientation, + sOrientation); + } + this._orientation = sOrientation; + this.recalculate(); + + } +}; + +Slider.prototype.recalculate = function() { + if (!Slider.isSupported || !this.element) return; + + var w = this.element.offsetWidth; + var h = this.element.offsetHeight; + var hw = this.handle.offsetWidth; + var hh = this.handle.offsetHeight; + var lw = this.line.offsetWidth; + var lh = this.line.offsetHeight; + + // this assumes a border-box layout + + if (this._orientation == "horizontal") { + this.handle.style.left = (w - hw) * (this.getValue() - this.getMinimum()) / + (this.getMaximum() - this.getMinimum()) + "px"; + this.handle.style.top = (h - hh) / 2 + "px"; + + this.line.style.top = (h - lh) / 2 + "px"; + this.line.style.left = hw / 2 + "px"; + //this.line.style.right = hw / 2 + "px"; + this.line.style.width = Math.max(0, w - hw - 2)+ "px"; + this.line.firstChild.style.width = Math.max(0, w - hw - 4)+ "px"; + } + else { + this.handle.style.left = (w - hw) / 2 + "px"; + this.handle.style.top = h - hh - (h - hh) * (this.getValue() - this.getMinimum()) / + (this.getMaximum() - this.getMinimum()) + "px"; + + this.line.style.left = (w - lw) / 2 + "px"; + this.line.style.top = hh / 2 + "px"; + this.line.style.height = Math.max(0, h - hh - 2) + "px"; //hard coded border width + //this.line.style.bottom = hh / 2 + "px"; + this.line.firstChild.style.height = Math.max(0, h - hh - 4) + "px"; //hard coded border width + } +}; + +Slider.prototype.ontimer = function () { + var hw = this.handle.offsetWidth; + var hh = this.handle.offsetHeight; + var hl = this.handle.offsetLeft; + var ht = this.handle.offsetTop; + + if (this._orientation == "horizontal") { + if (this._mouseX > hl + hw && + (this._increasing == null || this._increasing)) { + this.setValue(this.getValue() + this.getBlockIncrement()); + this._increasing = true; + } + else if (this._mouseX < hl && + (this._increasing == null || !this._increasing)) { + this.setValue(this.getValue() - this.getBlockIncrement()); + this._increasing = false; + } + } + else { + if (this._mouseY > ht + hh && + (this._increasing == null || !this._increasing)) { + this.setValue(this.getValue() - this.getBlockIncrement()); + this._increasing = false; + } + else if (this._mouseY < ht && + (this._increasing == null || this._increasing)) { + this.setValue(this.getValue() + this.getBlockIncrement()); + this._increasing = true; + } + } + + this._timer.start(); +}; \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/slider/timer.js b/pub/qdb/res/themes/default/js/slider/timer.js new file mode 100644 index 0000000..e96fe19 --- /dev/null +++ b/pub/qdb/res/themes/default/js/slider/timer.js @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------\ +| Timer Class | +|-----------------------------------------------------------------------------| +| Created by Erik Arvidsson | +| (http://webfx.eae.net/contact.html#erik) | +| For WebFX (http://webfx.eae.net/) | +|-----------------------------------------------------------------------------| +| Object Oriented Encapsulation of setTimeout fires ontimer when the timer | +| is triggered. Does not work in IE 5.00 | +|-----------------------------------------------------------------------------| +| Copyright (c) 2002, 2006 Erik Arvidsson | +|-----------------------------------------------------------------------------| +| Licensed under the Apache License, Version 2.0 (the "License"); you may not | +| use this file except in compliance with the License. You may obtain a copy | +| of the License at http://www.apache.org/licenses/LICENSE-2.0 | +| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | +| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | +| License for the specific language governing permissions and limitations | +| under the License. | +|-----------------------------------------------------------------------------| +| 2002-10-14 | Original version released | +| 2006-05-28 | Changed license to Apache Software License 2.0. | +|-----------------------------------------------------------------------------| +| Created 2002-10-14 | All changes are in the log above. | Updated 2006-05-28 | +\----------------------------------------------------------------------------*/ + +function Timer(nPauseTime) { + this._pauseTime = typeof nPauseTime == "undefined" ? 1000 : nPauseTime; + this._timer = null; + this._isStarted = false; +} + +Timer.prototype.start = function () { + if (this.isStarted()) + this.stop(); + var oThis = this; + this._timer = window.setTimeout(function () { + if (typeof oThis.ontimer == "function") + oThis.ontimer(); + }, this._pauseTime); + this._isStarted = false; +}; + +Timer.prototype.stop = function () { + if (this._timer != null) + window.clearTimeout(this._timer); + this._isStarted = false; +}; + +Timer.prototype.isStarted = function () { + return this._isStarted; +}; + +Timer.prototype.getPauseTime = function () { + return this._pauseTime; +}; + +Timer.prototype.setPauseTime = function (nPauseTime) { + this._pauseTime = nPauseTime; +}; \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/style_switcher.js b/pub/qdb/res/themes/default/js/style_switcher.js new file mode 100644 index 0000000..d8c1b90 --- /dev/null +++ b/pub/qdb/res/themes/default/js/style_switcher.js @@ -0,0 +1 @@ +/* ############################################################################### # Chirpy!, a quote management system # # Copyright (C) 2005-2007 Tim De Pauw # ############################################################################### # This program is free software; you can redistribute it and/or modify it # # under the terms of the GNU General Public License as published by the Free # # Software Foundation; either version 2 of the License, or (at your option) # # any later version. # # # # This program is distributed in the hope that it will be useful, but WITHOUT # # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # # more details. # # # # You should have received a copy of the GNU General Public License along # # with this program; if not, write to the Free Software Foundation, Inc., 51 # # Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # ############################################################################### ############################################################################### # style_switcher.js # # Makes the user's alternate stylesheet choice persistent using a cookie # # Largely based on http://www.alistapart.com/articles/alternate/ # ############################################################################### # $Id:: style_switcher.js 291 2007-02-05 21:24:46Z ceetee $ # ############################################################################### */ function setActiveStyleSheet (title) { var a; var l = document.getElementsByTagName("link"); var found = false; for (var i = 0; a = l[i]; i++) { var t = a.getAttribute("title"); if (a.getAttribute("rel").indexOf("stylesheet") >= 0 && t) { var d = (t != title); a.disabled = d; if (!d) found = true; } } return found; } function getActiveStyleSheet () { var a; var l = document.getElementsByTagName("link"); for (var i = 0; a = l[i]; i++) { var t = a.getAttribute("title"); if (a.getAttribute("rel").indexOf("stylesheet") >= 0 && t && !a.disabled) return t; } return null; } function getPreferredStyleSheet () { var a; var l = document.getElementsByTagName("link"); for (var i = 0; a = l[i]; i++) { var t = a.getAttribute("title"); if (t && a.getAttribute("rel") == "stylesheet") return t; } return null; } function readCookie (name) { var nameEq = name + "="; var ca = document.cookie.split(/; */); for (var i = 0; i < ca.length; i++) { var c = ca[i]; if (c.indexOf(nameEq) == 0) return c.substring(nameEq.length, c.length); } return null; } function createCookie (name, value) { var date = new Date(); date.setYear(1900 + date.getYear() + 1); document.cookie = name + "=" + value + "; expires=" + date.toGMTString() + "; path=" + cookiePath + "; domain=" + cookieDomain; } function addOnunloadFunction (f) { if (window.onunload != null) { var old = window.onunload; window.onunload = function (e) { old(e); f(); }; } else { window.onunload = f; } } addOnunloadFunction(function () { var style = getActiveStyleSheet(); if (style) createCookie("style", style); }); setActiveStyleSheet(readCookie("style")) || setActiveStyleSheet(getPreferredStyleSheet()); \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/tabbed_pane.js b/pub/qdb/res/themes/default/js/tabbed_pane.js new file mode 100644 index 0000000..80db8d9 --- /dev/null +++ b/pub/qdb/res/themes/default/js/tabbed_pane.js @@ -0,0 +1,65 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# tabbed_pane.js # +# Emulates a tabbed pane for the administrative interface # +############################################################################### +# $Id:: tabbed_pane.js 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +var tabs, activeTab; + +function initializeTabbedPane (name, initialTab) { + tabs = document.getElementById(name + '-navigation').childNodes; + contents = document.getElementById(name + '-contents').childNodes; + for (var i = 0; i < tabs.length; i++) { + initializeTab(tabs[i].firstChild); + } + setActiveTab(initialTab ? initialTab : tabs[0].firstChild); +} + +function initializeTab (tab) { + tab.onclick = function() { + displayTab(tab); + return false; + }; + tab.onmousedown = tab.onselectstart = function () { + return false; + }; + tab.removeAttribute('href'); +} + +function displayTab (tab) { + if (tab == activeTab) return; + setActiveTab(tab); +} + +function setActiveTab (tab) { + activeTab = tab; + for (var i = 0; i < tabs.length; i++) { + var tab = tabs[i].firstChild; + var active = (tab == activeTab); + var className = active ? 'active-tab' : ''; + tab.className = className; + document.getElementById(tab.id.substring(4)).style.display = (active ? '' : 'none'); + } +} \ No newline at end of file diff --git a/pub/qdb/res/themes/default/js/tag_cloud.js b/pub/qdb/res/themes/default/js/tag_cloud.js new file mode 100644 index 0000000..fc7ec7e --- /dev/null +++ b/pub/qdb/res/themes/default/js/tag_cloud.js @@ -0,0 +1,119 @@ +/* +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# tag_cloud.js # +# Allows user interaction with the tag cloud # +############################################################################### +# $Id:: tag_cloud.js 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### +*/ + +var tagCloudUpdateTime = 5; + +var tagUseRE = new RegExp("\\bused-(\\d+)\\b"); +var tagUseMinimum = 1; +var tagCloudSliderLabelText; +var tagNodes; + +function initializeTagCloudSlider (labelPrefix) { + var cloud = document.getElementById("tag-cloud"); + tagNodes = new Array(); + var maxUseCount = 0; + for (var i = 0; i < cloud.childNodes.length; i++) { + var child = cloud.childNodes[i]; + if (child.nodeName && child.nodeName.toUpperCase() == "LI") { + var matches = child.className.match(tagUseRE); + var cnt = parseInt(matches[1]); + if (!tagNodes[cnt]) tagNodes[cnt] = new Array(); + tagNodes[cnt].push(child); + if (cnt > maxUseCount) maxUseCount = cnt; + } + } + var newMaxUseCount = Math.floor(maxUseCount / 4); + if (newMaxUseCount > 1) maxUseCount = newMaxUseCount; + var val = readCookie("tag_use"); + if (!val || val <= 0 || val > maxUseCount) val = 1; + var container = document.createElement("div"); + container.id = "tag-cloud-slider-container"; + var sl = document.createElement("div"); + sl.id = "tag-cloud-slider"; + var form = document.createElement("form"); + form.action = "#"; + var input = document.createElement("input"); + input.id = input.name = "tag-cloud-slider-input"; + var label = document.createElement("div"); + label.id = "tag-usage-minimum"; + label.appendChild(document.createTextNode(labelPrefix + " ")); + tagCloudSliderLabelText = document.createTextNode(val); + label.appendChild(tagCloudSliderLabelText); + form.appendChild(input); + sl.appendChild(form); + container.appendChild(label); + container.appendChild(sl); + var placeholder = document.getElementById("tag-cloud-slider-placeholder"); + placeholder.parentNode.replaceChild(container, placeholder); + var slider = new Slider(sl, input); + slider.setMinimum(1); + slider.setMaximum(maxUseCount); + slider.setBlockIncrement(1); + slider.setValue(val); + slider.onchange = function () { setTagUseMinimum(slider.getValue()); }; + setTagUseMinimum(val); +} + +function setTagUseMinimum (min) { + if (tagUseMinimum == min) return; + tagCloudSliderLabelText.nodeValue = min; + createCookie("tag_use", min); + if (min > tagUseMinimum) + setTagNodesVisible(tagUseMinimum, min - 1, false); + else + setTagNodesVisible(min, tagUseMinimum - 1, true); + tagUseMinimum = min; +} + +function setTagNodesVisible (first, last, visible) { + var disp = (visible ? "" : "none"); + for (var i = first; i <= last; i++) { + var nodes = tagNodes[i]; + if (!nodes) continue; + for (var j = 0; j < nodes.length; j++) + nodes[j].style.display = disp; + } +} + +function readCookie (name) { + var nameEq = name + "="; + var ca = document.cookie.split(/; */); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + if (c.indexOf(nameEq) == 0) + return c.substring(nameEq.length, c.length); + } + return null; +} + +function createCookie (name, value) { + var date = new Date(); + date.setYear(1900 + date.getYear() + 1); + document.cookie = name + "=" + value + "; expires=" + date.toGMTString() + + "; path=" + cookiePath + "; domain=" + cookieDomain; +} \ No newline at end of file diff --git a/pub/qdb/src/.htaccess b/pub/qdb/src/.htaccess new file mode 100644 index 0000000..ed2f60f --- /dev/null +++ b/pub/qdb/src/.htaccess @@ -0,0 +1,4 @@ + +order deny,allow +deny from all + \ No newline at end of file diff --git a/pub/qdb/src/cache/template/05/97c435de1f438f8dcb708dc88fa3f9 b/pub/qdb/src/cache/template/05/97c435de1f438f8dcb708dc88fa3f9 new file mode 100644 index 0000000..34b76e6 Binary files /dev/null and b/pub/qdb/src/cache/template/05/97c435de1f438f8dcb708dc88fa3f9 differ diff --git a/pub/qdb/src/cache/template/3e/46cdf3a4a085f1b3af728e719f8c06 b/pub/qdb/src/cache/template/3e/46cdf3a4a085f1b3af728e719f8c06 new file mode 100644 index 0000000..09a9005 Binary files /dev/null and b/pub/qdb/src/cache/template/3e/46cdf3a4a085f1b3af728e719f8c06 differ diff --git a/pub/qdb/src/cache/template/76/3e31852f24c555c67307d4f934cdae b/pub/qdb/src/cache/template/76/3e31852f24c555c67307d4f934cdae new file mode 100644 index 0000000..8a30f86 Binary files /dev/null and b/pub/qdb/src/cache/template/76/3e31852f24c555c67307d4f934cdae differ diff --git a/pub/qdb/src/locales/en-US.ini b/pub/qdb/src/locales/en-US.ini new file mode 100644 index 0000000..b51dfe2 --- /dev/null +++ b/pub/qdb/src/locales/en-US.ini @@ -0,0 +1,282 @@ +[properties] +full_name=U.S. English +author_name=Tim De Pauw +author_email=ceetee@users.sourceforge.net +author_uri=http://chirpy.sourceforge.net/ +version=0.3 +chirpy_version=0.3 + +[strings] +error_title=An Error Occurred +quote_browser=Quote Browser +random_quotes=Random Quotes +view_quote=View Quote +top_quotes=Top Quotes +bottom_quotes=Bottom Quotes +quotes_of_the_week=Quotes of the Week +search_for_quotes=Search for Quotes +tag_cloud=Tag Cloud +statistics=Statistics +administration=Administration +welcome=Welcome +edit_quote=Edit Quote +remove_quote=Remove Quote +latest_news=Latest News +unknown_action=The requested action is unknown. +no_quotes=There do not appear to be any quotes to display. +quote_browser_description=Browse Quotes +quote_browser_short_title=Browse +random_quotes_description=View Random Quotes +random_quotes_short_title=Random +top_quotes_description=View Highest Rated Quotes +top_quotes_short_title=Top +bottom_quotes_description=View Lowest Rated Quotes +bottom_quotes_short_title=Bottom +quotes_of_the_week_description=View Quotes of the Week +quotes_of_the_week_short_title=QotW +quote_search_description=Search for Quotes +quote_search_short_title=Search +tag_cloud_description=View Tag Usage +tag_cloud_short_title=Tags +statistics_description=View Statistics +statistics_short_title=Stats +quote_count_by_date=Number of Quotes per Day +quote_count_by_hour=Number of Quotes per Hour +quote_count_by_month=Number of Quotes per Month +quote_count_by_day=Number of Quotes per Day of the Month +quote_count_by_weekday=Number of Quotes per Weekday +quote_count_by_rating=Number of Quotes per Rating +quote_count_by_vote_count=Number of Quotes per Number of Votes +vote_count_by_rating=Number of Votes per Rating +submit_quote_description=Submit New Quote +submit_quote_short_title=Submit +moderation_queue_description=View Unmoderated Quotes +moderation_queue_short_title=Queue +administration_description=Administrative Interface +administration_short_title=Admin +login_description=Authenticate by User Account +login_short_title=Log In +logout_description=Clear Authentication Data +logout_short_title=Log Out +quote_title=Quote %1% +quote_report_description=Report Quote +quote_rating_up_description=Quote Rating Up +quote_rating_down_description=Quote Rating Down +quote_report_confirmation_request=Are you sure you would like to report the following quote? +quote_rating_up_confirmation_request=Are you sure you would like to increase the rating of the following quote? +quote_rating_down_confirmation_request=Are you sure you would like to decrease the rating of the following quote? +quote_rating_description=Quote Rating +quote_vote_count_description=Quote Vote Count +quote_date_description=Quote Submission Date +quote_edit_description=Edit Quote +quote_remove_description=Remove Quote +quote_notes_title=Notes: +quote_tags_title=Tags: +search_query_title=Find: +search_button_label=Go! +search_results=Search Results +submit_quote=Submit Quote +unmoderated_quotes=Unmoderated Quotes +submission_title=Enter your quote in this field: +notes_title=Enter additional information about the quote, if any: +tags_title=Enter some relevant tags for the quote: +submit_button_label=Submit Quote for Approval +submit_button_label_no_approval=Add Quote to Database +quote_submitted=Quote Submitted +quote_submitted_no_approval=Quote Added +quote_submission_thanks=Thank you for submitting a quote to our database. A site administrator will review it shortly. If it gets approved, it will appear on this web site. Fingers crossed! +quote_submission_thanks_no_approval=Thank you for submitting a quote to our database. Because you are a site administrator, your quote does not need to be approved before it appears on the web site. +approve_quotes=Approve Quotes +flagged_quotes=Flagged Quotes +manage_news=Manage News +manage_quotes=Manage Quotes +add_news=Add News +edit_news=Edit News +remove_news=Remove News +manage_accounts=Manage Accounts +view_event_log=View Event Log +no_unapproved_quotes=No quotes are currently awaiting approval. +no_flagged_quotes=No quotes are currently flagged. +quote_rating_up_short_title=Up +quote_rating_down_short_title=Down +report_quote_short_title=Report +edit=Edit +remove=Remove +unflag=Unflag +flagged=Flagged +quote_removal_confirmation=Are you sure you would like to remove this quote? +news_removal_confirmation=Are you sure you would like to remove this news item? +quote_rating_increased=Quote Rating Increased +quote_rating_decreased=Quote Rating Decreased +quote_rating_thanks=Your vote has been processed. Thank you for your input! +quote_reported=Quote Flagged +quote_report_thanks=Thank you for reporting the quote. A site administrator will review your report shortly. +quote_already_rated=You have already rated this quote. You cannot rate the same quote twice. +quote_rating_limit_exceeded=You have exceeded the maximum number of votes allowed. This system maximally allows %1% vote(s) every %2% seconds. +login_title=Please Log In +invalid_login_title=Invalid Login +username_title=User Name: +password_title=Password: +login_button_label=Log In +invalid_login_instructions=We were unable to log you in using the given credentials. Note that while your user name is not case sensitive, your password is. Please try to log in again. +logged_in_as=You are currently logged in as %1% (%2%). +change_password=Change Password +processing=Processing +timed_out=Timed Out +none=None +error=Error +no_search_results=No Results +no_search_results_text=Unfortunately, your search did not return any results. +quote_not_found=Quote Not Found +quote_not_found_text=The quote you requested does not exist in the database. You might want to try the search feature instead. +rated_quote_not_found_text=The quote you attempted to rate could not be found in the database. +reported_quote_not_found_text=The quote you attempted to report could not be found in the database. +user_level_3=Moderator +user_level_6=Administrator +user_level_9=Owner +password_changed=Password Changed +password_changed_text=Your password was successfully modified. Make sure you keep it in a safe place! +current_password_title=Current Password: +new_password_title=New Password: +repeat_new_password_title=Repeat New Password: +change_password_button_label=Change Password +change_password_current_password_invalid_text=The password you have entered is not your current password. +change_password_new_password_invalid_text=The new password you have entered is invalid. +change_password_passwords_differ_text=The passwords you have entered do not match. Please make sure you enter the same password twice. +do_nothing=Do Nothing +approve_unapproved_quote=Approve Quote +discard_unapproved_quote=Discard Quote +approve_all_unapproved_quotes=Approve All +approve_all_unapproved_quotes_confirm=Are you sure you would like to approve all unapproved quotes? +discard_all_unapproved_quotes=Discard All +discard_all_unapproved_quotes_confirm=Are you sure you would like to discard all unapproved quotes? +update_database=Update Database +reset_form=Reset Form +keep_flagged_quote=Keep Quote +remove_flagged_quote=Remove Quote +keep_all_flagged_quotes=Keep All +keep_all_flagged_quotes_confirm=Are you sure you would like to keep all flagged quotes? +remove_all_flagged_quotes=Remove All +remove_all_flagged_quotes_confirm=Are you sure you would like to remove all flagged quotes? +quote_removed=The selected quote was successfully removed from the database. +quote_to_edit_not_found=The selected quote could not be found. Perhaps it had already been removed or you entered an invalid ID. +quote_to_remove_not_found=The selected quote could not be found. Perhaps it has been removed or you entered an invalid ID. +quote_modified=The selected quote was successfully updated. +quote_id_title=Quote ID: +save_quote=Save Quote +go=Go +news_item_added=Your news post was successfully added to the database. +news_item_modified=The selected news item was successfully updated. +news_item_to_edit_not_found=The selected news item could not be found. Perhaps it has been removed or you entered an invalid ID. +news_item_removed=The selected news item was successfully removed from the database. +news_item_to_remove_not_found=The selected news item could not be found. Perhaps it had already been removed. +new_news_item_title=New News Item: +add_news_item=Add News Item +news_poster_title=Poster: +save_news_item=Save News Item +account_to_modify_not_found=Please select the account you would like to modify. +account_to_remove_not_found=Please select the account you would like to remove. +last_owner_account_removal_error=Must have at least one Owner account. +modified_account_information_required=Please enter the new data for the account to modify. +invalid_username=The user name you entered is invalid. +username_exists=A user already exists by that name. +invalid_password=The password you entered is invalid. +different_passwords=The passwords do not match. Please make sure you enter the same password twice. +invalid_user_level=The user level you selected is invalid. +account_removed=The selected account has been removed. +account_modified=The account has been updated. +account_created=The new account has been created. +new_account=New Account +new_username_title=New User Name: +new_password_title=New Password: +repeat_new_password_title=Repeat Password: +new_user_level_title=New User Level: +update_accounts=Update Accounts +remove_account=Remove Account +no_change=No Change +unknown=Unknown +account_removal_confirmation=Are you sure you would like to remove the selected account? This cannot be undone. +insufficient_administrative_privileges=You are not authorized to use this component because your administrative privileges are insufficient. +no_tagged_quotes=There do not appear to be any quotes with tags. +statistics_unavailable=Statistics are currently unavailable. +tag_link_description=View Quotes Tagged %1% +update_available=Update Available +update_available_text=Chirpy! version %1% was released on %2%. +update_link_text=Click here for more information. +update_check_failed=Update Check Failed +update_check_failed_text=Chirpy! attempted to check for updates, but failed. The reported error was: +event_100_name=Login Success +event_101_name=Login Failure +event_102_name=Change Password +event_200_name=Add Quote +event_201_name=Edit Quote +event_202_name=Remove Quote +event_203_name=Quote Rating Up +event_204_name=Quote Rating Down +event_205_name=Report Quote +event_206_name=Approve Quote +event_207_name=Unflag Quote +event_300_name=Add News +event_301_name=Edit News +event_302_name=Remove News +event_400_name=Add Account +event_401_name=Edit Account +event_402_name=Remove Account +date=Date +username=Username +event=Event +guest=Guest +empty=Empty +ok=OK +cancel=Cancel +sunday=Sunday +monday=Monday +tuesday=Tuesday +wednesday=Wednesday +thursday=Thursday +friday=Friday +saturday=Saturday +january=January +february=February +march=March +april=April +may=May +june=June +july=July +august=August +september=September +october=October +november=November +december=December +january_short=Jan +february_short=Feb +march_short=Mar +april_short=Apr +may_short=May +june_short=Jun +july_short=Jul +august_short=Aug +september_short=Sep +october_short=Oct +november_short=Nov +december_short=Dec +webapp.start_page_description=Start Page +webapp.start_page_short_title=Home +webapp.next_page_title=Next Page +webapp.previous_page_title=Previous Page +webapp.current_page_title=Current Page +webapp.quote_link_description=Permanent Link to Quote +webapp.footer_text=This page was generated in %1% ms by %2%. +webapp.footer_text_no_time=This site is powered by %1%. +webapp.manage_quote_instructions=To edit or remove a quote, just browse to it while logged in. You will then see the appropriate links next to it. Alternatively, if you know the ID of the quote you would like to edit, you can enter it below. +webapp.remove_quote_without_viewing_confirmation=Are you sure you would like to remove this quote without viewing it first? This operation cannot be undone and is NOT recommended. +webapp.manage_news_instructions=To edit or remove a news item, just open the home page while logged in. You will then see the appropriate links next to the items. +webapp.session_required=The action you are attempting to take requires that session information be set. This is done by offering a cookie to your browser. Unfortunately, it seems it has failed to accept this cookie. Please review your cookie settings and try again. +webapp.quote_rating_timed_out=The quote rating request timed out, possibly due to a connection problem. Please try again. +webapp.captcha_code_label=Type the text you see in this image: +webapp.captcha_image_text=Captcha +webapp.minimum_tag_usage_count_title=Minimum Quotes: +webapp.top_quote_prefix=Top Quote: +webapp.bottom_quote_prefix=Bottom Quote: +webapp.latest_quote_prefix=Latest Quote: +webapp.latest_unmoderated_quote_prefix=Latest Unmoderated Quote: diff --git a/pub/qdb/src/modules/Chirpy.pm b/pub/qdb/src/modules/Chirpy.pm new file mode 100644 index 0000000..f7ebf32 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy.pm @@ -0,0 +1,850 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Chirpy.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy - Main coordination class + +=head1 REQUIREMENTS + +Chirpy! uses the UTF-8 character encoding, and because of that, I is +required. A lot of systems still have Perl 5.6 and, unfortunately, Chirpy! will +not run there. This might change in future releases. + +It also relies on a couple of Perl modules, most of which are part of standard +Perl distributions. The base classes require these modules: + + Carp + Digest::MD5 + Encode + POSIX + Storable + +Additionally, the default data manager and user interface classes have their +own requirements. Before using them, please consult +L +and L. + +=head1 SYNOPSIS + + use Chirpy 0.3; + + chirpy(); + + chirpy($configuration_file, $data_manager_type); + + $chirpy = new Chirpy(); + $chirpy->run(); + + $chirpy = new Chirpy($configuration_file, $data_manager_type); + $chirpy->run(); + +=head1 DESCRIPTION + +This module is Chirpy!'s main coordination class and the only one that scripts +that use it should access directly. Everything else is part of the inner +workings of Chirpy!. + +An instance of this module really represents an entire Chirpy! configuration, +along with everything it uses at runtime. This means that you can have several +instances of it simultaneously and exchange information between them. + +=head1 USAGE + +There are two ways to use the class: + +=over 4 + +=item Procedural Interface + +The easiest way is to just run the C procedure, which is exported by +default. The code below will attempt to run Chirpy! with a configuration file +located at either of the default paths F and F, +relative to the current working directory. + + chirpy; + +That's it! Now, if you wanted to specify your own configuration file, you would +just pass it as the first parameter, like so: + + chirpy('/home/joe/chirpy.ini'); + +The path can also be relative, but make sure the working directory is correct. + +=item Object-Oriented Interface + +As you may want to distinguish between different installations, you can have +several instances of this module in the same script. Instantiating the module +is a lot like invoking C, except that it doesn't create and run the +user interface instance yet. + + $chirpy = new Chirpy(); + + $chirpy = new Chirpy('/home/joe/chirpy.ini'); + +If you wanted to create and run the configured user interface, you would just: + + $chirpy->run(); + +Simple as that. + +In addition, you can add a second parameter to the constructor to override the +data manager type specified in the configuration file, which can be useful for +migration: + + $chirpy_old = new Chirpy('/home/joe/chirpy.ini'); + $chirpy_new = new Chirpy('/home/joe/chirpy.ini', 'MyNewDataManager'); + +While the C procedure also takes that parameter, I don't see any real +use for it. + +Note that if you want to use the default configuration file path, but with an +alternate data manager, you just pass C as the first parameter: + + $chirpy = new Chirpy(undef, 'MyNewDataManager'); + +=back + +=head1 CONFIGURATION FILE + +A Chirpy! configuration file is a standard INI file, so it looks a little +something like this: + + [general] + title=My Little QDB + description=A place for my quotes + locale=en-US + ... + + [data] + type=MySQL + ... + +Chirpy! adds a third level of parameter nesting to this format by separating +the class and parameter name by a dot. For instance, the password for the +MySQL data manager is stored like: + + [data] + mysql.password=mypassword + +Now, let's go over the default configuration values. + +=head2 General Section + +The C section configures ... general settings! + +=over 4 + +=item base_path + +The local path (on the file system) where locales, templates, etc. are stored. +Do I include a trailing slash. + +=item title + +The title of your QDB. + +=item description + +A brief description of the purpose of your QDB. + +=item locale + +The code of the locale to use. + +=item rating_limit_count + +=item rating_limit_time + +Limit the maximum number of votes per time frame using these two parameters. +The former sets the maximum number, the latter sets the time period in seconds. + +=item quote_score_calculation_mode + +Since Chirpy! 0.3, quote scores, which are used to order the quotes for the Top +and Bottom Quotes pages, are calculated using the following formula: + + positive votes + 1 + score = -------------------- + negative votes + 1 + +This results in a fairly decent distribution. However, if you prefer the old +way, based on a quote's rating, i.e. + + rating = positive votes - negative votes + +you can set C to C<1>. Note that the default way +corresponds with a value of C<0>; this value may correspond with a different +formula in future releases. + +=back + +=head2 Data Section + +The C section configures everything related to the data manager, or the +backend, if you will. + +This section only has one default parameter, namely C. It contains the +name of the data manager to use. This will be translated to +C>, so that module will need to be installed. + +Apart from that, there are parameters specific to the data manager of your +choice. Please refer to its documentation for an explanation. If you use the +default data manager, L, you can find the +parameters in L. + +=head2 UI Section + +The C section configures the frontend or user interface. It includes these +parameters by default: + +=over 4 + +=item type + +Similar to the C parameter under the C section, this one sets the +name of the user interface module and will be translated to +C>. + +=item date_time_format + +The string that describes the format in which to display a date along with the +time. This string is passed to the C method of +L. + +=item date_format + +Similar to the above, but for dates only. + +=item time_format + +Similar to the above, but for times only. + +=item use_gmt + +Set this parameter to 0 if you wish to display times in local time instead of +Greenwich Mean Time. For GMT, set it to 1. + +=item quotes_per_page + +The maximum number of quotes to display per page. + +=item recent_news_items + +How many news items to display on the home page. + +=item moderation_queue_public + +Set this to C<1> if you want to make the list of unmoderated quotes available to +the public. To hide the list from everybody except moderators, set it to 0. + +=item tag_cloud_logarithmic + +Set this to C<1> if you want to determine the tag cloud's font sizes using a +logarithmic algorithm instead of a linear one. Most people will probably prefer +this, as it gives better results if some of the tags are used extremely often. + +=back + +Apart from that, there are parameters specific to the user interface of your +choice. Please refer to its documentation for an explanation. If you use the +default user interface, L, you can find the +parameters in L. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L, +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy; + +require 5.008; + +use strict; +use warnings; +require Exporter; + +BEGIN { + use vars qw($VERSION @EXPORT @ISA $DEBUG $hires_timing); + $VERSION = '0.3'; + @ISA = qw(Exporter); + @EXPORT = qw(chirpy); + eval 'use Time::HiRes qw//'; + $DEBUG = 0; + $hires_timing = 1 unless ($@); +} + +use constant PRODUCT_NAME => 'Chirpy!'; +use constant VERSION_STRING => 'v0.3'; +use constant FULL_PRODUCT_NAME => PRODUCT_NAME . ' ' . VERSION_STRING; +use constant URL => 'http://chirpy.sourceforge.net/'; + +use Chirpy::Configuration 0.3; +use Chirpy::Locale 0.3; + +use Chirpy::Quote 0.3; +use Chirpy::NewsItem 0.3; +use Chirpy::Account 0.3; +use Chirpy::Event 0.3; + +use constant USER_LEVELS => [ + Chirpy::Account::USER_LEVEL_9, + Chirpy::Account::USER_LEVEL_6, + Chirpy::Account::USER_LEVEL_3 +]; + +use Carp qw/croak confess/; + +sub new { + my ($class, $configuration_file, $dm_override) = @_; + my $st = ($hires_timing ? Time::HiRes::time() : undef); + my $self = bless {}, $class; + $self->{'start_time'} = $st; + $self->{'debug_events'} = [] if ($DEBUG && $hires_timing); + unless (defined $configuration_file) { + foreach my $file (qw(src/chirpy.ini chirpy.ini)) { + next unless (-f $file); + $configuration_file = $file; + last; + } + Chirpy::die('No valid configuration file found') + unless (defined $configuration_file); + } + $self->mark_debug_event('Load configuration'); + my $configuration = new Chirpy::Configuration( + defined $configuration_file ? $configuration_file : 'chirpy.ini'); + $self->mark_debug_event('Configuration loaded'); + $self->{'configuration'} = $configuration; + $self->mark_debug_event('Load locale'); + my $locale = new Chirpy::Locale($configuration->get('general', 'base_path') + . '/locales/' . $configuration->get('general', 'locale') . '.ini'); + $self->mark_debug_event('Locale loaded'); + $self->{'locale'} = $locale; + my $locale_version = $locale->get_target_version(); + Chirpy::die('Locale outdated: wanted target version ' . $Chirpy::VERSION + . ', got ' . $locale_version) + unless ($locale_version ge $Chirpy::VERSION); + my $dm_type = defined $dm_override + ? $dm_override : $configuration->get('data', 'type'); + $self->{'data_manager_type'} = $dm_type; + my $dm_params = $configuration->get_parameter_hash('data', $dm_type); + $self->mark_debug_event('Create data manager'); + my $dm = &_create_data_manager($dm_type, $dm_params); + $self->mark_debug_event('Data manager created'); + $self->{'data_manager'} = $dm; + my $ui_type = $configuration->get('ui', 'type'); + $self->{'ui_type'} = $ui_type; + return $self; +} + +sub run { + my $self = shift; + my $configuration = $self->configuration(); + my $ui_type = $self->{'ui_type'}; + my $ui_params = $configuration->get_parameter_hash('ui', $ui_type); + $self->mark_debug_event('Create user interface'); + $self->{'ui'} = &_create_ui($ui_type, $self, $ui_params); + $self->mark_debug_event('User interface created'); + $self->{'ui'}->run(); +} + +sub chirpy { + new Chirpy(@_)->run(); +} + +sub configuration { + my $self = shift; + return $self->{'configuration'}; +} + +sub locale { + my $self = shift; + return $self->{'locale'}; +} + +sub get_parameter { + my ($self, $name) = @_; + return $self->{'data_manager'}->get_parameter($name); +} + +sub set_parameter { + my ($self, $name, $value) = @_; + $self->{'data_manager'}->set_parameter($name, $value); +} + +sub user_level_name { + my ($self, $id) = @_; + $self->locale->get_string('user_level_' . $id); +} + +sub user_levels { + return @{USER_LEVELS()}; +} + +sub get_quotes { + my ($self, $start, $count, $sort) = @_; + $self->mark_debug_event('Request quotes'); + return $self->_data_manager()->get_quotes({ + 'approved' => 1, + 'sort' => (defined $sort ? $sort : [ [ 'id', 1 ] ]), + 'first' => $start, + 'count' => (defined $count ? $count : $self->quotes_per_page()) + }); +} + +sub approved_quote_count { + my $self = shift; + return $self->_data_manager()->quote_count({ 'approved' => 1 }); +} + +sub unapproved_quote_count { + my $self = shift; + return $self->_data_manager()->quote_count({ 'approved' => 0 }); +} + +sub total_quote_count { + my $self = shift; + return $self->_data_manager()->quote_count(); +} + +sub get_matching_quotes { + my ($self, $start, $queries, $tags) = @_; + return $self->_data_manager()->get_quotes({ + 'approved' => 1, + 'contains' => $queries, + 'sort' => [ [ 'id', 1 ] ], + 'first' => $start, + 'count' => $self->quotes_per_page(), + 'tags' => $tags + }); +} + +sub get_quotes_of_the_week { + my ($self, $start) = @_; + return $self->_data_manager()->get_quotes({ + 'approved' => 1, + 'since' => time - 7 * 24 * 60 * 60, + 'sort' => [ [ 'id', 1 ] ], + 'first' => $start, + 'count' => $self->quotes_per_page() + }); +} + +sub get_quote { + my ($self, $id) = @_; + return undef unless (defined $id); + my $quotes = $self->_data_manager()->get_quotes({ + 'id' => $id + }); + return undef unless (defined $quotes); + return $quotes->[0]; +} + +sub get_random_quotes { + my $self = shift; + return $self->_data_manager()->get_quotes({ + 'approved' => 1, + 'count' => $self->quotes_per_page(), + 'random' => 1 + }); +} + +sub get_top_quotes { + my ($self, $start) = @_; + my $cm = $self->quote_score_calculation_mode(); + return $self->_data_manager()->get_quotes({ + 'approved' => 1, + 'sort' => [ [ ($cm == 1 ? 'rating' : 'score'), 1 ], [ 'id', 1 ] ], + 'first' => $start, + 'count' => $self->quotes_per_page() + }); +} + +sub get_bottom_quotes { + my ($self, $start) = @_; + my $cm = $self->quote_score_calculation_mode(); + return $self->_data_manager()->get_quotes({ + 'approved' => 1, + 'sort' => [ [ ($cm == 1 ? 'rating' : 'score'), 0 ], [ 'id', 1 ] ], + 'first' => $start, + 'count' => $self->quotes_per_page() + }); +} + +sub get_flagged_quotes { + my ($self, $start) = @_; + return $self->_data_manager()->get_quotes({ + 'flagged' => 1, + 'sort' => [ [ 'id', 1 ] ], + 'first' => $start, + 'count' => (defined $start ? $self->quotes_per_page() : undef) + }); +} + +sub get_unapproved_quotes { + my ($self, $start) = @_; + return $self->_data_manager()->get_quotes({ + 'approved' => 0, + 'sort' => [ [ 'id', 1 ] ], + 'first' => $start, + 'count' => (defined $start ? $self->quotes_per_page() : undef) + }); +} + +sub add_quote { + my ($self, $body, $notes, $approved, $tags) = @_; + my $quote = new Chirpy::Quote( + undef, + $body, + $notes, + 0, + 0, + undef, + $approved, + 0, + $tags + ); + $self->_data_manager()->add_quote($quote); + return $quote; +} + +sub modify_quote { + my ($self, $quote, $text, $notes, $tags) = @_; + Chirpy::die('Not a Chirpy::Quote') + unless (ref $quote eq 'Chirpy::Quote'); + $quote->set_body(Chirpy::Util::clean_up_submission($text)); + $quote->set_notes($notes + ? Chirpy::Util::clean_up_submission($notes) + : undef); + $quote->set_tags($tags) if (defined $tags); + return $self->_data_manager->modify_quote($quote); +} + +sub remove_quotes { + my ($self, @ids) = @_; + return $self->_data_manager()->remove_quotes(@ids); +} + +sub increase_quote_rating { + my ($self, $id, $revert) = @_; + return undef unless (defined $id); + my ($rating, $votes) = $self->_data_manager() + ->increase_quote_rating($id, $revert); + return ($rating, $votes); +} + +sub decrease_quote_rating { + my ($self, $id, $revert) = @_; + return undef unless (defined $id); + my ($rating, $votes) = $self->_data_manager() + ->decrease_quote_rating($id, $revert); + return ($rating, $votes); +} + +sub get_tag_use_counts { + my $self = shift; + return $self->_data_manager()->get_tag_use_counts(); +} + +sub flag_quotes { + my ($self, @ids) = @_; + return $self->_data_manager()->flag_quotes(@ids); +} + +sub unflag_quotes { + my ($self, @ids) = @_; + return $self->_data_manager()->unflag_quotes(@ids); +} + +sub approve_quotes { + my ($self, @ids) = @_; + return $self->_data_manager()->approve_quotes(@ids); +} + +sub get_news_item { + my ($self, $id) = @_; + return undef unless (defined $id); + my $items = $self->_data_manager()->get_news_items({ 'id' => $id }); + return (defined $items ? $items->[0] : undef); +} + +sub get_latest_news_items { + my $self = shift; + return $self->_data_manager()->get_news_items( + { 'count' => $self->configuration()->get('ui', 'recent_news_items') }); +} + +sub add_news_item { + my ($self, $text, $author) = @_; + my $item = new Chirpy::NewsItem( + undef, + Chirpy::Util::clean_up_submission($text), + $author + ); + $self->_data_manager()->add_news_item($item); + return $item; +} + +sub modify_news_item { + my ($self, $item, $text, $poster) = @_; + Chirpy::die('Not a Chirpy::NewsItem') + unless (ref $item eq 'Chirpy::NewsItem'); + $item->set_body($text); + $item->set_poster($poster); + return $self->_data_manager()->modify_news_item($item); +} + +sub remove_news_items { + my ($self, @ids) = @_; + return $self->_data_manager()->remove_news_items(@ids); +} + +sub get_accounts { + my $self = shift; + return $self->_data_manager()->get_accounts(); +} + +sub get_accounts_by_level { + my ($self, @levels) = @_; + return $self->_data_manager()->get_accounts({ 'levels' => \@levels }); +} + +sub get_account_by_id { + my ($self, $id) = @_; + return undef unless (defined $id); + my $accounts = $self->_data_manager()->get_accounts({ 'id' => $id }); + return (defined $accounts ? $accounts->[0] : undef); +} + +sub get_account_by_username { + my ($self, $username) = @_; + my $accounts = $self->_data_manager()->get_accounts( + { 'username' => $username }); + return (defined $accounts ? $accounts->[0] : undef); +} + +sub account_count { + my $self = shift; + return $self->_data_manager()->account_count(); +} + +sub account_count_by_level { + my ($self, $level) = @_; + return $self->_data_manager()->account_count({ 'levels' => [ $level ] }); +} + +sub username_exists { + my ($self, $username) = @_; + return $self->_data_manager()->username_exists($username); +} + +sub add_account { + my ($self, $username, $password, $level) = @_; + my $account = new Chirpy::Account( + undef, + $username, + Chirpy::Util::encrypt($password), + $level + ); + $self->_data_manager()->add_account($account); + return $account; +} + +sub modify_account { + my ($self, $account, $username, $password, $level) = @_; + Chirpy::die('Not a Chirpy::Account') + unless (ref $account eq 'Chirpy::Account'); + if (defined $username) { + Chirpy::die('Invalid username') + unless (Chirpy::Util::valid_username($username)); + $account->set_username($username); + } + if (defined $password) { + Chirpy::die('Invalid password') + unless (Chirpy::Util::valid_password($password)); + $account->set_password(Chirpy::Util::encrypt($password)); + } + if (defined $level) { + $account->set_level($level); + } + return $self->_data_manager()->modify_account($account); +} + +sub remove_accounts { + my ($self, @ids) = @_; + return $self->_data_manager()->remove_accounts(@ids); +} + +sub log_event { + my ($self, $code, $user, $data) = @_; + return $self->_data_manager()->log_event( + new Chirpy::Event(undef, undef, $code, $user, $data) + ); +} + +sub get_events { + my ($self, $start, $count, $desc, $code, $user, $data) = @_; + $self->mark_debug_event('Request events'); + return $self->_data_manager()->get_events({ + 'reverse' => $desc, + 'first' => $start, + 'count' => $count, + 'code' => $code, + 'user' => $user, + 'data' => $data + }); +} + +sub attempt_login { + my ($self, $username, $password) = @_; + my $account = $self->get_account_by_username($username); + return undef unless (defined $account); + return ($account->get_password() eq Chirpy::Util::encrypt($password) + ? $account : undef); +} + +sub quotes_per_page { + my ($self, $value) = @_; + $self->{'quotes_per_page'} = $value if ($value); + return $self->{'quotes_per_page'} if (defined $self->{'quotes_per_page'}); + return $self->configuration()->get('ui', 'quotes_per_page'); +} + +sub quote_score_calculation_mode { + my $self = shift; + my $mode = $self->configuration()->get('general', + 'quote_score_calculation_mode'); + return (defined $mode && $mode == 1 ? 1 : 0); +} + +sub timing_enabled { + return $hires_timing; +} + +sub start_time { + my $self = shift; + return $self->{'start_time'}; +} + +sub total_time { + my $self = shift; + return ($hires_timing + ? Time::HiRes::time() - $self->{'start_time'} + : undef); +} + +sub set_up { + my ($self, $accounts, $news, $quotes) = @_; + $self->_data_manager()->set_up($accounts, $news, $quotes); +} + +sub remove { + my $self = shift; + $self->_data_manager()->remove(); +} + +sub die { + my $message = shift; + $message = 'Unknown error' unless (defined $message); + if ($DEBUG) { + confess $message; + } + else { + croak $message; + } +} + +sub mark_debug_event { + my ($self, $event) = @_; + if (exists $self->{'debug_events'}) { + my $now = Time::HiRes::time(); + push @{$self->{'debug_events'}}, [ $now, $event ]; + } +} + +sub debug_events { + my $self = shift; + return $self->{'debug_events'}; +} + +sub _data_manager { + my $self = shift; + return $self->{'data_manager'}; +} + +sub _create_data_manager { + my ($type, $params) = @_; + my $dm; + eval qq{ + use Chirpy::DataManager::$type; + \$dm = new Chirpy::DataManager::$type(\$params); + }; + Chirpy::die('Failed to load data manager "' . $type . '": ' . $@) + if ($@ || !defined $dm); + &_check_version($dm); + return $dm; +} + +sub _create_ui { + my ($type, $parent, $params) = @_; + my $ui; + eval qq{ + use Chirpy::UI::$type; + \$ui = new Chirpy::UI::$type(\$parent, \$params); + }; + Chirpy::die('Failed to load UI "' . $type . '": ' . $@) + if ($@ || !defined $ui); + &_check_version($ui); + return $ui; +} + +sub _check_version { + my $obj = shift; + my $version = (defined $obj ? $obj->get_target_version() : undef); + Chirpy::die(ref($obj) . ' incompatible: wanted target version ' + . $Chirpy::VERSION . ', got ' . $version) + unless ($version eq $Chirpy::VERSION); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Account.pm b/pub/qdb/src/modules/Chirpy/Account.pm new file mode 100644 index 0000000..63d8dad --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Account.pm @@ -0,0 +1,172 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Account.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Account - Represents a user account + +=head1 SYNOPSIS + + $account = new Chirpy::Account($id, $username, $password, $level); + + $id = $account->get_id(); + $account->set_id($id); + + $username = $account->get_username($username); + $account->set_username($username); + + $password = $account->get_password(); + $account->set_password($password); + + $level = $account->get_level(); + $account->set_level($level); + +=head1 CONSTRAINTS + +=over 4 + +=item ID + +The account ID must be a positive non-zero integer. + +=item Username + +The username must be valid against the C function of +L. + +=item Password + +Encryption is done I invoking the constructor or the C +function. The C function returns the I password. +The password must be valid against the C function and +encrypted using the C function, both part of L. + +=item User Level + +The user level must be one of the user level constants described below. + +=back + +=head1 USER LEVEL CONSTANTS + +The following constants are recommended for use as user levels: + + Chirpy::Account::USER_LEVEL_3 + Chirpy::Account::USER_LEVEL_6 + Chirpy::Account::USER_LEVEL_9 + +Note that the value of these is the integer representing the user level and +that the constants are only for the sake of code readability. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Account; + +use strict; +use warnings; + +use constant USER_LEVEL_3 => 3; +use constant USER_LEVEL_6 => 6; +use constant USER_LEVEL_9 => 9; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +sub new { + my ($class, $id, $username, $password, $level) = @_; + my $self = { + 'id' => $id, + 'username' => $username, + 'password' => $password, + 'level' => $level + }; + return bless($self, $class); +} + +sub get_id { + my $self = shift; + return $self->{'id'}; +} + +sub set_id { + my $self = shift; + return ($self->{'id'} = shift); +} + +sub get_username { + my $self = shift; + return $self->{'username'}; +} + +sub set_username { + my $self = shift; + return ($self->{'username'} = shift); +} + +sub get_password { + my $self = shift; + return $self->{'password'}; +} + +sub set_password { + my $self = shift; + return ($self->{'password'} = shift); +} + +sub get_level { + my $self = shift; + return $self->{'level'}; +} + +sub set_level { + my $self = shift; + return ($self->{'level'} = shift); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Configuration.pm b/pub/qdb/src/modules/Chirpy/Configuration.pm new file mode 100644 index 0000000..aa21595 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Configuration.pm @@ -0,0 +1,93 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Configuration.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Configuration - Represents a configuration + +=head1 SYNOPSIS + + $configuration = new Chirpy::Configuration('/path/to/chirpy.ini'); + + $value = $configuration->get($section, $name); + + $hash_ref = $configuration->get_parameter_hash($section); + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Configuration; + +use strict; +use warnings; + +use vars qw($VERSION @ISA); + +$VERSION = '0.3'; +@ISA = qw(Chirpy::Util::IniFile); + +use Chirpy 0.3; +use Chirpy::Util::IniFile 0.3; + +sub new { + my ($class, $file) = @_; + return $class->SUPER::new($file || 'chirpy.ini'); +} + +sub get_parameter_hash { + my ($self, $level1, $level2) = @_; + my $level1_hash = $self->get($level1); + return undef unless ($level1_hash); + my %hash = (); + my $e = quotemeta lc $level2; + while (my ($key, $value) = each %$level1_hash) { + next unless ($key =~ s/^$e\.//); + $hash{$key} = $value; + } + return \%hash; +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/DataManager.pm b/pub/qdb/src/modules/Chirpy/DataManager.pm new file mode 100644 index 0000000..ed35523 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/DataManager.pm @@ -0,0 +1,569 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: DataManager.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::DataManager - Abstract data manager class + +=head1 IMPLEMENTATION + +This section should tell you just about everything you need to know if you want +to write your own Chirpy! data manager. + +First of all, it must be a class that extends this abstract class, and it must +have something along the lines of + + use vars qw($VERSION @ISA); + $VERSION = '0.3'; + @ISA = qw(Chirpy::DataManager); + +All you need to do then, really, is implement this class's abstract methods. +Unfortunately, there are quite a few of them. However, a lot of them are fairly +trivial, as you will quickly learn. All of them are object methods. + +=head2 Compatibility + +=over 4 + +=item get_target_version() + +The return value of this function is compared to Chirpy!'s version. If the +version numbers do not match, execution will be aborted. Hence, it needs to be +exactly the same as the version of Chirpy! the data manager was built for. + +=back + +=head2 Parameters + +=over 4 + +=item get_parameter($name) + +Gets the value of a persistent parameter. + +=item set_parameter($name, $value) + +Sets the value of a persistent parameter. + +=back + +=head2 Installation & Removal + +=over 4 + +=item set_up($accounts, $news, $quotes) + +Called at installation time, this method should create all necessary resources +to use the data manager. For instance, if the module uses a database, this +method should create the necessary tables in it. + +In addition, the method takes 3 arguments, each either an array reference or +C. They represent the initial data to be stored in the installation. +C<$accounts> holds instances of L, C<$news> instances of +L and C<$quotes> instances of L. + +=item remove() + +The exact opposite of the C method. Removes all applicable data. + +=back + +=head2 Quotes + +=over 4 + +=item get_quotes($options) + +Accepts C<$options>, a reference to a hash containing parameters defining the +output, or C. + +In a scalar context, returns a reference to an array of matching quotes (as +instances of L), or C if there are no matches. Otherwise, +returns that array, along with 2 boolean values. The first is true if there +are quotes I the start of the results, the second is true if there are +quotes I the end of the results. + +The option hash may contain any of the keys below. If multiple keys are +present, the properties they imply must I apply to the resulting quotes. + +=over 8 + +=item id + +ID of the quote to retrieve. + +=item contains + +Reference to an array of strings to find in either the quote body or notes. May +contain wildcard characters: an asterisk (C<*>) may represent any character +sequence, including an empty one; a question mark (C) represents a single +character. To search for the wildcard characters themselves, they may be +prefixed with a backslash (C<\>). Consequently, backslashes themselves must +also be prefixed with a backslash. + +=item tags + +Reference to an array of tags, any of which must be a tag of the quote. + +=item approved + +Boolean value indicating the I status of the quote. Note that a false +value implies that the quotes must not be approved, while C cannot +affect the results. + +=item flagged + +Boolean value indicating the I status of the quote. Note that a false +value implies that the quotes must not be flagged, while C cannot +affect the results. + +=item since + +UNIX timestamp representing the earliest date allowed for the date when quotes +were submitted. + +=item sort + +Properties of the quote to sort on, represented as a two-dimensional array +reference. Here is an example: + + [ [ 'score', 0 ], [ 'id', 1 ] ] + +The boolean value is true if the results should be in descending order. Hence, +the sorting instruction above means the data manager should sort by score +first, in ascending order. If the ratings are equal, then it should sort by ID, +in descending order. + +The possible properties to sort on are C, C, C, C, +and C. It is assumed that sorting on the date when the quote was +submitted has the same effect as sorting on quote ID. + +Sorting on C was introduced in Chirpy! 0.3 and is used to determine the +top and bottom quotes. Quotes' scores are calculated as follows: + + votes + rating + ---------------- + 1 + positive votes + 1 2 + score = -------------------- = ---------------------- + negative votes + 1 votes - rating + ---------------- + 1 + 2 + +Data managers may prefer to cache this value for performance purposes. + +=item random + +Boolean value indicating if results should be randomly selected. Overrides the +"sort" option. + +=item first + +The number of the first result to return, 0 being the first in the result list. + +=item count + +The number of quotes to maximally return. + +=back + +=item quote_count($options) + +Returns the number of quotes in the database, either approved, unapproved, or +both. Optionally accepts C<$options>, a reference to a hash of parameters +defining which quotes are to be counted. Currently, C<$options> may contain only +one parameter, namely C. If C is undefined, all quotes are +included in the count; if it is a true value, only approved quotes are included, +and vice versa. + +=item add_quote($quote) + +Adds the L C<$quote> to the collection. Assigns an +ID to the quote and updates the object with it. Returns a true value upon +success. + +The properties to be saved by this method are I, I, I +and I. + +=item modify_quote($quote) + +Updates the L C<$quote> in the collection. Returns +a true value on success. + +The properties to be saved by this method are I and I. + +=item remove_quote($quote) + +Removes the L C<$quote> from the collection. +Returns a true value on success. + +=item remove_quotes(@ids) + +Removes all quotes whose ID is in C<@ids> from the collection. Returns the +number of removed quotes. + +=item increase_quote_rating($id, $revert) + +Increases the rating of quote number C<$id>. If C<$revert> is a true value, +increases the rating by 2, as the user is reverting his vote; otherwise, +increases it by 1. If the user is not reverting his vote, increases the number +of votes for the quote by 1 as well. Returns a list containing the updated +rating and vote count. + +=item decrease_quote_rating($id, $revert) + +Decreases the rating of quote number C<$id>. If C<$revert> is a true value, +decreases the rating by 2, as the user is reverting his vote; otherwise, +decreases it by 1. If the user is not reverting his vote, increases the number +of votes for the quote by 1 as well. Returns a list containing the updated +rating and vote count. + +=item get_tag_use_counts() + +Returns a reference to a hash, mapping tags to the number of times they were +used. Only tags for approved quotes should be counted. + +=item approve_quotes(@ids) + +Sets the quotes associated with an ID in C<@ids> to I. Returns the +number of affected quotes. + +=item unflag_quotes(@ids) + +Sets the quotes associated with an ID in C<@ids> to I. Returns the +number of affected quotes. + +=back + +=head2 News Items + +=over 4 + +=item get_news_items($options) + +Retrieves news items, a lot like L +retrieves quotes. The possible options are now: + +=over 8 + +=item id + +ID of the news item to retrieve. + +=item count + +Maximum number of news items returned. + +=back + +Resulting news items are always sorted by date, newest first. + +The function returns a reference to an array of instances of +L, or C if no matching news items were found. + +=item add_news_item($news_item) + +Adds the L C<$news_item> to the collection. +Assigns an ID to the news item and updates the object with it. Returns a true +value upon success. + +The properties to be saved by this method are I and I. I +may be C if the poster is unknown. + +=item modify_news_item($news_item) + +Updates the L C<$news_item> in the +collection. Returns a true value on success. + +The properties to be saved by this method are I and I. I +may be C if the poster is unknown. + +=item remove_news_item($news_item) + +Removes the L C<$news_item> from the +collection. Returns a true value on success. + +=item remove_news_items(@ids) + +Removes all news items whose ID is in C<@ids> from the collection. Returns the +number of removed news items. + +=back + +=head2 User Accounts + +=over 4 + +=item get_accounts($options) + +Retrieves accounts, a lot like L retrieves +quotes and L retrieves news items. +The possible options are now: + +=over 8 + +=item id + +ID of the account to retrieve. + +=item username + +User name of the account to retrieve. + +=item levels + +Reference to an array containing allowed user levels. + +=back + +Resulting accounts are always sorted by user level, highest first, then by user +name. + +The function returns a reference to an array of instances of +L, or C if no matching accounts were found. + +=item add_account($account) + +Adds the L C<$account> to the collection. +Assigns an ID to the account and updates the object with it. Returns a true +value upon success. + +The properties to be saved by this method are I, I and +I. + +=item modify_account($account) + +Updates the L C<$account> in the collection. +Returns a true value on success. + +The properties to be saved by this method are I, I and +I. + +=item remove_account($account) + +Removes the L C<$account> from the collection. +Returns a true value on success. + +Note that upon removal of an account, news items associated with it must be +kept, but their author becomes unknown. + +=item remove_accounts(@ids) + +Removes all accounts whose ID is in C<@ids> from the collection. Returns the +number of removed accounts. + +Note that upon removal of an account, news items associated with it must be +kept, but their author becomes unknown. + +=item username_exists($username) + +Returns a true value if the given username exists in the collection. + +=item account_count($params) + +Returns the current number of accounts. Takes an optional hash reference to +retrieval options. For now, it can only contain the key C, whose value +is a reference to an array of user levels. If this key is set, the function +returns the number of accounts with any of those levels. + +=back + +=head2 Logging + +=over 4 + +=item get_events($options) + +Retrieves log events, taking a hash reference like the other C functions. +Returns results in the same fashion as L, +but always sorted chronologically. This time, the available options are: + +=over 8 + +=item code + +Either a single event code or a reference to an array of codes to match. This +option may be omitted. + +=item user + +Either a single user ID or a reference to an array of IDs to match. This option +may be omitted. A user ID of 0 represents users who are not logged in. + +=item first + +The number of the first result to return, 0 being the first in the result list. +If this option is omitted, the default value of 0 is assumed. + +=item count + +The number of events to maximally return. If this option is omitted, there is +no limit on the number of results. + +=item reverse + +If this option has a true value, the order is reversed, so the events are in +reverse chronological order. + +=item data + +May be set to a hash reference to filter the events on metadata. If any of the +key-value pairs in the hash are equal to a metadata property of the event, it +is considered a match. This option may be omitted. + +=back + +=item log_event($event) + +Logs the L C<$event>. Returns a true value on +success. + +The properties to be saved by this method are I, I and I. + +=back + +=head2 Sessions + +I + +In addition, you may want to provide compatibility with +L's session manager by making your data +manager class extend L as well. +Please check L for +instructions. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L, L, +L, L, +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::DataManager; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; +use Chirpy::Util 0.3; + +sub new { + my ($class, $params) = @_; + return bless { + 'params' => $params + }, $class; +} + +sub param { + my ($self, $name) = @_; + return defined $self->{'params'} ? $self->{'params'}{$name} : undef; +} + +*get_target_version = \&Chirpy::Util::abstract_method; + +*set_up = \&Chirpy::Util::abstract_method; + +*remove = \&Chirpy::Util::abstract_method; + +*get_quote = \&Chirpy::Util::abstract_method; + +*quote_count = \&Chirpy::Util::abstract_method; + +*get_quotes = \&Chirpy::Util::abstract_method; + +*add_quote = \&Chirpy::Util::abstract_method; + +*modify_quote = \&Chirpy::Util::abstract_method; + +*increase_quote_rating = \&Chirpy::Util::abstract_method; + +*decrease_quote_rating = \&Chirpy::Util::abstract_method; + +*get_tag_use_counts = \&Chirpy::Util::abstract_method; + +*approve_quotes = \&Chirpy::Util::abstract_method; + +*flag_quotes = \&Chirpy::Util::abstract_method; + +*unflag_quotes = \&Chirpy::Util::abstract_method; + +*remove_quote = \&Chirpy::Util::abstract_method; + +*remove_quotes = \&Chirpy::Util::abstract_method; + +*get_news_items = \&Chirpy::Util::abstract_method; + +*add_news_item = \&Chirpy::Util::abstract_method; + +*modify_news_item = \&Chirpy::Util::abstract_method; + +*remove_news_item = \&Chirpy::Util::abstract_method; + +*remove_news_items = \&Chirpy::Util::abstract_method; + +*get_accounts = \&Chirpy::Util::abstract_method; + +*add_account = \&Chirpy::Util::abstract_method; + +*modify_account = \&Chirpy::Util::abstract_method; + +*remove_account = \&Chirpy::Util::abstract_method; + +*remove_accounts = \&Chirpy::Util::abstract_method; + +*username_exists = \&Chirpy::Util::abstract_method; + +*account_count = \&Chirpy::Util::abstract_method; + +*get_events = \&Chirpy::Util::abstract_method; + +*log_event = \&Chirpy::Util::abstract_method; + +*get_parameter = \&Chirpy::Util::abstract_method; + +*set_parameter = \&Chirpy::Util::abstract_method; + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/DataManager/MySQL.pm b/pub/qdb/src/modules/Chirpy/DataManager/MySQL.pm new file mode 100644 index 0000000..5270e33 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/DataManager/MySQL.pm @@ -0,0 +1,1256 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: MySQL.pm 304 2007-02-09 01:06:15Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::DataManager::MySQL - Data manager class, based on a MySQL backend + +=head1 REQUIREMENTS + +Apart from a proper Chirpy! installation, this module requires the following +Perl modules: + + Data::Dumper + DBD::mysql + DBI + +In addition, it needs access to a server running MySQL version 4.1 or higher. + +=head1 CONFIGURATION + +This module uses the following values from your configuration file: + +=over 4 + +=item mysql.hostname + +The name of the server to use. In most cases, this will be C. + +=item mysql.port + +The port on which the MySQL server accepts connections. The default is C<3306>. + +=item mysql.username + +The username to use for MySQL connections. + +=item mysql.password + +The password to use for MySQL connections. Note that this password is readable +if the user gains access to the file, and while Chirpy! takes precautions to +make the configuration file inaccessible, it is also your responsibility to +make sure that no one finds your password. + +=item mysql.database + +The name of the MySQL database where Chirpy! will store its data. + +=item mysql.prefix + +If your host only allows one database, Chirpy! can prefix its table names with +a custom prefix, so you can easily distinguish them. For instance, if you set +this value to C, the tables will be called C, +C, etc. + +=back + +=head1 SESSIONS + +This class fully implements L, which +allows L to use sessions. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::DataManager::MySQL; + +use strict; +use warnings; + +use vars qw($VERSION $TARGET_VERSION @ISA $SCORE_EXPR); + +$VERSION = '0.3'; +@ISA = qw(Chirpy::DataManager Chirpy::UI::WebApp::Session::DataManager); + +$TARGET_VERSION = '0.3'; + +$SCORE_EXPR = '`score` = ((`votes` + `rating`) / 2 + 1)' + . ' / ((`votes` - `rating`) / 2 + 1)'; + +use Chirpy 0.3; + +use Chirpy::DataManager 0.3; +use Chirpy::UI::WebApp::Session::DataManager 0.3; + +use Chirpy::Quote 0.3; +use Chirpy::Account 0.3; +use Chirpy::NewsItem 0.3; +use Chirpy::Event 0.3; + +use DBI; + +use Data::Dumper; + +sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + my $dbh = DBI->connect('DBI:mysql:database=' . $self->param('database') + . ($self->param('hostname') + ? ';host=' . $self->param('hostname') + : '') + . ($self->param('port') ? ';port=' . $self->param('port') : ''), + $self->param('username'), $self->param('password')); + Chirpy::die('Failed to connect to database: ' . DBI->errstr()) + unless (defined $dbh); + $dbh->do('SET NAMES utf8'); + $self->{'dbh'} = $dbh; + $self->{'prefix'} = $self->param('prefix'); + return $self; +} + +sub get_target_version { + return $TARGET_VERSION; +} + +sub DESTROY { + my $self = shift; + my $handle = $self->handle(); + return unless (defined $handle); + $handle->disconnect(); +} + +sub set_up { + my ($self, $accounts, $news, $quotes) = @_; + my $prefix = $self->table_name_prefix(); + my $handle = $self->handle(); + my $table = $prefix . 'accounts'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` int unsigned NOT NULL auto_increment, + `username` varchar(32) NOT NULL, + `password` varchar(32) NOT NULL, + `level` tinyint(1) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'news'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` int unsigned NOT NULL auto_increment, + `body` text NOT NULL, + `poster` int unsigned default NULL, + `date` timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'quotes'; + my $determine_votes = 0; + my $determine_scores = 0; + if ($self->_table_exists($table)) { + unless ($self->_column_exists($table, 'votes')) { + $handle->do(q| + ALTER TABLE `| . $table . q|` + ADD `votes` int unsigned NOT NULL default 0 AFTER `rating` + |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr()); + $determine_votes = 1; + } + unless ($self->_column_exists($table, 'score')) { + $handle->do(q| + ALTER TABLE `| . $table . q|` + ADD `score` double unsigned NOT NULL default 1 + |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr()); + $determine_scores = 1; + } + } + else { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` int unsigned NOT NULL auto_increment, + `body` text NOT NULL, + `notes` text, + `rating` int NOT NULL default 0, + `votes` int unsigned NOT NULL default 0, + `submitted` timestamp NOT NULL default CURRENT_TIMESTAMP, + `approved` tinyint(1) unsigned NOT NULL default 0, + `flagged` tinyint(1) unsigned NOT NULL default 0, + `score` double unsigned NOT NULL default 1, + PRIMARY KEY (`id`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'tags'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` int unsigned NOT NULL auto_increment, + `tag` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + INDEX (`tag`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'quote_tag'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `quote_id` int unsigned NOT NULL, + `tag_id` int unsigned NOT NULL, + INDEX (`quote_id`), + INDEX (`tag_id`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'events'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` int unsigned NOT NULL auto_increment, + `date` timestamp NOT NULL default CURRENT_TIMESTAMP, + `code` int unsigned NOT NULL, + `user` int unsigned, + PRIMARY KEY (`id`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'event_metadata'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` int unsigned NOT NULL, + `name` varchar(32) NOT NULL, + `value` text NOT NULL, + INDEX (`id`), + INDEX (`name`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + if ($self->_table_exists($prefix . 'log')) { + $self->_migrate_log(); + $self->_remove_table($prefix . 'log'); + } + $self->_determine_votes() if ($determine_votes); + $self->_determine_scores() if ($determine_scores); + $table = $prefix . 'sessions'; + if ($self->_table_exists($table)) { + unless ($self->_column_exists($table, 'expires')) { + # XXX: Maybe unserialize session data to get the right expiry time. + # It's not really worth the effort though. + my $exp = time() + 3 * 24 * 60 * 60; + $handle->do(q| + ALTER TABLE `| . $table . q|` + ADD `expires` int unsigned NOT NULL AFTER `id` + |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr()); + $handle->do(q| + ALTER TABLE `| . $table . q|` ADD INDEX (`expires`) + |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr()); + $handle->do(q| + UPDATE `| . $table . q|`SET `expires` = | . $exp + ) or Chirpy::die('Cannot update ' . $table . ': ' . DBI->errstr()); + } + } + else { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `id` varchar(32) NOT NULL, + `expires` int unsigned NOT NULL, + `data` text NOT NULL, + UNIQUE KEY `id` (`id`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + $table = $prefix . 'vars'; + unless ($self->_table_exists($table)) { + $handle->do(q| + CREATE TABLE `| . $table . q|` ( + `name` varchar(32) NOT NULL, + `value` varchar(255) NOT NULL, + PRIMARY KEY (`name`) + ) TYPE=MyISAM DEFAULT CHARSET=utf8 + |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr()); + } + if (defined $accounts) { + foreach my $account (@$accounts) { + $self->add_account($account); + } + } + if (defined $news) { + foreach my $item (@$news) { + $self->add_news_item($item); + } + } + if (defined $quotes) { + foreach my $quote (@$quotes) { + $self->add_quote($quote); + } + } +} + +sub remove { + my ($self, $accounts, $news, $quotes) = @_; + my @tables = qw/accounts news quotes tags quote_tag log sessions vars/; + foreach my $table (@tables) { + $self->_remove_table($self->table_name_prefix() . $table); + } +} + +sub _migrate_log { + my $self = shift; + my $prefix = $self->table_name_prefix(); + my $old_table = $prefix . 'log'; + my $event_table = $prefix . 'events'; + my $metadata_table = $prefix . 'event_metadata'; + my $query = 'SELECT * FROM `' . $old_table . '`'; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + while (my $row = $sth->fetchrow_hashref()) { + my $date = $row->{'date'}; + my $code = $row->{'code'}; + my $user = $row->{'user'}; + my $data = $row->{'data'}; + eval '$data = ' . $data; + if ($data && ref $data eq 'HASH') { + $self->_do('INSERT INTO `' . $event_table . '`' + . ' (`date`, `code`, `user`) VALUES (?, ?, ?)', + $date, $code, $user); + my $id = $self->handle()->{'mysql_insertid'}; + my $params = (exists $data->{'parameters'} + ? $data->{'parameters'} : {}); + if (exists $data->{'user'}) { + while (my ($name, $value) = each %{$data->{'user'}}) { + $params->{'user:' . $name} = $value; + } + } + while (my ($name, $value) = each %$params) { + if (ref $value) { + if ($name eq 'old_tags' || $name eq 'new_tags') { + # Tags were stored as an arrayref. + $value = join(' ', @$value); + } + elsif ($name eq 'poster' || $name eq 'old_poster') { + # Old versions used the object instead of its ID. + $value = $value->{'id'}; + } + else { + # This should never happen. + $value = &_serialize($value); + } + } + elsif ($name eq 'quote' && $value eq 'id') { + # Bug 1493589. The quote ID is lost, so we use 0. + $name = 'id'; + $value = 0; + } + next unless (defined $value && $value ne ''); + my $query = 'INSERT INTO `' . $metadata_table . '`' + . ' (`id`, `name`, `value`) VALUES (?, ?, ?)'; + my @params = ($id, $name, $value); + # XXX: This causes Unicode breakage sometimes. + #$self->_do($query, @params); + my $success = $self->handle()->do($query, undef, @params); + unless ($success) { + $params[2] = 'ERROR'; + $self->_do($query, @params); + } + } + } + else { + # TODO: Failed to unserialize--report. + } + } +} + +sub _determine_votes { + my $self = shift; + my $prefix = $self->table_name_prefix(); + my $query = 'SELECT `value`, COUNT(*)' + . ' FROM `' . $prefix . 'events` AS ev' + . ' LEFT JOIN `' . $prefix . 'event_metadata` AS md' + . ' ON ev.id = md.id' + . ' WHERE `code` = ' . Chirpy::Event::QUOTE_RATING_DOWN + . ' AND `name` = "id"' + . ' GROUP BY `value`'; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + my %neg = (); + while (my $row = $sth->fetchrow_arrayref()) { + my ($id, $votes) = @$row; + $neg{$id} = $votes; + } + $query = 'SELECT `id`, `rating` FROM `' . $prefix . 'quotes`'; + $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + $query = 'UPDATE `' . $prefix . 'quotes`' + . ' SET `votes` = ? WHERE `id` = ? LIMIT 1'; + my $s = $self->handle()->prepare($query); + while (my $row = $sth->fetchrow_hashref()) { + my ($id, $rating) = ($row->{'id'}, $row->{'rating'}); + my $votes = $rating; + $votes += $neg{$id} * 2 if (exists $neg{$id} && $neg{$id}); + # This is only meaningful if the log is inconsistent. + $votes = abs($rating) if ($votes < 0); + $rows = $s->execute($votes, $id); + $self->_db_error() unless (defined $rows); + } +} + +sub _determine_scores { + my $self = shift; + my $prefix = $self->table_name_prefix(); + my $query = 'UPDATE `' . $prefix . 'quotes`' + . ' SET ' . $SCORE_EXPR; + $self->_do($query); +} + +sub _remove_table { + my ($self, $table) = @_; + return unless ($self->_table_exists($table)); + defined $self->_do('DROP TABLE `' . $table . '`') + or Chirpy::die('Cannot remove "' . $table . '": ' . DBI->errstr()); +} + +sub _table_exists { + my ($self, $table) = @_; + my $query = "SHOW TABLES LIKE '$table'"; + return defined $self->_execute_scalar($query); +} + +sub _column_exists { + my ($self, $table, $column) = @_; + my $query = "SHOW COLUMNS FROM $table"; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + while (my $row = $sth->fetchrow_hashref()) { + return 1 if ($row->{'Field'} eq $column); + } + return 0; +} + +# TODO: Implement a ResultSet interface in order to make this more lightweight +sub get_quotes { + my ($self, $params) = @_; + $params = {} unless (ref $params eq 'HASH'); + my $query = 'SELECT DISTINCT `q`.`id` AS `id`, `body`, `notes`,' + . ' `rating`, `votes`,' + . ' UNIX_TIMESTAMP(`submitted`) AS `submitted`, `approved`, `flagged`' + . ' FROM `' . $self->table_name_prefix() . 'quotes` AS `q`'; + my @par = (); + my @cond = (); + if (defined $params->{'flagged'}) { + push @cond, '`flagged` ' + . ($params->{'flagged'} ? '<>' : '=') . ' 0'; + } + if (defined $params->{'approved'}) { + push @cond, '`approved` ' + . ($params->{'approved'} ? '<>' : '=') . ' 0'; + } + if (defined $params->{'contains'} && @{$params->{'contains'}}) { + foreach my $q (@{$params->{'contains'}}) { + (my $query = $q) =~ s/(?{'tags'} && @{$params->{'tags'}}) { + $query .= ' JOIN `' . $self->table_name_prefix() . 'quote_tag` AS `qt`' + . ' ON `q`.`id` = `qt`.`quote_id`' + . ' JOIN `' . $self->table_name_prefix() . 'tags` AS `t`' + . ' ON `qt`.`tag_id` = `t`.`id`'; + my @tags = @{$params->{'tags'}}; + push @cond, '`tag` IN (?' . (',?' x (scalar(@tags) - 1)) . ')'; + push @par, @tags; + } + if (defined $params->{'since'}) { + push @cond, '`submitted` > FROM_UNIXTIME(?)'; + push @par, $params->{'since'}; + } + if (defined $params->{'id'}) { + push @cond, '`id` = ?'; + push @par, $params->{'id'}; + delete $params->{'first'}; + delete $params->{'sort'}; + } + if (@cond) { + $query .= ' WHERE ' . join(' AND ', @cond); + } + if ($params->{'random'}) { + $query .= ' ORDER BY RAND()'; + } + elsif (ref $params->{'sort'} eq 'ARRAY') { + $query .= ' ORDER BY ' . join(', ', map { + '`q`.`' . $_->[0] . '`' . ($_->[1] ? ' DESC' : '') + } @{$params->{'sort'}}); + } + my $per_page; + my $leading; + if (defined $params->{'id'}) { + $query .= ' LIMIT 1'; + } + elsif ($params->{'count'}) { + $query .= ' LIMIT '; + if ($params->{'first'}) { + $leading = 1; + $query .= int($params->{'first'}) . ','; + } + $query .= int($params->{'count'}) + 1; + $per_page = $params->{'count'}; + } + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(@par); + $self->_db_error() unless (defined $rows); + my $trailing; + my @result = (); + while (my $row = $sth->fetchrow_hashref()) { + if ($per_page && @result >= $per_page) { + $trailing = 1; + last; + } + push @result, new Chirpy::Quote( + $row->{'id'}, Chirpy::Util::decode_utf8($row->{'body'}), + Chirpy::Util::decode_utf8($row->{'notes'}), + $row->{'rating'}, $row->{'votes'}, $row->{'submitted'}, + $row->{'approved'}, $row->{'flagged'}, + $self->_quote_tags($row->{'id'}) + ); + } + my $result = (@result ? \@result : undef); + return ($result, $leading, $trailing) if (wantarray); + return $result; +} + +sub quote_count { + my ($self, $params) = @_; + $params = {} unless (ref $params eq 'HASH'); + my $appr = $params->{'approved'}; + return $self->_execute_scalar('SELECT COUNT(*) FROM `' + . $self->table_name_prefix() . 'quotes`' + . (defined $appr ? ' WHERE `approved` = ' . ($appr ? '1' : '0') : '')); +} + +sub add_quote { + my ($self, $quote) = @_; + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'quotes`' + . ' (`body`, `notes`, `approved`)' + . ' VALUES (?, ?, ?)', + $quote->get_body(), + $quote->get_notes(), + $quote->is_approved() || 0); + my $id = $self->handle()->{'mysql_insertid'}; + $quote->set_id($id); + $self->_tag($id, $quote->get_tags()); + return 1; +} + +sub modify_quote { + my ($self, $quote) = @_; + Chirpy::die('Not a Chirpy::Quote') + unless (ref $quote eq 'Chirpy::Quote'); + my $result = $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`' + . ' SET `body` = ?, `notes` = ? WHERE `id` = ?', + $quote->get_body(), $quote->get_notes(), $quote->get_id()); + $self->_replace_tags($quote->get_id(), $quote->get_tags()); + return $result; +} + +sub increase_quote_rating { + my ($self, $id, $revert) = @_; + my ($r, $v) = ($revert + ? ('`rating` + 2', '`votes`') : ('`rating` + 1', '`votes` + 1')); + $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`' + . ' SET `rating` = ' . $r . ', `votes` = ' . $v . ',' + . ' ' . $SCORE_EXPR + . ' WHERE `id` = ' . $id . ' LIMIT 1') + or return undef; + return $self->_get_quote_rating_and_vote_count($id); +} + +sub decrease_quote_rating { + my ($self, $id, $revert) = @_; + my ($r, $v) = ($revert + ? ('`rating` - 2', '`votes`') : ('`rating` - 1', '`votes` + 1')); + $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`' + . ' SET `rating` = ' . $r . ', `votes` = ' . $v . ',' + . ' ' . $SCORE_EXPR + . ' WHERE `id` = ' . $id . ' LIMIT 1') + or return undef; + return $self->_get_quote_rating_and_vote_count($id); +} + +sub get_tag_use_counts { + my $self = shift; + my $query = 'SELECT `tag`, COUNT(`tag_id`) AS `cnt`' + . ' FROM `' . $self->table_name_prefix() . 'quote_tag` AS `qt`' + . ' JOIN `' . $self->table_name_prefix() . 'tags` AS `t`' + . ' ON `qt`.`tag_id` = `t`.`id`' + . ' JOIN `' . $self->table_name_prefix() . 'quotes` AS `q`' + . ' ON `qt`.`quote_id` = `q`.`id`' + . ' WHERE `approved` = 1' + . ' GROUP BY `tag`'; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + my %result = (); + while (my $row = $sth->fetchrow_hashref()) { + $result{$row->{'tag'}} = $row->{'cnt'}; + } + return \%result; +} + +sub approve_quotes { + my ($self, @ids) = @_; + return undef unless (@ids); + return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`' + . ' SET `approved` = 1 WHERE `id` IN (' + . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids); +} + +sub flag_quotes { + my ($self, @ids) = @_; + return undef unless (@ids); + return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`' + . ' SET `flagged` = 1 WHERE `id` IN (' + . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids); +} + +sub unflag_quotes { + my ($self, @ids) = @_; + return undef unless (@ids); + return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`' + . ' SET `flagged` = 0 WHERE `id` IN (' + . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids); +} + +sub remove_quote { + my ($self, $quote) = @_; + Chirpy::die('Not a Chirpy::Quote') + unless (ref $quote eq 'Chirpy::Quote'); + my $id = quote->get_id(); + $self->_untag_all($id); + return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quotes`' + . ' WHERE `id` = ' . $id + . ' LIMIT 1'); +} + +sub remove_quotes { + my ($self, @ids) = @_; + return undef unless (@ids); + $self->_untag_all(@ids); + return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quotes`' + . ' WHERE `id` IN (' . join(',', map { int } @ids) . ')' + . ' LIMIT ' . scalar @ids); +} + +sub get_news_items { + my ($self, $params) = @_; + my $query = 'SELECT N.id, N.body, N.poster, ' + . 'UNIX_TIMESTAMP(N.date) AS `date`, A.username ' + . 'FROM `' . $self->table_name_prefix() . 'news` N' + . ' LEFT JOIN `' . $self->table_name_prefix() . 'accounts` A' + . ' ON N.poster = A.id'; + my @par = (); + if ($params->{'id'}) { + $query .= ' WHERE N.id = ?'; + push @par, $params->{'id'}; + $params->{'count'} = 1; + } + $query .= ' ORDER BY `date` DESC'; + if ($params->{'count'}) { + $query .= ' LIMIT ' . int $params->{'count'}; + } + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(@par); + $self->_db_error() unless (defined $rows); + my @result = (); + while (my $row = $sth->fetchrow_hashref()) { + my $item = new Chirpy::NewsItem( + $row->{'id'}, Chirpy::Util::decode_utf8($row->{'body'}), + ($row->{'username'} + ? new Chirpy::Account($row->{'poster'}, $row->{'username'}) + : undef), + $row->{'date'} + ); + push @result, $item; + } + return (@result ? \@result : undef); +} + +sub add_news_item { + my ($self, $news) = @_; + Chirpy::die('Not a Chirpy::NewsItem') + unless (ref $news eq 'Chirpy::NewsItem'); + my $poster = $news->get_poster(); + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'news`' + . ' (`body`, `poster`)' + . ' VALUES (?, ?)', + $news->get_body(), (defined $poster ? $poster->get_id() : undef), + $news->get_date()); + my $id = $self->handle()->{'mysql_insertid'}; + $news->set_id($id); + return 1; +} + +sub modify_news_item { + my ($self, $item) = @_; + Chirpy::die('Not a Chirpy::NewsItem') + unless (ref $item eq 'Chirpy::NewsItem'); + my $poster = $item->get_poster(); + return $self->_do('UPDATE `' . $self->table_name_prefix() . 'news`' + . ' SET `body` = ?, `poster` = ? WHERE `id` = ? LIMIT 1', + $item->get_body(), (defined $poster ? $poster->get_id() : undef), + $item->get_id()); +} + +sub remove_news_item { + my ($self, $item) = @_; + Chirpy::die('Not a Chirpy::NewsItem') + unless (ref $item eq 'Chirpy::NewsItem'); + return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'news`' + . ' WHERE `id` = ' . $item->get_id() + . ' LIMIT 1'); +} + +sub remove_news_items { + my ($self, @ids) = @_; + return undef unless (@ids); + return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'news`' + . ' WHERE `id` IN (' . join(',', map { int } @ids) . ')' + . ' LIMIT ' . scalar @ids); +} + +sub get_accounts { + my ($self, $params) = @_; + $params = {} unless (ref $params eq 'HASH'); + my $query = 'SELECT * FROM `' . $self->table_name_prefix() . 'accounts`'; + my @cond = (); + my @par = (); + if (ref $params->{'levels'} eq 'ARRAY') { + push @cond, '`level` IN (' . join (',', @{$params->{'levels'}}) . ')'; + } + if ($params->{'id'}) { + push @cond, '`id` = ?'; + push @par, $params->{'id'}; + } + if ($params->{'username'}) { + push @cond, '`username` = ?'; + push @par, $params->{'username'}; + } + if (@cond) { + $query .= ' WHERE ' . join(' AND ', @cond); + } + $query .= ' ORDER BY `level` DESC, `username`'; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(@par); + $self->_db_error() unless (defined $rows); + my @result = (); + while (my $row = $sth->fetchrow_hashref()) { + push @result, new Chirpy::Account( + $row->{'id'}, $row->{'username'}, $row->{'password'}, + $row->{'level'} + ); + } + return (@result ? \@result : undef); +} + +sub add_account { + my ($self, $user) = @_; + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'accounts`' + . ' (`username`, `password`, `level`) VALUES (?, ?, ?)', + $user->get_username(), $user->get_password(), $user->get_level()); + my $id = $self->handle()->{'mysql_insertid'}; + $user->set_id($id); + return 1; +} + +sub modify_account { + my ($self, $account) = @_; + Chirpy::die('Not a Chirpy::Account') + unless (ref $account eq 'Chirpy::Account'); + return $self->_do('UPDATE `' . $self->table_name_prefix() . 'accounts`' + . ' SET `username` = ?, `password` = ?, `level` = ?' + . ' WHERE `id` = ? LIMIT 1', + $account->get_username(), $account->get_password(), + $account->get_level(), $account->get_id()); +} + +sub remove_account { + my ($self, $account) = @_; + Chirpy::die('Not a Chirpy::Account') + unless (ref $account eq 'Chirpy::Account'); + $self->_do('UPDATE `' . $self->table_name_prefix() + . 'news` SET `poster` = NULL WHERE `poster` = ' . $account->get_id() + . ' LIMIT 1'); + return $self->_do('DELETE FROM `' . $self->table_name_prefix() + . 'accounts` WHERE `id` = ' . $account->get_id() + . ' LIMIT 1'); +} + +sub remove_accounts { + my ($self, @ids) = @_; + return undef unless (@ids); + my $ids = join(',', map { int } @ids); + my $num = scalar @ids; + $self->_do('UPDATE `' . $self->table_name_prefix() + . 'news` SET `poster` = NULL WHERE `poster` IN (' . $ids . ')' + . ' LIMIT ' . $num); + return $self->_do('DELETE FROM `' . $self->table_name_prefix() + . 'accounts` WHERE `id` IN (' . $ids . ')' + . ' LIMIT ' . $num); +} + +sub username_exists { + my ($self, $username) = @_; + return $self->_do('SELECT `id` FROM `' . $self->table_name_prefix() + . 'accounts` WHERE `username` = ? LIMIT 1', $username) ? 1 : 0; +} + +sub account_count { + my ($self, $params) = @_; + $params = {} unless (ref $params eq 'HASH'); + return $self->_execute_scalar('SELECT COUNT(*) FROM `' + . $self->table_name_prefix() . 'accounts`' + . (defined $params->{'levels'} + ? ' WHERE `level` IN (' . join(',', @{$params->{'levels'}}) . ')' + : '')); +} + +sub get_events { + my ($self, $params) = @_; + $params = {} unless (ref $params eq 'HASH'); + my $query = 'SELECT DISTINCT `e`.`id` AS `id`,' + . ' UNIX_TIMESTAMP(`date`) AS `date`, `code`, `user`' + . ' FROM `' . $self->table_name_prefix() . 'events` AS `e`'; + my @conditions = (); + my @param = (); + if (defined $params->{'data'} && %{$params->{'data'}}) { + $query .= ' JOIN `' . $self->table_name_prefix() + . 'event_metadata` AS `m`' + . ' ON `e`.`id` = `m`.`id`'; + my @cond = (); + while (my ($key, $value) = each %{$params->{'data'}}) { + push @cond, '(`name` = ? AND `value` = ?)'; + push @param, $key, $value; + } + push @conditions, '(' . join(' OR ', @cond) . ')'; + } + if (my $code = $params->{'code'}) { + if (ref $code eq 'ARRAY') { + my $count = scalar @$code; + if ($count) { + push @conditions, '`code` IN (?' . (',?' x ($count - 1)) . ')'; + push @param, @$code; + } + } + else { + push @conditions, '`code` = ?'; + push @param, $code; + } + } + my $user = $params->{'user'}; + if (defined $user) { + if (ref $user eq 'ARRAY') { + if (@$user) { + my @set = (); + my $guest = 0; + foreach my $u (@$user) { + if ($u) { + push @set, $u; + } + else { + $guest = 1; + } + } + my $cond; + if (@set) { + $cond = '`user` IN (?' . (',?' x (scalar(@set) - 1)) . ')'; + push @param, @set; + } + if ($guest) { + $cond = (defined $cond + ? '(' . $cond . ' OR `user` IS NULL)' + : '`user` IS NULL'); + } + push @conditions, $cond; + } + } + elsif ($user) { + push @conditions, '`user` = ?'; + push @param, $user; + } + else { + push @conditions, '`user` IS NULL'; + } + } + if (@conditions) { + $query .= ' WHERE ' . join(' AND ', @conditions); + } + $query .= ' ORDER BY `id` ' . ($params->{'reverse'} ? 'DESC' : 'ASC'); + my $per_page; + my $leading; + if ($params->{'count'}) { + $query .= ' LIMIT '; + if ($params->{'first'}) { + $leading = 1; + $query .= int($params->{'first'}) . ','; + } + $query .= int($params->{'count'}) + 1; + $per_page = $params->{'count'}; + } + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(@param); + $self->_db_error() unless (defined $rows); + my $trailing; + my @result = (); + while (my $row = $sth->fetchrow_hashref()) { + if ($per_page && @result >= $per_page) { + $trailing = 1; + last; + } + my $id = $row->{'id'}; + my $data = $self->_get_event_metadata($id); + push @result, new Chirpy::Event( + $id, $row->{'date'}, $row->{'code'}, $row->{'user'}, $data + ); + } + my $result = (@result ? \@result : undef); + return ($result, $leading, $trailing) if (wantarray); + return $result; +} + +sub _get_event_metadata { + my ($self, $id) = @_; + my $query = 'SELECT `name`, `value`' + . ' FROM `' . $self->table_name_prefix() . 'event_metadata`' + . ' WHERE `id` = ' . $id; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + my %result = (); + while (my $row = $sth->fetchrow_hashref()) { + $result{$row->{'name'}} = $row->{'value'}; + } + return \%result; +} + +sub log_event { + my ($self, $event) = @_; + Chirpy::die('Not a Chirpy::Event') + unless (ref $event eq 'Chirpy::Event'); + my $user = $event->get_user(); + my $prefix = $self->table_name_prefix(); + $self->_do('INSERT INTO `' . $prefix . 'events`' + . ' (`code`, `user`) VALUES (?, ?)', + $event->get_code(), + (defined $user ? $user->get_id() : undef)); + my $id = $self->handle()->{'mysql_insertid'}; + $event->set_id($id); + while (my ($name, $value) = each %{$event->get_data()}) { + $self->_do('INSERT INTO `' . $prefix . 'event_metadata`' + . ' (`id`, `name`, `value`) VALUES (?, ?, ?)', + $id, $name, $value); + } + return $id; +} + +sub add_session { + my ($self, $id, $data) = @_; + my $string = &_serialize($data); + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'sessions`' + . ' (`id`, `expires`, `data`) VALUES (?, ?, ?)', $id, + $data->{'_SESSION_ATIME'} + $data->{'_SESSION_ETIME'}, $string); + return 1; +} + +sub get_sessions { + my ($self, @ids) = @_; + my $query = 'SELECT `id`, `data` FROM `' . $self->table_name_prefix() + . 'sessions`'; + $query .= ' WHERE `id` IN (?' . (',?' x (scalar(@ids) - 1)) . ')' + . ' LIMIT ' . scalar(@ids) if (@ids); + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(@ids); + $self->_db_error() unless (defined $rows); + my @results = (); + while (my $row = $sth->fetchrow_hashref()) { + push @results, &_unserialize($row->{'data'}); + } + return @results; +} + +sub modify_session { + my ($self, $id, $data) = @_; + my $string = &_serialize($data); + my $sql = 'UPDATE `' . $self->table_name_prefix() + . 'sessions` SET `expires` = ?, `data` = ? WHERE `id` = ? LIMIT 1'; + $self->_do($sql, + $data->{'_SESSION_ATIME'} + $data->{'_SESSION_ETIME'}, $string, $id); +} + +sub remove_sessions { + my ($self, @ids) = @_; + return unless (@ids); + $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'sessions`' + . ' WHERE `id` IN (?' . (',?' x (scalar(@ids) - 1)) . ')' + . ' LIMIT ' . scalar(@ids), @ids); +} + +# Overrides Chirpy::UI::WebApp::Session::DataManager's default implementation +sub remove_expired_sessions { + my $self = shift; + $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'sessions`' + . ' WHERE `expires` < ' . time()); +} + +sub get_parameter { + my ($self, $name) = @_; + return $self->_execute_scalar('SELECT `value` FROM `' + . $self->table_name_prefix() . 'vars`' + . ' WHERE `name` = ?', $name); +} + +sub set_parameter { + my ($self, $name, $value) = @_; + if (!defined $value) { + $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'vars`' + . ' WHERE `name` = ? LIMIT 1', $name); + return; + } + my $res = $self->_do('UPDATE `' . $self->table_name_prefix() . 'vars`' + . ' SET `value` = ?' + . ' WHERE `name` = ? LIMIT 1', + $value, $name); + unless ($res) { + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'vars`' + . ' (`name`, `value`) VALUES (?, ?)', + $name, $value); + } +} + +sub handle { + my $self = shift; + return $self->{'dbh'}; +} + +sub table_name_prefix { + my $self = shift; + return $self->{'prefix'}; +} + +sub _quote_tags { + my ($self, $quote_id, $mode) = @_; + $mode = 0 unless (defined $mode); + my $cols = ($mode == 2 ? '`tag`, `id`' : ($mode == 1 ? '`id`' : '`tag`')); + my $query = 'SELECT ' .$cols + . ' FROM `' . $self->table_name_prefix() . 'tags` AS `t`' + . ' JOIN `' . $self->table_name_prefix() . 'quote_tag` AS `qt`' + . ' ON `t`.`id` = `qt`.`tag_id`' + . ' WHERE `quote_id` = ?'; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute($quote_id); + $self->_db_error() unless (defined $rows); + if ($mode == 2) { + my %result = (); + while (my $row = $sth->fetchrow_arrayref()) { + $result{$row->[0]} = $row->[1]; + } + return \%result; + } + my @result = (); + while (my $row = $sth->fetchrow_arrayref()) { + push @result, $row->[0]; + } + return \@result; +} + +sub _tag { + my ($self, $quote_id, $tags) = @_; + return unless (@$tags); + foreach my $tag (@$tags) { + my $tag_id = $self->_tag_id($tag); + unless (defined $tag_id) { + $tag_id = $self->_create_tag($tag); + } + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'quote_tag`' + . ' (`quote_id`, `tag_id`) VALUES (?, ?)', $quote_id, $tag_id); + } +} + +sub _untag { + my ($self, $quote_id, $tag_ids) = @_; + my $cnt = scalar @$tag_ids; + return unless ($cnt); + $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quote_tag`' + . ' WHERE `quote_id` = ? AND `tag_id` IN (?' . (',?' x ($cnt - 1)) . ')' + . ' LIMIT ' . $cnt, $quote_id, @$tag_ids); +} + +sub _untag_all { + my ($self, @quote_ids) = @_; + my $cnt = scalar @quote_ids; + return unless ($cnt); + $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quote_tag`' + . ' WHERE `quote_id` IN (?' . (',?' x ($cnt - 1)) . ')', @quote_ids); + $self->_clean_up_tags(); +} + +sub _tag_id { + my ($self, $tag) = @_; + return $self->_execute_scalar('SELECT `id` FROM `' + . $self->table_name_prefix() . 'tags` WHERE `tag` = ? LIMIT 1', $tag); +} + +sub _create_tag { + my ($self, $tag) = @_; + $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'tags`' + . ' (`tag`) VALUES (?)', $tag); + return $self->handle()->{'mysql_insertid'}; +} + +sub _replace_tags { + my ($self, $quote_id, $new_tags) = @_; + my $old_tags = $self->_quote_tags($quote_id, 2); + my @add = (); + foreach my $tag (@$new_tags) { + if (exists $old_tags->{$tag}) { + delete $old_tags->{$tag}; + } + else { + push @add, $tag; + } + } + my @remove = values %$old_tags; + $self->_untag($quote_id, \@remove); + $self->_clean_up_tags(); + $self->_tag($quote_id, \@add); +} + +sub _clean_up_tags { + my $self = shift; + $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'tags`' + . ' WHERE `id` NOT IN (' + . 'SELECT `tag_id` FROM `' . $self->table_name_prefix() . 'quote_tag`' + . ')'); +} + +sub _get_quote_rating_and_vote_count { + my ($self, $id) = @_; + my $sth = $self->handle()->prepare('SELECT `rating`, `votes`' + . ' FROM `' . $self->table_name_prefix() . 'quotes`' + . ' WHERE `id` = ' . $id . ' LIMIT 1'); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + my @row = $sth->fetchrow_array(); + return ($row[0], $row[1]); +} + +sub _get_quote_vote_count { + my ($self, $id) = @_; + my $sth = $self->handle()->prepare('SELECT `votes`' + . ' FROM `' . $self->table_name_prefix() . 'quotes`' + . ' WHERE `id` = ' . $id . ' LIMIT 1'); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(); + $self->_db_error() unless (defined $rows); + my @row = $sth->fetchrow_array(); + return $row[0]; +} + +sub _do { + my ($self, $query, @params) = @_; + my $rows = $self->handle()->do($query, undef, @params); + $self->_db_error() unless (defined $rows); + return ($rows eq '0E0' ? 0 : $rows); +} + +sub _execute_scalar { + my ($self, $query, @params) = @_; + my $sth = $self->handle()->prepare($query); + $self->_db_error() unless (defined $sth); + my $rows = $sth->execute(@params); + $self->_db_error() unless (defined $rows); + my @row = $sth->fetchrow_array(); + return (scalar(@row) ? $row[0] : undef); +} + +sub _serialize { + my $dumper = new Data::Dumper(\@_)->Terse(1)->Indent(0); + return $dumper->Dump(); +} + +sub _unserialize { + my $string = shift; + return (defined $string ? eval $string : undef); +} + +sub _db_error { + my $self = shift; + my $msg = $self->handle()->errstr(); + Chirpy::die($msg); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Event.pm b/pub/qdb/src/modules/Chirpy/Event.pm new file mode 100644 index 0000000..14030ab --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Event.pm @@ -0,0 +1,219 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Event.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Event - Represents a log event + +=head1 SYNOPSIS + + $event = new Chirpy::Event($id, $date, $code, $user, $data); + + $id = $event->get_id(); + $event->set_id($id); + + $date = $event->get_date(); + $event->set_date($date); + + $code = $event->get_code(); + $event->set_code($code); + + $user = $event->get_user(); + $event->set_user($user); + + $data = $event->get_data(); + $event->set_data($data); + + $event_code = Chirpy::Event::translate_code($code); + +=head1 CONSTRAINTS + +=over 4 + +=item ID + +The event ID must be a positive non-zero integer. + +=item Date + +The event date must be a UNIX timestamp. + +=item Code + +The event code must be one of the event code constants described below. + +=item User + +The user who was logged in at the time of the event must be an instance of +L, if any. + +=item Data + +The event data must be a reference to a hash containing information about the +event. + +=back + +=head1 EVENT CODE CONSTANTS + + Chirpy::Event::LOGIN_SUCCESS + Chirpy::Event::LOGIN_FAILURE + Chirpy::Event::CHANGE_PASSWORD + + Chirpy::Event::ADD_QUOTE + Chirpy::Event::EDIT_QUOTE + Chirpy::Event::REMOVE_QUOTE + Chirpy::Event::QUOTE_RATING_UP + Chirpy::Event::QUOTE_RATING_DOWN + Chirpy::Event::REPORT_QUOTE + Chirpy::Event::APPROVE_QUOTE + Chirpy::Event::UNFLAG_QUOTE + + Chirpy::Event::ADD_NEWS + Chirpy::Event::EDIT_NEWS + Chirpy::Event::REMOVE_NEWS + + Chirpy::Event::ADD_ACCOUNT + Chirpy::Event::EDIT_ACCOUNT + Chirpy::Event::REMOVE_ACCOUNT + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Event; + +use strict; +use warnings; + +use constant LOGIN_SUCCESS => 100; +use constant LOGIN_FAILURE => 101; +use constant CHANGE_PASSWORD => 102; + +use constant ADD_QUOTE => 200; +use constant EDIT_QUOTE => 201; +use constant REMOVE_QUOTE => 202; +use constant QUOTE_RATING_UP => 203; +use constant QUOTE_RATING_DOWN => 204; +use constant REPORT_QUOTE => 205; +use constant APPROVE_QUOTE => 206; +use constant UNFLAG_QUOTE => 207; + +use constant ADD_NEWS => 300; +use constant EDIT_NEWS => 301; +use constant REMOVE_NEWS => 302; + +use constant ADD_ACCOUNT => 400; +use constant EDIT_ACCOUNT => 401; +use constant REMOVE_ACCOUNT => 402; + +use vars qw($VERSION $CODES); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +sub new { + my ($class, $id, $date, $code, $user, $data) = @_; + my $self = { + 'id' => $id, + 'date' => $date, + 'code' => $code, + 'user' => $user, + 'data' => $data + }; + return bless($self, $class); +} + +sub get_id { + my $self = shift; + return $self->{'id'}; +} + +sub set_id { + my $self = shift; + return ($self->{'id'} = shift); +} + +sub get_date { + my $self = shift; + return $self->{'date'}; +} + +sub set_date { + my $self = shift; + return ($self->{'date'} = shift); +} + +sub get_code { + my $self = shift; + return $self->{'code'}; +} + +sub set_code { + my $self = shift; + return ($self->{'code'} = shift); +} + +sub get_user { + my $self = shift; + return $self->{'user'}; +} + +sub set_user { + my $self = shift; + return ($self->{'user'} = shift); +} + +sub get_data { + my $self = shift; + return $self->{'data'}; +} + +sub set_data { + my $self = shift; + return ($self->{'data'} = shift); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Locale.pm b/pub/qdb/src/modules/Chirpy/Locale.pm new file mode 100644 index 0000000..938d7c3 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Locale.pm @@ -0,0 +1,1159 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Locale.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Locale - Represents a locale + +=head1 SYNOPSIS + + $locale = new Chirpy::Locale('/path/to/locale.ini'); + + $value = $locale->get_string($name); + $value = $locale->get_string($name, @params); + + $target_version = $locale->get_target_version(); + $full_name = $locale->get_full_name(); + $version = $locale->get_version(); + $author_hash_ref = $locale->get_author_information(); + +=head1 CREATING LOCALES + +Locales are INI files with two sections, one with information about the locale +and one with the localized strings. It is recommended that you use the language +code as the filename, e.g. F for I or F for +I. + +=head2 Information Section + +Information about the locale is stored in the INI file's C section. +Locales use the following values for storing information about the locale: + +=over 4 + +=item chirpy_version + +The version of Chirpy! that the locale was made for. + +=item full_name + +The full name of the locale. Usually, this is the name of the locale language. + +=item version + +The version number of the locale. You can use any version number scheme you +like, but make sure people can distinguish between versions easily. + +=item author_name + +Your full name. + +=item author_email + +The e-mail address where people can contact you. + +=item author_uri + +The URL to your homepage, if any. + +=back + +=head2 Strings Section + +The localized strings are stored in the INI file's C section. Mocales +must define the strings listed below. For examples, please consult +F, the I locale, bundled with Chirpy! by default. + +Note that locales may contain extra strings which are specific to a user +interface class. These must be prepended by the name of the class and a dot. +The default L class requires a few +strings already, which are listed under +L in its documentation. + +=over 4 + +=item error_title + +Title used for errors. + +=item quote_browser + +Translation of I, for the title of that section or links to it. + +=item random_quotes + +Translation of I, for the title of that section or links to it. + +=item view_quote + +Translation of I, for the title of that section or links to it. + +=item top_quotes + +Translation of I, for the title of that section or links to it. + +=item bottom_quotes + +Translation of I, for the title of that section or links to it. + +=item quotes_of_the_week + +Translation of I, for the title of that section or links to +it. + +=item search_for_quotes + +Translation of I, for the title of that section or links to +it. + +=item tag_cloud + +Translation of I, for the title of that section or links to it. + +=item statistics + +Translation of I, for the title of that section or links to it. + +=item administration + +Translation of I, for the title of that section or links to it. + +=item edit_quote + +Translation of I, for the title of that section or links to it. + +=item remove_quote + +Translation of I, for the title of that section or links to it. + +=item welcome + +Generic translation of the string I. + +=item latest_news + +Translation of I, for the title of the list of recent news items. + +=item unknown_action + +Text describing that the action the user requested is unknown, for error +messages. + +=item no_quotes + +Text describing that there aren't any quotes to display. + +=item quote_browser_description + +Brief description of what the quote browser does, for tooltips and such. + +=item quote_browser_short_title + +Abbreviated version of I, for use in compact menus. + +=item random_quotes_description + +Brief description of what the list of random quotes does, for tooltips and +such. + +=item random_quotes_short_title + +Abbreviated version of I, for use in compact menus. + +=item top_quotes_description + +Brief description of what the list of top quotes does, for tooltips and such. + +=item top_quotes_short_title + +Abbreviated version of I, for use in compact menus. + +=item bottom_quotes_description + +Brief description of what the list of bottom quotes does, for tooltips and +such. + +=item bottom_quotes_short_title + +Abbreviated version of I, for use in compact menus. + +=item quotes_of_the_week_description + +Brief description of what the list of quotes of the week does, for tooltips and +such. + +=item quotes_of_the_week_short_title + +Abbreviated version of I, for use in compact menus. + +=item quote_search_description + +Brief description of what the quote search does, for tooltips and such. + +=item quote_search_short_title + +Abbreviated version of I, for use in compact menus. + +=item tag_cloud_description + +Brief description of what the tag cloud is, for tooltips and such. + +=item tag_cloud_short_title + +Abbreviated version of I, for use in compact menus. + +=item statistics_description + +Brief description of what the statistics do, for tooltips and such. + +=item submit_quote_description + +Brief description of what the quote submission interface does, for tooltips +and such. + +=item submit_quote_short_title + +Abbreviated version of I, for use in compact menus. + +=item moderation_queue_description + +Brief description of the quote moderation queue, for tooltips and such. + +=item moderation_queue_short_title + +Abbreviated version of I, for use in compact menus. + +=item administration_description + +Brief description of what the administration interface does, for tooltips and +such. + +=item administration_short_title + +Abbreviated version of I, for use in compact menus. + +=item login_description + +Brief description of what the login interface does, for tooltips and such. + +=item login_short_title + +Abbreviated version of I, for use in compact menus. + +=item logout_description + +Brief description of what the logout interface does, for tooltips and such. + +=item logout_short_title + +Abbreviated version of I, for use in compact menus. + +=item quote_title + +Generic title of a quote, i.e. something along the lines of the word "Quote" +followed by the quote's ID. Use C<%1%> to represent the ID. + +=item quote_report_description + +Brief description of what reporting quotes is, for tooltips and such. + +=item quote_rating_up_description + +Brief description of what increasing a quote's rating is, for tooltips and +such. + +=item quote_rating_down_description + +Brief description of what decreasing a quote's rating is, for tooltips and +such. + +=item quote_report_confirmation_request + +Text requesting that the user confirm that he wishes to report the quote that +follows. + +=item quote_rating_up_confirmation_request + +Text requesting that the user confirm that he wishes to increase the rating +of the quote that follows. + +=item quote_rating_down_confirmation_request + +Text requesting that the user confirm that he wishes to decrease the rating +of the quote that follows. + +=item quote_rating_description + +Brief description of what the quote's rating is, for tooltips and such. + +=item quote_vote_count_description + +Brief description of what the quote's vote count is, for tooltips and such. + +=item quote_date_description + +Brief description of what the date when the quote was submitted is, for +tooltips and such. + +=item quote_edit_description + +Brief description of what editing the quote does, for tooltips and such. + +=item quote_remove_description + +Brief description of what removing the quote does, for tooltips and such. + +=item quote_notes_title + +Title of the notes section in the quote list, followed by a colon. + +=item quote_tags_title + +Title of the tags section in the quote list, followed by a colon. + +=item no_tagged_quotes + +Message displayed instead of the tag cloud when no quotes have been tagged. + +=item statistics_unavailable + +Generic message, displayed when statistics are unavailable. Usually, this means +there are no quotes in the database, but this may change. + +=item statistics_short_title + +Abbreviated version of I, for use in compact menus. + +=item quote_count_by_date + +Title of the I section in the statistics. + +=item quote_count_by_hour + +Title of the I section in the statistics. + +=item quote_count_by_month + +Title of the I section in the statistics. + +=item quote_count_by_day + +Title of the I section in the statistics. + +=item quote_count_by_weekday + +Title of the I section in the statistics. + +=item quote_count_by_rating + +Title of the I section in the statistics. + +=item quote_count_by_vote_count + +Title of the I section in the statistics. + +=item vote_count_by_rating + +Title of the I section in the statistics. + +=item tag_link_description + +Description text for the link to the list of quotes with the current tag. +C<%1%> is replaced with the tag. + +=item search_query_title + +Title of the query field on the search interface, followed by a colon. + +=item search_button_label + +Label text for the button that initiates searches. + +=item search_results + +Translation of I, for the title of that section or links to it. + +=item submit_quote + +Translation of I, for the title of that section or links to it. + +=item unmoderated_quotes + +Translation of I, for the title of that section or links to +it. + +=item submission_title + +Directions telling the user to enter his quote in the input field that follows +the text. + +=item notes_title + +Directions telling the user to optionally enter notes about the quote in the +input field that follows the text. + +=item submit_button_label + +Label text for the button that submits the newly entered quote. + +=item submit_button_label_no_approval + +Label text for the button that submits the newly entered quote, suggesting that +the quote will be immediately added without approval, since the user has that +privilege. + +=item quote_submitted + +Title for the interface stating that the quote has been submitted. + +=item quote_submitted_no_approval + +Title for the interface stating that the quote has been submitted and approval +is not necessary. Something like I is appropriate. + +=item quote_submission_thanks + +Message thanking the user for the submitted quote and explaining that it will +now be reviewed before it is added. + +=item quote_submission_thanks_no_approval + +Message thanking the user for the submitted quote and explaining that it has +been added without the need for approval. + +=item approve_quotes + +Translation of I, for titles and labels in the administrative +interface. + +=item flagged_quotes + +Translation of I, for titles and labels in the administrative +interface. + +=item manage_news + +Translation of I, for titles and labels in the administrative +interface. + +=item manage_quotes + +Translation of I, for titles and labels in the administrative +interface. + +=item add_news + +Translation of I, for titles and labels in the administrative +interface. + +=item edit_news + +Translation of I, for titles and labels in the administrative +interface. + +=item remove_news + +Translation of I, for titles and labels in the administrative +interface. + +=item manage_accounts + +Translation of I, for titles and labels in the administrative +interface. + +=item manage_accounts + +Translation of I, for titles and labels in the administrative +interface. + +=item no_unapproved_quotes + +Message describing that there are currently no quotes waiting for approval. + +=item no_flagged_quotes + +Message describing that there are currently no quotes that are flagged. + +=item quote_rating_up_short_title + +Short label text for the link that increases the quote's rating, e.g. I. + +=item quote_rating_down_short_title + +Short label text for the link that decreases the quote's rating, e.g. I. + +=item report_quote_short_title + +Short label text for the link that reports the quote, e.g. I. + +=item edit + +Generic translation of I, which can be used for links to edit quotes, +news items, etc. + +=item remove + +Generic translation of I, which can be used for links to remove quotes, +news items, etc. + +=item unflag + +Text for the link that removes the report for the quote. + +=item flagged + +Text that indicates that the quote has been reported in the past. + +=item quote_removal_confirmation + +Question that asks the user if he is sure that he would like to remove the +quote. + +=item news_removal_confirmation + +Question that asks the user if he is sure that he would like to remove the news +item. + +=item quote_rating_increased + +Title for the interface stating that the quote's rating has been increased. + +=item quote_rating_decreased + +Title for the interface stating that the quote's rating has been decreased. + +=item quote_rating_thanks + +Message confirming that the user's vote has been processed, used for both +positive and negative votes. + +=item quote_reported + +Title for the interface stating that the quote has been reported. + +=item quote_report_thanks + +Message confirming that the quote has been reported. + +=item quote_already_rated + +Error message stating that the user can only rate the same quote once per +session and that he has already rated the selected quote. + +=item quote_rating_limit_exceeded + +Error message stating that the user has exceeded the maximum number of votes +allowed. C<%1%> is replaced with the maximum number of votes, C<%2%> with the +number of seconds in the time frame for that maximum. + +=item login_title + +Title of the login interface. + +=item invalid_login_title + +Title of the interface stating that logging in has failed. + +=item username_title + +Title of the field where the user inputs his username, followed by a colon. + +=item password_title + +Title of the field where the user inputs his password, followed by a colon. + +=item login_button_label + +Label of the button that submits the login information the user has entered. + +=item invalid_login_instructions + +Error message stating that the credentials the user has entered are incorrect, +with the additional information that the password is case-sensitive, while the +username is not. + +=item logged_in_as + +Message stating that the user is logged in. C<%1%> is replaced with the user's +username, C<%2%> with his user level. + +=item change_password + +Translation of I, for titles and labels in the administrative +interface. + +=item none + +Generic translation of the string I. + +=item error + +Generic translation of the string I. + +=item processing + +Generic translation of the string I, used for operations that may +take a while. + +=item timed_out + +Generic translation of the string I, used for operations that have +failed to complete. + +=item no_search_results + +Title of the interface stating that there are no search results. + +=item no_search_results_text + +Message explaining that none of the quotes match the search query. + +=item quote_not_found + +Title of the interface stating that the requested quote was not found. + +=item quote_not_found_text + +Message explaining that the quote ID the user has provided does not exist. + +=item rated_quote_not_found_text + +Message explaining that the quote the user is trying to rate does not exist. + +=item reported_quote_not_found_text + +Message explaining that the quote the user is trying to report does not exist. + +=item user_level_3 + +Title of the user level with numeric ID 3. + +=item user_level_6 + +Title of the user level with numeric ID 6. + +=item user_level_9 + +Title of the user level with numeric ID 9. + +=item password_changed + +Title of the interface stating that the user's password has been changed. + +=item password_changed_text + +Message stating that the user's password has been changed, reminding him to +keep it in a safe place. + +=item current_password_title + +Title of the field where the user inputs his current password, followed by a +colon. + +=item new_password_title + +Title of the field where the user inputs his new password, followed by a colon. + +=item repeat_new_password_title + +Title of the field where the user inputs his new password again for +verification, followed by a colon. + +=item change_password_button_label + +Label of the button that submits the user's new password. + +=item change_password_current_password_invalid_text + +Error message stating that the user failed to enter his current password. + +=item change_password_new_password_invalid_text + +Error message stating that the new password the user has entered is invalid. + +=item change_password_passwords_differ_text + +Error message stating that the new password the user has entered does not match +the verification entry. + +=item do_nothing + +Generic translation of the string I, useful for the quote approval +and report review interfaces. + +=item approve_unapproved_quote + +Text for the option that approves the new quote in the approval interface. + +=item discard_unapproved_quote + +Text for the option that removes the new quote in the approval interface. + +=item update_database + +Generic translation of the string I, useful for the quote +approval and report review interfaces. + +=item reset_form + +Generic translation of the string I, useful for various form-based +interfaces. + +=item keep_flagged_quote + +Text for the option that keeps the flagged quote in the report review +interface. + +=item remove_flagged_quote + +Text for the option that removes the flagged quote in the report review +interface. + +=item quote_removed + +Text confirming that the selected quote has been removed. + +=item quote_to_edit_not_found + +Error message stating that the quote the user tried to edit could not be found. + +=item quote_to_remove_not_found + +Error message stating that the quote the user tried to remove could not be +found. + +=item quote_modified + +Message confirming that the user's modifications to the quote have been saved. + +=item quote_id_title + +Title text for a quote ID input field, followed by a colon. + +=item save_quote + +Label text for the button that saves modifications to the quote. + +=item go + +Generic translation of the string I. + +=item news_item_added + +Message confirming that the submitted news item has been added. + +=item news_item_modified + +Message confirming that the modifications to the news item have been saved. + +=item news_item_to_edit_not_found + +Error message stating that the news item the user tried to edit was not found. + +=item news_item_removed + +Message confirming that the news item has been removed. + +=item news_item_to_remove_not_found + +Error message stating that the news item the user tried to remove was not +found. + +=item new_news_item_title + +Title text for the new news item input field, followed by a colon. + +=item add_news_item + +Label text for the button that adds the newly entered news item. + +=item news_poster_title + +Title text for the poster selection field upon modification of a news item, +followed by a colon. + +=item save_news_item + +Label text for the button that saves modifications to a news item. + +=item account_to_modify_not_found + +Error message stating that the account the user tried to modify could not be +found. + +=item account_to_remove_not_found + +Error message stating that the account the user tried to remove could not be +found. + +=item last_owner_account_removal_error + +Error message stating that there must be at least one account with the maximum +user level, and therefore, the selected account cannot be removed. + +=item modified_account_information_required + +Error message stating that the user failed to enter any updated account +information for the selected account. + +=item invalid_username + +Error message stating the username the user has entered is invalid. + +=item username_exists + +Error message stating the username the user has entered already exists. + +=item invalid_password + +Error message stating the password the user has entered is invalid. + +=item different_passwords + +Error message stating the password and confirmation the user has entered are +not equal. + +=item invalid_user_level + +Error message stating the user level the user has selected is invalid. + +=item account_removed + +Message confirming that the account has been removed. + +=item account_modified + +Message confirming that the account has been modified. + +=item account_created + +Message confirming that the account has been created. + +=item new_account + +Generic translation of the string I. + +=item new_username_title + +Title for the field where the user enters the username for the account he is +adding, followed by a colon. + +=item new_password_title + +Title for the field where the user enters the password for the account he is +adding, followed by a colon. + +=item repeat_new_password_title + +Title for the field where the user enters the password for the account he is +adding again for confirmation, followed by a colon. + +=item new_user_level_title + +Title for the field where the user selects the user level for the account he is +adding, followed by a colon. + +=item update_accounts + +Label for the button that processes changes to the account overview. + +=item remove_account + +Label for the button that removes the selected account. + +=item no_change + +Generic translation of the string I. + +=item unknown + +Generic translation of the string I. + +=item account_removal_confirmation + +Question confirming that the user is sure he wishes to remove the selected +account. + +=item insufficient_administrative_privileges + +Error message displayed when the user attempts to access an administration +interface he is not allowed to use. + +=item update_available + +Title for the message indicating that a new version of Chirpy! is available. + +=item update_available_text + +Text giving basic information about a Chirpy! update. C<%1%> is replaced with +the version number C<%2%> with the release date. + +=item update_link_text + +Text for a link to an available update. Usually something along the lines of +"Click here for more information." + +=item update_check_failed + +Title for the message indicating that checking for updates has failed. + +=item update_check_failed_text + +Message explaining that Chirpy! failed to check for updates, and indicating +that an error report follows. + +=item event_100_name + +Name of the "Login Success" event. + +=item event_101_name + +Name of the "Login Failure" event. + +=item event_102_name + +Name of the "Change Password" event. + +=item event_200_name + +Name of the "Add Quote" event. + +=item event_201_name + +Name of the "Edit Quote" event. + +=item event_202_name + +Name of the "Remove Quote" event. + +=item event_203_name + +Name of the "Quote Rating Up" event. + +=item event_204_name + +Name of the "Quote Rating Down" event. + +=item event_205_name + +Name of the "Report Quote" event. + +=item event_206_name + +Name of the "Approve Quote" event. + +=item event_207_name + +Name of the "Unflag Quote" event. + +=item event_300_name + +Name of the "Add News" event. + +=item event_301_name + +Name of the "Edit News" event. + +=item event_302_name + +Name of the "Remove News" event. + +=item event_400_name + +Name of the "Add Account" event. + +=item event_401_name + +Name of the "Edit Account" event. + +=item event_402_name + +Name of the "Remove Account" event. + +=item date + +=item username + +=item event + +=item guest + +=item empty + +=item ok + +=item cancel + +Literal translation of each word; must start with a capital letter. + +=item sunday + +=item monday + +=item tuesday + +=item wednesday + +=item thursday + +=item friday + +=item saturday + +The full names of the days of the week. + +=item january + +=item february + +=item march + +=item april + +=item may + +=item june + +=item july + +=item august + +=item september + +=item october + +=item november + +=item december + +The full names of the months of the year. + +=item january_short + +=item february_short + +=item march_short + +=item april_short + +=item may_short + +=item june_short + +=item july_short + +=item august_short + +=item september_short + +=item october_short + +=item november_short + +=item december_short + +The names of the months of the year, abbreviated however feels natural. + +=back + +=head1 TODO + +The list of strings needs some organization and some of the identifier names +should probably be revised. Sorry for the inconvenience. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Locale; + +use strict; +use warnings; + +use vars qw($VERSION @ISA); + +$VERSION = '0.3'; +@ISA = qw(Chirpy::Util::IniFile); + +use Chirpy 0.3; +use Chirpy::Util::IniFile 0.3; + +sub new { + my ($class, $file) = @_; + return $class->SUPER::new($file); +} + +sub get_string { + my ($self, $name, @vars) = @_; + my $string = $self->SUPER::get('strings', $name) or return ''; + $string =~ s/\%(\d+)\%/$vars[$1 - 1] || ''/eg; + return $string; +} + +sub get_target_version { + my $self = shift; + return $self->get('properties', 'chirpy_version'); +} + +sub get_full_name { + my $self = shift; + return $self->get('properties', 'full_name'); +} + +sub get_version { + my $self = shift; + return $self->get('properties', 'version'); +} + +sub get_author_information { + my $self = shift; + return { + 'name' => $self->get('properties', 'author_name'), + 'email' => $self->get('properties', 'author_email'), + 'uri' => $self->get('properties', 'author_uri') + }; +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/NewsItem.pm b/pub/qdb/src/modules/Chirpy/NewsItem.pm new file mode 100644 index 0000000..8c3e7e4 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/NewsItem.pm @@ -0,0 +1,153 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: NewsItem.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::NewsItem - Represents a news item + +=head1 SYNOPSIS + + $item = new Chirpy::NewsItem($id, $body, $poster, $date); + + $id = $item->get_id(); + $item->set_id($id); + + $body = $item->get_body(); + $item->set_body($body); + + $poster = $item->get_poster(); + $item->set_poster($poster); + + $date = $item->get_date(); + $item->set_date($date); + +=head1 CONSTRAINTS + +=over 4 + +=item ID + +The news item ID must be a positive non-zero integer. + +=item Body + +The news item body can be any text string. + +=item Poster + +The poster of the news item must be an instance of L, if any. + +=item Date + +The date when the news item was posted must be a UNIX timestamp. + +=back + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::NewsItem; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +sub new { + my ($class, $id, $body, $poster, $date) = @_; + my $self = { + 'id' => $id, + 'body' => $body, + 'poster' => $poster, + 'date' => $date + }; + return bless($self, $class); +} + +sub get_id { + my $self = shift; + return $self->{'id'}; +} + +sub set_id { + my $self = shift; + return ($self->{'id'} = shift); +} + +sub get_body { + my $self = shift; + return $self->{'body'}; +} + +sub set_body { + my $self = shift; + return ($self->{'body'} = shift); +} + +sub get_poster { + my $self = shift; + return $self->{'poster'}; +} + +sub set_poster { + my $self = shift; + return ($self->{'poster'} = shift); +} + +sub get_date { + my $self = shift; + return $self->{'date'}; +} + +sub set_date { + my $self = shift; + return ($self->{'date'} = shift); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Quote.pm b/pub/qdb/src/modules/Chirpy/Quote.pm new file mode 100644 index 0000000..3949250 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Quote.pm @@ -0,0 +1,254 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Quote.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Quote - Represents a quote + +=head1 SYNOPSIS + + $quote = new Chirpy::Quote( + $id, $body, $notes, $rating, $vote_count, + $submitted, $approved, $flagged, $tags); + + $id = $quote->get_id(); + $quote->set_id($id); + + $body = $quote->get_body(); + $quote->set_body($body); + + $notes = $quote->get_notes(); + $quote->set_notes($notes); + + $rating = $quote->get_rating(); + $quote->set_rating($rating); + + $vote_count = $quote->get_vote_count(); + $quote->set_vote_count($vote_count); + + $submitted = $quote->get_date_submitted(); + $quote->set_date_submitted($submitted); + + $approved = $quote->is_approved(); + $quote->set_approved($approved); + + $flagged = $quote->is_flagged(); + $quote->set_flagged($flagged); + + $tags = $quote->get_tags(); + $quote->set_tags($tags); + $quote->add_tag($tag); + $quote->remove_tag($tag); + +=head1 CONSTRAINTS + +=over 4 + +=item ID + +The quote ID must be a positive non-zero integer. + +=item Body + +The quote body can be any text string. + +=item Notes + +The quote notes can be any text string, if any. + +=item Rating + +The quote rating must be an integer. + +=item Vote Count + +The vote count must be a positive integer; a value of zero is allowed. + +=item Submitted + +The date when the quote was submitted must be a UNIX timestamp. + +=item Approved + +The quote approval status is 1 if the quote has been approved, 0 if it has not. + +=item Flagged + +The quote flag status is 1 if the quote has been reported, 0 if it has not. + +=item Tags + +Tags must be passed as a reference to an array of strings, each of which +constructed of lowercase non-whitespace characters. + +=back + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Quote; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +sub new { + my ($class, $id, $body, $notes, + $rating, $vote_count, $submitted, $approved, $flagged, $tags) = @_; + my $self = { + 'id' => $id, + 'body' => $body, + 'notes' => (defined $notes && $notes ne '' ? $notes : undef), + 'rating' => $rating, + 'vote_count' => $vote_count, + 'submitted' => $submitted, + 'approved' => $approved, + 'flagged' => $flagged, + 'tags' => (ref $tags eq 'ARRAY' ? [ sort @$tags ] : []) + }; + return bless($self, $class); +} + +sub get_id { + my $self = shift; + return $self->{'id'}; +} + +sub set_id { + my $self = shift; + return ($self->{'id'} = shift); +} + +sub get_body { + my $self = shift; + return $self->{'body'}; +} + +sub set_body { + my $self = shift; + return ($self->{'body'} = shift); +} + +sub get_notes { + my $self = shift; + return $self->{'notes'}; +} + +sub set_notes { + my ($self, $notes) = @_; + return ($self->{'notes'} + = (defined $notes && $notes ne '' ? $notes : undef)); +} + +sub get_rating { + my $self = shift; + return $self->{'rating'}; +} + +sub set_vote_count { + my $self = shift; + return ($self->{'vote_count'} = shift); +} + +sub get_vote_count { + my $self = shift; + return $self->{'vote_count'}; +} + +sub set_rating { + my $self = shift; + return ($self->{'rating'} = shift); +} + +sub get_date_submitted { + my $self = shift; + return $self->{'submitted'}; +} + +sub set_date_submitted { + my $self = shift; + return ($self->{'submitted'} = shift); +} + +sub is_approved { + my $self = shift; + return $self->{'approved'}; +} + +sub set_approved { + my $self = shift; + return ($self->{'approved'} = shift); +} + +sub is_flagged { + my $self = shift; + return $self->{'flagged'}; +} + +sub set_flagged { + my $self = shift; + return ($self->{'flagged'} = shift); +} + +sub get_tags { + my $self = shift; + return $self->{'tags'}; +} + +sub set_tags { + my $self = shift; + return ($self->{'tags'} = shift); +} + +*get_approved = \&is_approved; + +*get_flagged = \&is_flagged; + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI.pm b/pub/qdb/src/modules/Chirpy/UI.pm new file mode 100644 index 0000000..227720e --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI.pm @@ -0,0 +1,1444 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: UI.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI - Abstract user interface class + +=head1 TODO + +A detailed description of this module's API will be available in a future +release. If you want to write your own user interface implementation, you could +try analyzing the source code of this module and its only implementation so +far, L. I apologize for the inconvenience. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; +use Chirpy::Util 0.3; +use Chirpy::UpdateChecker 0.3; + +use constant START_PAGE => 1; +use constant QUOTE_BROWSER => 2; +use constant SINGLE_QUOTE => 3; +use constant RANDOM_QUOTES => 4; +use constant TOP_QUOTES => 5; +use constant BOTTOM_QUOTES => 6; +use constant QUOTES_OF_THE_WEEK => 7; +use constant QUOTE_SEARCH => 8; +use constant TAG_CLOUD => 9; +use constant STATISTICS => 10; +use constant SUBMIT_QUOTE => 11; +use constant QUOTE_RATING_UP => 12; +use constant QUOTE_RATING_DOWN => 13; +use constant REPORT_QUOTE => 14; +use constant LOGIN => 15; +use constant LOGOUT => 16; +use constant ADMINISTRATION => 17; +use constant MODERATION_QUEUE => 18; + +use constant CHANGE_PASSWORD => 100; +use constant MANAGE_UNAPPROVED_QUOTES => 110; +use constant MANAGE_FLAGGED_QUOTES => 120; +use constant EDIT_QUOTE => 130; +use constant REMOVE_QUOTE => 131; +use constant ADD_NEWS => 140; +use constant EDIT_NEWS => 141; +use constant REMOVE_NEWS => 142; +use constant ADD_ACCOUNT => 150; +use constant EDIT_ACCOUNT => 151; +use constant REMOVE_ACCOUNT => 152; +use constant CHECK_FOR_UPDATE => 160; +use constant VIEW_EVENT_LOG => 170; + +use constant CURRENT_PASSWORD_INVALID => -1; +use constant NEW_PASSWORD_INVALID => -2; +use constant PASSWORDS_DIFFER => -3; + +# TODO: make this easily configurable one day +use constant ADMIN_PERMISSIONS => { + MANAGE_UNAPPROVED_QUOTES() => { + Chirpy::Account::USER_LEVEL_3 => 1, + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + MANAGE_FLAGGED_QUOTES() => { + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + EDIT_QUOTE() => { + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + REMOVE_QUOTE() => { + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + ADD_NEWS() => { + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + EDIT_NEWS() => { + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + REMOVE_NEWS() => { + Chirpy::Account::USER_LEVEL_6 => 1, + Chirpy::Account::USER_LEVEL_9 => 1 + }, + VIEW_EVENT_LOG() => { + Chirpy::Account::USER_LEVEL_9 => 1 + }, + ADD_ACCOUNT() => { + Chirpy::Account::USER_LEVEL_9 => 1 + }, + EDIT_ACCOUNT() => { + Chirpy::Account::USER_LEVEL_9 => 1 + }, + REMOVE_ACCOUNT() => { + Chirpy::Account::USER_LEVEL_9 => 1 + }, + CHECK_FOR_UPDATE() => { + Chirpy::Account::USER_LEVEL_9 => 1 + } +}; + +use constant STATISTICS_UPDATE_INTERVAL => 60 * 60; +use constant UPDATE_CHECK_INTERVAL => 7 * 24 * 60 * 60; + +sub new { + my ($class, $parent, $params) = @_; + return bless { + 'parent' => $parent, + 'params' => $params + }, $class; +} + +sub run { + my $self = shift; + my $page = $self->get_current_page(); + if ($page == START_PAGE) { + $self->welcome_user( + $self->parent()->get_latest_news_items() + ); + } + elsif ($page == QUOTE_BROWSER) { + my $start = $self->get_first_quote_index(); + $self->_browse_quotes_segmented( + $page, + $start, + $self->parent()->get_quotes($start) + ); + } + elsif ($page == QUOTES_OF_THE_WEEK) { + my $start = $self->get_first_quote_index(); + $self->_browse_quotes_segmented( + $page, + $start, + $self->parent()->get_quotes_of_the_week($start), + ); + } + elsif ($page == QUOTE_SEARCH) { + my $start = $self->get_first_quote_index(); + my ($queries, $tags) = $self->get_search_instruction(); + if (@$queries || @$tags) { + $self->_browse_quotes_segmented( + $page, + $start, + $self->parent()->get_matching_quotes($start, $queries, $tags) + ); + } + else { + $self->provide_quote_search_interface(); + } + } + elsif ($page == SINGLE_QUOTE) { + my $quote = $self->parent()->get_quote( + $self->get_selected_quote_id()); + if (defined $quote && ($quote->is_approved() || $self->moderation_queue_is_public())) { + $self->browse_quotes([ $quote ], $page); + } + else { + $self->report_inexistent_quote(); + } + } + elsif ($page == RANDOM_QUOTES) { + my $quotes = $self->parent()->get_random_quotes(); + (defined $quotes + ? $self->browse_quotes($quotes, $page) + : $self->report_no_quotes_to_display($page)); + } + elsif ($page == TOP_QUOTES) { + my $start = $self->get_first_quote_index(); + $self->_browse_quotes_segmented( + $page, + $start, + $self->parent()->get_top_quotes($start), + ); + } + elsif ($page == BOTTOM_QUOTES) { + my $start = $self->get_first_quote_index(); + $self->_browse_quotes_segmented( + $page, + $start, + $self->parent()->get_bottom_quotes($start), + ); + } + elsif ($page == MODERATION_QUEUE) { + if ($self->moderation_queue_is_public()) { + my $start = $self->get_first_quote_index(); + $self->_browse_quotes_segmented( + $page, + $start, + $self->parent()->get_unapproved_quotes($start), + ); + } + else { + $self->report_unknown_action(); + } + } + elsif ($page == SUBMIT_QUOTE) { + my ($body, $notes, $tags) = $self->get_submitted_quote(); + if (defined $body && $body) { + my $approved + = $self->administration_allowed(Chirpy::UI::MANAGE_UNAPPROVED_QUOTES); + $body = Chirpy::Util::clean_up_submission($body); + $notes = (defined $notes + ? Chirpy::Util::clean_up_submission($notes) + : undef); + $tags = Chirpy::Util::parse_tags($tags); + my $quote = $self->parent()->add_quote( + $body, + $notes, + $approved, + $tags + ); + $self->confirm_quote_submission($approved); + my $id = $quote->get_id(); + $self->_log_event(Chirpy::Event::ADD_QUOTE, { + 'id' => $id, + 'body' => $body, + (defined $notes && length($notes) ? ('notes' => $notes) : ()), + (@$tags ? ('tags' => join(' ', sort @$tags)) : ()) + }); + if ($approved) { + $self->_log_event(Chirpy::Event::APPROVE_QUOTE, + { 'id' => $id }); + } + } + else { + $self->provide_quote_submission_interface(); + } + } + elsif ($page == QUOTE_RATING_UP || $page == QUOTE_RATING_DOWN) { + my $up = ($page == QUOTE_RATING_UP); + my $id = $self->get_selected_quote_id(); + my $quote = $self->parent()->get_quote($id); + if (defined $quote + && ($quote->is_approved() || $self->moderation_queue_is_public())) { + my $last_rating = $self->_last_rating($id); + if (($page == QUOTE_RATING_UP && $last_rating > 0) + || ($page == QUOTE_RATING_DOWN && $last_rating < 0)) { + $self->report_quote_already_rated($id); + } + else { + my ($history, $full) = $self->_rating_history(); + if (!$full) { + if ($self->quote_rating_confirmed()) { + $self->_rate_quote($id, $up, abs($last_rating), $history); + } + else { + $self->request_quote_rating_confirmation( + $quote, $up, abs($last_rating)); + } + } + else { + $self->report_quote_rating_limit_excess(); + } + } + } + else { + $self->report_rated_quote_not_found(); + } + } + elsif ($page == REPORT_QUOTE) { + my $id = $self->get_selected_quote_id(); + my $quote = $self->parent()->get_quote($id); + if (defined $quote && $quote->is_approved()) { + if ($self->quote_report_confirmed()) { + $self->parent()->flag_quotes($id); + $self->confirm_quote_report($id); + $self->_log_event(Chirpy::Event::REPORT_QUOTE, { 'id' => $id }); + } + else { + $self->request_quote_report_confirmation($quote); + } + } + else { + $self->report_reported_quote_not_found(); + } + } + elsif ($page eq TAG_CLOUD) { + my $tag_counts = $self->parent()->get_tag_use_counts(); + if (%$tag_counts) { + $self->_provide_tag_cloud($tag_counts); + } + else { + $self->report_no_tagged_quotes(); + } + } + elsif ($page eq STATISTICS) { + $self->_provide_statistics(); + } + elsif ($page == LOGIN) { + if ($self->attempting_login()) { + my ($username, $password) + = $self->get_supplied_username_and_password(); + my $account = $self->parent() + ->attempt_login($username, $password); + if (defined $account) { + $self->set_logged_in_user($account->get_id()); + $self->confirm_login(); + $self->_log_event(Chirpy::Event::LOGIN_SUCCESS); + } + else { + $self->report_invalid_login(); + $self->_log_event(Chirpy::Event::LOGIN_FAILURE, + { 'username' => $username }); + } + } + else { + $self->provide_login_interface(); + } + } + elsif ($page == LOGOUT) { + $self->set_logged_in_user(undef); + $self->confirm_logout(); + } + elsif ($page == ADMINISTRATION) { + if (defined $self->get_logged_in_user_account()) { + $self->_provide_administration_interface() + } + else { + $self->provide_login_interface(); + } + } + else { + $self->report_unknown_action(); + } +} + +sub statistics_update_allowed { + return 1; +} + +sub _provide_administration_interface { + my $self = shift; + if ($self->configuration()->get('general', 'update_check') + && $self->administration_allowed(CHECK_FOR_UPDATE)) { + $self->_maybe_check_for_update(); + } + my $page = $self->get_current_administration_page() || 0; + if ($page == CHANGE_PASSWORD) { + my $user = $self->get_logged_in_user_account(); + if ($self->attempting_password_change()) { + my ($current_password, $new_password, $repeat_password) + = $self->get_supplied_passwords(); + if (!defined $self->parent() + ->attempt_login( + $user->get_username(), $current_password)) { + $self->provide_password_change_interface( + CURRENT_PASSWORD_INVALID); + } + elsif (!Chirpy::Util::valid_password($new_password)) { + $self->provide_password_change_interface( + NEW_PASSWORD_INVALID); + } + elsif ($new_password ne $repeat_password) { + $self->provide_password_change_interface( + PASSWORDS_DIFFER); + } + else { + $self->parent()->modify_account( + $user, undef, $new_password); + $self->confirm_password_change(); + $self->_log_event(Chirpy::Event::CHANGE_PASSWORD); + } + } + else { + $self->provide_password_change_interface(); + } + } + elsif ($page == MANAGE_UNAPPROVED_QUOTES) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my ($approve, $remove, $edited) + = $self->get_quote_approval_result(); + my @approve = (defined $approve && ref $approve eq 'ARRAY' + ? @$approve : ()); + my @remove = (defined $remove && ref $remove eq 'ARRAY' + ? @$remove : ()); + if (@approve) { + $self->parent()->approve_quotes(@approve); + foreach my $id (@approve) { + if (exists $edited->{$id}) { + my $quote = $self->parent()->get_quote($id); + if (defined $quote) { + my $body = $edited->{$id}->{'body'}; + my $notes = $edited->{$id}->{'notes'}; + my $tags = $edited->{$id}->{'tags'}; + if ($body) { + $self->_modify_quote( + $quote, $body, $notes, $tags); + } + } + } + $self->_log_event(Chirpy::Event::APPROVE_QUOTE, + { 'id' => $id }); + } + } + if (@remove) { + $self->parent()->remove_quotes(@remove); + foreach my $id (@remove) { + $self->_log_event(Chirpy::Event::REMOVE_QUOTE, + { 'id' => $id }); + } + } + $self->provide_quote_approval_interface( + @approve ? \@approve : undef, + @remove ? \@remove : undef); + } + } + elsif ($page == MANAGE_FLAGGED_QUOTES) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my ($unflag, $remove) = $self->get_quote_flag_management_result(); + my @unflag = (defined $unflag && ref $unflag eq 'ARRAY' + ? @$unflag : ()); + my @remove = (defined $remove && ref $remove eq 'ARRAY' + ? @$remove : ()); + if (@unflag) { + $self->parent()->unflag_quotes(@unflag); + foreach my $id (@unflag) { + $self->_log_event(Chirpy::Event::UNFLAG_QUOTE, + { 'id' => $id }); + } + } + if (@remove) { + $self->parent()->remove_quotes(@remove); + foreach my $id (@remove) { + $self->_log_event(Chirpy::Event::REMOVE_QUOTE, + { 'id' => $id }); + } + } + $self->provide_quote_flag_management_interface( + @unflag ? \@unflag : undef, + @remove ? \@remove : undef); + } + } + elsif ($page == EDIT_QUOTE) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my $id = $self->get_quote_to_edit(); + if ($id) { + my $quote = $self->parent()->get_quote($id); + if (defined $quote && $quote->is_approved()) { + my ($body, $notes, $tags) + = $self->get_modified_quote_information(); + if ($body) { + $self->_modify_quote($quote, $body, $notes, $tags); + $self->confirm_quote_modification($quote); + } + else { + $self->provide_quote_editing_interface($quote); + } + } + else { + $self->report_quote_to_edit_not_found(); + } + } + else { + $self->provide_quote_selection_for_modification_interface(); + } + } + } + elsif ($page == REMOVE_QUOTE) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my $id = $self->get_quote_to_remove(); + if ($id) { + my $quote = $self->parent()->get_quote($id); + if (defined $quote && $quote->is_approved()) { + if ($self->quote_removal_confirmed()) { + $self->parent()->remove_quotes($id); + $self->confirm_quote_removal(); + $self->_log_event(Chirpy::Event::REMOVE_QUOTE, + { 'id' => $id }); + } + else { + $self->request_quote_removal_confirmation($quote); + } + } + else { + $self->report_quote_to_remove_not_found(); + } + } + else { + $self->provide_quote_selection_for_removal_interface(); + } + } + } + elsif ($page == ADD_NEWS) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + if (my $news = $self->get_news_item_to_add()) { + my $news = Chirpy::Util::clean_up_submission($news); + my $item = $self->parent()->add_news_item( + $news, + $self->get_logged_in_user_account() + ); + $self->confirm_news_submission($news); + $self->_log_event(Chirpy::Event::ADD_NEWS, { + 'id' => $item->get_id(), 'body' => $news + }); + } + else { + $self->provide_news_submission_interface(); + } + } + } + elsif ($page == EDIT_NEWS) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my $id = $self->get_news_item_to_edit(); + if ($id) { + my $item = $self->parent()->get_news_item($id); + if (defined $item) { + my ($text, $poster_id) = $self->get_modified_news_item(); + if ($text) { + my $old_body = $item->get_body(); + my $old_poster = $item->get_poster(); + $text = Chirpy::Util::clean_up_submission($text); + $self->parent()->modify_news_item( + $item, + $text, + $self->parent()->get_account_by_id($poster_id) + ); + $self->confirm_news_item_modification(); + $self->_log_event(Chirpy::Event::EDIT_NEWS, { + 'id' => $id, + ($old_body ne $text ? ('new_body' => $text) : ()), + (!defined $old_poster + || $old_poster->get_id() != $poster_id + ? ('new_poster' => $poster_id) : ()) + }); + } + else { + $self->provide_news_item_editing_interface($item); + } + } + else { + $self->report_news_item_to_edit_not_found(); + } + } + else { + $self->provide_news_item_selection_for_modification_interface(); + } + } + } + elsif ($page == REMOVE_NEWS) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my $id = $self->get_news_item_to_remove(); + if ($id) { + my $item = $self->parent()->get_news_item($id); + if (defined $item) { + $self->parent()->remove_news_items($id); + $self->confirm_news_item_removal(); + $self->_log_event(Chirpy::Event::REMOVE_NEWS, + { 'id' => $id }); + } + else { + $self->report_news_item_to_remove_not_found(); + } + } + else { + $self->provide_news_item_selection_for_removal_interface(); + } + } + } + elsif ($page == ADD_ACCOUNT) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my ($username, $password, $repeat_password, $level) + = $self->get_account_information_to_add(); + if (defined $username) { + if (!Chirpy::Util::valid_username($username)) { + $self->report_invalid_new_username(); + } + elsif ($self->parent()->username_exists($username)) { + $self->report_new_username_exists(); + } + elsif (!Chirpy::Util::valid_password($password)) { + $self->report_invalid_new_password(); + } + elsif ($password ne $repeat_password) { + $self->report_different_new_passwords(); + } + elsif ($level <= 0) { + $self->report_invalid_new_user_level(); + } + else { + my $account = $self->parent()->add_account( + $username, $password, $level); + $self->confirm_account_creation(); + $self->_log_event(Chirpy::Event::ADD_ACCOUNT, { + 'id' => $account->get_id(), + 'username' => $username, + 'level' => $level + }); + } + } + else { + $self->provide_account_creation_interface(); + } + } + } + elsif ($page == EDIT_ACCOUNT) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my $id = $self->get_account_to_modify(); + if (defined $id) { + my $account = $self->parent()->get_account_by_id($id); + if (defined $account) { + my ($username, $password, $repeat_password, $level) + = $self->get_modified_account_information(); + if (defined $username + && !Chirpy::Util::valid_username($username)) { + $self->report_invalid_modified_username(); + } + elsif ($self->parent()->username_exists($username)) { + $self->report_modified_username_exists(); + } + elsif (defined $password + && !Chirpy::Util::valid_password($password)) { + $self->report_invalid_modified_password(); + } + elsif (defined $password + && $password ne $repeat_password) { + $self->report_different_modified_passwords(); + } + elsif (defined $level && $level <= 0) { + $self->report_invalid_modified_user_level(); + } + elsif (!defined $username && !defined $password + && !defined $level) { + $self->report_modified_account_information_required(); + } + else { + my $old_username = $account->get_username(); + my $old_password = $account->get_password(); + my $old_level = $account->get_level(); + $self->parent()->modify_account( + $account, $username, $password, $level); + $self->confirm_account_modification(); + $self->_log_event(Chirpy::Event::EDIT_ACCOUNT, { + 'id' => $id, + (defined $username && $old_username ne $username + ? ('new_username' => $username) : ()), + (defined $level && $old_level != $level + ? ('new_level' => $level) : ()), + (defined $password + ? ('password_changed' => 1) : ()) + }); + } + } + else { + $self->report_account_to_modify_not_found(); + } + } + else { + $self->provide_account_selection_for_modification_interface(); + } + } + } + elsif ($page == REMOVE_ACCOUNT) { + if (!$self->administration_allowed($page)) { + $self->report_administration_user_level_insufficient($page); + } + else { + my $parent = $self->parent(); + if ($parent->account_count() > 1) { + my $id = $self->get_account_to_remove(); + if (defined $id) { + my $account = $self->parent()->get_account_by_id($id); + my $level = Chirpy::Account::USER_LEVEL_9; + if ($account->get_level() == $level + && $parent->account_count_by_level($level) <= 1) { + $self->report_last_owner_account_removal_error(); + } + elsif (defined $account) { + if ($account->get_id() + == $self->get_logged_in_user_account()->get_id()) { + $self->set_logged_in_user(undef); + } + my $username = $account->get_username(); + my $level = $account->get_level(); + $parent->remove_accounts($id); + $self->confirm_account_removal(); + $self->_log_event(Chirpy::Event::REMOVE_ACCOUNT, + { 'id' => $id }); + } + else { + $self->report_account_to_remove_not_found(); + } + } + else { + $self->provide_account_selection_for_removal_interface(); + } + } + else { + $self->report_last_owner_account_removal_error(); + } + } + } + else { + $self->welcome_administrator(); + } +} + +sub _maybe_check_for_update { + my $self = shift; + my $last_check = $self->get_parameter('last_update_check'); + my $now = time(); + my @update_info; + my $update_check_error; + if (!defined $last_check + || $last_check + UPDATE_CHECK_INTERVAL < $now) { + $self->set_parameter('last_update_check', $now); + my $upd_status = $self->_check_for_update(); + if (defined $upd_status) { + if (ref $upd_status eq 'ARRAY') { + $self->set_parameter('update_version', $upd_status->[0]); + $self->set_parameter('update_released', $upd_status->[1]); + $self->set_parameter('update_url', $upd_status->[2]); + $self->set_parameter('update_version_current', + $Chirpy::VERSION); + @update_info = @$upd_status; + } + else { + $update_check_error = $upd_status; + } + } + } + else { + my $version = $self->get_parameter('update_version'); + if ($version) { + my $version_at_check + = $self->get_parameter('update_version_current'); + if ($version_at_check == $Chirpy::VERSION) { + my $date = $self->get_parameter('update_released'); + my $url = $self->get_parameter('update_url'); + @update_info = ($version, $date, $url); + } + else { + $self->set_parameter('update_version', undef); + $self->set_parameter('update_released', undef); + $self->set_parameter('update_url', undef); + $self->set_parameter('update_version_current', undef); + } + } + } + if (defined $update_check_error) { + $self->update_check_error($update_check_error); + } + elsif (@update_info) { + $self->update_available(@update_info); + } +} + +sub _check_for_update { + my $self = shift; + my $uc = new Chirpy::UpdateChecker($self); + my $result = $uc->check_for_updates(); + return undef unless ($result); + return $uc->get_error_message() if (ref $result ne 'ARRAY'); + return $result; +} + +sub _browse_quotes_segmented { + my ($self, $page, $start, $quotes, $leading, $trailing) = @_; + if (defined $quotes) { + my $per_page = $self->parent()->quotes_per_page(); + my ($previous, $next); + if ($leading) { + $previous = $start - $per_page; + $previous = 0 if ($previous < 0); + } + if ($trailing) { + $next = $start + $per_page; + } + $self->browse_quotes($quotes, $page, $previous, $next); + } + elsif ($page == QUOTE_SEARCH) { + $self->report_no_search_results(); + } + else { + $self->report_no_quotes_to_display($page); + } +} + +sub _provide_tag_cloud { + my ($self, $tag_counts) = @_; + my $highest = 0; + my $lowest = undef; + foreach my $cnt (values %$tag_counts) { + $lowest = $cnt if (!defined $lowest || $cnt < $lowest); + $highest = $cnt if ($cnt > $highest); + } + my $difference = $highest - $lowest; + my @tag_info = (); + my $conf = $self->configuration(); + my @tags = $conf->get('ui', 'randomize_tag_cloud') + ? Chirpy::Util::shuffle_array(keys %$tag_counts) + : sort keys %$tag_counts; + my @tag_info_list; + my $factor = $conf->get('ui', 'tag_cloud_percentage_delta') || 100; + my $logarithmic = ($difference > 1 + && $conf->get('ui', 'tag_cloud_logarithmic')); + $factor /= log($difference) if ($logarithmic); + foreach my $tag (@tags) { + my $cnt = $tag_counts->{$tag}; + my $delta; + if (!$difference || $cnt == $lowest) { + $delta = 0; + } + elsif ($logarithmic) { + $delta = $factor * log($cnt - $lowest); + } + else { + $delta = $factor * ($cnt - $lowest) / $difference; + } + $tag = [ + $tag, + $cnt, + sprintf('%.0f', 100 + $delta) + ]; + } + $self->provide_tag_cloud(\@tags); +} + +sub _provide_statistics { + my $self = shift; + my $stats; + require Storable; + my $file = $self->_statistics_cache_file(); + my $exists = (-e $file); + my $tag_file = $file . '.tag'; + if ((!$exists || ( + $self->statistics_update_allowed() + && (stat($file))[9] + STATISTICS_UPDATE_INTERVAL < time + )) && (!-e $tag_file + # If the update seems to be taking longer than the update interval itself, + # something probably went wrong. In this case, we just try again. + || (stat($tag_file))[9] + STATISTICS_UPDATE_INTERVAL < time)) { + local *TAG; + open(TAG, '>', $tag_file) and close(TAG); + $stats = $self->_compute_statistics(); + if (defined $stats) { + Storable::store($stats, $file); + } + unlink $tag_file; + } + elsif ($exists) { + $stats = Storable::retrieve($file); + } + if (defined $stats) { + $self->provide_statistics(@$stats); + } + else { + $self->report_statistics_unavailable(); + } +} + +sub _compute_statistics { + my $self = shift; + my $quotes = $self->parent()->get_quotes(0, 0, [ [ 'submitted', 0 ] ]); + return undef unless (@$quotes); + my $by_date = []; + my $by_hour = &_init_array(0, 24); + my $by_month = &_init_array(0, 12); + my $by_day = &_init_array(0, 31); + my $by_weekday = &_init_array(0, 7); + my $by_rating = {}; + my $by_votes = {}; + my $votes_by_rating = [ 0, 0 ]; + my ($date, $weekday, $year, $month, $day, $prev_time); + foreach my $quote (@$quotes) { + my $time = $quote->get_date_submitted(); + my $d = $self->format_date($time); + my @time = $self->get_time($time); + if (!defined($date) || $d ne $date) { + if (defined $prev_time) { + $self->_pad_statistics($prev_time, $date, $d, $by_date); + } + $prev_time = $time; + $date = $d; + $day = $time[3] - 1; + $month = $time[4]; + $year = $time[5]; + $weekday = $time[6]; + } + &_add_statistic($date, 1, $by_date); + $by_weekday->[$weekday]++; + $by_day->[$day]++; + $by_month->[$month]++; + $by_hour->[$time[2]]++; + my $rating = $quote->get_rating(); + my $votes = $quote->get_vote_count(); + $by_rating->{$rating}++; + $by_votes->{$votes}++; + my $votes_down = ($votes - $rating) / 2; + my $votes_up = $votes - $votes_down; + $votes_by_rating->[0] += $votes_up; + $votes_by_rating->[1] += $votes_down; + } + return [ + $by_date, $by_hour, $by_weekday, $by_day, $by_month, + &_to_sorted_array($by_rating, 1), &_to_sorted_array($by_votes, 0), + $votes_by_rating + ]; +} + +sub _to_sorted_array { + my ($hashref, $negative) = @_; + my @keys = keys %$hashref; + my $highest = shift @keys; + $highest = abs $highest if ($negative); + foreach my $k (@keys) { + my $a = ($negative ? abs $k : $k); + if ($a > $highest) { + $highest = $a; + } + } + my @res = (); + for (my $i = ($negative ? - $highest : 0); $i <= $highest; $i++) { + push @res, [ $i, $hashref->{$i} || 0 ]; + } + return \@res; +} + +sub _init_array { + my ($value, $length) = @_; + my @array = (); + foreach (1..$length) { + push @array, $value; + } + return \@array; +} + +sub _add_statistic { + my ($name, $value, $aref) = @_; + if (@$aref && $aref->[scalar(@$aref) - 1]->[0] eq $name) { + $aref->[scalar(@$aref) - 1]->[1] += $value; + } + else { + push @$aref, [$name, $value]; + } +} + +sub _pad_statistics { + my ($self, $from, $from_date, $to, $by_date) = @_; + while (1) { + my ($next_date, $next_date_time) + = $self->_next_date($from, $from_date); + last if ($next_date eq $to); + &_add_statistic($next_date, 0, $by_date); + my @time = $self->get_time($next_date_time); + $from = $next_date_time; + $from_date = $next_date; + } +} + +sub _next_date { + my ($self, $time, $date) = @_; + while (1) { + $time += 23 * 60 * 60; + my $d = $self->format_date($time); + next if ($d eq $date); + return ($d, $time); + } +} + +sub _last_rating { + my ($self, $id) = @_; + my @list = $self->get_rated_quotes(); + foreach my $i (@list) { + next unless (abs($i) == $id); + return ($i < 0 ? -1 : 1); + } + return 0; +} + +sub _rate_quote { + my ($self, $id, $up, $revert, $history) = @_; + my $parent = $self->parent(); + my ($new_rating, $new_vote_count); + my @rated = $self->get_rated_quotes(); + if ($revert) { + ($new_rating, $new_vote_count) = ($up + ? $parent->increase_quote_rating($id, 1) + : $parent->decrease_quote_rating($id, 1)); + @rated = grep { abs($_) != $id } @rated; + } + else { + ($new_rating, $new_vote_count) = ($up + ? $parent->increase_quote_rating($id) + : $parent->decrease_quote_rating($id)); + $self->set_rating_history(@$history, time); + } + $self->set_rated_quotes(@rated, ($up ? $id : -$id)); + $self->confirm_quote_rating($id, $up, $new_rating, $new_vote_count); + $self->_log_event(($up + ? Chirpy::Event::QUOTE_RATING_UP + : Chirpy::Event::QUOTE_RATING_DOWN), + { 'id' => $id, 'new_rating' => $new_rating, + ($revert ? ('revert' => 1) : ()) }); +} + +sub _rating_history { + my $self = shift; + my $conf = $self->parent()->configuration(); + my $max_time = $conf->get('general', 'rating_limit_time'); + my $max_size = $conf->get('general', 'rating_limit_count'); + return undef if ($max_time <= 0 || $max_size <= 0); + my $time = time; + my $since = $time - $max_time; + my @history = $self->get_rating_history(); + shift @history + while (@history && $history[0] < $since); + return (\@history, @history >= $max_size); +} + +sub _modify_quote { + my ($self, $quote, $body, $notes, $tags) = @_; + my $id = $quote->get_id(); + $body = Chirpy::Util::clean_up_submission($body); + $notes = Chirpy::Util::clean_up_submission($notes); + $tags = Chirpy::Util::parse_tags($tags); + my $old_body = $quote->get_body(); + my $old_notes = $quote->get_notes(); + my $old_tags = join('', sort @{$quote->get_tags()}); + $self->parent()->modify_quote($quote, $body, $notes, $tags); + $tags = join(' ', sort @$tags); + $self->_log_event(Chirpy::Event::EDIT_QUOTE, { + 'id' => $id, + ($old_body ne $body ? ('new_body' => $body) : ()), + ($old_notes ne $notes ? ('new_notes' => $notes) : ()), + ($old_tags ne $tags ? ('new_tags' => $tags) : ()) + }); +} + +sub _log_event { + my ($self, $code, $params) = @_; + $params = {} unless (defined $params); + my $info = $self->get_user_information(); + while (my ($k, $v) = each %$info) { + $params->{'user:' . $k} = $v; + } + $self->parent()->log_event( + $code, + $self->get_logged_in_user_account(), + $params + ); +} + +sub _statistics_cache_file { + my $self = shift; + return $self->configuration()->get('general', 'base_path') + . '/cache/statistics'; +} + +sub moderation_queue_is_public { + my $self = shift; + return $self->configuration()->get('ui', 'moderation_queue_public'); +} + +sub get_news_posters { + my $self = shift; + return $self->parent()->get_accounts_by_level( + keys %{ADMIN_PERMISSIONS->{ADD_NEWS()}}); +} + +sub get_logged_in_user_account { + my $self = shift; + my $id = $self->get_logged_in_user(); + return (defined $id + ? $self->parent()->get_account_by_id($id) + : undef); +} + +sub administration_allowed { + my ($self, $action) = @_; + return 0 unless exists ADMIN_PERMISSIONS->{$action}; + my $user = $self->get_logged_in_user_account(); + return 0 unless defined $user; + my $level = $user->get_level(); + return ADMIN_PERMISSIONS->{$action}{$level}; +} + +sub get_parameter { + my ($self, $name) = @_; + return $self->parent()->get_parameter($name); +} + +sub set_parameter { + my ($self, $name, $value) = @_; + $self->parent()->set_parameter($name, $value); +} + +sub format_date_time { + my ($self, $timestamp) = @_; + return Chirpy::Util::format_date_time($timestamp, + $self->configuration()->get('ui', 'date_time_format'), + $self->configuration()->get('ui', 'use_gmt')); +} + +sub format_date { + my ($self, $timestamp) = @_; + return Chirpy::Util::format_date_time($timestamp, + $self->configuration()->get('ui', 'date_format'), + $self->configuration()->get('ui', 'use_gmt')); +} + +sub format_time { + my ($self, $timestamp) = @_; + return Chirpy::Util::format_date_time($timestamp, + $self->configuration()->get('ui', 'time_format'), + $self->configuration()->get('ui', 'use_gmt')); +} + +sub format_month { + my ($self, $month_id) = @_; + my @months = qw(january february march april may june + july august september october november december); + my $l = $self->locale(); + my $month = $month_id % 100; + my $year = $month_id - $month; + my $m = $months[$month]; + my $suffix = ($year ? ' ' . (1900 + $year / 100) : ''); + my $long = $l->get_string($m) . $suffix; + my $short = $l->get_string($m . '_short') . $suffix; + return ($short, $long); +} + +sub get_time { + my ($self, $timestamp) = @_; + return ($self->configuration()->get('ui', 'use_gmt') + ? gmtime($timestamp) : localtime($timestamp)); +} + +sub locale { + my $self = shift; + return $self->parent()->locale(); +} + +sub configuration { + my $self = shift; + return $self->parent()->configuration(); +} + +sub parent { + my $self = shift; + return $self->{'parent'}; +} + +sub param { + my ($self, $name) = @_; + return defined $self->{'params'} ? $self->{'params'}{$name} : undef; +} + +*get_target_version = \&Chirpy::Util::abstract_method; + +*get_current_page = \&Chirpy::Util::abstract_method; + +*get_selected_quote_id = \&Chirpy::Util::abstract_method; + +*get_first_quote_index = \&Chirpy::Util::abstract_method; + +*get_search_instruction = \&Chirpy::Util::abstract_method; + +*get_submitted_quote = \&Chirpy::Util::abstract_method; + +*attempting_login = \&Chirpy::Util::abstract_method; + +*get_supplied_username_and_password = \&Chirpy::Util::abstract_method; + +*get_rating_history = \&Chirpy::Util::abstract_method; + +*set_rating_history = \&Chirpy::Util::abstract_method; + +*get_rated_quotes = \&Chirpy::Util::abstract_method; + +*set_rated_quotes = \&Chirpy::Util::abstract_method; + +*get_logged_in_user = \&Chirpy::Util::abstract_method; + +*set_logged_in_user = \&Chirpy::Util::abstract_method; + +*report_no_quotes_to_display = \&Chirpy::Util::abstract_method; + +*report_unknown_action = \&Chirpy::Util::abstract_method; + +*welcome_user = \&Chirpy::Util::abstract_method; + +*browse_quotes = \&Chirpy::Util::abstract_method; + +*provide_quote_search_interface = \&Chirpy::Util::abstract_method; + +*provide_tag_cloud = \&Chirpy::Util::abstract_method; + +*report_no_tagged_quotes = \&Chirpy::Util::abstract_method; + +*provide_statistics = \&Chirpy::Util::abstract_method; + +*report_statistics_unavailable = \&Chirpy::Util::abstract_method; + +*report_no_search_results = \&Chirpy::Util::abstract_method; + +*report_inexistent_quote = \&Chirpy::Util::abstract_method; + +*provide_quote_submission_interface = \&Chirpy::Util::abstract_method; + +*confirm_quote_submission = \&Chirpy::Util::abstract_method; + +*confirm_quote_rating = \&Chirpy::Util::abstract_method; + +*quote_rating_confirmed = \&Chirpy::Util::abstract_method; + +*request_quote_rating_confirmation = \&Chirpy::Util::abstract_method; + +*report_rated_quote_not_found = \&Chirpy::Util::abstract_method; + +*report_quote_already_rated = \&Chirpy::Util::abstract_method; + +*report_quote_rating_limit_excess = \&Chirpy::Util::abstract_method; + +*confirm_quote_report = \&Chirpy::Util::abstract_method; + +*quote_report_confirmed = \&Chirpy::Util::abstract_method; + +*request_quote_report_confirmation = \&Chirpy::Util::abstract_method; + +*report_reported_quote_not_found = \&Chirpy::Util::abstract_method; + +*provide_login_interface = \&Chirpy::Util::abstract_method; + +*report_invalid_login = \&Chirpy::Util::abstract_method; + +*attempting_password_change = \&Chirpy::Util::abstract_method; + +*get_supplied_passwords = \&Chirpy::Util::abstract_method; + +*provide_password_change_interface = \&Chirpy::Util::abstract_method; + +*confirm_password_change = \&Chirpy::Util::abstract_method; + +*confirm_login = \&Chirpy::Util::abstract_method; + +*confirm_logout = \&Chirpy::Util::abstract_method; + +*get_current_administration_page = \&Chirpy::Util::abstract_method; + +*welcome_administrator = \&Chirpy::Util::abstract_method; + +*get_quote_to_remove = \&Chirpy::Util::abstract_method; + +*confirm_quote_removal = \&Chirpy::Util::abstract_method; + +*quote_removal_confirmed = \&Chirpy::Util::abstract_method; + +*request_quote_removal_confirmation = \&Chirpy::Util::abstract_method; + +*report_quote_to_remove_not_found = \&Chirpy::Util::abstract_method; + +*provide_quote_selection_for_removal_interface + = \&Chirpy::Util::abstract_method; + +*get_quote_to_edit = \&Chirpy::Util::abstract_method; + +*get_modified_quote_information = \&Chirpy::Util::abstract_method; + +*confirm_quote_modification = \&Chirpy::Util::abstract_method; + +*provide_quote_editing_interface = \&Chirpy::Util::abstract_method; + +*report_quote_to_edit_not_found = \&Chirpy::Util::abstract_method; + +*provide_quote_selection_for_modification_interface + = \&Chirpy::Util::abstract_method; + +*provide_quote_approval_interface = \&Chirpy::Util::abstract_method; + +*get_quote_approval_result = \&Chirpy::Util::abstract_method; + +*provide_quote_flag_management_interface = \&Chirpy::Util::abstract_method; + +*get_quote_flag_management_result = \&Chirpy::Util::abstract_method; + +*get_news_item_to_add = \&Chirpy::Util::abstract_method; + +*confirm_news_submission = \&Chirpy::Util::abstract_method; + +*provide_news_submission_interface = \&Chirpy::Util::abstract_method; + +*get_news_item_to_edit = \&Chirpy::Util::abstract_method; + +*get_modified_news_item = \&Chirpy::Util::abstract_method; + +*confirm_news_item_modification = \&Chirpy::Util::abstract_method; + +*report_news_item_to_edit_not_found = \&Chirpy::Util::abstract_method; + +*provide_news_item_editing_interface = \&Chirpy::Util::abstract_method; + +*provide_news_item_selection_for_modification_interface + = \&Chirpy::Util::abstract_method; + +*get_news_item_to_remove = \&Chirpy::Util::abstract_method; + +*confirm_news_item_removal = \&Chirpy::Util::abstract_method; + +*report_news_item_to_remove_not_found = \&Chirpy::Util::abstract_method; + +*provide_quote_selection_for_removal_interface + = \&Chirpy::Util::abstract_method; + +*get_account_information_to_add = \&Chirpy::Util::abstract_method; + +*report_invalid_new_username = \&Chirpy::Util::abstract_method; + +*report_new_username_exists = \&Chirpy::Util::abstract_method; + +*report_invalid_new_password = \&Chirpy::Util::abstract_method; + +*report_different_new_passwords = \&Chirpy::Util::abstract_method; + +*report_invalid_new_user_level = \&Chirpy::Util::abstract_method; + +*confirm_account_creation = \&Chirpy::Util::abstract_method; + +*provide_account_creation_interface = \&Chirpy::Util::abstract_method; + +*get_account_to_modify = \&Chirpy::Util::abstract_method; + +*get_modified_account_information = \&Chirpy::Util::abstract_method; + +*report_invalid_modified_username = \&Chirpy::Util::abstract_method; + +*report_modified_username_exists = \&Chirpy::Util::abstract_method; + +*report_invalid_modified_password = \&Chirpy::Util::abstract_method; + +*report_different_modified_passwords = \&Chirpy::Util::abstract_method; + +*report_invalid_modified_user_level = \&Chirpy::Util::abstract_method; + +*confirm_account_modification = \&Chirpy::Util::abstract_method; + +*report_account_to_modify_not_found = \&Chirpy::Util::abstract_method; + +*report_modified_account_information_required + = \&Chirpy::Util::abstract_method; + +*provide_account_selection_for_modification_interface + = \&Chirpy::Util::abstract_method; + +*get_account_to_remove = \&Chirpy::Util::abstract_method; + +*confirm_account_removal = \&Chirpy::Util::abstract_method; + +*report_account_to_remove_not_found = \&Chirpy::Util::abstract_method; + +*provide_account_selection_for_removal_interface + = \&Chirpy::Util::abstract_method; + +*report_last_owner_account_removal_error = \&Chirpy::Util::abstract_method; + +*get_user_information = \&Chirpy::Util::abstract_method; + +*update_available = \&Chirpy::Util::abstract_method; + +*update_check_error = \&Chirpy::Util::abstract_method; + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp.pm new file mode 100644 index 0000000..34d2234 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp.pm @@ -0,0 +1,2594 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: WebApp.pm 307 2007-02-09 01:16:00Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp - User interface class to use Chirpy! as a Web application + +=head1 REQUIREMENTS + +Apart from a proper Chirpy! installation, this module requires the following +Perl modules: + + HTML::Template + HTTP::Date + URI::Escape + +Optionally, for on-the-fly gzip compression, L is required. If +you wish to use captchas, you will need either L or +L. + +=head1 CONFIGURATION + +This module uses the following values from your configuration file: + +=over 4 + +=item webapp.webmaster_name + +The name of the site's webmaster. + +=item webapp.webmaster_email + +The e-mail address of the site's webmaster. + +=item webapp.site_url + +The base URL of the web site, i.e. the path to F, without the +trailing forward slash. + +=item webapp.resources_url + +The base URL of the path where resources, i.e. publicly available files used +by the output pages, are stored, again without the trailing forward slash. + +=item webapp.theme + +The identifier of the theme to use. + +=item webapp.welcome_text_file + +The name of the file that contains the welcome message to display on the home +page. Use a path name relative to the base path defined in the C +section. + +=item webapp.cookie_domain + +=item webapp.cookie_path + +When cookies are set, these two values are used to limit them to the correct +domain name and path. For example, if your QDB is located at +I, you would enter C and +C. + +=item webapp.session_expiry + +To keep track of users' actions, sessions are used. These are kept around for +a while if the user is inactive. This value sets how long they should be saved. +You can use a number for the expiry time in seconds, or use a number directly +followed by one of these suffixes to indicate time units: + + m: minutes + h: hours + d: days + M: months + y: years + +This format was borrowed from L. + +=item webapp.enable_short_urls + +If you run Chirpy! on an Apache web server that has the C module +installed, you can set this value to 1 to make URLs short, pretty and easy to +remember. + +=item webapp.enable_feeds + +Set this value to 1 to enable the RSS and Atom feeds the module offers. + +=item webapp.enable_gzip + +Apply gzip compression on output if possible. + +=item webapp.captcha_provider + +The captcha provider to use. Either C or C, +depending on which of the corresponding modules are available. Note that captcha +providers may offer additional configuration; see +L and +L. If this parameter is not set, +captchas will be disabled. + +=item webapp.captcha_path + +The physical path to the directory where public captcha data is to be stored. + +=item webapp.captcha_url + +The URL to the captcha path. + +=item webapp.captcha_expiry_time + +The number of seconds between the moment when the captcha was generated and the +moment when its code expires. + +=item webapp.enable_autolink + +Automatically turn hyperlinks and e-mail addresses in quotes into hyperlinks. + +=back + +=head1 LOCALE STRINGS + +This module uses the following strings from your locale. Please make sure they +are present before using the module. + +=over 4 + +=item webapp.start_page_description + +Brief description of what the start page does, for tooltips and such. + +=item webapp.start_page_short_title + +Abbreviated version of I, e.g. I, for use in compact menus. + +=item webapp.quote_link_description + +Brief description of what a link to the quote is for, for tooltips and such. + +=item webapp.next_page_title + +Translation of I, for multi-page quote lists. + +=item webapp.previous_page_title + +Translation of I, for multi-page quote lists. + +=item webapp.current_page_title + +Translation of I, for a link to the current page in the event log +viewer. + +=item webapp.footer_text + +Text stating that the page was generated by Chirpy! and informing the user of +the number of milliseconds used to do so. C<%1%> is replaced with the Chirpy! +product name and version, linked to the Chirpy! web site, C<%2%> with the +number of milliseconds (without a unit). + +=item webapp.footer_text_no_time + +Text stating that the page was generated by Chirpy!. C<%1%> is replaced with +the string I, possibly with formatting. + +=item webapp.manage_quote_instructions + +Message explaining to the user that, if he would like to modify or remove +a quote, the links to do so are available from the quote list. + +=item webapp.remove_quote_without_viewing_confirmation + +Question confirming the removal of a quote by entering its ID and stressing +that this is not a recommended action. + +=item webapp.manage_news_instructions + +Message explaining to the user that, if he would like to modify or remove +a news item, the links to do so are available from the list of recent news +items on the start page. + +=item webapp.session_required + +Message explaining that the session information cookie that Chirpy! tried to +store was not accepted by the user's browser and suggesting that he try again +after reviewing his cookie settings. + +=item webapp.timed_out_text + +Error message explaining that the connection has timed out while attempting to +rate the quote, and asking the user to try again. + +=item webapp.captcha_code_label + +The label text for the field where the user fills in the captcha code. + +=item webapp.captcha_image_text + +The alternate text for the captcha image. + +=item webapp.minimum_tag_usage_count_title + +The title for the tag cloud's slider label, i.e. "Minimum Quotes" followed by a +colon. + +=item webapp.top_quote_prefix + +The prefix for the "Top Quote" microsummary, i.e. "Top Quote" followed by a +colon. + +=item webapp.bottom_quote_prefix + +The prefix for the "Bottom Quote" microsummary, i.e. "Bottom Quote" followed by +a colon. + +=item webapp.latest_quote_prefix + +The prefix for the "Latest Quote" microsummary, i.e. "Latest Quote" followed by +a colon. + +=item webapp.latest_unmoderated_quote_prefix + +The prefix for the "Latest Unmoderated Quote" microsummary, i.e. "Latest +Unmoderated Quote" followed by a colon. + +=back + +=head1 TODO + +Split into smaller modules, expose more methods, tons of optimizations. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp; + +use strict; +use warnings; + +use vars qw($VERSION $TARGET_VERSION @ISA); + +$VERSION = '0.3'; +@ISA = qw(Chirpy::UI); + +$TARGET_VERSION = '0.3'; + +use Chirpy 0.3; +use Chirpy::UI 0.3; +use Chirpy::UI::WebApp::Session 0.3; +use Chirpy::Util 0.3; + +use HTML::Template; +use CGI; + +use constant ACTIONS => { + 'START_PAGE' => '', + 'QUOTE_RATING_UP' => 'up', + 'QUOTE_RATING_DOWN' => 'down', + 'REPORT_QUOTE' => 'report', + 'QUOTE_BROWSER' => 'browse', + 'RANDOM_QUOTES' => 'random', + 'TOP_QUOTES' => 'top', + 'BOTTOM_QUOTES' => 'bottom', + 'QUOTES_OF_THE_WEEK' => 'qotw', + 'QUOTE_SEARCH' => 'search', + 'TAG_CLOUD' => 'tags', + 'STATISTICS' => 'statistics', + 'MODERATION_QUEUE' => 'queue', + 'SUBMIT_QUOTE' => 'submit', + 'ADMINISTRATION' => 'admin', + 'LOGIN' => 'login', + 'LOGOUT' => 'logout' +}; + +use constant ADMIN_ACTIONS => { + 'CHANGE_PASSWORD' => 'password', + 'MANAGE_UNAPPROVED_QUOTES' => 'quote_approve', + 'MANAGE_FLAGGED_QUOTES' => 'quote_flags', + 'EDIT_QUOTE' => 'quote_edit', + 'REMOVE_QUOTE' => 'quote_remove', + 'ADD_NEWS' => 'news_add', + 'EDIT_NEWS' => 'news_edit', + 'REMOVE_NEWS' => 'news_remove', + 'ADD_ACCOUNT' => 'accounts', + 'EDIT_ACCOUNT' => 'accounts', + 'REMOVE_ACCOUNT' => 'accounts', + 'VIEW_EVENT_LOG' => 'log' +}; + +use constant STATUS_OK => 1; +use constant STATUS_ALREADY_RATED => 2; +use constant STATUS_RATING_LIMIT_EXCEEDED => 3; +use constant STATUS_QUOTE_NOT_FOUND => 4; +use constant STATUS_SESSION_REQUIRED => 5; + +sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + my $path = $self->_template_cache_path(); + $self->{'templates_path'} + = $self->configuration()->get('general', 'base_path') + . '/templates/' . $self->param('theme'); + $self->{'cgi'} = new CGI(); + $self->{'cookies'} = []; + my $session = new Chirpy::UI::WebApp::Session($self); + if (defined $session) { + $self->{'session'} = $session; + $self->_set_cookie($Chirpy::UI::WebApp::Session::NAME, + $session->id(), $self->param('session_expiry')); + } + return $self; +} + +sub get_target_version { + return $TARGET_VERSION; +} + +sub get_current_page { + my $self = shift; + return $self->{'page'} if (exists $self->{'page'}); + my $action = $self->_action(); + my $page; + if (defined $action && $action) { + while (my ($n, $v) = each %{ACTIONS()}) { + if ($v eq $action) { + $page = eval 'Chirpy::UI::' . $n; + last; + } + } + } + else { + $page = ($self->_id() + ? Chirpy::UI::SINGLE_QUOTE : Chirpy::UI::START_PAGE); + } + $page = Chirpy::UI::START_PAGE unless (defined $page); + $self->_provide_session_if_necessary($page); + # XXX: This is sort of hackish. What to do? + if (defined $self->_feed_type()) { + my $quotes_per_feed = $self->param('quotes_per_feed'); + $quotes_per_feed = 50 + unless (defined $quotes_per_feed && $quotes_per_feed > 0); + $self->parent()->quotes_per_page($quotes_per_feed); + } + elsif ($self->_wants_microsummary() + && $self->_page_offers_microsummary($page)) { + $self->parent()->quotes_per_page(1); + } + $self->{'page'} = $page; + return $page; +} + +sub get_selected_quote_id { + my $self = shift; + return $self->_id(); +} + +sub get_first_quote_index { + my $self = shift; + return $self->_cgi_param('start') || 0; +} + +sub get_search_instruction { + my $self = shift; + my $query = $self->_cgi_param('query'); + return ([], []) unless (defined $query); + my @queries = (); + my @tags = (); + while ($query =~ /"(.*?)"|(\S+)|"([^"]+)$/g) { + my $literal = defined $1 ? $1 : $3; + if (defined $literal) { + push @queries, '*' . $literal . '*'; + } + else { + my $word = $2; + if ($word =~ s/^tag://i) { + push @tags, $word; + } + else { + push @queries, '*' . $word . '*'; + } + } + } + return (\@queries, \@tags); +} + +sub get_submitted_quote { + my $self = shift; + if ($self->_requires_captcha()) { + my $code = $self->_cgi_param('captcha_code'); + my $hash = $self->_cgi_param('captcha_hash'); + return undef unless (defined $code && defined $hash + && $self->_captcha_provider($hash)->verify($code)); + } + return ($self->_cgi_param('quote'), + $self->_cgi_param('notes'), $self->_cgi_param('tags')); +} + +sub attempting_login { + my $self = shift; + return $self->_is_post(); +} + +sub get_supplied_username_and_password { + my $self = shift; + return ($self->_cgi_param('username'), $self->_cgi_param('password')); +} + +sub get_rating_history { + my $self = shift; + return undef unless (defined $self->_session()); + my $hist = $self->_session()->param(-name => 'history'); + return () unless (ref $hist eq 'ARRAY'); + return @$hist; +} + +sub set_rating_history { + my ($self, @history) = @_; + return undef unless (defined $self->_session()); + $self->_session()->param(-name => 'history', -value => \@history); +} + +sub get_rated_quotes { + my $self = shift; + return undef unless (defined $self->_session()); + my $list = $self->_session()->param(-name => 'rated'); + return () unless (ref $list eq 'ARRAY'); + return @$list; +} + +sub set_rated_quotes { + my ($self, @list) = @_; + return undef unless (defined $self->_session()); + $self->_session()->param(-name => 'rated', -value => \@list); +} + +sub get_logged_in_user { + my $self = shift; + return undef unless (defined $self->_session()); + return $self->_session()->param(-name => 'user'); +} + +sub set_logged_in_user { + my ($self, $id) = @_; + return undef unless (defined $self->_session()); + $self->_session()->param(-name => 'user', -value => $id); +} + +sub report_unknown_action { + my $self = shift; + $self->_report_error($self->locale()->get_string('unknown_action')); +} + +sub report_no_quotes_to_display { + my ($self, $page) = @_; + my $type = $self->_feed_type(); + if (defined $type) { + $self->_generate_feed([], $type, $page); + } + elsif ($self->_wants_microsummary() + && $self->_page_offers_microsummary($page)) { + $self->_generate_microsummary(undef, $page); + } + else { + my $name = &_get_page_name($page); + my $title = $self->locale()->get_string($name); + $self->_report_message( + &_text_to_xhtml($title), + $self->locale()->get_string('no_quotes')); + } +} + +sub _report_message { + my ($self, $title, $text) = @_; + my $template = $self->_load_template('message'); + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml($title), + 'MESSAGE_TEXT' => &_text_to_xhtml($text) + ); + $self->_output_template($template); +} + +sub _report_error { + my ($self, $error) = @_; + my $template = $self->_load_template('error'); + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $self->locale()->get_string('error_title')), + 'ERROR_MESSAGE' => &_text_to_xhtml($error) + ); + $self->_output_template($template); +} + +sub welcome_user { + my ($self, $news) = @_; + my $template = $self->_load_template('start_page'); + my $locale = $self->locale(); + $template->param('PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string('welcome'))); + $template->param('NEWS_TITLE' => &_text_to_xhtml( + $locale->get_string('latest_news'))); + if (defined $news) { + my @news_tmpl = (); + foreach my $item (@$news) { + my $poster = $item->get_poster(); + push @news_tmpl, { + 'BODY' => $self->_format_news_body($item->get_body()), + 'AUTHOR' => defined $poster + ? &_text_to_xhtml($poster->get_username()) + : undef, + 'DATE' => &_text_to_xhtml( + $self->format_date_time($item->get_date())), + 'ALLOW_EDIT' + => $self->administration_allowed(Chirpy::UI::EDIT_NEWS), + 'ALLOW_REMOVE' + => $self->administration_allowed(Chirpy::UI::REMOVE_NEWS), + 'EDIT' => &_text_to_xhtml( + $locale->get_string('edit')), + 'REMOVE' => &_text_to_xhtml( + $locale->get_string('remove')), + 'REMOVAL_CONFIRMATION' => &_text_to_xhtml( + $locale->get_string('news_removal_confirmation')), + 'EDIT_URL' => $self->_url( + ADMIN_ACTIONS->{'EDIT_NEWS'}, + 1, + 'id' => $item->get_id()), + 'REMOVE_URL' => $self->_url( + ADMIN_ACTIONS->{'REMOVE_NEWS'}, + 1, + 'id' => $item->get_id()) + }; + } + $template->param('NEWS' => \@news_tmpl); + } + my $motd_path = $self->configuration()->get('general', 'base_path') + . '/' . $self->param('welcome_text_file'); + # TODO: find a better way to include this + if (-f $motd_path) { + $template->param('MOTD' => $self->_process_template( + new HTML::Template( + 'filename' => $motd_path, + 'die_on_bad_params' => 0, + 'global_vars' => 1, + 'file_cache' => 1, + 'file_cache_dir' => $self->_template_cache_path(), + 'file_cache_dir_mode' => 0777 + ) + )); + } + $self->_output_template($template); +} + +sub browse_quotes { + my ($self, $quotes, $page, $previous, $next) = @_; + my $type = $self->_feed_type(); + if (defined $type) { + $self->_generate_feed($quotes, $type, $page); + } + elsif ($self->_wants_microsummary() + && $self->_page_offers_microsummary($page)) { + $self->_generate_microsummary($quotes->[0], $page); + } + else { + $self->_generate_xhtml($quotes, $page, $previous, $next); + } +} + +sub _generate_microsummary { + my ($self, $quote, $page) = @_; + my $locale = $self->locale(); + my $prefix; + if ($page == Chirpy::UI::TOP_QUOTES) { + $prefix = $locale->get_string('webapp.top_quote_prefix'); + } + elsif ($page == Chirpy::UI::BOTTOM_QUOTES) { + $prefix = $locale->get_string('webapp.bottom_quote_prefix'); + } + elsif ($page == Chirpy::UI::MODERATION_QUEUE) { + $prefix = $locale->get_string('webapp.latest_unmoderated_quote_prefix'); + } + else { + $prefix = $locale->get_string('webapp.latest_quote_prefix'); + } + my $summary = $prefix . ' ' + . (defined $quote + ? $quote->get_id() + : $locale->get_string('none')); + # Don't serve Last-Modified here, since not everything is chronological + $self->_maybe_gzip($summary, 'text/plain'); +} + +sub _generate_feed { + my ($self, $quotes, $type, $page) = @_; + my $date = $self->get_parameter('webapp.quote_feed_date'); + unless ($date) { + if (@$quotes) { + foreach my $quote (@$quotes) { + my $d = $quote->get_date_submitted(); + $date = $d if (!defined($date) || $d > $date); + } + } + else { + $date = time; + } + } + my $etag = sprintf('"%X"', $date); + require HTTP::Date; + my $ims = $self->{'cgi'}->http('If-Modified-Since'); + my $inm = $self->{'cgi'}->http('If-None-Match'); + if ((defined $ims || defined $inm) + && ((defined $ims && $date <= HTTP::Date::str2time($ims)) + || (defined $inm && $etag eq $inm))) { + print $self->{'cgi'}->header(-status => '304 Not Modified'); + return; + } + my $locale = $self->locale(); + my $conf = $self->configuration(); + my $site_title = &_text_to_xhtml( + $conf->get('general', 'title')); + my $page_title = &_text_to_xhtml( + $self->locale()->get_string(&_get_page_name($page))); + my $site_description = &_text_to_xhtml( + $conf->get('general', 'description')); + my $name = &_text_to_xhtml($self->param('webmaster_name')); + my $email = &_hide_email($self->param('webmaster_email')); + my $template = new HTML::Template( + 'filename' => $self->{'templates_path'} + . '/feeds/' . ($type eq 'atom' ? 'atom10' : 'rss20') . '.xml', + 'die_on_bad_params' => 0, + 'global_vars' => 1, + 'file_cache' => 1, + 'file_cache_dir' => $self->_template_cache_path(), + 'file_cache_dir_mode' => 0777 + ); + my @quotes = (); + foreach my $quote (@$quotes) { + my $id = $quote->get_id(); + my $d = $quote->get_date_submitted(); + $date = $d if (!defined($date) || $d > $date); + my $up_url = $self->_url(ACTIONS->{'QUOTE_RATING_UP'}, + undef, 'id' => $id); + my $down_url = $self->_url(ACTIONS->{'QUOTE_RATING_DOWN'}, + undef, 'id' => $id); + my $report_url = $self->_url(ACTIONS->{'REPORT_QUOTE'}, + undef, 'id' => $id); + my ($body, $notes, $tags) = $self->_format_quote($quote); + push @quotes, { + 'QUOTE_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_title', $id)), + 'QUOTE_ID' => $id, + 'QUOTE_URL' => &_text_to_xhtml($self->_quote_url($id)), + 'QUOTE_BODY' => $body, + 'QUOTE_NOTES' => $notes, + 'QUOTE_TAGS' => $tags, + 'QUOTE_NOTES_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_notes_title')), + 'QUOTE_TAGS_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_tags_title')), + 'QUOTE_RATING' + => Chirpy::Util::format_quote_rating($quote->get_rating()), + 'QUOTE_VOTE_COUNT' => $quote->get_vote_count(), + 'QUOTE_RATING_UP_URL' => $up_url, + 'QUOTE_RATING_DOWN_URL' => $down_url, + 'QUOTE_REPORT_URL' => $report_url, + 'QUOTE_RATING_UP_SHORT_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_rating_up_short_title')), + 'QUOTE_RATING_DOWN_SHORT_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_rating_down_short_title')), + 'QUOTE_REPORT_SHORT_TITLE' => &_text_to_xhtml( + $locale->get_string('report_quote_short_title')), + 'QUOTE_DATE_RFC822' => sub { + return &_format_date_time_rfc822($d); + }, + 'QUOTE_DATE_ISO8601' => sub { + return &_format_date_time_iso8601($d); + }, + 'QUOTE_IS_APPROVED' => $quote->is_approved(), + 'QUOTE_IS_FLAGGED' => $quote->is_flagged() + }; + } + my $act = $self->_action(); + $template->param( + 'SITE_TITLE' => $site_title, + 'PAGE_TITLE' => $page_title, + 'FEED_SUBTITLE' => $site_description, + 'FEED_URL' => $self->_feed_url($act, $type), + 'PAGE_URL' => $self->_url($act), + 'SITE_URL' => $self->_url(), + 'WEBMASTER_NAME' => $name, + 'WEBMASTER_EMAIL' => $email, + 'CHARACTER_ENCODING' => 'UTF-8', + 'CHIRPY_NAME' => Chirpy::PRODUCT_NAME, + 'CHIRPY_VERSION' => Chirpy::VERSION_STRING, + 'CHIRPY_URL' => Chirpy::URL, + 'QUOTES' => \@quotes, + 'FEED_DATE_RFC822' => sub { + return &_format_date_time_rfc822($date); + }, + 'FEED_DATE_ISO8601' => sub { + return &_format_date_time_iso8601($date); + } + ); + my $ctype = 'application/' . $type . '+xml'; + $ctype = 'text/xml' unless ($self->_accepts($ctype)); + $self->_maybe_gzip($template->output(), $ctype, + -Last_Modified => HTTP::Date::time2str($date), + -ETag => $etag); +} + +sub _generate_xhtml { + my ($self, $quotes, $page, $previous, $next) = @_; + $self->parent()->mark_debug_event('Build quote browser'); + my $name = &_get_page_name($page); + my $locale = $self->locale(); + my $page_title = &_text_to_xhtml( + $self->locale()->get_string($name)); + my $processing = &_text_to_xhtml( + $locale->get_string('processing')); + my $timed_out = &_text_to_xhtml( + $locale->get_string('timed_out')); + my $error = &_text_to_xhtml($locale->get_string('error')); + my $flagged = &_text_to_xhtml($locale->get_string('flagged')); + my $up = &_text_to_xhtml( + $locale->get_string('quote_rating_up_short_title')); + my $down = &_text_to_xhtml( + $locale->get_string('quote_rating_down_short_title')); + my $report = &_text_to_xhtml( + $locale->get_string('report_quote_short_title')); + my $edit = &_text_to_xhtml($locale->get_string('edit')); + my $remove = &_text_to_xhtml($locale->get_string('remove')); + my $unflag = &_text_to_xhtml($locale->get_string('unflag')); + my $template = $self->_load_template('quote_list'); + $template->param( + 'PAGE_TITLE' => $page_title, + 'FLAGGED' => $flagged, + 'ERROR' => $error, + 'PROCESSING' => $processing, + 'TIMED_OUT' => $timed_out, + 'QUOTE_RATING_TIMED_OUT' => &_text_to_xhtml( + $locale->get_string('webapp.quote_rating_timed_out')), + 'LIMIT_EXCEEDED_TEXT' => &_text_to_xhtml( + $self->_quote_rating_limit_text()), + 'QUOTE_ALREADY_RATED_TEXT' => &_text_to_xhtml( + $self->_quote_already_rated_text()), + 'QUOTE_NOT_FOUND_TEXT' => &_text_to_xhtml( + $locale->get_string('rated_quote_not_found_text')), + 'SESSION_REQUIRED_TEXT' => &_text_to_xhtml( + $locale->get_string('webapp.session_required')), + ); + my $query; + if ($page == Chirpy::UI::QUOTE_SEARCH) { + $query = $self->_cgi_param('query'); + $template->param( + 'SEARCHED' => 1, + 'SEARCH_QUERY' => &_text_to_xhtml($query) + ); + } + my $link_desc = &_text_to_xhtml( + $locale->get_string('webapp.quote_link_description')); + my $rating_up_desc = &_text_to_xhtml( + $locale->get_string('quote_rating_up_description')); + my $rating_down_desc = &_text_to_xhtml( + $locale->get_string('quote_rating_down_description')); + my $report_desc = &_text_to_xhtml( + $locale->get_string('quote_report_description')); + my $rating_desc = &_text_to_xhtml( + $locale->get_string('quote_rating_description')); + my $vote_count_desc = &_text_to_xhtml( + $locale->get_string('quote_vote_count_description')); + my $date_desc = &_text_to_xhtml( + $locale->get_string('quote_date_description')); + my $edit_desc = &_text_to_xhtml( + $locale->get_string('quote_edit_description')); + my $remove_desc = &_text_to_xhtml( + $locale->get_string('quote_remove_description')); + my $removal_conf = &_text_to_xhtml( + $locale->get_string('quote_removal_confirmation')); + my $notes_title = &_text_to_xhtml( + $locale->get_string('quote_notes_title')); + my $tags_title = &_text_to_xhtml( + $locale->get_string('quote_tags_title')); + my %static_strings = ( + 'RATING_UP_DESCRIPTION' => $rating_up_desc, + 'RATING_DOWN_DESCRIPTION' => $rating_down_desc, + 'REMOVE_DESCRIPTION' => $remove_desc, + 'LINK_DESCRIPTION' => $link_desc, + 'REMOVE_DESCRIPTION' => $remove_desc, + 'REPORT_DESCRIPTION' => $report_desc, + 'RATING_DESCRIPTION' => $rating_desc, + 'VOTE_COUNT_DESCRIPTION' => $vote_count_desc, + 'DATE_DESCRIPTION' => $date_desc, + 'EDIT_DESCRIPTION' => $edit_desc, + 'REMOVE_DESCRIPTION' => $remove_desc, + 'REMOVAL_CONFIRMATION' => $removal_conf, + 'UP' => $up, + 'DOWN' => $down, + 'EDIT' => $edit, + 'REMOVE' => $remove, + 'UNFLAG' => $unflag, + 'REPORT' => $report, + 'FLAGGED' => $flagged, + 'NOTES_TITLE' => $notes_title, + 'TAGS_TITLE' => $tags_title + ); + my %previous_rating = (); + foreach my $rated ($self->get_rated_quotes()) { + $previous_rating{abs($rated)} = ($rated < 0 ? -1 : 1); + } + my @quotes_tmpl = (); + $self->parent()->mark_debug_event('Parse quotes for template'); + foreach my $quote (@$quotes) { + my $up_url = $self->_url( + ACTIONS->{'QUOTE_RATING_UP'}, + undef, + 'id' => $quote->get_id()); + my $down_url = $self->_url( + ACTIONS->{'QUOTE_RATING_DOWN'}, + undef, + 'id' => $quote->get_id()); + my $report_url = $self->_url( + ACTIONS->{'REPORT_QUOTE'}, + undef, + 'id' => $quote->get_id()); + $self->parent()->mark_debug_event('Parse quote body'); + my ($body, $notes, $tags) = $self->_format_quote($quote); + $self->parent()->mark_debug_event('Quote body parsed'); + my $id = $quote->get_id(); + push @quotes_tmpl, { + 'ID' => $id, + 'TITLE' => &_text_to_xhtml( + $locale->get_string('quote_title', $id)), + 'BODY' => $body, + 'NOTES' => $notes, + 'TAGS' => $tags, + 'NOTES_OR_TAGS' => (defined $notes || @$tags ? 1 : 0), + 'RATING_NUMBER' => $quote->get_rating(), + 'RATING_TEXT' + => Chirpy::Util::format_quote_rating($quote->get_rating()), + 'VOTE_COUNT' => $quote->get_vote_count(), + 'SUBMITTED_TEXT' => &_text_to_xhtml( + $self->format_date_time($quote->get_date_submitted())), + 'IS_APPROVED' => $quote->is_approved(), + 'IS_FLAGGED' => $quote->is_flagged(), + 'WAS_VOTED_UP' => ($previous_rating{$id} + && $previous_rating{$id} > 0), + 'WAS_VOTED_DOWN' => ($previous_rating{$id} + && $previous_rating{$id} < 0), + 'LINK_URL' => &_text_to_xhtml( + $self->_quote_url($quote->get_id())), + 'RATING_UP_URL' => $up_url, + 'RATING_DOWN_URL' => $down_url, + 'REPORT_URL' => $report_url, + 'ALLOW_EDIT' + => $self->administration_allowed(Chirpy::UI::EDIT_QUOTE), + 'ALLOW_REMOVE' + => $self->administration_allowed(Chirpy::UI::REMOVE_QUOTE), + 'ALLOW_UNFLAG' + => $quote->is_flagged() && $self->administration_allowed( + Chirpy::UI::MANAGE_FLAGGED_QUOTES), + 'EDIT_URL' => sub { return $self->_url( + ADMIN_ACTIONS->{'EDIT_QUOTE'}, + 1, + 'id' => $quote->get_id()) }, + 'REMOVE_URL' => sub { return $self->_url( + ADMIN_ACTIONS->{'REMOVE_QUOTE'}, + 1, + 'id' => $quote->get_id()) }, + 'UNFLAG_URL' => sub { return $self->_url( + ADMIN_ACTIONS->{'MANAGE_FLAGGED_QUOTES'}, + 1, + 'action_' . $quote->get_id() => 1) }, + 'ADMINISTRATOR_LINKS' + => defined($self->get_logged_in_user_account()), + %static_strings + }; + } + $self->parent()->mark_debug_event('Quotes parsed'); + $template->param('QUOTES' => \@quotes_tmpl); + if (defined $previous || defined $next) { + my %query = (defined $query ? ('query' => $query) : ()); + $template->param('BROWSER' => 1); + $template->param('PREVIOUS_URL' => $self->_url( + $self->_action(), + undef, + 'start' => $previous, + %query + )) if (defined $previous); + $template->param('NEXT_URL' => $self->_url( + $self->_action(), + undef, + 'start' => $next, + %query + )) if (defined $next); + $template->param('START_URL' => $self->_url( + $self->_action(), + undef, + %query + )); + } + $self->_output_template($template); + $self->parent()->mark_debug_event('Quote browser displayed'); + my $dbg = $self->parent()->debug_events(); + if (defined $dbg) { + print "$/$/"; + } +} + +sub provide_quote_search_interface { + my $self = shift; + my $template = $self->_load_template('quote_search'); + my $locale = $self->locale(); + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string('search_for_quotes')) + ); + $self->_output_template($template); +} + +sub provide_tag_cloud { + my ($self, $tag_information) = @_; + my $template = $self->_load_template('tag_cloud'); + my $locale = $self->locale(); + my @tag_info = (); + foreach my $arrayref (@$tag_information) { + my ($tag, $cnt, $perc) = @$arrayref; + my $title = $self->locale()->get_string('tag_link_description', $tag); + push @tag_info, { + 'TAG' => &_text_to_xhtml($tag), + 'USAGE_COUNT' => $cnt, + 'SIZE_PERCENTAGE' => $perc, + 'URL' => &_text_to_xhtml($self->_tag_url($tag)), + 'LINK_DESCRIPTION' => &_text_to_xhtml($title) + }; + } + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string('tag_cloud')), + 'TAGS' => \@tag_info, + 'USAGE_SLIDER_TITLE' => &_text_to_xhtml( + $locale->get_string('webapp.minimum_tag_usage_count_title')) + ); + $self->_output_template($template); +} + +sub report_no_tagged_quotes { + my $self = shift; + $self->_report_error($self->locale()->get_string('no_tagged_quotes')); +} + +sub provide_statistics { + my ($self, $quotes_by_date, + $quotes_by_hour, $quotes_by_weekday, $quotes_by_day, $quotes_by_month, + $quotes_by_rating, $quotes_by_votes, $votes_by_rating) = @_; + if ($self->statistics_update_allowed()) { + $self->_output_xml('result'); + return; + } + my $template = $self->_load_template('statistics'); + my $locale = $self->locale(); + my @by_date = (); + foreach my $line (@$quotes_by_date) { + push @by_date, { + 'DATE' => $line->[0], + 'QUOTE_COUNT' => $line->[1] + }; + } + my @by_hour = (); + foreach my $h (0..23) { + push @by_hour, { + 'START_HOUR' => $h, + 'END_HOUR' => ($h == 23 ? 0 : $h + 1), + 'QUOTE_COUNT' => $quotes_by_hour->[$h] + }; + } + my @by_month = (); + foreach my $month (0..11) { + my ($short, $long) = $self->format_month($month); + push @by_month, { + 'MONTH_NAME_SHORT' => $short, + 'MONTH_NAME' => $long, + 'QUOTE_COUNT' => $quotes_by_month->[$month] + }; + } + my @by_day = (); + foreach my $day (0..30) { + push @by_day, { + 'DAY' => $day + 1, + 'QUOTE_COUNT' => $quotes_by_day->[$day] + }; + } + my @by_weekday = (); + my @days = qw(sunday monday tuesday wednesday thursday friday saturday); + foreach my $d (0..6) { + push @by_weekday, { + 'WEEKDAY' => $locale->get_string($days[$d]), + 'QUOTE_COUNT' => $quotes_by_weekday->[$d] + }; + } + my @by_rating = (); + foreach my $line (@$quotes_by_rating) { + push @by_rating, { + 'RATING' => Chirpy::Util::format_quote_rating($line->[0]), + 'QUOTE_COUNT' => $line->[1] + }; + } + my @by_votes = (); + foreach my $line (@$quotes_by_votes) { + push @by_votes, { + 'VOTE_COUNT' => $line->[0], + 'QUOTE_COUNT' => $line->[1] + }; + } + my @votes_by_rating = ( + { + 'RATING' => &_text_to_xhtml( + $locale->get_string('quote_rating_up_short_title')), + 'VOTE_COUNT' => $votes_by_rating->[0] + }, + { + 'RATING' => &_text_to_xhtml( + $locale->get_string('quote_rating_down_short_title')), + 'VOTE_COUNT' => $votes_by_rating->[1] + } + ); + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string('statistics')), + 'QUOTES_BY_DATE' => \@by_date, + 'QUOTES_BY_DATE_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_date')), + 'QUOTES_BY_HOUR' => \@by_hour, + 'QUOTES_BY_HOUR_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_hour')), + 'QUOTES_BY_MONTH' => \@by_month, + 'QUOTES_BY_MONTH_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_month')), + 'QUOTES_BY_DAY' => \@by_day, + 'QUOTES_BY_DAY_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_day')), + 'QUOTES_BY_WEEKDAY' => \@by_weekday, + 'QUOTES_BY_WEEKDAY_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_weekday')), + 'QUOTES_BY_RATING' => \@by_rating, + 'QUOTES_BY_RATING_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_rating')), + 'QUOTES_BY_VOTE_COUNT' => \@by_votes, + 'QUOTES_BY_VOTE_COUNT_TITLE' => &_text_to_xhtml( + $locale->get_string('quote_count_by_vote_count')), + 'VOTES_BY_RATING' => \@votes_by_rating, + 'VOTES_BY_RATING_TITLE' => &_text_to_xhtml( + $locale->get_string('vote_count_by_rating')), + 'UPDATE_URL' + => $self->_url(ACTIONS->{'STATISTICS'}, 0, 'update' => 1) + ); + $self->_output_template($template); +} + +sub report_statistics_unavailable { + my $self = shift; + $self->_report_error($self->locale()->get_string('statistics_unavailable')); +} + +sub statistics_update_allowed { + my $self = shift; + return $self->_url_param('update'); +} + +sub report_no_search_results { + my $self = shift; + my $type = $self->_feed_type(); + if (defined $type) { + $self->_generate_feed([], $type, Chirpy::UI::QUOTE_SEARCH); + } + else { + my $locale = $self->locale(); + $self->_report_message($locale->get_string('no_search_results'), + $locale->get_string('no_search_results_text')); + } +} + +sub report_inexistent_quote { + my $self = shift; + my $locale = $self->locale(); + $self->_report_message($locale->get_string('quote_not_found'), + $locale->get_string('quote_not_found_text')); +} + +sub provide_quote_submission_interface { + my $self = shift; + my $template = $self->_load_template('submit_quote'); + my $locale = $self->locale(); + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string('submit_quote')), + 'NO_APPROVAL' => $self->administration_allowed( + Chirpy::UI::MANAGE_UNAPPROVED_QUOTES), + 'SUBMIT_FORM_START' => '
', + 'SUBMIT_FORM_END' => '
', + 'QUOTE_LABEL' => &_text_to_xhtml( + $locale->get_string('submission_title')), + 'NOTES_LABEL' => &_text_to_xhtml( + $locale->get_string('notes_title')), + 'TAGS_LABEL' => &_text_to_xhtml( + $locale->get_string('tags_title')), + 'SUBMIT_LABEL' => &_text_to_xhtml( + $locale->get_string('submit_button_label')), + 'SUBMIT_LABEL_NO_APPROVAL' => &_text_to_xhtml( + $locale->get_string('submit_button_label_no_approval')) + ); + if ($self->_requires_captcha()) { + my ($hash, $url, $width, $height) = $self->_captcha_provider() + ->create(time() + ($self->param('captcha_expiry_time') || 300)); + $template->param( + 'USE_CAPTCHA' => 1, + 'CAPTCHA_HASH' => $hash, + 'CAPTCHA_IMAGE_URL' => &_text_to_xhtml($url), + 'CAPTCHA_CODE_LABEL' => &_text_to_xhtml( + $locale->get_string('webapp.captcha_code_label')), + 'CAPTCHA_IMAGE_TEXT' => &_text_to_xhtml( + $locale->get_string('webapp.captcha_image_text')), + 'CAPTCHA_IMAGE_WIDTH' => $width, + 'CAPTCHA_IMAGE_HEIGHT' => $height + ); + } + $self->_output_template($template); +} + +sub confirm_quote_submission { + my ($self, $admin) = @_; + my $loc = $self->locale(); + if ($admin) { + $self->_trigger_feed_update(); + $self->_report_message( + $loc->get_string('quote_submitted_no_approval'), + $loc->get_string('quote_submission_thanks_no_approval')); + } + else { + $self->_report_message($loc->get_string('quote_submitted'), + $loc->get_string('quote_submission_thanks')); + } +} + +sub quote_rating_confirmed { + my $self = shift; + return $self->_is_post(); +} + +sub request_quote_rating_confirmation { + my ($self, $quote, $up, $revert) = @_; + my $locale = $self->locale(); + my $action = 'quote_rating_' . ($up ? 'up' : 'down'); + $self->_confirmation_form( + $self->_url($self->_action(), 0, 'id' => $quote->get_id()), + 1, + $locale->get_string($action . '_description'), + $locale->get_string($action . '_confirmation_request'), + $quote); +} + +sub confirm_quote_rating { + my ($self, $id, $up, $new_rating, $new_vote_count) = @_; + $self->_trigger_feed_update(); + if ($self->_wants_xml()) { + $self->_output_xml('result', { + 'status' => STATUS_OK, + 'rating' => Chirpy::Util::format_quote_rating($new_rating), + 'votes' => $new_vote_count + }); + } + else { + my $loc = $self->locale(); + $self->_report_message( + $loc->get_string('quote_rating_' + . ($up ? 'increased' : 'decreased')), + $loc->get_string('quote_rating_thanks') + ); + } +} + +sub report_rated_quote_not_found { + my $self = shift; + if ($self->_wants_xml()) { + $self->_output_xml('result', { 'status' => STATUS_QUOTE_NOT_FOUND }); + } + else { + my $loc = $self->locale(); + $self->_report_message( + $loc->get_string('quote_not_found'), + $loc->get_string('rated_quote_not_found_text') + ); + } +} + +sub report_quote_already_rated { + my $self = shift; + if ($self->_wants_xml()) { + $self->_output_xml('result', { 'status' => STATUS_ALREADY_RATED }); + } + else { + $self->_report_error($self->_quote_already_rated_text()); + } +} + +sub report_quote_rating_limit_excess { + my $self = shift; + if ($self->_wants_xml()) { + $self->_output_xml('result', { 'status' => STATUS_RATING_LIMIT_EXCEEDED }); + } + else { + $self->_report_error($self->_quote_rating_limit_text()); + } +} + +sub quote_report_confirmed { + my $self = shift; + return $self->_is_post(); +} + +sub request_quote_report_confirmation { + my ($self, $quote) = @_; + my $locale = $self->locale(); + my $action = 'quote_report'; + $self->_confirmation_form( + $self->_url($self->_action(), 0, 'id' => $quote->get_id()), + 1, + $locale->get_string($action . '_description'), + $locale->get_string($action . '_confirmation_request'), + $quote); +} + +sub confirm_quote_report { + my $self = shift; + $self->_trigger_feed_update(); + if ($self->_wants_xml()) { + $self->_output_xml('result', { 'status' => STATUS_OK }); + } + else { + my $loc = $self->locale(); + $self->_report_message( + $loc->get_string('quote_reported'), + $loc->get_string('quote_report_thanks') + ); + } +} + +sub report_reported_quote_not_found { + my $self = shift; + if ($self->_wants_xml()) { + $self->_output_xml('result', { 'status' => STATUS_QUOTE_NOT_FOUND }); + } + else { + my $loc = $self->locale(); + $self->_report_message( + $loc->get_string('quote_not_found'), + $loc->get_string('reported_quote_not_found_text') + ); + } +} + +sub provide_login_interface { + my ($self, $invalid) = @_; + my $template = $self->_load_template('login'); + my $locale = $self->locale(); + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string($invalid + ? 'invalid_login_title' + : 'login_title')), + 'USERNAME_TITLE' => &_text_to_xhtml( + $locale->get_string('username_title')), + 'PASSWORD_TITLE' => &_text_to_xhtml( + $locale->get_string('password_title')), + 'LOGIN_BUTTON_LABEL' => &_text_to_xhtml( + $locale->get_string('login_button_label')), + 'LOGIN_FORM_START' => '
', + 'LOGIN_FORM_END' => '
', + 'INVALID_LOGIN_INSTRUCTIONS' => &_text_to_xhtml( + $locale->get_string( + 'invalid_login_instructions')), + 'INVALID_LOGIN' => $invalid + ); + $self->_output_template($template); +} + +sub report_invalid_login { + my $self = shift; + $self->provide_login_interface(1); +} + +sub attempting_password_change { + my $self = shift; + return $self->_is_post(); +} + +sub get_supplied_passwords { + my $self = shift; + return ($self->_cgi_param('current_password'), + $self->_cgi_param('new_password'), + $self->_cgi_param('repeat_new_password')); +} + +sub update_available { + my ($self, $version, $released, $url) = @_; + $self->{'available_update'} = [ $version, $released, $url ]; +} + +sub update_check_error { + my ($self, $error) = @_; + $self->{'update_check_error'} = $error; +} + +sub get_current_administration_page { + my $self = shift; + my $action = $self->_admin_action(); + if (defined $action && $action) { + if ($action eq ADMIN_ACTIONS->{'ADD_ACCOUNT'}) { + return ($self->_cgi_param('account_remove') + ? Chirpy::UI::REMOVE_ACCOUNT + : $self->get_account_to_modify() < 0 + ? Chirpy::UI::ADD_ACCOUNT + : Chirpy::UI::EDIT_ACCOUNT); + } + while (my ($n, $v) = each %{ADMIN_ACTIONS()}) { + if ($v eq $action) { + return eval 'Chirpy::UI::' . $n; + return undef; + } + } + } + return undef; +} + +sub report_administration_user_level_insufficient { + my ($self, $page) = @_; + $self->_output_administration_page(); +} + +sub welcome_administrator { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_quote_to_remove { + my $self = shift; + return $self->_id(); +} + +sub confirm_quote_removal { + my $self = shift; + $self->_trigger_feed_update(); + $self->_output_administration_page( + 'quote_removed' => 1 + ); +} + +sub quote_removal_confirmed { + my $self = shift; + return $self->_cgi_param('confirm'); +} + +sub request_quote_removal_confirmation { + my ($self, $quote) = @_; + $self->_output_administration_page( + 'confirm_quote_removal' => $quote + ); +} + +sub report_quote_to_remove_not_found { + my $self = shift; + $self->_output_administration_page( + 'quote_to_remove_not_found' => 1 + ); +} + +sub provide_quote_selection_for_removal_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub provide_quote_selection_for_modification_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_quote_to_edit { + my $self = shift; + return $self->_id(); +} + +sub get_modified_quote_information { + my $self = shift; + return ($self->_cgi_param('quote'), + $self->_cgi_param('notes'), $self->_cgi_param('tags')); +} + +sub confirm_quote_modification { + my $self = shift; + $self->_trigger_feed_update(); + $self->_output_administration_page( + 'quote_modified' => 1 + ); +} + +sub provide_quote_editing_interface { + my ($self, $quote) = @_; + $self->_output_administration_page( + 'edit_quote' => $quote + ); +} + +sub report_quote_to_edit_not_found { + my $self = shift; + $self->_output_administration_page( + 'quote_to_edit_not_found' => 1 + ); +} + +sub provide_quote_approval_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_quote_flag_management_result { + my $self = shift; + my @unflag = (); + my @remove = (); + my @params = $self->_cgi_params(); + foreach my $name (@params) { + if ($name =~ /^action_(\d+)$/) { + my $id = $1; + my $action = $self->_cgi_param($name); + next unless $action; + if ($action == 1) { + push @unflag, $id; + } + elsif ($action == 2) { + push @remove, $id; + } + } + } + if (@unflag || @remove) { + $self->_trigger_feed_update(); + } + return (\@unflag, \@remove); +} + +sub provide_quote_flag_management_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_quote_approval_result { + my $self = shift; + my @approve = (); + my @remove = (); + my %edited = (); + my @params = $self->_cgi_params(); + foreach my $name (@params) { + if ($name =~ /^(action|body|notes|tags)_(\d+)$/) { + my ($type, $id) = ($1, $2); + if ($type eq 'action') { + my $action = $self->_cgi_param($name); + next unless $action; + if ($action == 1) { + push @approve, $id; + } + elsif ($action == 2) { + push @remove, $id; + } + } + else { + $edited{$id}{$type} = $self->_cgi_param($name); + } + } + } + # TODO: Make sure something changed. + $self->_trigger_feed_update(); + return (\@approve, \@remove, \%edited); +} + +sub get_news_item_to_add { + my $self = shift; + return $self->_cgi_param('news'); +} + +sub confirm_news_submission { + my $self = shift; + $self->_output_administration_page( + 'news_item_added' => 1 + ); +} + +sub provide_news_submission_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_news_item_to_edit { + my $self = shift; + return $self->_id(); +} + +sub get_modified_news_item { + my $self = shift; + return ($self->_cgi_param('body'), $self->_cgi_param('poster') || undef); +} + +sub confirm_news_item_modification { + my $self = shift; + $self->_output_administration_page( + 'news_item_modified' => 1 + ); +} + +sub report_news_item_to_edit_not_found { + my $self = shift; + $self->_output_administration_page( + 'news_item_to_edit_not_found' => 1 + ); +} + +sub provide_news_item_editing_interface { + my ($self, $item) = @_; + $self->_output_administration_page( + 'edit_news_item' => $item + ); +} + +sub provide_news_item_selection_for_modification_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_news_item_to_remove { + my $self = shift; + return $self->_id(); +} + +sub confirm_news_item_removal { + my $self = shift; + $self->_output_administration_page( + 'news_item_removed' => 1 + ); +} + +sub report_news_item_to_remove_not_found { + my $self = shift; + $self->_output_administration_page( + 'news_item_to_remove_not_found' => 1 + ); +} + +sub get_account_information_to_add { + my $self = shift; + return $self->_get_supplied_account_information(); +} + +sub report_invalid_new_username { + my $self = shift; + $self->_output_administration_page( + 'invalid_username' => 1 + ); +} + +sub report_new_username_exists { + my $self = shift; + $self->_output_administration_page( + 'username_exists' => 1 + ); +} + +sub report_invalid_new_password { + my $self = shift; + $self->_output_administration_page( + 'invalid_password' => 1 + ); +} + +sub report_different_new_passwords { + my $self = shift; + $self->_output_administration_page( + 'different_passwords' => 1 + ); +} + +sub report_invalid_new_user_level { + my $self = shift; + $self->_output_administration_page( + 'invalid_user_level' => 1 + ); +} + +sub confirm_account_creation { + my $self = shift; + $self->_output_administration_page( + 'account_created' => 1 + ); +} + +sub provide_account_creation_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_account_to_modify { + my $self = shift; + return $self->_id() || undef; +} + +sub get_modified_account_information { + my $self = shift; + return $self->_get_supplied_account_information(); +} + +sub report_invalid_modified_username { + my $self = shift; + $self->_output_administration_page( + 'invalid_username' => 1 + ); +} + +sub report_modified_username_exists { + my $self = shift; + $self->_output_administration_page( + 'username_exists' => 1 + ); +} + +sub report_invalid_modified_password { + my $self = shift; + $self->_output_administration_page( + 'invalid_password' => 1 + ); +} + +sub report_different_modified_passwords { + my $self = shift; + $self->_output_administration_page( + 'different_passwords' => 1 + ); +} + +sub report_invalid_modified_user_level { + my $self = shift; + $self->_output_administration_page( + 'invalid_user_level' => 1 + ); +} + +sub confirm_account_modification { + my $self = shift; + $self->_output_administration_page( + 'account_modified' => 1 + ); +} + +sub report_account_to_modify_not_found { + my $self = shift; + $self->_output_administration_page( + 'account_to_modify_not_found' => 1 + ); +} + +sub report_modified_account_information_required { + my $self = shift; + $self->_output_administration_page( + 'modified_account_information_required' => 1 + ); +} + +sub provide_account_selection_for_modification_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub get_account_to_remove { + my $self = shift; + return $self->_id() || undef; +} + +sub confirm_account_removal { + my $self = shift; + $self->_output_administration_page( + 'account_removed' => 1 + ); +} + +sub report_account_to_remove_not_found { + my $self = shift; + $self->_output_administration_page( + 'account_to_remove_not_found' => 1 + ); +} + +sub provide_account_selection_for_removal_interface { + my $self = shift; + $self->_output_administration_page(); +} + +sub report_last_owner_account_removal_error { + my $self = shift; + $self->_output_administration_page( + 'last_owner_account_removal' => 1 + ); +} + +sub provide_password_change_interface { + my ($self, $error) = @_; + $self->_output_administration_page( + 'password_change_error' => $error + ); +} + +sub confirm_password_change { + my $self = shift; + $self->_output_administration_page( + 'password_changed' => 1 + ); +} + +sub confirm_login { + my $self = shift; + $self->welcome_administrator(); +} + +sub confirm_logout { + my $self = shift; + $self->provide_login_interface(); +} + +sub get_user_information { + my $self = shift; + my $cgi = $self->{'cgi'}; + return { + 'remote_addr' => $cgi->remote_addr(), + 'user_agent' => $cgi->user_agent() + }; +} + +sub _trigger_feed_update { + my $self = shift; + $self->set_parameter('webapp.quote_feed_date', time); +} + +sub _confirmation_form { + my ($self, $url, $post, $title, $text, $quote) = @_; + my $template = $self->_load_template('confirm'); + my $locale = $self->locale(); + my $body; + if (defined $quote) { + $body = &_text_to_xhtml($quote->get_body()); + $body = ($self->configuration()->get('ui', 'webapp.enable_autolink') + ? &_auto_link($body) + : &_spam_protect_email_addresses($body)); + } + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml($title), + 'URL' => &_text_to_xhtml($url), + 'POST_FORM' => $post, + 'CONFIRMATION_REQUEST' => &_text_to_xhtml($text), + 'CONFIRMATION_TEXT' => &_text_to_xhtml( + $locale->get_string('ok')), + 'CANCELATION_TEXT' => &_text_to_xhtml( + $locale->get_string('cancel')), + 'QUOTE_BODY' => (defined $body + ? $body + : undef) + ); + $self->_output_template($template); +} + +sub _get_supplied_account_information { + my $self = shift; + my $level = $self->_cgi_param('new_level'); + return ($self->_cgi_param('new_username') || undef, + $self->_cgi_param('new_password') || undef, + $self->_cgi_param('new_password_repeat') || undef, + (!defined $level || $level < 0) ? undef : $level); +} + +sub _output_administration_page { + my ($self, %params) = @_; + require Chirpy::UI::WebApp::Administration; + my $adm = new Chirpy::UI::WebApp::Administration($self); + $adm->output(%params); +} + +sub _provide_session { + my $self = shift; + my $session = new Chirpy::UI::WebApp::Session($self, 1); + $self->_set_cookie($Chirpy::UI::WebApp::Session::NAME, + $session->id(), $self->param('session_expiry')); + return $session; +} + +sub _provide_session_if_necessary { + my ($self, $page) = @_; + return if (defined $self->_session()); + my $force = &_requires_session($page); + my $st = $self->_url_param('session_test'); + $st = 0 unless (defined $st); + if ($force) { + if ($self->_wants_xml()) { + $self->_output_xml('result', + { 'status' => STATUS_SESSION_REQUIRED }); + } + elsif ($st == 2) { + $self->_report_error($self->locale() + ->get_string('webapp.session_required')); + } + else { + if ($st == 1) { + $self->_provide_session(); + $st = 2; + } + else { + $st = 1; + } + my $cgi = $self->{'cgi'}; + my $uri = $cgi->url(-path_info => 1) . '?session_test=' . $st; + my @params = $self->_url_params(); + if ($self->param('enable_short_urls')) { + @params = grep + !/^(?:(?:admin_)?action|session_test)$/, + @params; + } + else { + @params = grep + { $_ ne 'session_test' } + @params; + } + if (@params) { + require URI::Escape; + foreach my $p (@params) { + $uri .= '&' + . URI::Escape::uri_escape($p) + . '=' + . URI::Escape::uri_escape($self->_url_param($p)); + } + } + print $cgi->header( + -location => $uri, + -cookie => $self->{'cookies'} + ); + } + exit; + } + else { + $self->_provide_session(); + } +} + +sub _get_page_name { + my $page = shift; + if ($page == Chirpy::UI::QUOTE_BROWSER) { + return 'quote_browser'; + } + elsif ($page == Chirpy::UI::RANDOM_QUOTES) { + return 'random_quotes'; + } + elsif ($page == Chirpy::UI::TOP_QUOTES) { + return 'top_quotes'; + } + elsif ($page == Chirpy::UI::BOTTOM_QUOTES) { + return 'bottom_quotes'; + } + elsif ($page == Chirpy::UI::QUOTES_OF_THE_WEEK) { + return 'quotes_of_the_week'; + } + elsif ($page == Chirpy::UI::SINGLE_QUOTE) { + return 'view_quote'; + } + elsif ($page == Chirpy::UI::QUOTE_SEARCH) { + return 'search_results'; + } + elsif ($page == Chirpy::UI::TAG_CLOUD) { + return 'tag_cloud'; + } + elsif ($page == Chirpy::UI::STATISTICS) { + return 'statistics'; + } + elsif ($page == Chirpy::UI::MODERATION_QUEUE) { + return 'unmoderated_quotes'; + } + return undef; +} + +sub _get_cookie { + my ($self, $name) = @_; + return $self->{'cgi'}->cookie(-name => $name); +} + +sub _set_cookie { + my ($self, $name, $value, $expires) = @_; + my $cookie = $self->{'cgi'}->cookie( + -name => $name, + -value => $value, -expires => $expires, + -domain => $self->param('cookie_domain'), + -path => $self->param('cookie_path') + ); + push @{$self->{'cookies'}}, $cookie; +} + +sub _output_xml { + my ($self, $root, $data) = @_; + $self->_print_http_header('text/xml'); + print '', $/; + print &_to_xml($data, $root); +} + +sub _to_xml { + my ($elem, $key) = @_; + my $content; + if (!defined $elem) { + $content = ''; + } + elsif (my $ref = ref $elem) { + if ($ref eq 'ARRAY') { + return join('', map { &_to_xml($_, $key) } @$elem); + } + elsif ($ref eq 'HASH') { + while (my ($key, $value) = each %$elem) { + $content .= &_to_xml($value, $key); + } + } + else { + Chirpy::die('Serialization error'); + } + } + else { + $content = $elem; + } + return '<' . $key . '>' . $content . ''; +} + +sub _load_template { + my ($self, $name) = @_; + my $template = new HTML::Template( + 'filename' => $self->{'templates_path'} . '/' . $name . '.html', + 'die_on_bad_params' => 0, + 'global_vars' => 1, + 'file_cache' => 1, + 'file_cache_dir' => $self->_template_cache_path(), + 'file_cache_dir_mode' => 0777 + ); + Chirpy::die('Failed to load template: ' . $!) unless ($template); + return $template; +} + +sub _output_template { + my ($self, $template) = @_; + my $output = $self->_process_template($template); + my $ctype = 'application/xhtml+xml'; + if ($output =~ m#^]*\bXHTML 1\.1\b[^>]*># + && $self->_accepts($ctype)) { + $output = '' . $/ + . $output; + } + else { + $ctype = 'text/html'; + } + $self->_maybe_gzip($output, $ctype); +} + +sub _maybe_gzip { + my ($self, $content, $ctype, %headers) = @_; + my $accenc = $self->{'cgi'}->http('Accept-Encoding'); + if (!defined $self->parent()->debug_events() + && $self->param('enable_gzip') + && defined $accenc && $accenc =~ /\bgzip\b/i) { + require Compress::Zlib; + $self->_print_http_header($ctype, + 'Content-Encoding' => 'gzip', %headers); + print Compress::Zlib::memGzip($content); + } + else { + $self->_print_http_header($ctype, %headers); + binmode(STDOUT, ':utf8'); + print $content; + } +} + +sub _print_http_header { + my ($self, $ctype, %headers) = @_; + print $self->{'cgi'}->header(-type => $ctype . '; charset=UTF-8', + -cookie => $self->{'cookies'}, + %headers); +} + +# TODO: make this faster +sub _process_template { + my ($self, $template) = @_; + my $locale = $self->locale(); + my $url = &_text_to_xhtml(Chirpy::URL); + my $link = '' . Chirpy::FULL_PRODUCT_NAME . ''; + $template->param('CHIRPY_PRODUCT_NAME' => Chirpy::PRODUCT_NAME); + $template->param('CHIRPY_VERSION' => Chirpy::VERSION_STRING); + $template->param('CHIRPY_FULL_PRODUCT_NAME' => Chirpy::FULL_PRODUCT_NAME); + $template->param('CHIRPY_URL' => $url); + $template->param('CHIRPY_LINK' => $link); + $template->param('SITE_TITLE' + => &_text_to_xhtml($self->configuration() + ->get('general', 'title'))); + $template->param('SITE_URL' => $self->_url()); + $template->param('WEBMASTER_EMAIL' + => sub { return &_hide_email($self->param('webmaster_email')) }); + my $page = $self->get_current_page(); + if ($self->param('enable_feeds')) { + my $page_feed = $self->_page_feed($page); + if (defined $page_feed) { + my $ft = &_text_to_xhtml( + $locale->get_string(&_get_page_name($page_feed))); + # TODO: Don't assume QotW is the default, perhaps make _page_feed + # supply action + my $action = ($page_feed == Chirpy::UI::QUOTES_OF_THE_WEEK + ? ACTIONS->{'QUOTES_OF_THE_WEEK'} + : $self->_action()); + $template->param('FEEDS' => [ + { + 'FEED_URL' => $self->_feed_url($action, 'rss'), + 'FEED_TITLE' => $ft . ' (RSS 2.0)', + 'FEED_MIME_TYPE' => 'application/rss+xml' + }, + { + 'FEED_URL' => $self->_feed_url($action, 'atom'), + 'FEED_TITLE' => $ft . ' (Atom 1.0)', + 'FEED_MIME_TYPE' => 'application/atom+xml' + } + ]); + } + } + if ($self->_page_offers_microsummary($page)) { + $template->param('MICROSUMMARIES' => [ + { + 'MICROSUMMARY_URL' => $self->_microsummary_url($self->_action()) + } + ]); + } + $template->param('APPROVED_QUOTE_COUNT' => sub { + return $self->parent()->approved_quote_count(); + }); + $template->param('UNAPPROVED_QUOTE_COUNT' => sub { + return $self->parent()->unapproved_quote_count(); + }); + $template->param('TOTAL_QUOTE_COUNT' => sub { + return $self->parent()->total_quote_count(); + }); + $template->param('COOKIE_DOMAIN' => sub { return &_text_to_xhtml( + $self->param('cookie_domain')) }); + $template->param('COOKIE_PATH' => sub { return &_text_to_xhtml( + $self->param('cookie_path')) }); + $template->param('RESOURCES_URL' => sub { return &_text_to_xhtml( + $self->_resources_url()) }); + foreach my $action (keys %{ACTIONS()}) { + $template->param($action . '_URL' + => $self->_url(ACTIONS->{$action})); + foreach my $string (qw(DESCRIPTION SHORT_TITLE)) { + my $name = $action . '_' . $string; + $template->param($name => &_text_to_xhtml( + $locale->get_string( + ($action eq 'START_PAGE' ? 'webapp.' : '') + . lc $name))); + } + } + $template->param('NEXT_PAGE_TITLE', sub { return &_text_to_xhtml( + $locale->get_string('webapp.next_page_title')) }); + $template->param('PREVIOUS_PAGE_TITLE', sub { return &_text_to_xhtml( + $locale->get_string('webapp.previous_page_title')) }); + $template->param('MODERATION_QUEUE_PUBLIC' => 1) + if ($self->moderation_queue_is_public()); + if (my $account = $self->get_logged_in_user_account()) { + $template->param('LOGGED_IN' => 1); + $template->param('LOGGED_IN_NOTICE' => &_text_to_xhtml( + $locale->get_string( + 'logged_in_as', $account->get_username(), + $self->parent()->user_level_name($account->get_level())))); + } + $template->param( + 'SEARCH_FORM_START' => ($self->param('enable_short_urls') + ? '
' + : '' + . ''), + 'SEARCH_FORM_END' => '
', + 'SEARCH_QUERY_LABEL' => &_text_to_xhtml( + $locale->get_string('search_query_title')), + 'SUBMIT_SEARCH_LABEL' => &_text_to_xhtml( + $locale->get_string('search_button_label')) + ); + $template->param('FOOTER_TEXT' => ($self->parent()->timing_enabled() + ? sub { + (my $str = &_text_to_xhtml( + $locale->get_string('webapp.footer_text', + sprintf('%.0f', 1000 * $self->parent()->total_time()), + "\0"))) =~ s/\0/$link/g; + return $str; + } + : sub { + (my $str = &_text_to_xhtml( + $locale->get_string('webapp.footer_text_no_time', "\0"))) + =~ s/\0/$link/g; + return $str; + }) + ); + return $template->output(); +} + +sub _template_cache_path { + my $self = shift; + my $path = $self->configuration()->get('general', 'base_path') + . '/cache/template'; + Chirpy::Util::ensure_writable_directory($path); + return $path; +} + +sub _text_to_xhtml { + my ($str, $leave_whitespaces) = @_; + return undef unless (defined $str); + return '' if ($str eq ''); + $str = Chirpy::Util::encode_xml_entities($str); + $str = &_whitespaces_to_xhtml($str) unless ($leave_whitespaces); + return $str; +} + +sub _whitespaces_to_xhtml { + my $str = shift; + $str =~ s|\r?\n([ \t]*)|"
\n" . (' ' x length($1))|eg; + $str =~ s/([ \t]{2,})/' ' x length($1)/eg; + $str =~ s/^([ \t]+)/' ' x length($1)/eg; + $str =~ s/([ \t]+)$/' ' x length($1)/eg; + return $str; +} + +sub _quick_style_to_xhtml { + my ($string, $quote_url_template) = @_; + $string = &_text_to_xhtml($string, 1); + $string =~ s{ + < + \s* + ( + (?:mailto:|(?:https?|ftp|irc)://) + .*? + ) + (?: + \s+ + (.*?) + )? + > + }{ + my ($url, $description) = ($1, $2); + unless (defined $description && $description ne '') { + $description = $url; + } + '' . $description . ''; + }esgx; + $string = &_whitespaces_to_xhtml($string); + 1 while $string =~ s{([*_])(.*?)\1}{ + my $tag = ($1 eq '*' ? 'strong' : 'em'); + '<' . $tag . '>' . $2 . ''; + }esg; + if (defined $quote_url_template) { + $quote_url_template = &_text_to_xhtml($quote_url_template); + $string =~ s{ (#(\d+))}{ + my ($text, $id) = ($1, $2); + (my $url = $quote_url_template) =~ s/\0/$id/g; + ' ' . $text . ''; + }eig; + } + return $string; +} + +sub _quote_already_rated_text { + my $self = shift; + my $conf = $self->configuration(); + return $self->locale()->get_string('quote_already_rated'); +} + +sub _quote_rating_limit_text { + my $self = shift; + my $conf = $self->configuration(); + return $self->locale()->get_string( + 'quote_rating_limit_exceeded', + $conf->get('general', 'rating_limit_count'), + $conf->get('general', 'rating_limit_time')); +} + +# XXX: Move to Chirpy::Util? +sub _format_date_time_rfc822 { + my $timestamp = shift; + my @parts = split /\s+/, gmtime($timestamp); + return $parts[0] . ', ' . $parts[2] . ' ' . $parts[1] . ' ' . $parts[4] + . ' ' . $parts[3] . ' +0000'; +} + +# XXX: Move to Chirpy::Util? +sub _format_date_time_iso8601 { + my $timestamp = shift; + my @time = gmtime($timestamp); + return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ', + 1900 + $time[5], $time[4] + 1, $time[3], + $time[2], $time[1], $time[0]); +} + +sub _format_quote { + my ($self, $quote) = @_; + my $body = &_text_to_xhtml($quote->get_body()); + my $notes = &_text_to_xhtml($quote->get_notes()); + my $tags = $self->_link_tags($quote); + if ($self->configuration()->get('ui', 'webapp.enable_autolink')) { + $body = &_auto_link($body); + $notes = &_auto_link($notes); + } + else { + $body = &_spam_protect_email_addresses($body); + $notes = &_spam_protect_email_addresses($notes); + } + return ($body, $notes, $tags); +} + +sub _auto_link { + my ($html, $no_antispam) = @_; + return undef unless (defined $html); + $no_antispam = 0 unless (defined $no_antispam); + # & is the only entity we allow in URLs, so we temporarily replace all + # of them with null bytes + $html =~ s/&/\0/ig; + $html =~ s{ + \b + ((?:https?|ftp|irc)://[^\s&<>()\[\]\{\}]+) + |([a-z0-9._\-\+]+\@[a-z0-9][a-z0-9\-.]+\.[a-z0-9]+) + }{ + my ($href, $text); + if (defined $2) { + $text = ($no_antispam ? $2 : &_hide_email($2)); + $href = 'mailto:' . $text; + } + else { + $text = $href = $1; + } + '' . $text . ''; + }eigx; + $html =~ s/\0/&/g; + return $html; +} + +sub _spam_protect_email_addresses { + my $html = shift; + $html =~ s/((?:mailto:)?([\w\.\+]+\@\S+\.\w+))/&_hide_email($1)/eig; + return $html; +} + +sub _hide_email { + my $email = shift; + $email =~ s{(.)}{ + my $n = ord $1; + '&#' . (int rand 2 ? sprintf('x%X', $n) : $n) . ';'; + }eg; + return $email; +} + +sub _format_news_body { + my ($self, $body) = @_; + # TODO: Make all this a little more efficient + $body = &_quick_style_to_xhtml( + $body, + $self->_quote_url("\0") + ); + my @paragraphs = split(/(?:<\s*br\s*\/\s*>\s*){2,}/, $body); + return join("\n", map { '

' . &_fix_xml($_) . '

' } @paragraphs); +} + +sub _fix_xml { + my $xml = shift; + my @stack = (); + my $out = ''; + while ($xml =~ s|^([^<]*)<\s*(/?)(\w*)(.*?)(/?)>||sg) { + my ($text, $closing, $element, $attributes, $selfclosing) + = ($1, $2, $3, $4, $5); + if (length($selfclosing)) { + $out .= $&; + } + elsif (!length($closing)) { + push @stack, $element; + $out .= $&; + } + elsif ($stack[-1] eq $element) { + pop @stack; + $out .= $text . ''; + } + else { + $out .= $text; + } + } + $out .= $xml; + while (my $element = pop @stack) { + $out .= ''; + } + return $out; +} + +sub _link_tags { + my ($self, $quote) = @_; + my $tags = $quote->get_tags(); + return [] unless (defined $tags && @$tags); + my @out = (); + foreach my $tag (sort @$tags) { + my $title = $self->locale()->get_string('tag_link_description', $tag); + push @out, { + 'TAG' => &_text_to_xhtml($tag), + 'URL' => &_text_to_xhtml($self->_tag_url($tag)), + 'LINK_DESCRIPTION' => &_text_to_xhtml($title) + }; + } + return \@out; +} + +sub _cgi_params { + my $self = shift; + return $self->{'cgi'}->param(); +} + +sub _cgi_param { + my ($self, $param) = @_; + return $self->{'cgi'}->param($param); +} + +sub _url_params { + my $self = shift; + return $self->{'cgi'}->url_param(); +} + +sub _url_param { + my ($self, $param) = @_; + return $self->{'cgi'}->url_param($param); +} + +sub _is_post { + my $self = shift; + return $self->_http_request_method() eq 'POST'; +} + +sub _http_request_method { + my $self = shift; + return uc $self->{'cgi'}->request_method(); +} + +sub _accepts { + my $self = shift; + my $type = lc shift; + foreach my $a ($self->{'cgi'}->Accept()) { + return 1 if (lc($a) eq $type); + } + return 0; +} + +sub _session { + my $self = shift; + return $self->{'session'}; +} + +sub _captcha_provider { + my ($self, $hash) = @_; + unless ($self->{'captcha'}) { + my $p = $self->param('captcha_provider'); + return undef unless (defined $p); + my $class = 'Chirpy::UI::WebApp::Captcha::' . $p; + my $provider; + eval qq{ + use $class; + \$provider = new $class(\$self); + }; + Chirpy::die('Failed to load captcha provider "' . $p . '": ' . $@) + if ($@ || !defined $provider); + $self->{'captcha'} = $provider; + } + $self->{'captcha'}->hash($hash); + return $self->{'captcha'}; +} + +sub _resources_url { + my $self = shift; + return $self->param('resources_url') . '/themes/' . $self->param('theme'); +} + +sub _feed_url { + my ($self, $action, $type) = @_; + return ($self->param('enable_short_urls') + # TODO: Update _url() for feeds + ? $self->_url($type . '/' . $action) + : $self->_url($action, undef, 'output' => $type)); +} + +sub _microsummary_url { + my ($self, $action) = @_; + return ($self->param('enable_short_urls') + # TODO: Update _url() for microsummaries + ? $self->_url('ms/' . $action) + : $self->_url($action, undef, 'output' => 'ms')); +} + +sub _quote_url { + my ($self, $id) = @_; + return ($self->param('enable_short_urls') + ? $self->_url() . $id + : $self->_url(undef, undef, 'id' => $id)); +} + +sub _tag_url { + my ($self, $tag) = @_; + return $self->_url(ACTIONS->{'QUOTE_SEARCH'}, undef, + 'query' => 'tag:' . $tag); +} + +sub _url { + my ($self, $action, $admin, %params) = @_; + my $string; + if (%params) { + require URI::Escape; + $string = join '&', map { + URI::Escape::uri_escape($_) + . '=' . URI::Escape::uri_escape($params{$_}) + } keys %params; + } + my $url = &_text_to_xhtml($self->param('site_url') + . ($self->param('enable_short_urls') + ? ($admin ? '/' . ACTIONS->{'ADMINISTRATION'} : '') + . ($action ? '/' . $action : (!$admin ? '/' : '')) + . (defined $string ? '?' . $string : '') + : '/index.cgi?' . ($admin + ? 'action=' . ACTIONS->{'ADMINISTRATION'} + . ($action + ? '&admin_action=' . $action + . (defined $string ? '&' : '') + : '') + : ($action + ? 'action=' . $action . (defined $string ? '&' : '') + : '') + ) . (defined $string ? $string : '') + ) + ); +} + +sub _action { + my $self = shift; + return $self->_url_param('action') || $self->_cgi_param('action'); +} + +sub _admin_action { + my $self = shift; + return $self->_url_param('admin_action') + || $self->_cgi_param('admin_action'); +} + +sub _output_type { + my $self = shift; + return $self->_url_param('output') || $self->_cgi_param('output'); +} + +sub _id { + my $self = shift; + my $id = $self->_url_param('id'); + $id = $self->_cgi_param('id') unless (defined $id); + return undef unless (defined $id); + return eval $id if ($id =~ /^0(?:x[0-9A-Fa-f]+|b[01]+)+$/); + return $id; +} + +sub _wants_microsummary { + my $self = shift; + my $ot = $self->_output_type(); + return (defined $ot && $ot eq 'ms'); +} + +sub _wants_xml { + my $self = shift; + my $ot = $self->_output_type(); + return (defined $ot && $ot eq 'xml'); +} + +sub _feed_type { + my $self = shift; + my $type = $self->_output_type(); + return ($self->param('enable_feeds') && &_valid_feed_type($type) + ? $type : undef); +} + +sub _valid_feed_type { + my $type = shift; + return (defined $type && ($type eq 'atom' || $type eq 'rss')); +} + +sub _page_feed { + my ($self, $page) = @_; + if ($page == Chirpy::UI::START_PAGE + || $page == Chirpy::UI::QUOTE_BROWSER + || $page == Chirpy::UI::QUOTES_OF_THE_WEEK) { + return Chirpy::UI::QUOTES_OF_THE_WEEK; + } + # TODO: Provide feeds for searches + if ($page == Chirpy::UI::RANDOM_QUOTES + || $page == Chirpy::UI::TOP_QUOTES + || $page == Chirpy::UI::BOTTOM_QUOTES + || $page == Chirpy::UI::MODERATION_QUEUE) { + return $page; + } + return undef; +} + +sub _page_offers_microsummary { + my ($self, $page) = @_; + return ($page == Chirpy::UI::QUOTE_BROWSER + || $page == Chirpy::UI::TOP_QUOTES + || $page == Chirpy::UI::BOTTOM_QUOTES + || $page == Chirpy::UI::MODERATION_QUEUE + || $page == Chirpy::UI::QUOTES_OF_THE_WEEK); +} + +sub _requires_session { + my $page = shift; + return ($page == Chirpy::UI::QUOTE_RATING_UP + || $page == Chirpy::UI::QUOTE_RATING_DOWN + || $page == Chirpy::UI::REPORT_QUOTE + || $page == Chirpy::UI::LOGIN + || $page == Chirpy::UI::LOGOUT + || $page == Chirpy::UI::ADMINISTRATION); +} + +sub _requires_captcha { + my $self = shift; + return (defined $self->param('captcha_provider') + && !defined $self->get_logged_in_user_account()); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp/Administration.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp/Administration.pm new file mode 100644 index 0000000..752c886 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp/Administration.pm @@ -0,0 +1,962 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Administration.pm 305 2007-02-09 01:06:56Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp::Administration - Administration-section related routines +of L + +=head1 TODO + +Make this template-based and avoid inline calls to parents. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp::Administration; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; +use Chirpy::UI::WebApp 0.3; +use Chirpy::Event 0.3; + +sub new { + my ($class, $parent) = @_; + return bless { 'parent' => $parent }, $class; +} + +sub parent { + my $self = shift; + return $self->{'parent'}; +} + +sub output { + my ($self, %params) = @_; + my $event_log_allowed = $self->parent()->administration_allowed( + Chirpy::UI::VIEW_EVENT_LOG); + if ($event_log_allowed && $self->parent()->_wants_xml()) { + $self->_serve_event_log_table_data(); + return; + } + my $template = $self->parent()->_load_template('administration'); + my $locale = $self->parent()->locale(); + my ($upd_url, $upd_text); + if (my $update = $self->parent()->{'available_update'}) { + $template->param('UPDATE_AVAILABLE' + => &_text_to_xhtml($locale->get_string('update_available'))); + $template->param('UPDATE_AVAILABLE_TEXT' => &_text_to_xhtml( + $locale->get_string('update_available_text', + $update->[0], $update->[1]))); + $template->param('UPDATE_LINK_TEXT' + => &_text_to_xhtml($locale->get_string('update_link_text'))); + $template->param('UPDATE_URL' => &_text_to_xhtml($update->[2])); + } + elsif (my $errmsg = $self->parent()->{'update_check_error'}) { + $template->param('UPDATE_CHECK_FAILED' + => &_text_to_xhtml($locale->get_string('update_check_failed'))); + $template->param('UPDATE_CHECK_FAILED_TEXT' => &_text_to_xhtml( + $locale->get_string('update_check_failed_text'))); + $template->param('UPDATE_CHECK_ERROR_MESSAGE' + => &_text_to_xhtml($errmsg)); + } + $template->param( + 'PAGE_TITLE' => &_text_to_xhtml( + $locale->get_string('administration')), + 'APPROVE_QUOTES' => &_text_to_xhtml( + $locale->get_string('approve_quotes')), + 'APPROVE_QUOTES_HTML' + => sub { return $self->get_approve_quotes_html(%params) }, + 'APPROVE_QUOTES_ALLOWED' + => $self->parent()->administration_allowed(Chirpy::UI::MANAGE_UNAPPROVED_QUOTES), + 'APPROVE_QUOTES_NOT_ALLOWED_HTML' + => sub { return $self->get_access_disallowed_html() }, + 'FLAGGED_QUOTES' => &_text_to_xhtml( + $locale->get_string('flagged_quotes')), + 'FLAGGED_QUOTES_HTML' + => sub { return $self->get_flagged_quotes_html(%params) }, + 'FLAGGED_QUOTES_ALLOWED' + => $self->parent()->administration_allowed(Chirpy::UI::MANAGE_FLAGGED_QUOTES), + 'FLAGGED_QUOTES_NOT_ALLOWED_HTML' + => sub { return $self->get_access_disallowed_html() }, + 'MANAGE_QUOTES' => &_text_to_xhtml( + $locale->get_string('manage_quotes')), + 'MANAGE_QUOTES_HTML' + => sub { return $self->get_manage_quotes_html(%params) }, + 'MANAGE_QUOTES_ALLOWED' + => $self->parent()->administration_allowed(Chirpy::UI::EDIT_QUOTE) + && $self->parent()->administration_allowed(Chirpy::UI::REMOVE_QUOTE), + 'MANAGE_QUOTES_NOT_ALLOWED_HTML' + => sub { return $self->get_access_disallowed_html() }, + 'MANAGE_NEWS' => &_text_to_xhtml( + $locale->get_string('manage_news')), + 'MANAGE_NEWS_HTML' + => sub { return $self->get_manage_news_html(%params) }, + 'MANAGE_NEWS_ALLOWED' + => $self->parent()->administration_allowed(Chirpy::UI::ADD_NEWS) + && $self->parent()->administration_allowed(Chirpy::UI::EDIT_NEWS) + && $self->parent()->administration_allowed(Chirpy::UI::REMOVE_NEWS), + 'MANAGE_NEWS_NOT_ALLOWED_HTML' + => sub { return $self->get_access_disallowed_html() }, + 'MANAGE_ACCOUNTS' => &_text_to_xhtml( + $locale->get_string('manage_accounts')), + 'MANAGE_ACCOUNTS_HTML' + => sub { return $self->get_manage_accounts_html(%params) }, + 'MANAGE_ACCOUNTS_ALLOWED' + => $self->parent()->administration_allowed(Chirpy::UI::ADD_ACCOUNT) + && $self->parent()->administration_allowed(Chirpy::UI::EDIT_ACCOUNT) + && $self->parent()->administration_allowed(Chirpy::UI::REMOVE_ACCOUNT), + 'MANAGE_ACCOUNTS_NOT_ALLOWED_HTML' + => sub { return $self->get_access_disallowed_html() }, + 'VIEW_EVENT_LOG' => &_text_to_xhtml( + $locale->get_string('view_event_log')), + 'VIEW_EVENT_LOG_HTML' + => sub { return $self->get_event_log_html(%params) }, + 'VIEW_EVENT_LOG_ALLOWED' + => $event_log_allowed, + 'VIEW_EVENT_LOG_NOT_ALLOWED_HTML' + => sub { return $self->get_access_disallowed_html() }, + 'CHANGE_PASSWORD' => &_text_to_xhtml( + $locale->get_string('change_password')), + 'CHANGE_PASSWORD_HTML' + => sub { return $self->get_change_password_html(%params) }, + 'ACTION_IS_' . $self->parent()->_admin_action() => 1 + ); + $self->parent()->_output_template($template); +} + +sub get_approve_quotes_html { + my ($self, %params) = @_; + my $locale = $self->parent()->locale(); + my $quotes = $self->parent()->parent()->get_unapproved_quotes(); + if (defined $quotes) { + my $html = '' . $/ + . '
' . $/ + . '
    ' . $/; + foreach my $quote (@$quotes) { + my $id = $quote->get_id(); + my $notes = $quote->get_notes(); + my $tags = $quote->get_tags(); + $html .= '
  • ' . $/ + . '
    ' . $/ + . '

    ' + . '#' . $quote->get_id() . ' ' + . ($self->parent()->moderation_queue_is_public() + ? '' + . Chirpy::Util::format_quote_rating($quote->get_rating()) + . '' + . '/' + . $quote->get_vote_count() + . ' ' + : '') + . '' + . $self->parent()->format_date_time($quote->get_date_submitted()) + . '' . $/ + . '[' + . &_text_to_xhtml($locale->get_string('edit')) + . ']' + . '

    ' . $/ + . '
    ' . $/ + . '
    ' . $/ + . '

    ' . &_text_to_xhtml($quote->get_body()) + . '

    ' . $/ + . '
    ' . $/ + . (defined $notes || @$tags ? + '' : '') + . '
    ' . $/ + . '
    ' . $/ + . '
    ' . $/ + . ' ' . $/ + . ' ' . $/ + . ' ' . $/ + . '
    ' . $/ + . '
  • ' . $/; + } + $html .= '
' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
'; + return $html; + } + else { + return '

' . &_text_to_xhtml( + $locale->get_string('no_unapproved_quotes')) . '

'; + } +} + +sub get_flagged_quotes_html { + my ($self, %params) = @_; + my $locale = $self->parent()->locale(); + my $quotes = $self->parent()->parent()->get_flagged_quotes(); + if (defined $quotes) { + my $html = '
' . $/ + . '
    ' . $/; + foreach my $quote (@$quotes) { + my $id = $quote->get_id(); + my $notes = $quote->get_notes(); + my $tags = $quote->get_tags(); + $html .= '
  • ' . $/ + . '
    ' . $/ + . '

    ' + . '#' . $quote->get_id() . ' ' + . '' + . Chirpy::Util::format_quote_rating($quote->get_rating()) + . '' + . '/' + . $quote->get_vote_count() + . ' ' + . '' + . $self->parent()->format_date_time($quote->get_date_submitted()) + . '' + . '

    ' . $/ + . '
    ' . $/ + . '

    ' . &_text_to_xhtml($quote->get_body()) + . '

    ' . $/ + . '
    ' . $/ + . (defined $notes || @$tags ? + '' : '') + . '
    ' . $/ + . '
    ' . $/ + . ' ' . $/ + . ' ' . $/ + . ' ' . $/ + . '
    ' . $/ + . '
  • ' . $/; + } + $html .= '
' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
'; + return $html; + } + else { + return '

' . &_text_to_xhtml( + $locale->get_string('no_flagged_quotes')) . '

'; + } +} + +sub get_manage_quotes_html { + my ($self, %params) = @_; + my $locale = $self->parent()->locale(); + if (my $quote = $params{'edit_quote'}) { + my $notes = $quote->get_notes(); + my $tags = $quote->get_tags(); + return '
' . $/ + . '
' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
'; + } + if (my $quote = $params{'confirm_quote_removal'}) { + my ($body, $notes, $tags) = $self->parent()->_format_quote($quote); + return '
' . $/ + . '
' . $/ + . '

' + . &_text_to_xhtml($locale->get_string('quote_removal_confirmation')) + . '

' . $/ + . '

' + . $body . '

' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
'; + } + my $result; + if ($params{'quote_removed'}) { + $result = $locale->get_string('quote_removed'); + } + elsif ($params{'quote_to_edit_not_found'}) { + $result = $locale->get_string('quote_to_edit_not_found'); + } + elsif ($params{'quote_to_remove_not_found'}) { + $result = $locale->get_string('quote_to_remove_not_found'); + } + elsif ($params{'quote_modified'}) { + $result = $locale->get_string('quote_modified'); + } + return (defined $result + ? '

' + . &_text_to_xhtml($result) . '

' + : '') + . '

' + . &_text_to_xhtml( + $locale->get_string('webapp.manage_quote_instructions')) + . '

' . $/ + . '
' . $/ + . '
' . $/ + . '
' + . &_text_to_xhtml( + $locale->get_string('quote_id_title')) . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . ' ' . $/ + . '
' . $/ + . '
' + . '
' . $/ + . '
'; +} + +sub get_manage_news_html { + my ($self, %params) = @_; + my $locale = $self->parent()->locale(); + if (my $item = $params{'edit_news_item'}) { + my $html = '
' . $/ + . '
' . $/ + . '
' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
'; + } + my $result; + if ($params{'news_item_added'}) { + $result = $locale->get_string('news_item_added'); + } + elsif ($params{'news_item_modified'}) { + $result = $locale->get_string('news_item_modified'); + } + elsif ($params{'news_item_to_edit_not_found'}) { + $result = $locale->get_string('news_item_to_edit_not_found'); + } + elsif ($params{'news_item_removed'}) { + $result = $locale->get_string('news_item_removed'); + } + elsif ($params{'news_item_to_remove_not_found'}) { + $result = $locale->get_string('news_item_to_remove_not_found'); + } + return (defined $result + ? '

' + . &_text_to_xhtml($result) . '

' + : '') + . '
' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '
' . $/ + . '

' + . &_text_to_xhtml( + $locale->get_string('webapp.manage_news_instructions')) + . '

'; +} + +sub get_manage_accounts_html { + my ($self, %params) = @_; + my $locale = $self->parent()->locale(); + my $status_message; + if ($params{'account_to_modify_not_found'}) { + $status_message = $locale->get_string('account_to_modify_not_found'); + } + elsif ($params{'account_to_remove_not_found'}) { + $status_message = $locale->get_string('account_to_remove_not_found'); + } + elsif ($params{'last_owner_account_removal'}) { + $status_message = $locale->get_string( + 'last_owner_account_removal_error'); + } + elsif ($params{'modified_account_information_required'}) { + $status_message = $locale->get_string( + 'modified_account_information_required'); + } + elsif ($params{'invalid_username'}) { + $status_message = $locale->get_string('invalid_username'); + } + elsif ($params{'username_exists'}) { + $status_message = $locale->get_string('username_exists'); + } + elsif ($params{'invalid_password'}) { + $status_message = $locale->get_string('invalid_password'); + } + elsif ($params{'different_passwords'}) { + $status_message = $locale->get_string('different_passwords'); + } + elsif ($params{'invalid_user_level'}) { + $status_message = $locale->get_string('invalid_user_level'); + } + elsif ($params{'account_removed'}) { + $status_message = $locale->get_string('account_removed'); + } + elsif ($params{'account_modified'}) { + $status_message = $locale->get_string('account_modified'); + } + elsif ($params{'account_created'}) { + $status_message = $locale->get_string('account_created'); + } + my $html = '
' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . (defined $status_message + ? '
' . $/ + . &_text_to_xhtml($status_message) + . $/ . '
' . $/ + : '') + . '
' . $/ + . '
' . $/ + . '
' . $/; + return $html; +} + +sub get_event_log_html { + my $self = shift; + my $locale = $self->parent()->locale(); + my $resurl = $self->parent()->_resources_url(); + my $url = $self->parent()->_url( + Chirpy::UI::WebApp::ADMIN_ACTIONS->{'VIEW_EVENT_LOG'}, + 1); + $url .= ($url =~ /\?/ ? '&' : '?'); + my $html = '' . $/ + . '' . $/ + . '' . $/ + . '
'; + return $html; +} + +sub _serve_event_log_table_data { + my $self = shift; + my $locale = $self->parent()->locale(); + my $count = $self->parent()->_cgi_param('count'); + $count = (defined $count ? int $count : undef); + my $start = $self->parent()->_cgi_param('start'); + $start = 0 unless (defined $start && $start >= 0); + my $desc = ($self->parent()->_cgi_param('asc') ? 0 : 1); + my $user = $self->parent()->_cgi_param('user'); + $user = undef if (defined $user && $user !~ /^\d+$/); + my $event = $self->parent()->_cgi_param('code'); + $event = undef if (defined $event && $event !~ /^\d+$/); + my $filter = $self->parent()->_cgi_param('data'); + if (defined $filter && $filter =~ /^([^=]+)=(.*)$/s) { + $filter = { $1 => $2 }; + } + else { + $filter = undef; + } + my ($events, $leading, $trailing) = $self->parent()->parent()->get_events( + $start, $count, $desc, $event, $user, $filter); + my @events = (); + foreach my $event (@$events) { + my $id = $event->get_id(); + my $date = $self->parent()->format_date_time($event->get_date()); + my $user = $event->get_user(); + my $username; + if (defined $user) { + my $acct = $self->parent()->parent()->get_account_by_id($user); + if (defined $acct) { + $username = $acct->get_username(); + } + } + my $description = &_text_to_xhtml($locale->get_string( + 'event_' . $event->get_code() . '_name')); + my $data = $event->get_data(); + my $result = { + 'id' => $id, + 'date' => $date, + (defined $username ? ('username' => $username) : ()), + 'userid' => (defined $user ? $user : 0), + 'description' => $description, + 'code' => $event->get_code() + }; + my @data = (); + foreach my $key (sort keys %$data) { + my $value = $data->{$key}; + push @data, { + 'name' => &_text_to_xhtml($key), + 'value' => &_text_to_xhtml($value) + }; + } + $result->{'data'} = \@data; + push @events, $result; + } + $self->parent()->_output_xml('result', { + 'event' => \@events, + ($leading ? ('leading' => 'true') : ()), + ($trailing ? ('trailing' => 'true') : ()) + }); +} + +sub get_access_disallowed_html { + my $self = shift; + return '

' + . &_text_to_xhtml($self->parent()->locale()->get_string( + 'insufficient_administrative_privileges')) + . '

'; +} + +sub get_change_password_html { + my ($self, %params) = @_; + my $locale = $self->parent()->locale(); + if ($params{'password_changed'}) { + return '
' . $/ + . '

' . $locale->get_string('password_changed_text') . '

' . $/ + . '
'; + } + my $html = ''; + if (my $error = $params{'password_change_error'}) { + $html = '
' . $/ . '

' + . &_text_to_xhtml($locale->get_string( + $error == Chirpy::UI::NEW_PASSWORD_INVALID + ? 'change_password_new_password_invalid_text' + : ($error == Chirpy::UI::PASSWORDS_DIFFER + ? 'change_password_passwords_differ_text' + : ($error == Chirpy::UI::CURRENT_PASSWORD_INVALID + ? 'change_password_current_password_invalid_text' + : undef)))) + . '

' . $/ . '
' . $/; + } + return $html . '
' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '' . $/ + . '
' . $/ + . '
' . $/ + . '
'; +} + +*_text_to_xhtml = \&Chirpy::UI::WebApp::_text_to_xhtml; + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha.pm new file mode 100644 index 0000000..3afa2a0 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha.pm @@ -0,0 +1,117 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Captcha.pm 292 2007-02-05 21:28:55Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp::Captcha - Captcha provider interface + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp::Captcha; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; +use Chirpy::Util 0.3; + +sub new { + my ($class, $parent, $hash) = @_; + my $self = { + 'parent' => $parent, + 'hash' => $hash + }; + return bless($self, $class); +} + +sub parent { + my $self = shift; + return $self->{'parent'}; +} + +sub hash { + my ($self, $hash) = @_; + $self->{'hash'} = $hash if (defined $hash); + return $self->{'hash'}; +} + +sub data_path { + my $self = shift; + my $path = $self->parent()->configuration()->get('general', 'base_path') + . '/cache/captcha'; + Chirpy::Util::ensure_writable_directory($path); + return $path; +} + +sub base_path { + my $self = shift; + my $path = $self->param('captcha_path'); + return $path if (defined $path); + return $self->parent()->configuration()->get('general', 'base_path') + . '/../res/captcha'; +} + +sub base_url { + my $self = shift; + my $url = $self->param('captcha_url'); + return $url if (defined $url); + return $self->param('site_url') . '/res/captcha'; +} + +sub param { + my ($self, $name) = @_; + return $self->parent()->param($name); +} + +*create = \&Chirpy::Util::abstract_method; + +*verify = \&Chirpy::Util::abstract_method; + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha/Authen_Captcha.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha/Authen_Captcha.pm new file mode 100644 index 0000000..38de020 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha/Authen_Captcha.pm @@ -0,0 +1,132 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Authen_Captcha.pm 293 2007-02-05 22:33:34Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp::Captcha::Authen_Captcha - Captcha provider interface using +L + +=head1 CONFIGURATION + +This module uses the following parameters from your configuration file: + +=over 4 + +=item webapp.authen_captcha_source_image_path + +The physical path to the source images to be used by L. + +=item webapp.authen_captcha_character_width + +The pixel width of each character in a captcha image. + +=item webapp.authen_captcha_character_height + +The pixel height of each character in a captcha image. + +=item webapp.authen_captcha_code_length + +The number of characters in the captcha code. + +=back + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp::Captcha::Authen_Captcha; + +use strict; +use warnings; + +use vars qw($VERSION @ISA); + +$VERSION = '0.3'; +@ISA = qw(Chirpy::UI::WebApp::Captcha); + +use Chirpy 0.3; +use Chirpy::UI::WebApp::Captcha 0.3; +use Authen::Captcha; + +sub new { + my $class = shift; + my $self = $class->SUPER::new(@_); + $self->{'ac'} = new Authen::Captcha( + 'data_folder' => $self->data_path(), + 'output_folder' => $self->base_path() + ); + return $self; +} + +sub create { + my ($self, $expire) = @_; + my $ac = $self->{'ac'}; + $ac->expire($expire); + my $length = $self->param('authen_captcha_code_length') || 4; + my $imgpath = $self->param('authen_captcha_source_image_path'); + my $width = $self->param('authen_captcha_character_width'); + my $height = $self->param('authen_captcha_character_height'); + my $set_dimensions = ($width && $height); + unless ($set_dimensions) { + $width = 25; + $height = 35; + } + if ($imgpath && -d $imgpath) { + $ac->images_folder($imgpath); + if ($set_dimensions) { + $ac->width($width); + $ac->height($height); + } + } + my $hash = $ac->generate_code($length); + my $imgurl = $self->base_url() . '/' . $hash . '.png'; + return ($hash, $imgurl, $length * $width, $height); +} + +sub verify { + my ($self, $code) = @_; + return ($self->{'ac'}->check_code($code, $self->hash()) == 1); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha/GD_SecurityImage.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha/GD_SecurityImage.pm new file mode 100644 index 0000000..cfc7e8f --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp/Captcha/GD_SecurityImage.pm @@ -0,0 +1,278 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: GD_SecurityImage.pm 301 2007-02-06 20:20:50Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp::Captcha::GD_SecurityImage - Captcha provider interface +using L + +=head1 CONFIGURATION + +The following parameters from your configuration file affect the behavior of +this module. Please see L +for a detailed explanation. Setting a value is optional for every parameter. + +=over 4 + +=item webapp.gd_securityimage_width + +=item webapp.gd_securityimage_height + +=item webapp.gd_securityimage_ptsize + +=item webapp.gd_securityimage_lines + +=item webapp.gd_securityimage_font + +=item webapp.gd_securityimage_gd_font + +=item webapp.gd_securityimage_bgcolor + +=item webapp.gd_securityimage_send_ctobg + +=item webapp.gd_securityimage_frame + +=item webapp.gd_securityimage_scramble + +=item webapp.gd_securityimage_angle + +=item webapp.gd_securityimage_thickness + +=item webapp.gd_securityimage_rndmax + +=item webapp.gd_securityimage_rnd_data + +=item webapp.gd_securityimage_method + +=item webapp.gd_securityimage_style + +=item webapp.gd_securityimage_text_color + +=item webapp.gd_securityimage_line_color + +=item webapp.gd_securityimage_particle_density + +=item webapp.gd_securityimage_particle_maxdots + +=back + +The value for C should simply be a sequence of characters to use. +Colors can only be passed as their hex values. + +=head1 NOTES + +This implementation is preliminary. You might have to set quite a few parameters +to get it in a usable state. + +If you have previously used C as a captcha provider, this module +should adapt its stored captcha information flawlessly. Therefore, +theoretically, you can switch back and forth between the two without any major +problems. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp::Captcha::GD_SecurityImage; + +use strict; +use warnings; + +use vars qw($VERSION @ISA); + +$VERSION = '0.3'; +@ISA = qw(Chirpy::UI::WebApp::Captcha); + +use Chirpy 0.3; +use Chirpy::UI::WebApp::Captcha 0.3; +use GD::SecurityImage; +use Digest::MD5 qw(md5_hex); + +sub create { + my ($self, $expire) = @_; + my ($image, $hash) = $self->_generate(); + $self->_write_img($hash, $image); + $self->_add_entry($hash, $expire); + my $url = $self->_img_url($hash); + my $width = $self->param('gd_securityimage_width'); + my $height = $self->param('gd_securityimage_height'); + return ($hash, $url, $width, $height); +} + +sub verify { + my ($self, $code) = @_; + my $hash = $self->hash(); + return 0 unless (defined $hash); + return $self->_check_entry($hash, $code); +} + +sub _add_entry { + my ($self, $hash, $expire) = @_; + my ($list, $update) = $self->_get_list(); + my $list_file = $self->_list_file(); + local *LIST; + if ($update) { + open(LIST, '>', $list_file) + or Chirpy::die('Failed to open "' . $list . '" for writing: ' . $!); + foreach my $line (@$list) { + print LIST $line, $/; + } + } + else { + open(LIST, '>>', $list_file) + or Chirpy::die('Failed to open "' . $list . '" for writing: ' . $!); + } + print LIST $expire, '::', $hash, $/; + close(LIST); +} + +sub _check_entry { + my ($self, $hash, $code) = @_; + my ($list, $update, $found) = $self->_get_list($hash); + $self->_write_list($list) if ($update); + return ($found && md5_hex($code) eq $hash); +} + +sub _get_list { + my ($self, $hash) = @_; + my $list_file = $self->_list_file(); + return ([], 0, 0) unless (-f $list_file); + my @list = (); + my $update = 0; + my $found = 0; + my $now = time(); + local *LIST; + open(LIST, '<', $list_file) + or Chirpy::die('Failed to read from "' . $list_file . '": ' . $!); + while () { + chomp; + my ($exp, $h) = split /::/; + if ($exp < $now) { + $update = 1; + unlink $self->_img_path($h); + } + elsif (defined $hash && $hash eq $h) { + $found = 1; + $update = 1; + unlink $self->_img_path($h); + } + else { + push @list, $_; + } + } + close(LIST); + return (\@list, $update, $found); +} + +sub _write_list { + my ($self, $list) = @_; + my $list_file = $self->_list_file(); + open(LIST, '>', $list_file) + or Chirpy::die('Failed to open "' . $list_file . '" for writing: ' . $!); + foreach my $line (@$list) { + print LIST $line, $/; + } + close LIST; +} + +sub _generate { + my $self = shift; + my $gdsi = $self->_gdsi(); + my $method = $self->param('gd_securityimage_method'); + my $style = $self->param('gd_securityimage_style'); + my $text_color = $self->param('gd_securityimage_text_color'); + my $line_color = $self->param('gd_securityimage_line_color'); + my $density = $self->param('gd_securityimage_particle_density'); + my $maxdots = $self->param('gd_securityimage_particle_maxdots'); + my ($image, $type, $rnd) = $gdsi->random() + ->create($method, $style, $text_color, $line_color) + ->particle($density, $maxdots) + ->out('force' => 'png', 'compress' => 1); + my $hash = md5_hex($rnd); + return ($image, $hash); +} + +sub _gdsi { + my $self = shift; + my @params = qw(width height ptsize lines font gd_font bgcolor send_ctobg + frame scramble angle thickness rndmax); + my %config = (); + foreach my $param (@params) { + my $value = $self->param('gd_securityimage_' . $param); + $config{$param} = $value if (defined $value); + } + my $rnd_data = $self->param('gd_securityimage_rnd_data'); + if (defined $rnd_data) { + $config{'rnd_data'} = [ split(//, $rnd_data) ]; + } + return new GD::SecurityImage(%config); +} + +sub _write_img { + my ($self, $hash, $image) = @_; + my $path = $self->_img_path($hash); + local *FILE; + open(FILE, '>', $path) + or Chirpy::die('Failed to open "' . $path . '" for writing: ' . $!); + binmode FILE; + print FILE $image; + close FILE; +} + +sub _list_file { + my $self = shift; + return $self->data_path() . '/codes.txt'; +} + +sub _img_path { + my ($self, $hash) = @_; + return $self->base_path() . '/' . $hash . '.png'; +} + +sub _img_url { + my ($self, $hash) = @_; + return $self->base_url() . '/' . $hash . '.png'; +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp/Session.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp/Session.pm new file mode 100644 index 0000000..be17c97 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp/Session.pm @@ -0,0 +1,231 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Session.pm 298 2007-02-06 17:08:08Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp::Session - Basic CGI session class + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp::Session; + +use strict; +use warnings; + +use vars qw($VERSION $NAME); + +$VERSION = '0.3'; +$NAME = 'sid'; + +use Chirpy 0.3; + +sub new { + my ($class, $parent, $create) = @_; + + $create = 0 unless (defined $create); + + my $dm = $parent->parent()->_data_manager(); + my $class_name = 'Chirpy::UI::WebApp::Session::DataManager'; + Chirpy::die('Data manager must implement ' . $class_name) + unless (UNIVERSAL::isa($dm, $class_name)); + + $dm->remove_expired_sessions_if_necessary(); + + my $self = { + 'dm' => $dm, + 'data' => undef, + 'ro' => 0 + }; + bless $self, $class; + + my $time = time(); + my $expire = $parent->param('session_expiry'); + $expire = ($expire ? &_parse_time($expire) : 3 * 24 * 60 * 60); + + my $cgi = $parent->{'cgi'}; + my $ip = $cgi->remote_addr(); + if ($create) { + my $sid = &_generate_id(); + $self->{'data'} = { + '_SESSION_ID' => $sid, + '_SESSION_CTIME' => $time, + '_SESSION_ATIME' => $time, + '_SESSION_ETIME' => $expire, + '_SESSION_REMOTE_ADDR' => $ip + }; + $dm->add_session($sid, $self->data()); + return $self; + } + else { + my $sid = $cgi->cookie(-name => $NAME); + if (&_valid_id($sid)) { + my @result = $dm->get_sessions($sid); + $self->{'data'} = $result[0] if (@result); + if ($self->id()) { + my $exp = $self->expire(); + if ($exp && $self->atime() + $exp < $time) { + $self->delete(); + } + elsif ($self->remote_addr() eq $ip) { + return $self; + } + } + } + } + return undef; +} + +sub DESTROY { + my $self = shift; + unless ($self->read_only()) { + $self->atime(time); + $self->update(); + } +} + +sub param { + my ($self, %params) = @_; + my $name; + Chirpy::die('Parameter name required') unless ($name = $params{'-name'}); + if (exists $params{'-value'}) { + $self->{'data'}->{$name} = $params{'-value'}; + } + return $self->{'data'}->{$name}; +} + +sub delete { + my $self = shift; + my $id = $self->id(); + $self->{'dm'}->remove_sessions($self->id()); + $self->{'data'} = {}; +} + +sub data { + my $self = shift; + return $self->{'data'}; +} + +sub id { + my $self = shift; + return $self->param(-name => '_SESSION_ID'); +} + +sub ctime { + my $self = shift; + return $self->param(-name => '_SESSION_CTIME'); +} + +sub atime { + my ($self, $value) = @_; + return (defined $value + ? $self->param(-name => '_SESSION_ATIME', -value => $value) + : $self->param(-name => '_SESSION_ATIME')); +} + +sub expire { + my ($self, $value) = @_; + return (defined $value + ? $self->param(-name => '_SESSION_ETIME', -value => $value) + : $self->param(-name => '_SESSION_ETIME')); +} + +sub remote_addr { + my $self = shift; + return $self->param(-name => '_SESSION_REMOTE_ADDR'); +} + +sub update { + my $self = shift; + $self->{'dm'}->modify_session($self->id(), $self->data()); +} + +sub read_only { + my ($self, $value) = @_; + if (defined $value) { + $self->{'ro'} = $value; + } + return $self->{'ro'}; +} + +sub _generate_id { + require Digest::MD5; + return Digest::MD5::md5_hex(time() . $$ . rand 9999); +} + +sub _valid_id { + my $id = shift; + return (defined $id && $id =~ /^[0-9a-f]{32}$/); +} + +sub _parse_time { + my $time = shift; + return $time if ($time =~ /^\d+$/); + if ($time =~ /^([+-]?\d+)([smhdMy])$/) { + my ($number, $unit) = ($1, $2); + if ($unit eq 'y') { + return $number * 365 * 24 * 60 * 60; + } + elsif ($unit eq 'M') { + return $number * 30 * 24 * 60 * 60; + } + elsif ($unit eq 'd') { + return $number * 24 * 60 * 60; + } + elsif ($unit eq 'h') { + return $number * 60 * 60; + } + elsif ($unit eq 'm') { + return $number * 60; + } + else { + return $number; + } + } + return 0; +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UI/WebApp/Session/DataManager.pm b/pub/qdb/src/modules/Chirpy/UI/WebApp/Session/DataManager.pm new file mode 100644 index 0000000..e32f0d0 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UI/WebApp/Session/DataManager.pm @@ -0,0 +1,157 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: DataManager.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UI::WebApp::Session::DataManager - Abstract data manager class specific +to L + +=head1 USAGE + +This class is required by L's session +manager L. It is an +abstract class representing a data manager for session information. + +If you wish to create an implementation of this class, the easiest way is to +extend an existing L implementation +with this class's methods. + +The class also has two non-abstract object method, namely +C and C. The +former returns a list containing the IDs of the sessions that have expired and +have consequently been removed. The latter does the same, but only every 24 +hours; otherwise, it returns C. + +=head1 IMPLEMENTATION + +If you want to make your L compatible +with L, all you need +to do is implement a few extra object methods for creating, retrieving, +updating and deleting sessions. You will probably also have to extend your +C and C methods accordingly. The extra methods to implement +are as follows: + +=over 4 + +=item add_session($id, $data) + +Stores the session with ID C<$id> and session data C<$data>. C<$data> is a hash +reference, so you will probably have to serialize it. How you do that is up to +you, but L makes it easy. Returns a true value upon success. + +=item get_sessions(@ids) + +Returns a list containing the data hash for each session whose ID is contained +in C<@ids>, or all sessions if C<@ids> is empty. If no sessions are found, +returns an empty list (and I C). + +=item modify_session($id, $data) + +Updates the existing session with ID C<$id> with the data from the hash +referred to by C<$data>. + +=item remove_sessions(@ids) + +Removes all sessions with an ID contained in C<@ids> from the system. Returns +the number of removed sessions. + +=back + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, +L, L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UI::WebApp::Session::DataManager; + +use strict; +use warnings; + +use vars qw($VERSION); + +use constant CLEANUP_INTERVAL => 24 * 60 * 60; + +$VERSION = '0.3'; + +use Chirpy 0.3; +use Chirpy::Util 0.3; + +sub remove_expired_sessions { + my $self = shift; + my @sessions = $self->get_sessions(); + return () unless (@sessions); + my @remove = (); + my $time = time; + foreach my $data (@sessions) { + if (my $exp = $data->{'_SESSION_ETIME'}) { + my $at = $data->{'_SESSION_ATIME'}; + next unless ($exp + $at < $time); + push @remove, $data->{'_SESSION_ID'}; + } + } + return () unless (@remove); + $self->remove_sessions(@remove); + return @remove; +} + +sub remove_expired_sessions_if_necessary { + my $self = shift; + my $now = time(); + my $last_cleanup = $self->get_parameter('last_session_cleanup'); + if (!defined $last_cleanup || $last_cleanup + CLEANUP_INTERVAL < $now) { + $self->set_parameter('last_session_cleanup', $now); + return $self->remove_expired_sessions(); + } + return undef; +} + +*add_session = \&Chirpy::Util::abstract_method; + +*get_sessions = \&Chirpy::Util::abstract_method; + +*modify_session = \&Chirpy::Util::abstract_method; + +*remove_sessions = \&Chirpy::Util::abstract_method; + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/UpdateChecker.pm b/pub/qdb/src/modules/Chirpy/UpdateChecker.pm new file mode 100644 index 0000000..601f1f4 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/UpdateChecker.pm @@ -0,0 +1,146 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: UpdateChecker.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::UpdateChecker - Update checker + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::UpdateChecker; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +use constant UPDATE_URL => Chirpy::URL . 'update/'; +use constant TIMEOUT => 5; +use constant KEY_VERSION_NUMBER => 'VersionNumber'; +use constant KEY_RELEASE_DATE => 'ReleaseDate'; +use constant KEY_DETAIL_URL => 'DetailURL'; + +sub new { + my ($class, $parent) = @_; + eval 'use LWP::UserAgent'; + my ($ua, $error); + if ($@) { + $error = 'LWP::UserAgent not available'; + } + else { + $ua = new LWP::UserAgent(); + $ua->timeout(TIMEOUT); + $ua->env_proxy(); + } + my $self = { + 'parent' => $parent, + 'ua' => $ua, + 'error' => $error + }; + return bless $self, $class; +} + +sub check_for_updates { + my $self = shift; + return 1 unless ($self->{'ua'}); + my $info = $self->get_version_information(); + if (ref $info ne 'ARRAY') { + $self->{'error'} = $info; + return 1; + } + if (shift(@$info)) { + return $info; + } + return 0; +} + +sub get_error_message { + my $self = shift; + return $self->{'error'}; +} + +sub get_version_information { + my $self = shift; + my $ua = $self->{'ua'}; + my $url = $self->{'parent'}->configuration()->get('ui', 'webapp.site_url'); + if (defined $url) { + eval 'use URI::Escape'; + $url = ($@ ? undef : URI::Escape::uri_escape($url)); + } + $url = UPDATE_URL . '?version=' . $Chirpy::VERSION + . (defined $url ? '&url=' . $url : ''); + my $response = $ua->get($url); + if ($response->is_success) { + my $xml = $response->content; + my %info = (); + if ($xml =~ m{ + \s*(.*?)\s* + }sx) { + my ($newer, $node) = ($1, $2); + $newer = (defined $newer && lc $newer eq 'true'); + my $pattern = join('|', map { quotemeta } + (KEY_VERSION_NUMBER, KEY_RELEASE_DATE, KEY_DETAIL_URL)); + while ($node =~ m!<($pattern)>\s*(.*?)\s*!sg) { + $info{$1} = $2; + } + $info{KEY_DETAIL_URL} =~ s/&/&/g; + return [ + $newer, + $info{KEY_VERSION_NUMBER()}, + $info{KEY_RELEASE_DATE()}, + $info{KEY_DETAIL_URL()} + ]; + } + return 'Unknown response from update server'; + } + return $response->status_line; +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Util.pm b/pub/qdb/src/modules/Chirpy/Util.pm new file mode 100644 index 0000000..c6e5755 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Util.pm @@ -0,0 +1,249 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Util.pm 302 2007-02-08 03:07:19Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Util - Static utility class + +=head1 FUNCTIONS + +=over 4 + +=item valid_username($username) + +Returns whether or not the given username is valid. A username is valid if it +is minimally 2, maximally 32 characters long. The characters can be letters, +numbers, underscores or dashes. + +=item valid_password($password) + +Returns whether or not the given password is valid. A password is valid if it +is minimally 4, maximally 256 characters long. The characters can be letters, +numbers, underscores or dashes. + +=item clean_up_submission($string) + +Performs various cleanup operations on the given string, which is assumed to be +filled in by the user somewhere. The operations include removal of leading and +trailing whitespaces, and trimming down sequences of more than 2 line feeds. + +=item parse_tags($tags) + +Parses the string C<$tags> into an array of valid tags and returns a reference +to it. The array may be empty. + +=item encrypt($string) + +Encrypts the given string using the MD5 algorithm. This function is used for +password encryption. + +=item format_quote_rating($rating) + +Returns a string representation of the given rating, i.e. the number prepended +with the Unicode representation of its sign. + +=item format_date_time($timestamp, $format, $gmt) + +Formats C<$timestamp> using L's C function +and C<$format> as the format. Returns Greenwich Mean Time if C<$gmt> is true. + +=item encode_xml_entities($string) + +Returns C<$string> with the character entities defined in XML encoded. The +entities and their respective codes are: + + Character Entity + ========= ====== + & & + " " + < < + > > + +=item decode_utf8($string) + +Returns the given string with UTF-8 characters decoded. + +=back + +=head1 PROCEDURES + +=over 4 + +=item ensure_writable_directory($path) + +Attempts to make the directory specified by C<$path> writable, creating it if it +does not exist. If, upon completion, C<$path> does represent a writable +directory, execution is aborted. + +=item abstract_method() + +Aborts execution immediately, stating that the method is abstract and must be +implemented. Hence, if you have an abstract method C, you +may define it as follows: + + *my_abstract_method = \&Chirpy::Util::abstract_method; + +Invoking it will then cause a fatal error unless it is overridden in the module +implementation. + +=back + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Util; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +use POSIX qw(strftime); + +sub valid_username { + my $string = shift; + return ($string =~ /^[a-zA-Z0-9_\-]{2,32}$/); +} + +sub valid_password { + my $string = shift; + return ($string =~ /^[a-zA-Z0-9_\-]{4,256}$/); +} + +sub clean_up_submission { + my $text = shift; + for ($text) { + s/\r\n?/\n/g; + s/^\s+//; + s/\s+$//; + s/\n{3,}/\n\n/g; + s/\t/ /g; + # Remove low ASCII chars (\12 = \n) + s/[\0-\11\13-\37]//g; + } + return $text; +} + +sub parse_tags { + my $tags = shift; + return [] unless (defined $tags && $tags ne ''); + $tags = lc $tags; + my %tags = (); + foreach my $tag (split(/[\s;,]+/, $tags)) { + next if (length($tag) < 2); + $tags{$tag} = 1; + } + return [ keys %tags ]; +} + +sub encrypt { + require Digest::MD5; + return Digest::MD5::md5_hex(shift); +} + +sub format_quote_rating { + my $rating = shift; + return ($rating + ? ($rating < 0 ? "\x{2212}" . (-$rating) : '+' . $rating) + : '0'); +} + +sub format_date_time { + my ($timestamp, $format, $gmt) = @_; + return strftime($format, ($gmt + ? gmtime($timestamp) : localtime($timestamp))); +} + +sub encode_xml_entities { + require HTML::Entities; + my $str = shift; + return HTML::Entities::encode($str, '<>&"'); +} + +sub decode_utf8 { + require Encode; + return Encode::decode('utf8', shift); +} + +sub shuffle_array { + my @array = @_; + my $len = scalar @array; + for (my $i = 0; $i < $len; $i++) { + my $j = int rand $len; + ($array[$i], $array[$j]) = ($array[$j], $array[$i]); + } + return @array; +} + +sub ensure_writable_directory { + my $path = shift; + if (-e $path) { + if (-d $path) { + if (!-w $path) { + chmod 0777, $path; + if (!-w $path) { + Chirpy::die('Directory "' . $path . '" not writable'); + } + } + } + else { + Chirpy::die('Path "' . $path . '" must be a directory'); + } + } + else { + mkdir $path + or die('Cannot create directory "' . $path . '": ' . $!); + } +} + +sub abstract_method { + Chirpy::die('Abstract method must be implemented'); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Util/IniFile.pm b/pub/qdb/src/modules/Chirpy/Util/IniFile.pm new file mode 100644 index 0000000..71c3133 --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Util/IniFile.pm @@ -0,0 +1,150 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: IniFile.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Util::IniFile - Load from and save to an INI file + +=head1 SYNOPSIS + + $inifile = new Chirpy::Util::IniFile('/path/to/inifile.ini'); + + $value = $inifile->get($section, $name); + + $inifile->set($section, $name, $value); + + undef $inifile; + +=head1 NOTE + +The C in the above example is not necessary to trigger an +update of the file's contents. The file is updated as soon as the object goes +out of scope (but only if the C function has been called). + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L, L, +L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Util::IniFile; + +use strict; +use warnings; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +sub new { + my ($class, $file, $create) = @_; + my $self = { + 'contents' => {}, + 'filename' => $file + }; + if (!defined $file) { + Chirpy::die('No filename specified'); + } + if ($create) { + $self->{'modified'} = 1; + } + elsif (!-f $file) { + Chirpy::die('File "' . $file . '" does not exist'); + } + local *FILE; + open(FILE, '<:utf8', $file) + or Chirpy::die('Failed to read from ' . $file . ': ' . $!); + my $section; + while () { + chomp; + next if (/^;/); + if (/^\s*\[([^\]]+)\]\s*$/) { + $section = $1; + } + elsif (defined($section) && /^([^=]+)=(.*)/) { + $self->{'contents'}{$section}{$1} = $2; + } + } + close FILE; + return bless($self, $class); +} + +sub DESTROY { + my $self = shift; + return unless ($self->{'modified'}); + local *FILE; + open(FILE, '>:utf8', $self->{'filename'}) + or Chirpy::die('Failed to write to ' . $self->{'filename'} + . ': ' . $!); + print FILE '; Automatically generated by ', __PACKAGE__, $/, + '; ', (my $str = gmtime()), $/; + foreach my $section (sort { lc($a) cmp lc($b) } + keys %{$self->{'contents'}}) { + print FILE $/, '[', $section, ']', $/; + foreach my $name (sort { lc($a) cmp lc($b) } + keys %{$self->{'contents'}{$section}}) { + print FILE $name, '=', + $self->{'contents'}{$section}{$name}, $/; + } + } + close FILE; +} + +sub get { + my ($self, $section, $name) = @_; + return (defined $name + ? $self->{'contents'}{$section}{$name} + : $self->{'contents'}{$section}); +} + +sub set { + my ($self, $section, $name, $value) = @_; + $self->{'contents'}{$section} = {} + if (!exists($self->{'contents'}{$section})); + $self->{'contents'}{$section}{$name} = $value; + $self->{'modified'} = 1; +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/modules/Chirpy/Webauth.pm b/pub/qdb/src/modules/Chirpy/Webauth.pm new file mode 100644 index 0000000..63d8dad --- /dev/null +++ b/pub/qdb/src/modules/Chirpy/Webauth.pm @@ -0,0 +1,172 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# $Id:: Account.pm 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +=head1 NAME + +Chirpy::Account - Represents a user account + +=head1 SYNOPSIS + + $account = new Chirpy::Account($id, $username, $password, $level); + + $id = $account->get_id(); + $account->set_id($id); + + $username = $account->get_username($username); + $account->set_username($username); + + $password = $account->get_password(); + $account->set_password($password); + + $level = $account->get_level(); + $account->set_level($level); + +=head1 CONSTRAINTS + +=over 4 + +=item ID + +The account ID must be a positive non-zero integer. + +=item Username + +The username must be valid against the C function of +L. + +=item Password + +Encryption is done I invoking the constructor or the C +function. The C function returns the I password. +The password must be valid against the C function and +encrypted using the C function, both part of L. + +=item User Level + +The user level must be one of the user level constants described below. + +=back + +=head1 USER LEVEL CONSTANTS + +The following constants are recommended for use as user levels: + + Chirpy::Account::USER_LEVEL_3 + Chirpy::Account::USER_LEVEL_6 + Chirpy::Account::USER_LEVEL_9 + +Note that the value of these is the integer representing the user level and +that the constants are only for the sake of code readability. + +=head1 AUTHOR + +Tim De Pauw Eceetee@users.sourceforge.netE + +=head1 SEE ALSO + +L, L + +=head1 COPYRIGHT + +Copyright 2005-2007 Tim De Pauw. All rights reserved. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +=cut + +package Chirpy::Account; + +use strict; +use warnings; + +use constant USER_LEVEL_3 => 3; +use constant USER_LEVEL_6 => 6; +use constant USER_LEVEL_9 => 9; + +use vars qw($VERSION); + +$VERSION = '0.3'; + +use Chirpy 0.3; + +sub new { + my ($class, $id, $username, $password, $level) = @_; + my $self = { + 'id' => $id, + 'username' => $username, + 'password' => $password, + 'level' => $level + }; + return bless($self, $class); +} + +sub get_id { + my $self = shift; + return $self->{'id'}; +} + +sub set_id { + my $self = shift; + return ($self->{'id'} = shift); +} + +sub get_username { + my $self = shift; + return $self->{'username'}; +} + +sub set_username { + my $self = shift; + return ($self->{'username'} = shift); +} + +sub get_password { + my $self = shift; + return $self->{'password'}; +} + +sub set_password { + my $self = shift; + return ($self->{'password'} = shift); +} + +sub get_level { + my $self = shift; + return $self->{'level'}; +} + +sub set_level { + my $self = shift; + return ($self->{'level'} = shift); +} + +1; + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/src/templates/default/_foot.html b/pub/qdb/src/templates/default/_foot.html new file mode 100644 index 0000000..7df909c --- /dev/null +++ b/pub/qdb/src/templates/default/_foot.html @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/pub/qdb/src/templates/default/_head.html b/pub/qdb/src/templates/default/_head.html new file mode 100644 index 0000000..3f1a838 --- /dev/null +++ b/pub/qdb/src/templates/default/_head.html @@ -0,0 +1,51 @@ + + + + /cite.ico" /> + /css/default.css"/> + /css/styles/default.css"/> + /css/styles/spring.css"/> + /css/styles/fish_tank.css"/> + /css/styles/grayscale.css"/> + /css/styles/nineties.css"/> + + " + title="" href="" /> + + + " type="text/plain" /> + + " /> + " /> + " /> + + + <TMPL_VAR NAME="SITE_TITLE">: <TMPL_VAR NAME="PAGE_TITLE"> + + +

+ "> +

+ +
diff --git a/pub/qdb/src/templates/default/_search_form.html b/pub/qdb/src/templates/default/_search_form.html new file mode 100644 index 0000000..f506735 --- /dev/null +++ b/pub/qdb/src/templates/default/_search_form.html @@ -0,0 +1,11 @@ +
+ +
+ + " id="query-field" accesskey="s" /> +
+
+ " id="submit-button" /> +
+ +
\ No newline at end of file diff --git a/pub/qdb/src/templates/default/administration.html b/pub/qdb/src/templates/default/administration.html new file mode 100644 index 0000000..63b3b92 --- /dev/null +++ b/pub/qdb/src/templates/default/administration.html @@ -0,0 +1,61 @@ + + +

+ +

+ +
+

+
+
+ + +
+ "> + +
+
+
+
+
    +
  • +

    + +
  • +

    + +
  • +

    + +
  • +

    + +
  • +

    + +
  • +

    + +
  • +

    + +
+ + \ No newline at end of file diff --git a/pub/qdb/src/templates/default/confirm.html b/pub/qdb/src/templates/default/confirm.html new file mode 100644 index 0000000..31bbcc1 --- /dev/null +++ b/pub/qdb/src/templates/default/confirm.html @@ -0,0 +1,18 @@ + +

+ +

+
postget" action="" id="confirmation-form"> +

+ +
+

+
+
+
+ " id="confirm-button" /> + " id="cancel-button" + onclick="history.go(-1);" /> +
+ + \ No newline at end of file diff --git a/pub/qdb/src/templates/default/error.html b/pub/qdb/src/templates/default/error.html new file mode 100644 index 0000000..b5dce49 --- /dev/null +++ b/pub/qdb/src/templates/default/error.html @@ -0,0 +1,6 @@ + +

+ +

+

+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/feeds/atom10.xml b/pub/qdb/src/templates/default/feeds/atom10.xml new file mode 100644 index 0000000..2016e02 --- /dev/null +++ b/pub/qdb/src/templates/default/feeds/atom10.xml @@ -0,0 +1,42 @@ +"?> + +<TMPL_VAR NAME="SITE_TITLE">: <TMPL_VAR NAME="PAGE_TITLE"> + + +"/> +" +title=": "/> +" +title=""/> + + + + + +" version=""> + + +<TMPL_VAR NAME="QUOTE_TITLE"> + +" /> + +"/> + + + + +
+

">[] +(/) +">[] + +">[] + +

+

+

+

+
+
+
+
\ No newline at end of file diff --git a/pub/qdb/src/templates/default/feeds/rss20.xml b/pub/qdb/src/templates/default/feeds/rss20.xml new file mode 100644 index 0000000..ab8c23d --- /dev/null +++ b/pub/qdb/src/templates/default/feeds/rss20.xml @@ -0,0 +1,34 @@ +"?> + + +<TMPL_VAR NAME="SITE_TITLE">: <TMPL_VAR NAME="PAGE_TITLE"> + + + + + + + +<TMPL_VAR NAME="QUOTE_TITLE"> + + + + + + +">[] +(/) +">[] + +">[] + +

+

+

+

+]]>
+
+
+
+
\ No newline at end of file diff --git a/pub/qdb/src/templates/default/login.html b/pub/qdb/src/templates/default/login.html new file mode 100644 index 0000000..85e5f3b --- /dev/null +++ b/pub/qdb/src/templates/default/login.html @@ -0,0 +1,25 @@ + +

+ +

+ +
+

+
+
+
+ +
+ + +
+
+ + +
+
+ " id="login-button" /> +
+ +
+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/message.html b/pub/qdb/src/templates/default/message.html new file mode 100644 index 0000000..13d9f4b --- /dev/null +++ b/pub/qdb/src/templates/default/message.html @@ -0,0 +1,6 @@ + +

+ +

+

+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/quote_list.html b/pub/qdb/src/templates/default/quote_list.html new file mode 100644 index 0000000..921feab --- /dev/null +++ b/pub/qdb/src/templates/default/quote_list.html @@ -0,0 +1,128 @@ + + + + + +
+ + ">← + + + + + "> + + + +
+
+

+ +

+ + + + + +
+ + ">← + + + + + "> + + + +
+
+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/quote_search.html b/pub/qdb/src/templates/default/quote_search.html new file mode 100644 index 0000000..72bf855 --- /dev/null +++ b/pub/qdb/src/templates/default/quote_search.html @@ -0,0 +1,12 @@ + +

+ +

+ +
+ <Ria> Type in the search word,
+ <Ria> apply pressure to button,
+ <Ria> then read the results.
+ <Ria> it's a haiku!
+
+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/start_page.html b/pub/qdb/src/templates/default/start_page.html new file mode 100644 index 0000000..da8304b --- /dev/null +++ b/pub/qdb/src/templates/default/start_page.html @@ -0,0 +1,35 @@ + +
+
+

+ +

+ +
+
+
+ +
+
+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/statistics.html b/pub/qdb/src/templates/default/statistics.html new file mode 100644 index 0000000..b89a7fb --- /dev/null +++ b/pub/qdb/src/templates/default/statistics.html @@ -0,0 +1,90 @@ + + + + + +

+ +

+
+
+

+
+ +
+
+
+
+
+
+

+
+ +
">:00–:00
+
+
+
+
+
+

+
+ +
+
+
+
+
+
+

+
+ +
+
+
+
+
+
+

+
+ +
">
+
+
+
+
+
+

+
+ +
+
+
+
+
+
+

+
+ +
+
+
+
+
+
+

+
+ +
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/submit_quote.html b/pub/qdb/src/templates/default/submit_quote.html new file mode 100644 index 0000000..abc2346 --- /dev/null +++ b/pub/qdb/src/templates/default/submit_quote.html @@ -0,0 +1,35 @@ + +

+ +

+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+ " /> + " + alt="" id="captcha-image" + width="" + height="" /> + + +
+
+
+ " id="submit-button" /> +
+ +
+ \ No newline at end of file diff --git a/pub/qdb/src/templates/default/tag_cloud.html b/pub/qdb/src/templates/default/tag_cloud.html new file mode 100644 index 0000000..9a7c7b7 --- /dev/null +++ b/pub/qdb/src/templates/default/tag_cloud.html @@ -0,0 +1,20 @@ + + + + + +
+

+ +

+ + + \ No newline at end of file diff --git a/pub/qdb/src/welcome.html b/pub/qdb/src/welcome.html new file mode 100644 index 0000000..8aaf81e --- /dev/null +++ b/pub/qdb/src/welcome.html @@ -0,0 +1,27 @@ +

Welcome to !

+

This web site is a collection of quotes from the Internet Relay Chat +network. Currently, the database contains a total of quote(s). + +We also have "> +quote(s) awaiting moderation. + +

+

Feel free to look around! Don’t forget to vote for the quotes +you love by clicking on the Up↑ link. Give the ones you hate the +old thumbs down using the Down↓ link. If you feel any of the +quotes should not be in the collection, use the Report link to +request removal. If you wish to contact the administrator of this web site, +you can do so by sending an e-mail to ">.

+

+is powered by +, +a rather fabulous and freely available quote management system.

+
"> +
+ + + +
+
\ No newline at end of file diff --git a/pub/qdb/util/chirpy_rqms_import.php b/pub/qdb/util/chirpy_rqms_import.php new file mode 100644 index 0000000..73523f3 --- /dev/null +++ b/pub/qdb/util/chirpy_rqms_import.php @@ -0,0 +1,195 @@ + # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# chirpy_rqms_import.php # +# Imports data from an existing RQMS installation into Chirpy! # +############################################################################### +# $Id:: chirpy_rqms_import.php 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +############################################################################### +# CONFIGURATION # +############################################################################### + +// Chirpy!'s table prefix: same as in the configuration. +$chirpy_table_prefix = 'chirpy_'; + +// Set this to false to keep this script from emptying Chirpy!'s tables first. +// This can be useful if you want to import your Rash installation's quotes +// AFTER using Chirpy! for a while. Note that setting this option to true will +// copy Rash's quote IDs, which can be useful if you want to keep existing +// links intact, while a value of false will not guarantee that. +$clear_chirpy_tables = true; + +// Rash only saves the date of news items, not the time. To compensate, time +// is set to midnight in your time zone. Enter your time zone here. This needs +// to be a time zone that Perl's strtotime() function can understand, or the +// script will not work properly. Both GMT offsets like ‘+0200’ (no colon!) +// and zones like ‘CET’ should be supported. +$timezone = 'GMT'; + +############################################################################### +# DO NOT TOUCH ANYTHING BELOW THIS LINE # +############################################################################### + +require('config.php'); + +set_magic_quotes_runtime(0); +error_reporting(E_ALL); + +header('Content-Type: text/plain; charset=UTF-8'); + +log_event('Connecting to ‘' . $hostname . '’ …', true); +mysql_connect($hostname, $username, $dbpasswd) + or die('Database connection failed: ' . mysql_error()); + +log_event('Accessing database ‘' . $dbname . '’ …', true); +mysql_select_db($dbname) + or die('Failed to select database: ' . mysql_error()); + +if ($clear_chirpy_tables) { + log_event('Clearing Chirpy!’s tables …'); + $tables = array(); + $tables[] = 'accounts'; + $tables[] = 'events'; + $tables[] = 'event_metadata'; + $tables[] = 'news'; + $tables[] = 'quotes'; + $tables[] = 'quote_tag'; + $tables[] = 'sessions'; + $tables[] = 'tags'; + $tables[] = 'vars'; + foreach ($tables as $table) + clear_table($chirpy_table_prefix . $table); + log_event('Tables cleared', true); +} + +log_event('Importing user information …'); +$users_result = mysql_query('SELECT * FROM `' . $rashusers . '`') + or die('Error retrieving user information: ' . mysql_error()); +$count_users = 0; +while ($row = mysql_fetch_array($users_result)) { + $count_users++; + mysql_query('INSERT INTO `' . $chirpy_table_prefix . 'accounts`' + . ' (`username`, `password`, `level`)' + . ' VALUES ("' . $row['user'] . '", "' . $row['password'] . '", ' + . convert_level($row['level']) . ')') + or die('Error importing user information: ' . mysql_error()); +} +log_event('Users imported: ' . $count_users, true); +mysqL_free_result($users_result); + +log_event('Importing news items …'); +$news_result = mysql_query('SELECT * FROM `' . $newstable . '`') + or die('Error retrieving news: ' . mysql_error()); +$count_news = 0; +while ($row = mysql_fetch_array($news_result)) { + $count_news++; + $date = date_to_timestamp($row['date']); + if ($date <= 0) + die('Unable to convert ‘' + $row['date'] + '’ into a UNIX timestamp'); + mysql_query('INSERT INTO `' . $chirpy_table_prefix . 'news`' + . ' (`body`, `date`)' + . ' VALUES ("' . addslashes(decode_html($row['news'])) + . '", FROM_UNIXTIME(' . $date . '))') + or die('Error importing news item: ' . mysql_error()); +} +log_event('News items imported: ' . $count_news, true); +mysqL_free_result($news_result); + +log_event('Importing quotes …'); +$quotes_result = mysql_query('SELECT * FROM `' . $quotetable . '`') + or die('Error retrieving quotes: ' . mysql_error()); +$count_quotes = 0; +while ($row = mysql_fetch_array($quotes_result)) { + $count_quotes++; + $rating = $row['rating']; + // We use abs($rating) as the number of votes here, since RQMS doesn't + // seem to keep it anywhere. + $votes = abs($rating); + $score = (($votes + $rating) / 2 + 1) / (($votes - $rating) / 2 + 1); + mysql_query('INSERT INTO `' . $chirpy_table_prefix . 'quotes` (' + . ($clear_chirpy_tables ? '`id`, ' : '') + . '`body`, `rating`, `votes`, `submitted`, `approved`, `flagged`,' + . ' `score`) VALUES (' + . ($clear_chirpy_tables ? $row['id'] . ', ' : '') + . '"' . addslashes(decode_html($row['quote'])) . '"' + . ', ' . $rating + . ', ' . $votes + . ', FROM_UNIXTIME(' . $row['date'] . ')' + . ', ' . ($row['approve'] ? 1 : 0) + . ', ' . ($row['check'] ? 0 : 1) + . ', ' . $score . ')') + or die('Error importing quote: ' . mysql_error()); +} +log_event('Quotes imported: ' . $count_quotes, true); +mysqL_free_result($quotes_result); + +log_event('Importing unverified quotes …'); +$unverified_quotes_result = mysql_query('SELECT * FROM `' . $subtable . '`') + or die('Error retrieving unverified quotes: ' . mysql_error()); +$count_unverified_quotes = 0; +while ($row = mysql_fetch_array($unverified_quotes_result)) { + $count_unverified_quotes++; + mysql_query('INSERT INTO `' . $chirpy_table_prefix . 'quotes`' + . ' (`body`, `submitted`)' + . ' VALUES ("' . decode_html($row['quote']) + . '", FROM_UNIXTIME(' . time() . '))') + or die('Error importing unverified quote: ' . mysql_error()); +} +log_event('Unverified quotes imported: ' . $count_unverified_quotes, true); +mysqL_free_result($unverified_quotes_result); + +log_event('Closing database connection …', true); +mysql_close(); + +log_event('Import finished!'); + +function clear_table ($name) { + mysql_query('TRUNCATE TABLE `' . $name . '`') + or die('Error clearing table ‘' . $name . '’: ' . mysql_error()); + mysql_query('ALTER TABLE `' . $name . '` AUTO_INCREMENT = 1') + or die('Error resetting auto-increment index for table ‘' . $name + . '’: ' . mysql_error()); +} + +function convert_level ($level) { + return ($level && $level >= 1 && $level <= 3 ? (4 - $level) * 3 : 0); +} + +function decode_html ($text) { + return html_entity_decode( + preg_replace('|\s*<\s*br\s*/?\s*>\s*|', "\n", $text)); +} + +function date_to_timestamp ($date) { + global $timezone; + $date .= ' 00:00 ' . $timezone; + return strtotime($date); +} + +function log_event ($text, $end_segment) { + echo $text . "\r\n"; + if ($end_segment) echo "\r\n"; +} + +############################################################################### +?> \ No newline at end of file diff --git a/pub/qdb/util/gzip.pl b/pub/qdb/util/gzip.pl new file mode 100644 index 0000000..dd1beea --- /dev/null +++ b/pub/qdb/util/gzip.pl @@ -0,0 +1,132 @@ +#!/usr/bin/perl + +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# gzip.pl # +# Provides compression for certain files, relying on .htaccess directives # +############################################################################### +# $Id:: gzip.pl 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +use strict; +use warnings; + +use CGI; +use CGI::Carp qw/fatalsToBrowser/; + +use constant MODULES => [ + 'HTTP::Date', + 'Digest::MD5', + 'Compress::Zlib' +]; +use constant CACHE_DIR => 'src/cache/gzip'; +use constant MIME_TYPES => { + 'css' => 'text/css', + 'js' => 'text/javascript' +}; + +my $cgi = new CGI(); +my $uri = $cgi->param('uri'); +my $filename = $cgi->param('filename'); + +foreach my $module (@{MODULES()}) { + eval 'require ' . $module; + &redirect() + if ($@); +} + +unless (-d CACHE_DIR) { + mkdir CACHE_DIR, 0777 + or &redirect(); +} + +&redirect() + unless (defined $uri && $ENV{'REDIRECT_URL'} eq $uri); + +&redirect() + unless (defined $filename && -s $filename); + +my $file_date = (stat($filename))[9]; + +my $md5 = Digest::MD5::md5_hex($filename); +my $etag = '"' . $md5 . '-' . sprintf('%x', $file_date) . '"'; + +my $ims = $cgi->http('If-Modified-Since'); +my $inm = $cgi->http('If-None-Match'); + +if ((defined $ims || defined $inm) +&& ((defined $ims && $file_date <= HTTP::Date::str2time($ims)) +|| (defined $inm && $etag eq $inm))) { + print $cgi->header(-status => '304 Not Modified'); + exit; +} + +my $cache_file = CACHE_DIR . '/' . $md5; +my $contents; +if (!-f $cache_file || (stat($cache_file))[9] < $file_date) { + $contents = Compress::Zlib::memGzip(&get_file_contents($filename)); + &put_file_contents($cache_file, $contents); +} +else { + $contents = &get_file_contents($cache_file); +} + +my $extension; +$filename =~ /([^.]+)$/ and $extension = $1; +my $ctype = (defined $extension && exists MIME_TYPES->{$extension} + ? MIME_TYPES->{$extension} : 'text/plain'); + +print $cgi->header( + -type => $ctype, + -Last_Modified => HTTP::Date::time2str($file_date), + -ETag => $etag, + -Content_Encoding => 'gzip', + -Content_Length => length($contents) +); +binmode STDOUT; +print $contents; + +sub redirect { + print $cgi->header(-Location => $uri . '?nogzip'); + exit; +} + +sub get_file_contents { + my $filename = shift; + local $/ = undef; + local *FILE; + open(FILE, '<', $filename) + or die 'Failed to read "' . $filename . '": ' . $!; + my $contents = ; + close(FILE); + return $contents; +} + +sub put_file_contents { + my ($filename, $contents) = @_; + local *FILE; + open(FILE, '>', $filename) + or die 'Failed to write to "' . $filename . '": ' . $!; + print FILE $contents; + close(FILE); +} + +############################################################################### \ No newline at end of file diff --git a/pub/qdb/util/setup.pl b/pub/qdb/util/setup.pl new file mode 100644 index 0000000..c4c79c5 --- /dev/null +++ b/pub/qdb/util/setup.pl @@ -0,0 +1,145 @@ +#!/usr/bin/perl + +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw # +############################################################################### +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; either version 2 of the License, or (at your option) # +# any later version. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 51 # +# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # +############################################################################### + +############################################################################### +# setup.pl # +# Generic installation/upgrade script # +############################################################################### +# $Id:: setup.pl 291 2007-02-05 21:24:46Z ceetee $ # +############################################################################### + +use strict; +use warnings; +use CGI; +use CGI::Carp qw(fatalsToBrowser); + +BEGIN { + unshift @INC, 'src/modules'; +} + +use Chirpy 0.3; +use Chirpy::Util 0.3; +use Chirpy::Account 0.3; +use Chirpy::NewsItem 0.3; + +use constant DEFAULT_USERNAME => 'superuser'; +use constant DEFAULT_PASSWORD => 'password'; +use constant DEFAULT_NEWS_ITEM => 'Welcome to this brand new ' + . Chirpy::FULL_PRODUCT_NAME . ' installation! For more about ' + . Chirpy::PRODUCT_NAME . ', be sure to visit the project homepage, ' + . 'located at <' . Chirpy::URL . '>!'; + +my $ch = new Chirpy('./chirpy.ini'); +my $cgi = new CGI(); + +print $cgi->header(-type => 'text/html; charset=US-ASCII'); + +&_header(); + +if ($cgi->request_method() eq 'POST') { + print '
';
+	
+	my $fresh = $cgi->param('fresh');
+
+	if ($fresh) {
+		&_log('Removing old installation (if any) ...');
+		$ch->remove();
+		&_log('Setting up ' . Chirpy::FULL_PRODUCT_NAME . ' ...');
+		my $account = new Chirpy::Account(
+			undef,
+			DEFAULT_USERNAME,
+			Chirpy::Util::encrypt(DEFAULT_PASSWORD),
+			Chirpy::Account::USER_LEVEL_9
+		);
+		my $news = new Chirpy::NewsItem(
+			undef,
+			DEFAULT_NEWS_ITEM,
+			$account,
+			time
+		);
+		$ch->set_up([ $account ], [ $news ]);
+		&_log('Account "' . DEFAULT_USERNAME . '" and news item added.');
+		&_log('Setup completed!');
+	}
+	else {
+		&_log('Upgrading to ' . Chirpy::FULL_PRODUCT_NAME . ' ...');
+		$ch->set_up();
+		&_log('Upgrade successful!');
+	}
+
+	print '
', $/, + '

Finally, you must remove this file (', + $0, ') on the ', + 'server immediately. Failing to do so will introduce a substantial ', + 'security hazard.

', $/, + '

Once you have completed the final step, you may click on the ', + 'button below to surf to your new ', Chirpy::PRODUCT_NAME, ' ', + 'installation.

', $/, + '
', $/, + '', $/, '
'; +} +else { + print '

Welcome to the ', Chirpy::FULL_PRODUCT_NAME, ' setup ', + 'script.

', $/, + '

Please make sure that all the necessary files are ', + 'present and that you have edited chirpy.ini ', + 'to match your configuration. Otherwise, the setup process ', + 'will fail.

', $/, + '

Now, please tell us if this is a fresh installation, or you ', + 'are upgrading an existing installation.

', $/, + '
', $/, + '', $/, + '', $/, + '
', $/, + '

Please click the button only once! ', + 'The operation might take a while to complete.

'; +} + +&_footer(); + +sub _header { + print '', $/, + '', $/, + '', $/, + '', $/, + '', Chirpy::FULL_PRODUCT_NAME, ' Setup', $/, + '', $/, + '', $/, + '

', Chirpy::FULL_PRODUCT_NAME, ' Setup

', $/; +} + +sub _footer { + print '', $/, ''; +} + +sub _log { + my $message = shift; + print $message, $/; +} + +############################################################################### diff --git a/templates/default.html b/templates/default.html index bad3f4f..dd325e1 100644 --- a/templates/default.html +++ b/templates/default.html @@ -18,7 +18,7 @@ webmail newsgroups mailman - quoteboard + quoteboard