summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--EVENTS.txt105
-rw-r--r--README148
-rw-r--r--actions/all.php36
-rw-r--r--actions/allrss.php14
-rw-r--r--actions/api.php12
-rw-r--r--actions/avatarsettings.php38
-rw-r--r--actions/doc.php12
-rw-r--r--actions/editgroup.php6
-rw-r--r--actions/emailsettings.php7
-rw-r--r--actions/facebookinvite.php25
-rw-r--r--actions/favorited.php8
-rw-r--r--actions/finishopenidlogin.php17
-rw-r--r--actions/finishremotesubscribe.php10
-rw-r--r--actions/grouprss.php15
-rw-r--r--actions/login.php6
-rw-r--r--actions/newgroup.php6
-rw-r--r--actions/newmessage.php2
-rw-r--r--actions/newnotice.php24
-rw-r--r--actions/noticesearch.php182
-rw-r--r--actions/peoplesearch.php26
-rw-r--r--actions/peopletag.php126
-rw-r--r--actions/profilesettings.php6
-rw-r--r--actions/public.php43
-rw-r--r--actions/register.php6
-rw-r--r--actions/remotesubscribe.php3
-rw-r--r--actions/replies.php28
-rw-r--r--actions/showfavorites.php33
-rw-r--r--actions/showgroup.php27
-rw-r--r--actions/showstream.php101
-rw-r--r--actions/tag.php22
-rw-r--r--actions/tagother.php5
-rw-r--r--actions/twitapiaccount.php31
-rw-r--r--actions/twitapisearchatom.php368
-rw-r--r--actions/twitapisearchjson.php149
-rw-r--r--actions/twitapistatuses.php61
-rw-r--r--actions/twitapitrends.php90
-rw-r--r--actions/twitapiusers.php52
-rw-r--r--actions/twittersettings.php5
-rw-r--r--actions/updateprofile.php16
-rw-r--r--actions/userauthorization.php20
-rw-r--r--actions/userrss.php11
-rw-r--r--bin/flowplayer-3.0.5.swfbin0 -> 92317 bytes
-rw-r--r--bin/flowplayer.audio-3.0.3.swfbin0 -> 2756 bytes
-rw-r--r--bin/flowplayer.controls-3.0.3.swfbin0 -> 15977 bytes
-rw-r--r--classes/Nonce.php9
-rw-r--r--classes/Notice.php170
-rw-r--r--classes/Notice_tag.php15
-rw-r--r--classes/Profile_tag.php43
-rw-r--r--classes/User.php32
-rwxr-xr-xclasses/laconica.ini8
-rw-r--r--config.php.sample25
-rw-r--r--db/carrier.sql61
-rw-r--r--db/foreign_services.sql7
-rw-r--r--db/laconica.sql22
-rw-r--r--db/laconica_pg.sql64
-rw-r--r--db/notice_source.sql46
-rw-r--r--db/sms_carrier.sql63
-rw-r--r--doc-src/about (renamed from doc/about)0
-rw-r--r--doc-src/badge65
-rw-r--r--doc-src/contact (renamed from doc/contact)0
-rw-r--r--doc-src/faq (renamed from doc/faq)0
-rw-r--r--doc-src/groups (renamed from doc/groups)0
-rw-r--r--doc-src/help (renamed from doc/help)0
-rw-r--r--doc-src/im (renamed from doc/im)0
-rw-r--r--doc-src/openid (renamed from doc/openid)0
-rw-r--r--doc-src/openmublog (renamed from doc/openmublog)0
-rw-r--r--doc-src/privacy (renamed from doc/privacy)0
-rw-r--r--doc-src/sms (renamed from doc/sms)0
-rw-r--r--doc-src/source (renamed from doc/source)0
-rw-r--r--doc-src/tags (renamed from doc/tags)0
-rw-r--r--extlib/Net/URL.php485
-rw-r--r--extlib/Net/URL/Mapper.php324
-rw-r--r--extlib/Net/URL/Mapper/Exception.php104
-rw-r--r--extlib/Net/URL/Mapper/Part.php142
-rw-r--r--extlib/Net/URL/Mapper/Part/Dynamic.php81
-rw-r--r--extlib/Net/URL/Mapper/Part/Fixed.php70
-rw-r--r--extlib/Net/URL/Mapper/Part/Wildcard.php80
-rw-r--r--extlib/Net/URL/Mapper/Path.php430
-rw-r--r--extlib/OAuth.php135
-rw-r--r--htaccess.sample162
-rw-r--r--index.php157
-rw-r--r--install.php255
-rw-r--r--js/flowplayer-3.0.5.min.js24
-rw-r--r--js/identica-badge.js3
-rw-r--r--js/jcrop/jquery.Jcrop.go.js7
-rw-r--r--js/jquery.js224
-rw-r--r--js/jquery.min.js12
-rw-r--r--js/jquery.simplemodal-1.2.2.pack.js8
-rw-r--r--js/video.js9
-rw-r--r--lib/action.php292
-rw-r--r--lib/channel.php (renamed from classes/Channel.php)1
-rw-r--r--lib/clienterroraction.php15
-rw-r--r--lib/clientexception.php56
-rw-r--r--lib/command.php (renamed from classes/Command.php)74
-rw-r--r--lib/commandinterpreter.php (renamed from classes/CommandInterpreter.php)3
-rw-r--r--lib/common.php46
-rw-r--r--lib/dberroraction.php73
-rw-r--r--lib/event.php113
-rw-r--r--lib/facebookutil.php109
-rw-r--r--lib/featureduserssection.php5
-rw-r--r--lib/feed.php110
-rw-r--r--lib/feedlist.php94
-rw-r--r--lib/grouplist.php4
-rw-r--r--lib/groupsbymemberssection.php2
-rw-r--r--lib/groupsbypostssection.php2
-rw-r--r--lib/grouptagcloudsection.php8
-rw-r--r--lib/htmloutputter.php21
-rw-r--r--lib/imagefile.php50
-rw-r--r--lib/jabber.php7
-rw-r--r--lib/jsonsearchresultslist.php270
-rw-r--r--lib/language.php61
-rw-r--r--lib/mail.php52
-rw-r--r--lib/noticesection.php7
-rw-r--r--lib/oauthstore.php9
-rw-r--r--lib/omb.php10
-rw-r--r--lib/openid.php5
-rw-r--r--lib/peoplesearchresults.php75
-rw-r--r--lib/personaltagcloudsection.php10
-rw-r--r--lib/ping.php79
-rw-r--r--lib/plugin.php79
-rw-r--r--lib/popularnoticesection.php19
-rw-r--r--lib/profilelist.php4
-rw-r--r--lib/publicgroupnav.php28
-rw-r--r--lib/router.php434
-rw-r--r--lib/rssaction.php24
-rw-r--r--lib/searchaction.php3
-rw-r--r--lib/section.php2
-rw-r--r--lib/servererroraction.php20
-rw-r--r--lib/serverexception.php55
-rw-r--r--lib/twitter.php97
-rw-r--r--lib/twitterapi.php63
-rw-r--r--lib/util.php472
-rw-r--r--lib/xmlstringer.php68
-rw-r--r--local/.gitignore0
-rw-r--r--plugins/BlogspamNetPlugin.php144
-rw-r--r--plugins/GoogleAnalyticsPlugin.php77
-rw-r--r--scripts/SearchMonkey-Om3.0.txt1
-rw-r--r--scripts/SearchMonkey-yQP.0.txt1
-rwxr-xr-xscripts/facebookqueuehandler.php71
-rw-r--r--scripts/laconica.spec87
-rw-r--r--scripts/pingqueuehandler.php64
-rwxr-xr-xscripts/sitemap.php3
-rwxr-xr-xscripts/startdaemons.sh3
-rwxr-xr-xscripts/stopdaemons.sh2
-rwxr-xr-xscripts/twitterqueuehandler.php71
-rwxr-xr-xscripts/update_facebook.php147
-rwxr-xr-xscripts/xmppdaemon.php2
-rw-r--r--theme/base/css/display.css8
-rw-r--r--theme/base/css/mobile.css150
-rw-r--r--theme/base/css/modal.css22
-rw-r--r--theme/base/css/modal_ie.css16
-rw-r--r--theme/base/css/print.css36
-rw-r--r--theme/base/images/x.pngbin0 -> 1066 bytes
-rw-r--r--theme/readme.txt3
155 files changed, 7147 insertions, 2040 deletions
diff --git a/.gitignore b/.gitignore
index f5a3e0212..83a53dfa3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,15 @@
avatar/*
files/*
_darcs/*
+logs/*
config.php
.htaccess
+httpd.conf
*.tmproj
dataobject.ini
*~
*.bak
*.orig
*.rej
+.#*
+*.swp
diff --git a/EVENTS.txt b/EVENTS.txt
new file mode 100644
index 000000000..5edf59245
--- /dev/null
+++ b/EVENTS.txt
@@ -0,0 +1,105 @@
+InitializePlugin: a chance to initialize a plugin in a complete
+ environment
+
+CleanupPlugin: a chance to cleanup a plugin at the end of a program
+
+StartPrimaryNav: Showing the primary nav menu
+- $action: the current action
+
+EndPrimaryNav: At the end of the primary nav menu
+- $action: the current action
+
+StartSecondaryNav: Showing the secondary nav menu
+- $action: the current action
+
+EndSecondaryNav: At the end of the secondary nav menu
+- $action: the current action
+
+StartShowStyles: Showing Style links; good place to add UA style resets
+- $action: the current action
+
+EndShowStyles: End showing Style links; good place to add custom styles
+- $action: the current action
+
+StartShowLaconicaStyles: Showing Laconica Style links
+- $action: the current action
+
+EndShowLaconicaStyles: End showing Laconica Style links; good place to add handheld or JavaScript dependant styles
+- $action: the current action
+
+StartShowUAStyles: Showing custom UA Style links
+- $action: the current action
+
+EndShowUAStyles: End showing custom UA Style links; good place to add user-agent (e.g., filter, -webkit, -moz) specific styles
+- $action: the current action
+
+StartShowScripts: Showing JavaScript links
+- $action: the current action
+
+EndShowScripts: End showing JavaScript links; good place to add custom
+ links like Google Analytics
+- $action: the current action
+
+StartShowJQueryScripts: Showing JQuery script links (use this to link to e.g. Google mirrors)
+- $action: the current action
+
+EndShowJQueryScripts: End showing JQuery script links
+- $action: the current action
+
+StartShowLaconicaScripts: Showing Laconica script links (use this to link to a CDN or something)
+- $action: the current action
+
+EndShowLaconicaScripts: End showing Laconica script links
+- $action: the current action
+
+StartShowSections: Start the list of sections in the sidebar
+- $action: the current action
+
+EndShowSections: End the list of sections in the sidebar
+- $action: the current action
+
+StartShowHeader: Showing before the header container
+- $action: the current action
+
+EndShowHeader: Showing after the header container
+- $action: the current action
+
+StartShowFooter: Showing before the footer container
+- $action: the current action
+
+EndShowFooter: Showing after the footer container
+- $action: the current action
+
+StartShowContentBlock: Showing before the content container
+- $action: the current action
+
+EndShowContentBlock: Showing after the content container
+- $action: the current action
+
+StartNoticeSave: before inserting a notice (good place for content filters)
+- $notice: notice being saved (no ID or URI)
+
+EndNoticeSave: after inserting a notice and related code
+- $notice: notice that was saved (with ID and URI)
+
+StartShowLocalNavBlock: Showing the local nav menu
+- $action: the current action
+
+EndShowLocalNavBlock: At the end of the local nav menu
+- $action: the current action
+
+StartShowHTML: Chance to set document headers (e.g., content type, charset, language), DOCTYPE and html element properties
+- $action: the current action
+
+EndShowHTML: Showing after the html element
+- $action: the current action
+
+StartPublicGroupNav: Showing the public group nav menu
+- $action: the current action
+
+EndPublicGroupNav: At the end of the public group nav menu
+- $action: the current action
+
+RouterInitialized: After the router instance has been initialized
+- $m: the Net_URL_Mapper that has just been set up
+
diff --git a/README b/README
index 989fcb7f0..ec2e2ec4f 100644
--- a/README
+++ b/README
@@ -236,21 +236,28 @@ especially if you've previously installed PHP/MySQL packages.
configure virtual hosts on your web server, you can try setting up
"http://micro.example.net/" or the like.
-3. You should also take this moment to make your avatar subdirectory
- writeable by the Web server. An insecure way to do this is:
+3. Make your target directory writeable by the Web server.
- chmod a+w /var/www/mublog/avatar
+ chmod a+w /var/www/mublog/
On some systems, this will probably work:
- chgrp www-data /var/www/mublog/avatar
- chmod g+w /var/www/mublog/avatar
+ chgrp www-data /var/www/mublog/
+ chmod g+w /var/www/mublog/
If your Web server runs as another user besides "www-data", try
that user's default group instead. As a last resort, you can create
- a new group like "avatar" and add the Web server's user to the group.
+ a new group like "mublog" and add the Web server's user to the group.
+
+4. You should also take this moment to make your avatar subdirectory
+ writeable by the Web server. An insecure way to do this is:
-4. Create a database to hold your microblog data. Something like this
+ chmod a+w /var/www/mublog/avatar
+
+ You can also make the avatar directory writeable by the Web server
+ group, as noted above.
+
+5. Create a database to hold your microblog data. Something like this
should work:
mysqladmin -u "username" --password="password" create laconica
@@ -263,63 +270,55 @@ especially if you've previously installed PHP/MySQL packages.
a tool like PHPAdmin to create a database. Check your hosting
service's documentation for how to create a new MySQL database.)
-5. Run the laconica.sql SQL script in the db subdirectory to create
- the database tables in the database. A typical system would work
- like this:
-
- mysql -u "username" --password="password" laconica < /var/www/mublog/db/laconica.sql
-
- You may want to test by logging into the database and checking that
- the tables were created. Here's an example:
-
- SHOW TABLES;
-
6. Create a new database account that Laconica will use to access the
database. If you have shell access, this will probably work from the
MySQL shell:
- GRANT SELECT,INSERT,DELETE,UPDATE on laconica.*
+ GRANT ALL on laconica.*
TO 'lacuser'@'localhost'
IDENTIFIED BY 'lacpassword';
You should change 'lacuser' and 'lacpassword' to your preferred new
- username and password. You may want to test logging in as this new
- user and testing that you can SELECT from some of the tables in the
- DB (use SHOW TABLES to see which ones are there).
-
-7. Copy the config.php.sample in the Laconica directory to config.php.
-
-8. Edit config.php to set the basic configuration for your system.
- (See descriptions below for basic config options.) Note that there
- are lots of options and if you try to do them all at once, you will
- have a hard time making sure what's working and what's not. So,
- stick with the basics at first. In particular, customizing the
- 'site' and 'db' settings will almost definitely be needed.
-
-9. At this point, you should be able to navigate in a browser to your
- microblog's main directory and see the "Public Timeline", which
- will be empty. If not, magic has happened! You can now register a
- new user, post some notices, edit your profile, etc. However, you
- may want to wait to do that stuff if you think you can set up
- "fancy URLs" (see below), since some URLs are stored in the database.
+ username and password. You may want to test logging in to MySQL as
+ this new user.
+
+7. In a browser, navigate to the Laconica install script; something like:
+
+ http://yourserver.example.com/mublog/install.php
+
+ Enter the database connection information and your site name. The
+ install program will configure your site and install the initial,
+ almost-empty database.
+
+8. You should now be able to navigate to your microblog's main directory
+ and see the "Public Timeline", which will be empty. If not, magic
+ has happened! You can now register a new user, post some notices,
+ edit your profile, etc. However, you may want to wait to do that stuff
+ if you think you can set up "fancy URLs" (see below), since some
+ URLs are stored in the database.
Fancy URLs
----------
-By default, Laconica will have big long sloppy URLs that are hard for
-people to remember or use. For example, a user's home profile might be
+By default, Laconica will use URLs that include the main PHP program's
+name in them. For example, a user's home profile might be
found at:
- http://example.org/mublog/index.php?action=showstream&nickname=fred
+ http://example.org/mublog/index.php/mublog/fred
+
+On certain systems that don't support this kind of syntax, they'll
+look like this:
+
+ http://example.org/mublog/index.php?p=mublog/fred
It's possible to configure the software so it looks like this instead:
http://example.org/mublog/fred
These "fancy URLs" are more readable and memorable for users. To use
-fancy URLs, you must either have Apache 2.2.x with .htaccess enabled
-and mod_redirect enabled, -OR- know how to configure "url redirection"
-in your server.
+fancy URLs, you must either have Apache 2.x with .htaccess enabled and
+mod_redirect enabled, -OR- know how to configure "url redirection" in
+your server.
1. Copy the htaccess.sample file to .htaccess in your Laconica
directory. Note: if you have control of your server's httpd.conf or
@@ -344,10 +343,6 @@ like:
If you changed your HTTP server configuration, you may need to restart
the server first.
-If you have problems with the .htaccess file on versions of Apache
-earlier than 2.2.x, try changing the regular expressions in the
-htaccess.sample file that use "\w" to just use ".".
-
Sphinx
------
@@ -507,7 +502,7 @@ server is probably a good idea for high-volume sites.
needs as a parameter the install path; if you run it from the
Laconica dir, "." should suffice.
-This will run six (for now) queue handlers:
+This will run eight (for now) queue handlers:
* xmppdaemon.php - listens for new XMPP messages from users and stores
them as notices in the database.
@@ -521,6 +516,10 @@ This will run six (for now) queue handlers:
of registered users.
* xmppconfirmhandler.php - sends confirmation messages to registered
users.
+* twitterqueuehandler.php - sends queued notices to Twitter for user
+ who have opted to set up Twitter bridging.
+* facebookqueuehandler.php - sends queued notices to Facebook for users
+ of the built-in Facebook application.
Note that these queue daemons are pretty raw, and need your care. In
particular, they leak memory, and you may want to restart them on a
@@ -553,6 +552,53 @@ Sample cron job:
# Update Twitter friends subscriptions every half hour
0,30 * * * * /path/to/php /path/to/laconica/scripts/synctwitterfriends.php>&/dev/null
+Built-in Facebook Application
+-----------------------------
+
+Laconica's Facebook application allows your users to automatically
+update their Facebook statuses with their latest notices, invite
+their friends to use the app (and thus your site), view their notice
+timelines, and post notices -- all from within Facebook. The application
+is built into Laconica and runs on your host. For automatic Facebook
+status updating to work you will need to enable queuing and run the
+facebookqueuehandler.php daemon (see the "Queues and daemons" section
+above).
+
+Quick setup instructions*:
+
+Install the Facebook Developer application on Facebook:
+
+ http://www.facebook.com/developers/
+
+Use it to create a new application and generate an API key and secret.
+Uncomment the Facebook app section of your config.php and copy in the
+key and secret, e.g.:
+
+ # Config section for the built-in Facebook application
+ $config['facebook']['apikey'] = 'APIKEY';
+ $config['facebook']['secret'] = 'SECRET';
+
+In Facebook's application editor, specify the following URLs for your app:
+
+- Callback URL: http://example.net/mublog/facebook/
+- Post-Remove URL: http://example.net/mublog/facebook/remove
+- Post-Add Redirect URL: http://apps.facebook.com/yourapp/
+- Canvas URL: http://apps.facebook.com/yourapp/
+
+(Replace 'example.net' with your host's URL, 'mublog' with the path
+to your Laconica installation, and 'yourapp' with the name of the
+Facebook application you created.)
+
+Additionally, Choose "Web" for Application type in the Advanced tab.
+In the "Canvas setting" section, choose the "FBML" for Render Method,
+"Smart Size" for IFrame size, and "Full width (760px)" for Canvas Width.
+Everything else can be left with default values.
+
+*For more detailed instructions please see the installation guide on the
+Laconica wiki:
+
+ http://laconi.ca/trac/wiki/FacebookApplication
+
Sitemaps
--------
@@ -593,7 +639,7 @@ to these resources.
Themes
------
-There are two themes shipped with this version of Laconica: "stoica",
+There are two themes shipped with this version of Laconica: "identica",
which is what the Identi.ca site uses, and "default", which is a good
basis for other sites.
@@ -833,6 +879,8 @@ notice: A plain string that will appear on every page. A good place
to put introductory information about your service, or info about
upgrades and outages, or other community info. Any HTML will
be escaped.
+dupelimit: Time in which it's not OK for the same person to post the
+ same notice; default = 60 seconds.
db
--
diff --git a/actions/all.php b/actions/all.php
index d75d1b946..8e67ec0f3 100644
--- a/actions/all.php
+++ b/actions/all.php
@@ -42,9 +42,9 @@ class AllAction extends Action
if (!$this->page) {
$this->page = 1;
}
-
+
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -69,13 +69,22 @@ class AllAction extends Action
}
}
- function showFeeds()
+ function getFeeds()
{
- $this->element('link', array('rel' => 'alternate',
- 'href' => common_local_url('allrss', array('nickname' =>
- $this->user->nickname)),
- 'type' => 'application/rss+xml',
- 'title' => sprintf(_('Feed for friends of %s'), $this->user->nickname)));
+ return array(new Feed(Feed::RSS1,
+ common_local_url('allrss', array('nickname' =>
+ $this->user->nickname)),
+ sprintf(_('Feed for friends of %s (RSS 1.0)'), $this->user->nickname)),
+ new Feed(Feed::RSS2,
+ common_local_url('api', array('apiaction' => 'statuses',
+ 'method' => 'friends_timeline',
+ 'argument' => $this->user->nickname.'.rss')),
+ sprintf(_('Feed for friends of %s (RSS 2.0)'), $this->user->nickname)),
+ new Feed(Feed::ATOM,
+ common_local_url('api', array('apiaction' => 'statuses',
+ 'method' => 'friends_timeline',
+ 'argument' => $this->user->nickname.'.atom')),
+ sprintf(_('Feed for friends of %s (Atom)'), $this->user->nickname)));
}
function showLocalNav()
@@ -84,15 +93,6 @@ class AllAction extends Action
$nav->show();
}
- function showExportData()
- {
- $fl = new FeedList($this);
- $fl->show(array(0=>array('href'=>common_local_url('allrss', array('nickname' => $this->user->nickname)),
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'allrss')));
- }
-
function showContent()
{
$notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
@@ -110,7 +110,7 @@ class AllAction extends Action
$user =& common_current_user();
if ($user && ($user->id == $this->user->id)) {
$this->element('h1', NULL, _("You and friends"));
- } else {
+ } else {
$this->element('h1', NULL, sprintf(_('%s and friends'), $this->user->nickname));
}
}
diff --git a/actions/allrss.php b/actions/allrss.php
index 05787f3f7..0114c4396 100644
--- a/actions/allrss.php
+++ b/actions/allrss.php
@@ -53,7 +53,9 @@ class AllrssAction extends Rss10Action
/**
* Initialization.
- *
+ *
+ * @param array $args Web and URL arguments
+ *
* @return boolean false if user doesn't exist
*/
function prepare($args)
@@ -81,7 +83,7 @@ class AllrssAction extends Rss10Action
{
$user = $this->user;
$notice = $user->noticesWithFriends(0, $limit);
-
+
while ($notice->fetch()) {
$notices[] = clone($notice);
}
@@ -104,7 +106,8 @@ class AllrssAction extends Rss10Action
'link' => common_local_url('all',
array('nickname' =>
$user->nickname)),
- 'description' => sprintf(_('Feed for friends of %s'), $user->nickname));
+ 'description' => sprintf(_('Feed for friends of %s'),
+ $user->nickname));
return $c;
}
@@ -123,10 +126,5 @@ class AllrssAction extends Rss10Action
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
return $avatar ? $avatar->url : null;
}
-
- function isReadOnly()
- {
- return true;
- }
}
diff --git a/actions/api.php b/actions/api.php
index 21fe4eea3..c18d551b6 100644
--- a/actions/api.php
+++ b/actions/api.php
@@ -127,18 +127,20 @@ class ApiAction extends Action
'laconica/wadl');
static $bareauth = array('statuses/user_timeline',
+ 'statuses/friends_timeline',
'statuses/friends',
+ 'statuses/replies',
'statuses/followers',
'favorites/favorites');
- # If the site is "private", all API methods need authentication
-
+ $fullname = "$this->api_action/$this->api_method";
+
+ // If the site is "private", all API methods except laconica/config
+ // need authentication
if (common_config('site', 'private')) {
- return true;
+ return $fullname != 'laconica/config' || false;
}
- $fullname = "$this->api_action/$this->api_method";
-
if (in_array($fullname, $bareauth)) {
# bareauth: only needs auth if without an argument
if ($this->api_arg) {
diff --git a/actions/avatarsettings.php b/actions/avatarsettings.php
index 7dd53f6eb..c2bb35a39 100644
--- a/actions/avatarsettings.php
+++ b/actions/avatarsettings.php
@@ -145,6 +145,7 @@ class AvatarsettingsAction extends AccountSettingsAction
'height' => AVATAR_PROFILE_SIZE,
'alt' => $user->nickname));
$this->elementEnd('div');
+ $this->submit('delete', _('Delete'));
$this->elementEnd('li');
}
@@ -256,6 +257,8 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->uploadAvatar();
} else if ($this->arg('crop')) {
$this->cropAvatar();
+ } else if ($this->arg('delete')) {
+ $this->deleteAvatar();
} else {
$this->showForm(_('Unexpected form submission.'));
}
@@ -321,13 +324,14 @@ class AvatarsettingsAction extends AccountSettingsAction
return;
}
- // If image is not being cropped assume pos & dimentions of original
+ $file_d = ($filedata['width'] > $filedata['height'])
+ ? $filedata['height'] : $filedata['width'];
+
$dest_x = $this->arg('avatar_crop_x') ? $this->arg('avatar_crop_x'):0;
$dest_y = $this->arg('avatar_crop_y') ? $this->arg('avatar_crop_y'):0;
- $dest_w = $this->arg('avatar_crop_w') ? $this->arg('avatar_crop_w'):$filedata['width'];
- $dest_h = $this->arg('avatar_crop_h') ? $this->arg('avatar_crop_h'):$filedata['height'];
- $size = min($dest_w, $dest_h);
- $size = ($size > MAX_ORIGINAL) ? MAX_ORIGINAL:$size;
+ $dest_w = $this->arg('avatar_crop_w') ? $this->arg('avatar_crop_w'):$file_d;
+ $dest_h = $this->arg('avatar_crop_h') ? $this->arg('avatar_crop_h'):$file_d;
+ $size = min($dest_w, $dest_h, MAX_ORIGINAL);
$user = common_current_user();
$profile = $user->getProfile();
@@ -340,10 +344,34 @@ class AvatarsettingsAction extends AccountSettingsAction
unset($_SESSION['FILEDATA']);
$this->mode = 'upload';
$this->showForm(_('Avatar updated.'), true);
+ common_broadcast_profile($profile);
} else {
$this->showForm(_('Failed updating avatar.'));
}
}
+
+ /**
+ * Get rid of the current avatar.
+ *
+ * @return void
+ */
+
+ function deleteAvatar()
+ {
+ $user = common_current_user();
+ $profile = $user->getProfile();
+
+ $avatar = $profile->getOriginalAvatar();
+ $avatar->delete();
+ $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
+ $avatar->delete();
+ $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
+ $avatar->delete();
+ $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
+ $avatar->delete();
+
+ $this->showForm(_('Avatar deleted.'), true);
+ }
/**
* Add the jCrop stylesheet
diff --git a/actions/doc.php b/actions/doc.php
index 6957659ad..ebffb7c15 100644
--- a/actions/doc.php
+++ b/actions/doc.php
@@ -50,7 +50,7 @@ class DocAction extends Action
/**
* Class handler.
- *
+ *
* @param array $args array of arguments
*
* @return nothing
@@ -59,7 +59,7 @@ class DocAction extends Action
{
parent::handle($args);
$this->title = $this->trimmed('title');
- $this->filename = INSTALLDIR.'/doc/'.$this->title;
+ $this->filename = INSTALLDIR.'/doc-src/'.$this->title;
if (!file_exists($this->filename)) {
$this->clientError(_('No such document.'));
return;
@@ -71,14 +71,14 @@ class DocAction extends Action
function showPageTitle() {
$this->element('h1', array('class' => 'entry-title'), $this->title());
}
-
+
// overrided to add hentry, and content-inner classes
function showContentBlock()
{
$this->elementStart('div', array('id' => 'content', 'class' => 'hentry'));
$this->showPageTitle();
$this->showPageNoticeBlock();
- $this->elementStart('div', array('id' => 'content_inner',
+ $this->elementStart('div', array('id' => 'content_inner',
'class' => 'entry-content'));
// show the actual content (forms, lists, whatever)
$this->showContent();
@@ -88,7 +88,7 @@ class DocAction extends Action
/**
* Display content.
- *
+ *
* @return nothing
*/
function showContent()
@@ -100,7 +100,7 @@ class DocAction extends Action
/**
* Page title.
- *
+ *
* @return page title
*/
function title()
diff --git a/actions/editgroup.php b/actions/editgroup.php
index 98ebcb87a..e7e79040a 100644
--- a/actions/editgroup.php
+++ b/actions/editgroup.php
@@ -191,13 +191,13 @@ class EditgroupAction extends Action
array('http', 'https')))) {
$this->showForm(_('Homepage is not a valid URL.'));
return;
- } else if (!is_null($fullname) && strlen($fullname) > 255) {
+ } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
$this->showForm(_('Full name is too long (max 255 chars).'));
return;
- } else if (!is_null($description) && strlen($description) > 140) {
+ } else if (!is_null($description) && mb_strlen($description) > 140) {
$this->showForm(_('description is too long (max 140 chars).'));
return;
- } else if (!is_null($location) && strlen($location) > 255) {
+ } else if (!is_null($location) && mb_strlen($location) > 255) {
$this->showForm(_('Location is too long (max 255 chars).'));
return;
}
diff --git a/actions/emailsettings.php b/actions/emailsettings.php
index b84acb214..634388fdd 100644
--- a/actions/emailsettings.php
+++ b/actions/emailsettings.php
@@ -164,6 +164,11 @@ class EmailsettingsAction extends AccountSettingsAction
$user->emailnotifymsg);
$this->elementEnd('li');
$this->elementStart('li');
+ $this->checkbox('emailnotifyattn',
+ _('Send me email when someone sends me an "@-reply".'),
+ $user->emailnotifyattn);
+ $this->elementEnd('li');
+ $this->elementStart('li');
$this->checkbox('emailnotifynudge',
_('Allow friends to nudge me and send me an email.'),
$user->emailnotifynudge);
@@ -255,6 +260,7 @@ class EmailsettingsAction extends AccountSettingsAction
$emailnotifyfav = $this->boolean('emailnotifyfav');
$emailnotifymsg = $this->boolean('emailnotifymsg');
$emailnotifynudge = $this->boolean('emailnotifynudge');
+ $emailnotifyattn = $this->boolean('emailnotifyattn');
$emailmicroid = $this->boolean('emailmicroid');
$emailpost = $this->boolean('emailpost');
@@ -270,6 +276,7 @@ class EmailsettingsAction extends AccountSettingsAction
$user->emailnotifyfav = $emailnotifyfav;
$user->emailnotifymsg = $emailnotifymsg;
$user->emailnotifynudge = $emailnotifynudge;
+ $user->emailnotifyattn = $emailnotifyattn;
$user->emailmicroid = $emailmicroid;
$user->emailpost = $emailpost;
diff --git a/actions/facebookinvite.php b/actions/facebookinvite.php
index 3c872f94b..1302064ad 100644
--- a/actions/facebookinvite.php
+++ b/actions/facebookinvite.php
@@ -71,13 +71,13 @@ class FacebookinviteAction extends FacebookAction
common_config('site', 'name')));
$this->element('p', null, _('Invitations have been sent to the following users:'));
- $friend_ids = $_POST['ids']; // XXX: Hmm... is this the best way to acces the list?
+ $friend_ids = $_POST['ids']; // XXX: Hmm... is this the best way to access the list?
$this->elementStart('ul', array('id' => 'facebook-friends'));
foreach ($friend_ids as $friend) {
$this->elementStart('li');
- $this->element('fb:profile-pic', array('uid' => $friend));
+ $this->element('fb:profile-pic', array('uid' => $friend, 'size' => 'square'));
$this->element('fb:name', array('uid' => $friend,
'capitalize' => 'true'));
$this->elementEnd('li');
@@ -92,6 +92,12 @@ class FacebookinviteAction extends FacebookAction
// Get a list of users who are already using the app for exclusion
$exclude_ids = $this->facebook->api_client->friends_getAppUsers();
+ $exclude_ids_csv = null;
+
+ // fbml needs these as a csv string, not an array
+ if ($exclude_ids) {
+ $exclude_ids_csv = implode(',', $exclude_ids);
+ }
$content = sprintf(_('You have been invited to %s'), common_config('site', 'name')) .
htmlentities('<fb:req-choice url="' . $this->app_uri . '" label="Add"/>');
@@ -103,10 +109,17 @@ class FacebookinviteAction extends FacebookAction
'content' => $content));
$this->hidden('invite', 'true');
$actiontext = sprintf(_('Invite your friends to use %s'), common_config('site', 'name'));
- $this->element('fb:multi-friend-selector', array('showborder' => 'false',
- 'actiontext' => $actiontext,
- 'exclude_ids' => implode(',', $exclude_ids),
- 'bypass' => 'cancel'));
+
+ $multi_params = array('showborder' => 'false');
+ $multi_params['actiontext'] = $actiontext;
+
+ if ($exclude_ids_csv) {
+ $multi_params['exclude_ids'] = $exclude_ids_csv;
+ }
+
+ $multi_params['bypass'] = 'cancel';
+
+ $this->element('fb:multi-friend-selector', $multi_params);
$this->elementEnd('fb:request-form');
diff --git a/actions/favorited.php b/actions/favorited.php
index fd5ff413c..5082f4a4e 100644
--- a/actions/favorited.php
+++ b/actions/favorited.php
@@ -169,8 +169,14 @@ class FavoritedAction extends Action
function showContent()
{
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - fave.modified) / %s))';
+ }
+
$qry = 'SELECT notice.*, '.
- 'sum(exp(-(now() - fave.modified) / %s)) as weight ' .
+ $weightexpr . ' as weight ' .
'FROM notice JOIN fave ON notice.id = fave.notice_id ' .
'GROUP BY fave.notice_id ' .
'ORDER BY weight DESC';
diff --git a/actions/finishopenidlogin.php b/actions/finishopenidlogin.php
index bc9151120..6d92cb9aa 100644
--- a/actions/finishopenidlogin.php
+++ b/actions/finishopenidlogin.php
@@ -83,7 +83,7 @@ class FinishopenidloginAction extends Action
function showContent()
{
- if ($this->message_text) {
+ if (!empty($this->message_text)) {
$this->element('p', null, $this->message);
return;
}
@@ -232,7 +232,8 @@ class FinishopenidloginAction extends Action
return;
}
- if ($sreg['country']) {
+ $location = '';
+ if (!empty($sreg['country'])) {
if ($sreg['postcode']) {
# XXX: use postcode to get city and region
# XXX: also, store postcode somewhere -- it's valuable!
@@ -242,12 +243,16 @@ class FinishopenidloginAction extends Action
}
}
- if ($sreg['fullname'] && strlen($sreg['fullname']) <= 255) {
+ if (!empty($sreg['fullname']) && mb_strlen($sreg['fullname']) <= 255) {
$fullname = $sreg['fullname'];
+ } else {
+ $fullname = '';
}
- if ($sreg['email'] && Validate::email($sreg['email'], true)) {
+ if (!empty($sreg['email']) && Validate::email($sreg['email'], true)) {
$email = $sreg['email'];
+ } else {
+ $email = '';
}
# XXX: add language
@@ -328,7 +333,7 @@ class FinishopenidloginAction extends Action
# Try the passed-in nickname
- if ($sreg['nickname']) {
+ if (!empty($sreg['nickname'])) {
$nickname = $this->nicknamize($sreg['nickname']);
if ($this->isNewNickname($nickname)) {
return $nickname;
@@ -337,7 +342,7 @@ class FinishopenidloginAction extends Action
# Try the full name
- if ($sreg['fullname']) {
+ if (!empty($sreg['fullname'])) {
$fullname = $this->nicknamize($sreg['fullname']);
if ($this->isNewNickname($fullname)) {
return $fullname;
diff --git a/actions/finishremotesubscribe.php b/actions/finishremotesubscribe.php
index f9094a50c..acfacbdc1 100644
--- a/actions/finishremotesubscribe.php
+++ b/actions/finishremotesubscribe.php
@@ -237,7 +237,13 @@ class FinishremotesubscribeAction extends Action
{
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
copy($url, $temp_filename);
- return $profile->setOriginal($temp_filename);
+ $imagefile = new ImageFile($profile->id, $temp_filename);
+ $filename = Avatar::filename($profile->id,
+ image_type_to_extension($imagefile->type),
+ null,
+ common_timestamp());
+ rename($temp_filename, Avatar::path($filename));
+ return $profile->setOriginal($filename);
}
function access_token($omb)
@@ -277,7 +283,7 @@ class FinishremotesubscribeAction extends Action
$fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
$result = $fetcher->post($req->get_normalized_http_url(),
$req->to_postdata(),
- array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
+ array('User-Agent: Laconica/' . LACONICA_VERSION));
common_debug('got result: "'.print_r($result,true).'"', __FILE__);
diff --git a/actions/grouprss.php b/actions/grouprss.php
index 1a7b858b1..de76a5960 100644
--- a/actions/grouprss.php
+++ b/actions/grouprss.php
@@ -111,13 +111,13 @@ class groupRssAction extends Rss10Action
{
$group = $this->group;
-
+
if (is_null($group)) {
return null;
}
-
+
$notice = $group->getNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit);
-
+
while ($notice->fetch()) {
$notices[] = clone($notice);
}
@@ -141,13 +141,4 @@ class groupRssAction extends Rss10Action
{
return $this->group->homepage_logo;
}
-
- # override parent to add X-SUP-ID URL
-
- function initRss($limit=0)
- {
- $url = common_local_url('sup', null, $this->group->id);
- header('X-SUP-ID: '.$url);
- parent::initRss($limit);
- }
}
diff --git a/actions/login.php b/actions/login.php
index 71e467929..b049791fb 100644
--- a/actions/login.php
+++ b/actions/login.php
@@ -108,13 +108,15 @@ class LoginAction extends Action
$nickname = common_canonical_nickname($this->trimmed('nickname'));
$password = $this->arg('password');
- if (!common_check_user($nickname, $password)) {
+ $user = common_check_user($nickname, $password);
+
+ if (!$user) {
$this->showForm(_('Incorrect username or password.'));
return;
}
// success!
- if (!common_set_user($nickname)) {
+ if (!common_set_user($user)) {
$this->serverError(_('Error setting user.'));
return;
}
diff --git a/actions/newgroup.php b/actions/newgroup.php
index 42fd380df..cbd8dfeec 100644
--- a/actions/newgroup.php
+++ b/actions/newgroup.php
@@ -142,13 +142,13 @@ class NewgroupAction extends Action
array('http', 'https')))) {
$this->showForm(_('Homepage is not a valid URL.'));
return;
- } else if (!is_null($fullname) && strlen($fullname) > 255) {
+ } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
$this->showForm(_('Full name is too long (max 255 chars).'));
return;
- } else if (!is_null($description) && strlen($description) > 140) {
+ } else if (!is_null($description) && mb_strlen($description) > 140) {
$this->showForm(_('description is too long (max 140 chars).'));
return;
- } else if (!is_null($location) && strlen($location) > 255) {
+ } else if (!is_null($location) && mb_strlen($location) > 255) {
$this->showForm(_('Location is too long (max 255 chars).'));
return;
}
diff --git a/actions/newmessage.php b/actions/newmessage.php
index f83015a37..82276ff34 100644
--- a/actions/newmessage.php
+++ b/actions/newmessage.php
@@ -201,7 +201,7 @@ class NewmessageAction extends Action
function showNoticeForm()
{
- $message_form = new MessageForm($this, $this->to, $this->content);
+ $message_form = new MessageForm($this, $this->other, $this->content);
$message_form->show();
}
}
diff --git a/actions/newnotice.php b/actions/newnotice.php
index 5e7691f33..cbd04c58b 100644
--- a/actions/newnotice.php
+++ b/actions/newnotice.php
@@ -98,7 +98,12 @@ class NewnoticeAction extends Action
return;
}
- $this->saveNewNotice();
+ try {
+ $this->saveNewNotice();
+ } catch (Exception $e) {
+ $this->showForm($e->getMessage());
+ return;
+ }
} else {
$this->showForm();
}
@@ -123,15 +128,13 @@ class NewnoticeAction extends Action
$content = $this->trimmed('status_textarea');
if (!$content) {
- $this->showForm(_('No content!'));
- return;
+ $this->clientError(_('No content!'));
} else {
$content_shortened = common_shorten_links($content);
if (mb_strlen($content_shortened) > 140) {
- $this->showForm(_('That\'s too long. '.
- 'Max notice size is 140 chars.'));
- return;
+ $this->clientError(_('That\'s too long. '.
+ 'Max notice size is 140 chars.'));
}
}
@@ -149,12 +152,17 @@ class NewnoticeAction extends Action
}
$replyto = $this->trimmed('inreplyto');
+ #If an ID of 0 is wrongly passed here, it will cause a database error,
+ #so override it...
+ if ($replyto == 0) {
+ $replyto = 'false';
+ }
$notice = Notice::saveNew($user->id, $content, 'web', 1,
($replyto == 'false') ? null : $replyto);
if (is_string($notice)) {
- $this->showForm($notice);
+ $this->clientError($notice);
return;
}
@@ -250,7 +258,7 @@ class NewnoticeAction extends Action
}
}
- $notice_form = new NoticeForm($this, $content);
+ $notice_form = new NoticeForm($this, '', $content);
$notice_form->show();
}
diff --git a/actions/noticesearch.php b/actions/noticesearch.php
index a5f01350c..83e59dd9a 100644
--- a/actions/noticesearch.php
+++ b/actions/noticesearch.php
@@ -57,11 +57,11 @@ class NoticesearchAction extends SearchAction
return true;
}
-
+
/**
* Get instructions
- *
- * @return string instruction text
+ *
+ * @return string instruction text
*/
function getInstructions()
{
@@ -70,7 +70,7 @@ class NoticesearchAction extends SearchAction
/**
* Get title
- *
+ *
* @return string title
*/
function title()
@@ -78,6 +78,20 @@ class NoticesearchAction extends SearchAction
return _('Text search');
}
+ function getFeeds()
+ {
+ $q = $this->trimmed('q');
+
+ if (!$q) {
+ return null;
+ }
+
+ return array(new Feed(Feed::RSS1, common_local_url('noticesearchrss',
+ array('q' => $q)),
+ sprintf(_('Search results for "%s" on %s'),
+ $q, common_config('site', 'name'))));
+ }
+
/**
* Show results
*
@@ -99,143 +113,58 @@ class NoticesearchAction extends SearchAction
} else {
$cnt = $notice->find();
}
- if ($cnt > 0) {
- $terms = preg_split('/[\s,]+/', $q);
- $this->elementStart('ul', array('class' => 'notices'));
- for ($i = 0; $i < min($cnt, NOTICES_PER_PAGE); $i++) {
- if ($notice->fetch()) {
- $this->showNotice($notice, $terms);
- } else {
- // shouldn't happen!
- break;
- }
- }
- $this->elementEnd('ul');
- } else {
+ if ($cnt === 0) {
$this->element('p', 'error', _('No results'));
+ return;
}
+ $terms = preg_split('/[\s,]+/', $q);
+ $nl = new SearchNoticeList($notice, $this, $terms);
- $this->pagination($page > 1, $cnt > NOTICES_PER_PAGE,
- $page, 'noticesearch', array('q' => $q));
- }
+ $cnt = $nl->show();
- /**
- * Show header
- *
- * @param array $arr array containing the query
- *
- * @return void
- */
+ $this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
+ $this->page, 'noticesearch', array('q' => $q));
+ }
+ function isReadOnly()
+ {
+ return true;
+ }
+}
- function extraHead()
+class SearchNoticeList extends NoticeList {
+ function __construct($notice, $out=null, $terms)
{
- $q = $this->trimmed('q');
- if ($q) {
- $this->element('link', array('rel' => 'alternate',
- 'href' => common_local_url('noticesearchrss',
- array('q' => $q)),
- 'type' => 'application/rss+xml',
- 'title' => _('Search Stream Feed')));
- }
+ parent::__construct($notice, $out);
+ $this->terms = $terms;
}
- /**
- * Show notice
- *
- * @param class $notice notice
- * @param array $terms terms to highlight
- *
- * @return void
- *
- * @todo refactor and combine with StreamAction::showNotice()
- */
- function showNotice($notice, $terms)
+ function newListItem($notice)
{
- $profile = $notice->getProfile();
- if (!$profile) {
- common_log_db_error($notice, 'SELECT', __FILE__);
- $this->serverError(_('Notice without matching profile'));
- return;
- }
- // XXX: RDFa
- $this->elementStart('li', array('class' => 'hentry notice',
- 'id' => 'notice-' . $notice->id));
+ return new SearchNoticeListItem($notice, $this->out, $this->terms);
+ }
+}
- $this->elementStart('div', 'entry-title');
- $this->elementStart('span', 'vcard author');
- $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
- $this->elementStart('a', array('href' => $profile->profileurl,
- 'class' => 'url'));
- $this->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE),
- 'class' => 'avatar photo',
- 'width' => AVATAR_STREAM_SIZE,
- 'height' => AVATAR_STREAM_SIZE,
- 'alt' =>
- ($profile->fullname) ? $profile->fullname :
- $profile->nickname));
- $this->element('span', 'nickname fn', $profile->nickname);
- $this->elementEnd('a');
- $this->elementEnd('span');
+class SearchNoticeListItem extends NoticeListItem {
+ function __construct($notice, $out=null, $terms)
+ {
+ parent::__construct($notice, $out);
+ $this->terms = $terms;
+ }
+ function showContent()
+ {
// FIXME: URL, image, video, audio
- $this->elementStart('p', array('class' => 'entry-content'));
- if ($notice->rendered) {
- $this->raw($this->highlight($notice->rendered, $terms));
+ $this->out->elementStart('p', array('class' => 'entry-content'));
+ if ($this->notice->rendered) {
+ $this->out->raw($this->highlight($this->notice->rendered, $this->terms));
} else {
// XXX: may be some uncooked notices in the DB,
// we cook them right now. This should probably disappear in future
// versions (>> 0.4.x)
- $this->raw($this->highlight(common_render_content($notice->content, $notice), $terms));
- }
- $this->elementEnd('p');
- $this->elementEnd('div');
-
- $noticeurl = common_local_url('shownotice', array('notice' => $notice->id));
- $this->elementStart('div', 'entry-content');
- $this->elementStart('dl', 'timestamp');
- $this->element('dt', null, _('Published'));
- $this->elementStart('dd', null);
- $this->elementStart('a', array('rel' => 'bookmark',
- 'href' => $noticeurl));
- $dt = common_date_iso8601($notice->created);
- $this->element('abbr', array('class' => 'published',
- 'title' => $dt),
- common_date_string($notice->created));
- $this->elementEnd('a');
- $this->elementEnd('dd');
- $this->elementEnd('dl');
-
- if ($notice->reply_to) {
- $replyurl = common_local_url('shownotice',
- array('notice' => $this->notice->reply_to));
- $this->elementStart('dl', 'response');
- $this->element('dt', null, _('To'));
- $this->elementStart('dd');
- $this->element('a', array('href' => $replyurl,
- 'rel' => 'in-reply-to'),
- _('in reply to'));
- $this->elementEnd('dd');
- $this->elementEnd('dl');
+ $this->out->raw($this->highlight(common_render_content($this->notice->content, $this->notice), $this->terms));
}
- $this->elementEnd('div');
-
- $this->elementStart('div', 'notice-options');
+ $this->out->elementEnd('p');
- $reply_url = common_local_url('newnotice',
- array('replyto' => $profile->nickname));
-
- $this->elementStart('dl', 'notice_reply');
- $this->element('dt', null, _('Reply to this notice'));
- $this->elementStart('dd');
- $this->elementStart('a', array('href' => $reply_url,
- 'title' => _('Reply to this notice')));
- $this->text(_('Reply'));
- $this->element('span', 'notice_id', $notice->id);
- $this->elementEnd('a');
- $this->elementEnd('dd');
- $this->elementEnd('dl');
- $this->elementEnd('div');
- $this->elementEnd('li');
}
/**
@@ -248,7 +177,7 @@ class NoticesearchAction extends SearchAction
*/
function highlight($text, $terms)
{
- /* Highligh serach terms */
+ /* Highligh search terms */
$pattern = '/('.implode('|', array_map('htmlspecialchars', $terms)).')/i';
$result = preg_replace($pattern, '<strong>\\1</strong>', $text);
@@ -259,10 +188,5 @@ class NoticesearchAction extends SearchAction
} while ($count);
return $result;
}
-
- function isReadOnly()
- {
- return true;
- }
}
diff --git a/actions/peoplesearch.php b/actions/peoplesearch.php
index 615201c46..14177fcf0 100644
--- a/actions/peoplesearch.php
+++ b/actions/peoplesearch.php
@@ -86,33 +86,9 @@ class PeoplesearchAction extends SearchAction
}
$profile->free();
-
+
$this->pagination($page > 1, $cnt > PROFILES_PER_PAGE,
$page, 'peoplesearch', array('q' => $q));
}
}
-class PeopleSearchResults extends ProfileList
-{
- var $terms = null;
- var $pattern = null;
-
- function __construct($profile, $terms, $action)
- {
- parent::__construct($profile, $terms, $action);
- $this->terms = array_map('preg_quote',
- array_map('htmlspecialchars', $terms));
- $this->pattern = '/('.implode('|',$terms).')/i';
- }
-
- function highlight($text)
- {
- return preg_replace($this->pattern, '<strong>\\1</strong>', htmlspecialchars($text));
- }
-
- function isReadOnly()
- {
- return true;
- }
-}
-
diff --git a/actions/peopletag.php b/actions/peopletag.php
index 3578c53fd..6b1e34f1a 100644
--- a/actions/peopletag.php
+++ b/actions/peopletag.php
@@ -1,9 +1,12 @@
<?php
-/*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+/**
+ * Laconica, the distributed open-source microblogging tool
*
- * This program is free software: you can redistribute it and/or modify
+ * Action for showing profiles self-tagged with a given tag
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
@@ -15,78 +18,131 @@
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once INSTALLDIR.'/lib/profilelist.php';
+/**
+ * This class outputs a paginated list of profiles self-tagged with a given tag
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Action
+ */
+
class PeopletagAction extends Action
{
-
- var $tag = null;
+
+ var $tag = null;
var $page = null;
-
- function handle($args)
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+ function prepare($argarray)
{
- parent::handle($args);
-
- parent::prepare($args);
+ parent::prepare($argarray);
$this->tag = $this->trimmed('tag');
if (!common_valid_profile_tag($this->tag)) {
- $this->clientError(sprintf(_('Not a valid people tag: %s'), $this->tag));
+ $this->clientError(sprintf(_('Not a valid people tag: %s'),
+ $this->tag));
return;
}
- $this->page = $this->trimmed('page');
+ $this->page = ($this->arg('page')) ? $this->arg('page') : 1;
- if (!$this->page) {
- $this->page = 1;
- }
-
+ common_set_returnto($this->selfUrl());
+
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return boolean is read only action?
+ */
+ function handle($argarray)
+ {
+ parent::handle($argarray);
$this->showPage();
}
-
+
+ /**
+ * Whips up a query to get a list of profiles based on the provided
+ * people tag and page, initalizes a ProfileList widget, and displays
+ * it to the user.
+ *
+ * @return nothing
+ */
function showContent()
{
-
+
$profile = new Profile();
- $offset = ($page-1)*PROFILES_PER_PAGE;
- $limit = PROFILES_PER_PAGE + 1;
-
- if (common_config('db','type') == 'pgsql') {
+ $offset = ($this->page - 1) * PROFILES_PER_PAGE;
+ $limit = PROFILES_PER_PAGE + 1;
+
+ if (common_config('db', 'type') == 'pgsql') {
$lim = ' LIMIT ' . $limit . ' OFFSET ' . $offset;
} else {
$lim = ' LIMIT ' . $offset . ', ' . $limit;
}
- # XXX: memcached this
-
+ // XXX: memcached this
+
$qry = 'SELECT profile.* ' .
'FROM profile JOIN profile_tag ' .
'ON profile.id = profile_tag.tagger ' .
'WHERE profile_tag.tagger = profile_tag.tagged ' .
'AND tag = "%s" ' .
- 'ORDER BY profile_tag.modified DESC';
-
+ 'ORDER BY profile_tag.modified DESC%s';
+
$profile->query(sprintf($qry, $this->tag, $lim));
- $pl = new ProfileList($profile, null, $this);
+ $pl = new ProfileList($profile, null, $this);
$cnt = $pl->show();
-
+
$this->pagination($this->page > 1,
$cnt > PROFILES_PER_PAGE,
$this->page,
- $this->trimmed('action'),
+ 'peopletag',
array('tag' => $this->tag));
}
-
- function title()
+
+ /**
+ * Returns the page title
+ *
+ * @return string page title
+ */
+ function title()
{
- return sprintf( _('Users self-tagged with %s - page %d'), $this->tag, $this->page);
+ return sprintf(_('Users self-tagged with %s - page %d'),
+ $this->tag, $this->page);
}
-
+
}
diff --git a/actions/profilesettings.php b/actions/profilesettings.php
index 82e6c3c82..60f7c0796 100644
--- a/actions/profilesettings.php
+++ b/actions/profilesettings.php
@@ -198,13 +198,13 @@ class ProfilesettingsAction extends AccountSettingsAction
!Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) {
$this->showForm(_('Homepage is not a valid URL.'));
return;
- } else if (!is_null($fullname) && strlen($fullname) > 255) {
+ } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
$this->showForm(_('Full name is too long (max 255 chars).'));
return;
- } else if (!is_null($bio) && strlen($bio) > 140) {
+ } else if (!is_null($bio) && mb_strlen($bio) > 140) {
$this->showForm(_('Bio is too long (max 140 chars).'));
return;
- } else if (!is_null($location) && strlen($location) > 255) {
+ } else if (!is_null($location) && mb_strlen($location) > 255) {
$this->showForm(_('Location is too long (max 255 chars).'));
return;
} else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) {
diff --git a/actions/public.php b/actions/public.php
index cc6537f74..a20ae4032 100644
--- a/actions/public.php
+++ b/actions/public.php
@@ -73,9 +73,9 @@ class PublicAction extends Action
{
parent::prepare($args);
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
-
+
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -119,12 +119,20 @@ class PublicAction extends Action
* @return void
*/
- function showFeeds()
+ function getFeeds()
{
- $this->element('link', array('rel' => 'alternate',
- 'href' => common_local_url('publicrss'),
- 'type' => 'application/rss+xml',
- 'title' => _('Public Stream Feed')));
+ return array(new Feed(Feed::RSS1, common_local_url('publicrss'),
+ _('Public Stream Feed (RSS 1.0)')),
+ new Feed(Feed::RSS2,
+ common_local_url('api',
+ array('apiaction' => 'statuses',
+ 'method' => 'public_timeline.rss')),
+ _('Public Stream Feed (RSS 2.0)')),
+ new Feed(Feed::ATOM,
+ common_local_url('api',
+ array('apiaction' => 'statuses',
+ 'method' => 'public_timeline.atom')),
+ _('Public Stream Feed (Atom)')));
}
/**
@@ -185,27 +193,6 @@ class PublicAction extends Action
$this->page, 'public');
}
- /**
- * Makes a list of exported feeds for this page
- *
- * @return void
- *
- * @todo I18N
- */
-
- function showExportData()
- {
- $fl = new FeedList($this);
- $fl->show(array(0 => array('href' => common_local_url('publicrss'),
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'publicrss'),
- 1 => array('href' => common_local_url('publicatom'),
- 'type' => 'atom',
- 'version' => 'Atom 1.0',
- 'item' => 'publicatom')));
- }
-
function showSections()
{
// $top = new TopPostersSection($this);
diff --git a/actions/register.php b/actions/register.php
index 01d94f488..5d7a8ce69 100644
--- a/actions/register.php
+++ b/actions/register.php
@@ -167,13 +167,13 @@ class RegisterAction extends Action
array('http', 'https')))) {
$this->showForm(_('Homepage is not a valid URL.'));
return;
- } else if (!is_null($fullname) && strlen($fullname) > 255) {
+ } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
$this->showForm(_('Full name is too long (max 255 chars).'));
return;
- } else if (!is_null($bio) && strlen($bio) > 140) {
+ } else if (!is_null($bio) && mb_strlen($bio) > 140) {
$this->showForm(_('Bio is too long (max 140 chars).'));
return;
- } else if (!is_null($location) && strlen($location) > 255) {
+ } else if (!is_null($location) && mb_strlen($location) > 255) {
$this->showForm(_('Location is too long (max 255 chars).'));
return;
} else if (strlen($password) < 6) {
diff --git a/actions/remotesubscribe.php b/actions/remotesubscribe.php
index f727a63b8..7ea7acd6d 100644
--- a/actions/remotesubscribe.php
+++ b/actions/remotesubscribe.php
@@ -321,8 +321,7 @@ class RemotesubscribeAction extends Action
$result = $fetcher->post($req->get_normalized_http_url(),
$req->to_postdata(),
- array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
-
+ array('User-Agent: Laconica/' . LACONICA_VERSION));
if ($result->status != 200) {
return null;
}
diff --git a/actions/replies.php b/actions/replies.php
index 7eff74a66..4ab9b14ed 100644
--- a/actions/replies.php
+++ b/actions/replies.php
@@ -84,7 +84,7 @@ class RepliesAction extends Action
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -129,16 +129,13 @@ class RepliesAction extends Action
* @return void
*/
- function showFeeds()
+ function getFeeds()
{
$rssurl = common_local_url('repliesrss',
array('nickname' => $this->user->nickname));
$rsstitle = sprintf(_('Feed for replies to %s'), $this->user->nickname);
- $this->element('link', array('rel' => 'alternate',
- 'href' => $rssurl,
- 'type' => 'application/rss+xml',
- 'title' => $rsstitle));
+ return array(new Feed(Feed::RSS1, $rssurl, $rsstitle));
}
/**
@@ -154,25 +151,6 @@ class RepliesAction extends Action
}
/**
- * Show the replies feed links
- *
- * @return void
- */
-
- function showExportData()
- {
- $fl = new FeedList($this);
-
- $rssurl = common_local_url('repliesrss',
- array('nickname' => $this->user->nickname));
-
- $fl->show(array(0=>array('href'=> $rssurl,
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'repliesrss')));
- }
-
- /**
* Show the content
*
* A list of notices that are replies to the user, plus pagination.
diff --git a/actions/showfavorites.php b/actions/showfavorites.php
index 31479e1a7..d1c9283f0 100644
--- a/actions/showfavorites.php
+++ b/actions/showfavorites.php
@@ -113,7 +113,7 @@ class ShowfavoritesAction extends Action
}
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -136,10 +136,10 @@ class ShowfavoritesAction extends Action
/**
* Feeds for the <head> section
*
- * @return void
+ * @return array Feed objects to show
*/
- function showFeeds()
+ function getFeeds()
{
$feedurl = common_local_url('favoritesrss',
array('nickname' =>
@@ -147,10 +147,7 @@ class ShowfavoritesAction extends Action
$feedtitle = sprintf(_('Feed for favorites of %s'),
$this->user->nickname);
- $this->element('link', array('rel' => 'alternate',
- 'href' => $feedurl,
- 'type' => 'application/rss+xml',
- 'title' => $feedtitle));
+ return array(new Feed(Feed::RSS1, $feedurl, $feedtitle));
}
/**
@@ -166,28 +163,6 @@ class ShowfavoritesAction extends Action
}
/**
- * Show the replies feed links
- *
- * @return void
- */
-
- function showExportData()
- {
- $feedurl = common_local_url('favoritesrss',
- array('nickname' =>
- $this->user->nickname));
-
- $fl = new FeedList($this);
-
- // XXX: I18N
-
- $fl->show(array(0=>array('href'=> $feedurl,
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'Favorites')));
- }
-
- /**
* Show the content
*
* A list of notices that this user has marked as a favorite
diff --git a/actions/showgroup.php b/actions/showgroup.php
index 7bc68fbc6..c20941a35 100644
--- a/actions/showgroup.php
+++ b/actions/showgroup.php
@@ -244,7 +244,7 @@ class ShowgroupAction extends Action
if ($this->group->location) {
$this->elementStart('dl', 'entity_location');
$this->element('dt', null, _('Location'));
- $this->element('dd', 'location', $this->group->location);
+ $this->element('dd', 'label', $this->group->location);
$this->elementEnd('dl');
}
@@ -292,37 +292,18 @@ class ShowgroupAction extends Action
}
/**
- * Show a list of links to feeds this page produces
+ * Get a list of the feeds for this page
*
* @return void
*/
- function showExportData()
- {
- $fl = new FeedList($this);
- $fl->show(array(0=>array('href'=>common_local_url('grouprss',
- array('nickname' => $this->group->nickname)),
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'notices')));
- }
-
- /**
- * Show a list of links to feeds this page produces
- *
- * @return void
- */
-
- function showFeeds()
+ function getFeeds()
{
$url =
common_local_url('grouprss',
array('nickname' => $this->group->nickname));
- $this->element('link', array('rel' => 'alternate',
- 'href' => $url,
- 'type' => 'application/rss+xml',
- 'title' => sprintf(_('Notice feed for %s group'),
+ return array(new Feed(Feed::RSS1, $url, sprintf(_('Notice feed for %s group'),
$this->group->nickname)));
}
diff --git a/actions/showstream.php b/actions/showstream.php
index 28bb8453f..65482167e 100644
--- a/actions/showstream.php
+++ b/actions/showstream.php
@@ -111,7 +111,7 @@ class ShowstreamAction extends Action
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -155,58 +155,39 @@ class ShowstreamAction extends Action
return;
}
- function showExportData()
- {
- $fl = new FeedList($this);
- $fl->show(array(0=>array('href'=>common_local_url('userrss',
- array('nickname' => $this->user->nickname)),
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'notices'),
- 1=>array('href'=>common_local_url('usertimeline',
- array('nickname' => $this->user->nickname)),
- 'type' => 'atom',
- 'version' => 'Atom 1.0',
- 'item' => 'usertimeline'),
- 2=>array('href'=>common_local_url('foaf',
- array('nickname' => $this->user->nickname)),
- 'type' => 'rdf',
- 'version' => 'FOAF',
- 'item' => 'foaf')));
- }
-
- function showFeeds()
+ function getFeeds()
{
- $this->element('link', array('rel' => 'alternate',
- 'type' => 'application/rss+xml',
- 'href' => common_local_url('userrss',
- array('nickname' => $this->user->nickname)),
- 'title' => sprintf(_('Notice feed for %s (RSS)'),
- $this->user->nickname)));
-
- $this->element('link',
- array('rel' => 'alternate',
- 'href' => common_local_url('api',
- array('apiaction' => 'statuses',
- 'method' => 'user_timeline.atom',
- 'argument' => $this->user->nickname)),
- 'type' => 'application/atom+xml',
- 'title' => sprintf(_('Notice feed for %s (Atom)'),
- $this->user->nickname)));
+ return array(new Feed(Feed::RSS1,
+ common_local_url('userrss',
+ array('nickname' => $this->user->nickname)),
+ sprintf(_('Notice feed for %s (RSS 1.0)'),
+ $this->user->nickname)),
+ new Feed(Feed::RSS2,
+ common_local_url('api',
+ array('apiaction' => 'statuses',
+ 'method' => 'user_timeline',
+ 'argument' => $this->user->nickname.'.rss')),
+ sprintf(_('Notice feed for %s (RSS 2.0)'),
+ $this->user->nickname)),
+ new Feed(Feed::ATOM,
+ common_local_url('api',
+ array('apiaction' => 'statuses',
+ 'method' => 'user_timeline',
+ 'argument' => $this->user->nickname.'.atom')),
+ sprintf(_('Notice feed for %s (Atom)'),
+ $this->user->nickname)),
+ new Feed(Feed::FOAF,
+ common_local_url('foaf', array('nickname' =>
+ $this->user->nickname)),
+ sprintf(_('FOAF for %s'), $this->user->nickname)));
}
function extraHead()
{
- // FOAF
- $this->element('link', array('rel' => 'meta',
- 'href' => common_local_url('foaf', array('nickname' =>
- $this->user->nickname)),
- 'type' => 'application/rdf+xml',
- 'title' => 'FOAF'));
// for remote subscriptions etc.
$this->element('meta', array('http-equiv' => 'X-XRDS-Location',
'content' => common_local_url('xrds', array('nickname' =>
- $this->user->nickname))));
+ $this->user->nickname))));
if ($this->profile->bio) {
$this->element('meta', array('name' => 'description',
@@ -248,6 +229,15 @@ class ShowstreamAction extends Action
'height' => AVATAR_PROFILE_SIZE,
'alt' => $this->profile->nickname));
$this->elementEnd('dd');
+
+ $user = User::staticGet('id', $this->profile->id);
+ $cur = common_current_user();
+ if ($cur && $cur->id == $user->id) {
+ $this->elementStart('dd');
+ $this->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar'));
+ $this->elementEnd('dd');
+ }
+
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_nickname');
@@ -256,7 +246,7 @@ class ShowstreamAction extends Action
$hasFN = ($this->profile->fullname) ? 'nickname url uid' : 'fn nickname url uid';
$this->element('a', array('href' => $this->profile->profileurl,
'rel' => 'me', 'class' => $hasFN),
- $this->profile->nickname);
+ $this->profile->nickname);
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -272,7 +262,7 @@ class ShowstreamAction extends Action
if ($this->profile->location) {
$this->elementStart('dl', 'entity_location');
$this->element('dt', null, _('Location'));
- $this->element('dd', 'location', $this->profile->location);
+ $this->element('dd', 'label', $this->profile->location);
$this->elementEnd('dl');
}
@@ -302,11 +292,11 @@ class ShowstreamAction extends Action
$this->elementStart('ul', 'tags xoxo');
foreach ($tags as $tag) {
$this->elementStart('li');
- $this->element('span', 'mark_hash', '#');
- $this->element('a', array('rel' => 'tag',
- 'href' => common_local_url('peopletag',
- array('tag' => $tag))),
- $tag);
+ // Avoid space by using raw output.
+ $pt = '<span class="mark_hash">#</span><a rel="tag" href="' .
+ common_local_url('peopletag', array('tag' => $tag)) .
+ '">' . $tag . '</a>';
+ $this->raw($pt);
$this->elementEnd('li');
}
$this->elementEnd('ul');
@@ -324,7 +314,7 @@ class ShowstreamAction extends Action
$this->elementStart('li', 'entity_edit');
$this->element('a', array('href' => common_local_url('profilesettings'),
'title' => _('Edit profile settings')),
- _('Edit'));
+ _('Edit'));
$this->elementEnd('li');
}
@@ -346,9 +336,8 @@ class ShowstreamAction extends Action
$this->elementEnd('li');
}
- $user = User::staticGet('id', $this->profile->id);
if ($cur && $cur->id != $user->id && $cur->mutuallySubscribed($user)) {
- $this->elementStart('li', 'entity_send-a-message');
+ $this->elementStart('li', 'entity_send-a-message');
$this->element('a', array('href' => common_local_url('newmessage', array('to' => $user->id)),
'title' => _('Send a direct message to this user')),
_('Message'));
@@ -490,7 +479,7 @@ class ShowstreamAction extends Action
$this->elementStart('dl', 'entity_member-since');
$this->element('dt', null, _('Member since'));
$this->element('dd', null, date('j M Y',
- strtotime($this->profile->created)));
+ strtotime($this->profile->created)));
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_subscriptions');
diff --git a/actions/tag.php b/actions/tag.php
index 4401f892a..231f2c299 100644
--- a/actions/tag.php
+++ b/actions/tag.php
@@ -37,9 +37,9 @@ class TagAction extends Action
}
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
-
+
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -61,12 +61,11 @@ class TagAction extends Action
$this->showPage();
}
- function showFeeds()
+ function getFeeds()
{
- $this->element('link', array('rel' => 'alternate',
- 'href' => common_local_url('tagrss', array('tag' => $this->tag)),
- 'type' => 'application/rss+xml',
- 'title' => sprintf(_('Feed for tag %s'), $this->tag)));
+ return array(new Feed(Feed::RSS1,
+ common_local_url('tagrss', array('tag' => $this->tag)),
+ sprintf(_('Feed for tag %s'), $this->tag)));
}
function showPageNotice()
@@ -74,15 +73,6 @@ class TagAction extends Action
return sprintf(_('Messages tagged "%s", most recent first'), $this->tag);
}
- function showExportData()
- {
- $fl = new FeedList($this);
- $fl->show(array(0=>array('href'=>common_local_url('tagrss', array('tag' => $this->tag)),
- 'type' => 'rss',
- 'version' => 'RSS 1.0',
- 'item' => 'tagrss')));
- }
-
function showContent()
{
$notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1);
diff --git a/actions/tagother.php b/actions/tagother.php
index 3e8a12fd6..0d18945a0 100644
--- a/actions/tagother.php
+++ b/actions/tagother.php
@@ -110,7 +110,7 @@ class TagotherAction extends Action
if ($this->profile->location) {
$this->elementStart('dl', 'entity_location');
$this->element('dt', null, _('Location'));
- $this->element('dd', 'location', $this->profile->location);
+ $this->element('dd', 'label', $this->profile->location);
$this->elementEnd('dl');
}
if ($this->profile->homepage) {
@@ -135,7 +135,8 @@ class TagotherAction extends Action
'id' => 'form_tag_user',
'class' => 'form_settings',
'name' => 'tagother',
- 'action' => $this->selfUrl()));
+ 'action' => common_local_url('tagother', array('id' => $this->profile->id))));
+
$this->elementStart('fieldset');
$this->element('legend', null, _('Tag user'));
$this->hidden('token', common_session_token());
diff --git a/actions/twitapiaccount.php b/actions/twitapiaccount.php
index dc8e2e798..68a18cb57 100644
--- a/actions/twitapiaccount.php
+++ b/actions/twitapiaccount.php
@@ -23,23 +23,24 @@ require_once(INSTALLDIR.'/lib/twitterapi.php');
class TwitapiaccountAction extends TwitterapiAction
{
-
- function verify_credentials($args, $apidata)
+ function verify_credentials($args, $apidata)
{
+ parent::handle($args);
- if ($apidata['content-type'] == 'xml') {
- header('Content-Type: application/xml; charset=utf-8');
- print '<authorized>true</authorized>';
- } elseif ($apidata['content-type'] == 'json') {
- header('Content-Type: application/json; charset=utf-8');
- print '{"authorized":true}';
- } else {
- common_user_error(_('API method not found!'), $code=404);
- }
-
- }
+ switch ($apidata['content-type']) {
+ case 'xml':
+ case 'json':
+ $action_obj = new TwitapiusersAction();
+ $action_obj->prepare($args);
+ call_user_func(array($action_obj, 'show'), $args, $apidata);
+ break;
+ default:
+ header('Content-Type: text/html; charset=utf-8');
+ print 'Authorized';
+ }
+ }
- function end_session($args, $apidata)
+ function end_session($args, $apidata)
{
parent::handle($args);
$this->serverError(_('API method under construction.'), $code=501);
@@ -56,7 +57,7 @@ class TwitapiaccountAction extends TwitterapiAction
$location = trim($this->arg('location'));
- if (!is_null($location) && strlen($location) > 255) {
+ if (!is_null($location) && mb_strlen($location) > 255) {
// XXX: But Twitter just truncates and runs with it. -- Zach
$this->clientError(_('That\'s too long. Max notice size is 255 chars.'), 406, $apidate['content-type']);
diff --git a/actions/twitapisearchatom.php b/actions/twitapisearchatom.php
new file mode 100644
index 000000000..3ab82bfb6
--- /dev/null
+++ b/actions/twitapisearchatom.php
@@ -0,0 +1,368 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Action for showing Twitter-like Atom search results
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/twitterapi.php';
+
+/**
+ * Action for outputting search results in Twitter compatible Atom
+ * format.
+ *
+ * TODO: abstract Atom stuff into a ruseable base class like
+ * RSS10Action.
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see TwitterapiAction
+ */
+
+class TwitapisearchatomAction extends TwitterapiAction
+{
+
+ var $notices;
+ var $cnt;
+ var $query;
+ var $lang;
+ var $rpp;
+ var $page;
+ var $since_id;
+ var $geocode;
+
+ /**
+ * Constructor
+ *
+ * Just wraps the Action constructor.
+ *
+ * @param string $output URI to output to, default = stdout
+ * @param boolean $indent Whether to indent output, default true
+ *
+ * @see Action::__construct
+ */
+
+ function __construct($output='php://output', $indent=true)
+ {
+ parent::__construct($output, $indent);
+ }
+
+ /**
+ * Do we need to write to the database?
+ *
+ * @return boolean true
+ */
+
+ function isReadonly()
+ {
+ return true;
+ }
+
+ /**
+ * Read arguments and initialize members
+ *
+ * @param array $args Arguments from $_REQUEST
+ *
+ * @return boolean success
+ *
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->query = $this->trimmed('q');
+ $this->lang = $this->trimmed('lang');
+ $this->rpp = $this->trimmed('rpp');
+
+ if (!$this->rpp) {
+ $this->rpp = 15;
+ }
+
+ if ($this->rpp > 100) {
+ $this->rpp = 100;
+ }
+
+ $this->page = $this->trimmed('page');
+
+ if (!$this->page) {
+ $this->page = 1;
+ }
+
+ // TODO: Suppport since_id -- we need to tweak the backend
+ // Search classes to support it.
+
+ $this->since_id = $this->trimmed('since_id');
+ $this->geocode = $this->trimmed('geocode');
+
+ // TODO: Also, language and geocode
+
+ return true;
+ }
+
+ /**
+ * Handle a request
+ *
+ * @param array $args Arguments from $_REQUEST
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showAtom();
+ }
+
+ /**
+ * Get the notices to output as results. This also sets some class
+ * attrs so we can use them to calculate pagination, and output
+ * since_id and max_id.
+ *
+ * @return array an array of Notice objects sorted in reverse chron
+ */
+
+ function getNotices()
+ {
+ // TODO: Support search operators like from: and to:, boolean, etc.
+
+ $notice = new Notice();
+
+ // lcase it for comparison
+ $q = strtolower($this->query);
+
+ $search_engine = $notice->getSearchEngine('identica_notices');
+ $search_engine->set_sort_mode('chron');
+ $search_engine->limit(($this->page - 1) * $this->rpp,
+ $this->rpp + 1, true);
+ $search_engine->query($q);
+ $this->cnt = $notice->find();
+
+ $cnt = 0;
+
+ while ($notice->fetch()) {
+
+ ++$cnt;
+
+ if (!$this->max_id) {
+ $this->max_id = $notice->id;
+ }
+
+ if ($cnt > $this->rpp) {
+ break;
+ }
+
+ $notices[] = clone($notice);
+ }
+
+ return $notices;
+ }
+
+ /**
+ * Output search results as an Atom feed
+ *
+ * @return void
+ */
+
+ function showAtom()
+ {
+ $notices = $this->getNotices();
+
+ $this->initAtom();
+ $this->showFeed();
+
+ foreach ($notices as $n) {
+ $this->showEntry($n);
+ }
+
+ $this->endAtom();
+ }
+
+ /**
+ * Show feed specific Atom elements
+ *
+ * @return void
+ */
+
+ function showFeed()
+ {
+ // TODO: A9 OpenSearch stuff like search.twitter.com?
+
+ $lang = common_config('site', 'language');
+ $server = common_config('site', 'server');
+ $sitename = common_config('site', 'name');
+
+ // XXX: Use xmlns:laconica instead?
+
+ $this->elementStart('feed',
+ array('xmlns' => 'http://www.w3.org/2005/Atom',
+ 'xmlns:twitter' => 'http://api.twitter.com/',
+ 'xml:lang' => $lang));
+
+ $year = date('Y');
+ $this->element('id', null, "tag:$server,$year:search/$server");
+
+ $site_uri = common_path(false);
+
+ $search_uri = $site_uri . 'api/search.atom?q=' . urlencode($this->query);
+
+ if ($this->rpp != 15) {
+ $search_uri .= '&rpp=' . $this->rpp;
+ }
+
+ // FIXME: this alternate link is not quite right because our
+ // web-based notice search doesn't support a rpp (responses per
+ // page) param yet
+
+ $this->element('link', array('type' => 'text/html',
+ 'rel' => 'alternate',
+ 'href' => $site_uri . 'search/notice?q=' .
+ urlencode($this->query)));
+
+ // self link
+
+ $self_uri = $search_uri . '&page=' . $this->page;
+
+ $this->element('link', array('type' => 'application/atom+xml',
+ 'rel' => 'self',
+ 'href' => $self_uri));
+
+ $this->element('title', null, "$this->query - $sitename Search");
+
+ // refresh link
+
+ $refresh_uri = $search_uri . "&since_id=" . $this->max_id;
+
+ $this->element('link', array('type' => 'application/atom+xml',
+ 'rel' => 'refresh',
+ 'href' => $refresh_uri));
+
+ // pagination links
+
+ if ($this->cnt > $this->rpp) {
+
+ $next_uri = $search_uri . "&max_id=" . $this->max_id .
+ '&page=' . ($this->page + 1);
+
+ $this->element('link', array('type' => 'application/atom+xml',
+ 'rel' => 'next',
+ 'href' => $next_uri));
+ }
+
+ if ($this->page > 1) {
+
+ $previous_uri = $search_uri . "&max_id=" . $this->max_id .
+ '&page=' . ($this->page - 1);
+
+ $this->element('link', array('type' => 'application/atom+xml',
+ 'rel' => 'previous',
+ 'href' => $previous_uri));
+ }
+
+ }
+
+ /**
+ * Build an Atom entry similar to search.twitter.com's based on
+ * a given notice
+ *
+ * @param Notice $notice the notice to use
+ *
+ * @return void
+ */
+
+ function showEntry($notice)
+ {
+ $server = common_config('site', 'server');
+ $profile = $notice->getProfile();
+ $nurl = common_local_url('shownotice', array('notice' => $notice->id));
+
+ $this->elementStart('entry');
+
+ $year = date('Y', strtotime($notice->created));
+
+ $this->element('id', null, "tag:$server,$year:$notice->id");
+ $this->element('published', null, common_date_w3dtf($notice->created));
+ $this->element('link', array('type' => 'text/html',
+ 'rel' => 'alternate',
+ 'href' => $nurl));
+ $this->element('title', null, common_xml_safe_str(trim($notice->content)));
+ $this->element('content', array('type' => 'text/html'), $notice->rendered);
+ $this->element('updated', null, common_date_w3dtf($notice->created));
+ $this->element('link', array('type' => 'image/png',
+ 'rel' => 'image',
+ 'href' => $profile->avatarUrl()));
+
+ // TODO: Here is where we'd put in a link to an atom feed for threads
+
+ $this->element("twitter:source", null,
+ htmlentities($this->source_link($notice->source)));
+
+ $this->elementStart('author');
+
+ $name = $profile->nickname;
+
+ if ($profile->fullname) {
+ $name .= ' (' . $profile->fullname . ')';
+ }
+
+ $this->element('name', null, $name);
+ $this->element('uri', null, common_profile_uri($profile));
+ $this->elementEnd('author');
+
+ $this->elementEnd('entry');
+ }
+
+ /**
+ * Initialize the Atom output, send headers
+ *
+ * @return void
+ */
+
+ function initAtom()
+ {
+ header('Content-Type: application/atom+xml; charset=utf-8');
+ $this->startXml();
+ }
+
+ /**
+ * End the Atom feed
+ *
+ * @return void
+ */
+
+ function endAtom()
+ {
+ $this->elementEnd('feed');
+ }
+
+}
diff --git a/actions/twitapisearchjson.php b/actions/twitapisearchjson.php
new file mode 100644
index 000000000..0f9f523a1
--- /dev/null
+++ b/actions/twitapisearchjson.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Action for showing Twitter-like JSON search results
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/twitterapi.php';
+require_once INSTALLDIR.'/lib/jsonsearchresultslist.php';
+
+/**
+ * Action handler for Twitter-compatible API search
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @see TwitterapiAction
+ */
+
+class TwitapisearchjsonAction extends TwitterapiAction
+{
+ var $query;
+ var $lang;
+ var $rpp;
+ var $page;
+ var $since_id;
+ var $limit;
+ var $geocode;
+
+ /**
+ * Initialization.
+ *
+ * @param array $args Web and URL arguments
+ *
+ * @return boolean true if nothing goes wrong
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->query = $this->trimmed('q');
+ $this->lang = $this->trimmed('lang');
+ $this->rpp = $this->trimmed('rpp');
+
+ if (!$this->rpp) {
+ $this->rpp = 15;
+ }
+
+ if ($this->rpp > 100) {
+ $this->rpp = 100;
+ }
+
+ $this->page = $this->trimmed('page');
+
+ if (!$this->page) {
+ $this->page = 1;
+ }
+
+ $this->since_id = $this->trimmed('since_id');
+ $this->geocode = $this->trimmed('geocode');
+
+ return true;
+ }
+
+ /**
+ * Handle a request
+ *
+ * @param array $args Arguments from $_REQUEST
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showResults();
+ }
+
+ /**
+ * Show search results
+ *
+ * @return void
+ */
+
+ function showResults()
+ {
+
+ // TODO: Support search operators like from: and to:, boolean, etc.
+
+ $notice = new Notice();
+
+ // lcase it for comparison
+ $q = strtolower($this->query);
+
+ $search_engine = $notice->getSearchEngine('identica_notices');
+ $search_engine->set_sort_mode('chron');
+ $search_engine->limit(($this->page - 1) * $this->rpp, $this->rpp + 1, true);
+ $search_engine->query($q);
+ $cnt = $notice->find();
+
+ // TODO: since_id, lang, geocode
+
+ $results = new JSONSearchResultsList($notice, $q, $this->rpp, $this->page);
+
+ $this->init_document('json');
+ $results->show();
+ $this->end_document('json');
+ }
+
+ /**
+ * Do we need to write to the database?
+ *
+ * @return boolean true
+ */
+
+ function isReadOnly()
+ {
+ return true;
+ }
+} \ No newline at end of file
diff --git a/actions/twitapistatuses.php b/actions/twitapistatuses.php
index 18e24c0f5..63e29068b 100644
--- a/actions/twitapistatuses.php
+++ b/actions/twitapistatuses.php
@@ -115,9 +115,14 @@ class TwitapistatusesAction extends TwitterapiAction
$since = strtotime($this->arg('since'));
- $user = $this->get_user(null, $apidata);
+ $user = $this->get_user($apidata['api_arg'], $apidata);
$this->auth_user = $user;
+ if (empty($user)) {
+ $this->clientError(_('No such user!'), 404, $apidata['content-type']);
+ return;
+ }
+
$profile = $user->getProfile();
$sitename = common_config('site', 'name');
@@ -204,7 +209,7 @@ class TwitapistatusesAction extends TwitterapiAction
# FriendFeed's SUP protocol
# Also added RSS and Atom feeds
- $suplink = common_local_url('sup', null, $user->id);
+ $suplink = common_local_url('sup', null, null, $user->id);
header('X-SUP-ID: '.$suplink);
# XXX: since
@@ -470,19 +475,28 @@ class TwitapistatusesAction extends TwitterapiAction
return $this->subscriptions($apidata, 'subscribed', 'subscriber');
}
- function followers($args, $apidata)
+ function friendsIDs($args, $apidata)
{
parent::handle($args);
+ return $this->subscriptions($apidata, 'subscribed', 'subscriber', true);
+ }
+ function followers($args, $apidata)
+ {
+ parent::handle($args);
return $this->subscriptions($apidata, 'subscriber', 'subscribed');
}
- function subscriptions($apidata, $other_attr, $user_attr)
+ function followersIDs($args, $apidata)
{
+ parent::handle($args);
+ return $this->subscriptions($apidata, 'subscriber', 'subscribed', true);
+ }
- # XXX: lite
+ function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false)
+ {
- $this->auth_user = $apidate['user'];
+ $this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
if (!$user) {
@@ -514,7 +528,10 @@ class TwitapistatusesAction extends TwitterapiAction
}
$sub->orderBy('created DESC');
- $sub->limit(($page-1)*100, 100);
+
+ if (!$onlyIDs) {
+ $sub->limit(($page-1)*100, 100);
+ }
$others = array();
@@ -529,7 +546,13 @@ class TwitapistatusesAction extends TwitterapiAction
$type = $apidata['content-type'];
$this->init_document($type);
- $this->show_profiles($others, $type);
+
+ if ($onlyIDs) {
+ $this->showIDs($others, $type);
+ } else {
+ $this->show_profiles($others, $type);
+ }
+
$this->end_document($type);
}
@@ -555,6 +578,28 @@ class TwitapistatusesAction extends TwitterapiAction
}
}
+ function showIDs($profiles, $type)
+ {
+ switch ($type) {
+ case 'xml':
+ $this->elementStart('ids');
+ foreach ($profiles as $profile) {
+ $this->element('id', null, $profile->id);
+ }
+ $this->elementEnd('ids');
+ break;
+ case 'json':
+ $ids = array();
+ foreach ($profiles as $profile) {
+ $ids[] = (int)$profile->id;
+ }
+ print json_encode($ids);
+ break;
+ default:
+ $this->clientError(_('unsupported file type'));
+ }
+ }
+
function featured($args, $apidata)
{
parent::handle($args);
diff --git a/actions/twitapitrends.php b/actions/twitapitrends.php
new file mode 100644
index 000000000..c73d89446
--- /dev/null
+++ b/actions/twitapitrends.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * List of replies
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/twitterapi.php';
+
+/**
+ * Returns the top ten queries that are currently trending
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see TwitterapiAction
+ */
+
+class TwitapitrendsAction extends TwitterapiAction
+{
+
+ var $callback;
+
+ /**
+ * Initialization.
+ *
+ * @param array $args Web and URL arguments
+ *
+ * @return boolean false if user doesn't exist
+ */
+ function prepare($args)
+ {
+ parent::prepare($args);
+ return true;
+ }
+
+ /**
+ * Handle a request
+ *
+ * @param array $args Arguments from $_REQUEST
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showTrends();
+ }
+
+ /**
+ * Output the trends
+ *
+ * @return void
+ */
+ function showTrends()
+ {
+ $this->serverError(_('API method under construction.'), $code = 501);
+ }
+
+} \ No newline at end of file
diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php
index 8f16e5613..2894b7486 100644
--- a/actions/twitapiusers.php
+++ b/actions/twitapiusers.php
@@ -25,25 +25,29 @@ class TwitapiusersAction extends TwitterapiAction
{
function show($args, $apidata)
- {
+ {
parent::handle($args);
- if (!in_array($apidata['content-type'], array('xml', 'json'))) {
+ if (!in_array($apidata['content-type'], array('xml', 'json'))) {
$this->clientError(_('API method not found!'), $code = 404);
return;
}
-
- $this->auth_user = $apidata['user'];
+
$user = null;
$email = $this->arg('email');
+ $user_id = $this->arg('user_id');
if ($email) {
$user = User::staticGet('email', $email);
+ } elseif ($user_id) {
+ $user = $this->get_user($user_id);
} elseif (isset($apidata['api_arg'])) {
$user = $this->get_user($apidata['api_arg']);
- }
-
- if (!$user) {
+ } elseif (isset($apidata['user'])) {
+ $user = $apidata['user'];
+ }
+
+ if (!$user) {
// XXX: Twitter returns a random(?) user instead of throwing and err! -- Zach
$this->client_error(_('Not found.'), 404, $apidata['content-type']);
return;
@@ -74,9 +78,12 @@ class TwitapiusersAction extends TwitterapiAction
// Other fields Twitter sends...
$twitter_user['profile_background_color'] = '';
+ $twitter_user['profile_background_image_url'] = '';
$twitter_user['profile_text_color'] = '';
$twitter_user['profile_link_color'] = '';
$twitter_user['profile_sidebar_fill_color'] = '';
+ $twitter_user['profile_sidebar_border_color'] = '';
+ $twitter_user['profile_background_tile'] = 'false';
$faves = DB_DataObject::factory('fave');
$faves->user_id = $user->id;
@@ -94,18 +101,27 @@ class TwitapiusersAction extends TwitterapiAction
$twitter_user['utc_offset'] = $t->format('Z');
$twitter_user['time_zone'] = $timezone;
- if (isset($this->auth_user)) {
+ if (isset($apidata['user'])) {
- if ($this->auth_user->isSubscribed($profile)) {
+ if ($apidata['user']->isSubscribed($profile)) {
$twitter_user['following'] = 'true';
} else {
$twitter_user['following'] = 'false';
}
-
- // Not implemented yet
- $twitter_user['notifications'] = 'false';
- }
-
+
+ // Notifications on?
+ $sub = Subscription::pkeyGet(array('subscriber' =>
+ $apidata['user']->id, 'subscribed' => $profile->id));
+
+ if ($sub) {
+ if ($sub->jabber || $sub->sms) {
+ $twitter_user['notifications'] = 'true';
+ } else {
+ $twitter_user['notifications'] = 'false';
+ }
+ }
+ }
+
if ($apidata['content-type'] == 'xml') {
$this->init_document('xml');
$this->show_twitter_xml_user($twitter_user);
@@ -114,7 +130,13 @@ class TwitapiusersAction extends TwitterapiAction
$this->init_document('json');
$this->show_json_objects($twitter_user);
$this->end_document('json');
- }
+ } else {
+
+ // This is in case 'show' was called via /account/verify_credentials
+ // without a format (xml or json).
+ header('Content-Type: text/html; charset=utf-8');
+ print 'Authorized';
+ }
}
}
diff --git a/actions/twittersettings.php b/actions/twittersettings.php
index 2d41469bb..a79859bbf 100644
--- a/actions/twittersettings.php
+++ b/actions/twittersettings.php
@@ -32,6 +32,7 @@ if (!defined('LACONICA')) {
}
require_once INSTALLDIR.'/lib/connectsettingsaction.php';
+require_once INSTALLDIR.'/lib/twitter.php';
define('SUBSCRIPTIONS', 80);
@@ -90,7 +91,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$fuser = null;
- $flink = Foreign_link::getByUserID($user->id, 1); // 1 == Twitter
+ $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
if ($flink) {
$fuser = $flink->getForeignUser();
@@ -358,7 +359,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$flink->user_id = $user->id;
$flink->foreign_id = $twit_user->id;
- $flink->service = 1; // Twitter
+ $flink->service = TWITTER_SERVICE;
$flink->credentials = $password;
$flink->created = common_sql_now();
diff --git a/actions/updateprofile.php b/actions/updateprofile.php
index c79112dac..4751a04ff 100644
--- a/actions/updateprofile.php
+++ b/actions/updateprofile.php
@@ -93,22 +93,22 @@ class UpdateprofileAction extends Action
}
# optional stuff
$fullname = $req->get_parameter('omb_listenee_fullname');
- if ($fullname && strlen($fullname) > 255) {
+ if ($fullname && mb_strlen($fullname) > 255) {
$this->clientError(_("Full name is too long (max 255 chars)."));
return false;
}
$homepage = $req->get_parameter('omb_listenee_homepage');
- if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
+ if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
$this->clientError(sprintf(_("Invalid homepage '%s'"), $homepage));
return false;
}
$bio = $req->get_parameter('omb_listenee_bio');
- if ($bio && strlen($bio) > 140) {
+ if ($bio && mb_strlen($bio) > 140) {
$this->clientError(_("Bio is too long (max 140 chars)."));
return false;
}
$location = $req->get_parameter('omb_listenee_location');
- if ($location && strlen($location) > 255) {
+ if ($location && mb_strlen($location) > 255) {
$this->clientError(_("Location is too long (max 255 chars)."));
return false;
}
@@ -162,7 +162,13 @@ class UpdateprofileAction extends Action
if ($avatar) {
$temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
copy($avatar, $temp_filename);
- if (!$profile->setOriginal($temp_filename)) {
+ $imagefile = new ImageFile($profile->id, $temp_filename);
+ $filename = Avatar::filename($profile->id,
+ image_type_to_extension($imagefile->type),
+ null,
+ common_timestamp());
+ rename($temp_filename, Avatar::path($filename));
+ if (!$profile->setOriginal($filename)) {
$this->serverError(_('Could not save avatar info'), 500);
return false;
}
diff --git a/actions/userauthorization.php b/actions/userauthorization.php
index 58fc96c0e..0566b4b70 100644
--- a/actions/userauthorization.php
+++ b/actions/userauthorization.php
@@ -105,7 +105,7 @@ class UserauthorizationAction extends Action
$this->elementStart('div', 'profile');
if ($avatar) {
$this->element('img', array('src' => $avatar,
- 'class' => 'avatar profile',
+ 'class' => 'avatar',
'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE,
'alt' => $nickname));
@@ -197,7 +197,7 @@ class UserauthorizationAction extends Action
}
$parts = array();
foreach ($params as $k => $v) {
- $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
+ $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
}
$query_string = implode('&', $parts);
$parsed = parse_url($callback);
@@ -330,7 +330,13 @@ class UserauthorizationAction extends Action
{
$temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
copy($url, $temp_filename);
- return $profile->setOriginal($temp_filename);
+ $imagefile = new ImageFile($profile->id, $temp_filename);
+ $filename = Avatar::filename($profile->id,
+ image_type_to_extension($imagefile->type),
+ null,
+ common_timestamp());
+ rename($temp_filename, Avatar::path($filename));
+ return $profile->setOriginal($filename);
}
function showAcceptMessage($tok)
@@ -469,19 +475,19 @@ class UserauthorizationAction extends Action
}
# optional stuff
$fullname = $req->get_parameter('omb_listenee_fullname');
- if ($fullname && strlen($fullname) > 255) {
+ if ($fullname && mb_strlen($fullname) > 255) {
throw new OAuthException("Full name '$fullname' too long.");
}
$homepage = $req->get_parameter('omb_listenee_homepage');
- if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
+ if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
throw new OAuthException("Invalid homepage '$homepage'");
}
$bio = $req->get_parameter('omb_listenee_bio');
- if ($bio && strlen($bio) > 140) {
+ if ($bio && mb_strlen($bio) > 140) {
throw new OAuthException("Bio too long '$bio'");
}
$location = $req->get_parameter('omb_listenee_location');
- if ($location && strlen($location) > 255) {
+ if ($location && mb_strlen($location) > 255) {
throw new OAuthException("Location too long '$location'");
}
$avatar = $req->get_parameter('omb_listenee_avatar');
diff --git a/actions/userrss.php b/actions/userrss.php
index 04855ccca..a3e5a3aab 100644
--- a/actions/userrss.php
+++ b/actions/userrss.php
@@ -46,13 +46,13 @@ class UserrssAction extends Rss10Action
{
$user = $this->user;
-
+
if (is_null($user)) {
return null;
}
-
+
$notice = $user->getNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit);
-
+
while ($notice->fetch()) {
$notices[] = clone($notice);
}
@@ -87,10 +87,10 @@ class UserrssAction extends Rss10Action
}
# override parent to add X-SUP-ID URL
-
+
function initRss($limit=0)
{
- $url = common_local_url('sup', null, $this->user->id);
+ $url = common_local_url('sup', null, null, $this->user->id);
header('X-SUP-ID: '.$url);
parent::initRss($limit);
}
@@ -100,4 +100,3 @@ class UserrssAction extends Rss10Action
return true;
}
}
-
diff --git a/bin/flowplayer-3.0.5.swf b/bin/flowplayer-3.0.5.swf
new file mode 100644
index 000000000..05b64a032
--- /dev/null
+++ b/bin/flowplayer-3.0.5.swf
Binary files differ
diff --git a/bin/flowplayer.audio-3.0.3.swf b/bin/flowplayer.audio-3.0.3.swf
new file mode 100644
index 000000000..ef85f1bff
--- /dev/null
+++ b/bin/flowplayer.audio-3.0.3.swf
Binary files differ
diff --git a/bin/flowplayer.controls-3.0.3.swf b/bin/flowplayer.controls-3.0.3.swf
new file mode 100644
index 000000000..09a27e8a9
--- /dev/null
+++ b/bin/flowplayer.controls-3.0.3.swf
Binary files differ
diff --git a/classes/Nonce.php b/classes/Nonce.php
index 2c0edfa14..486a65a3c 100644
--- a/classes/Nonce.php
+++ b/classes/Nonce.php
@@ -4,22 +4,21 @@
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Nonce extends Memcached_DataObject
+class Nonce extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'nonce'; // table name
public $consumer_key; // varchar(255) primary_key not_null
- public $tok; // char(32) primary_key not_null
+ public $tok; // char(32)
public $nonce; // char(32) primary_key not_null
- public $ts; // datetime() not_null
+ public $ts; // datetime() primary_key not_null
public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
/* Static get */
- function staticGet($k,$v=null)
- { return Memcached_DataObject::staticGet('Nonce',$k,$v); }
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Nonce',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
diff --git a/classes/Notice.php b/classes/Notice.php
index 329988368..eac90ce95 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -34,22 +34,23 @@ class Notice extends Memcached_DataObject
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
- public $__table = 'notice'; // table name
- public $id; // int(4) primary_key not_null
- public $profile_id; // int(4) not_null
+ public $__table = 'notice'; // table name
+ public $id; // int(4) primary_key not_null
+ public $profile_id; // int(4) not_null
public $uri; // varchar(255) unique_key
public $content; // varchar(140)
- public $rendered; // text()
+ public $rendered; // text()
public $url; // varchar(255)
- public $created; // datetime() not_null
- public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
- public $reply_to; // int(4)
- public $is_local; // tinyint(1)
- public $source; // varchar(32)
+ public $created; // datetime() not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+ public $reply_to; // int(4)
+ public $is_local; // tinyint(1)
+ public $source; // varchar(32)
/* Static get */
- function staticGet($k,$v=null)
- { return Memcached_DataObject::staticGet('Notice',$k,$v); }
+ function staticGet($k,$v=NULL) {
+ return Memcached_DataObject::staticGet('Notice',$k,$v);
+ }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
@@ -94,27 +95,34 @@ class Notice extends Memcached_DataObject
/* Add them to the database */
foreach(array_unique($match[1]) as $hashtag) {
/* elide characters we don't want in the tag */
- $hashtag = common_canonical_tag($hashtag);
-
- $tag = DB_DataObject::factory('Notice_tag');
- $tag->notice_id = $this->id;
- $tag->tag = $hashtag;
- $tag->created = $this->created;
- $id = $tag->insert();
- if (!$id) {
- $last_error = PEAR::getStaticProperty('DB_DataObject','lastError');
- common_log(LOG_ERR, 'DB error inserting hashtag: ' . $last_error->message);
- common_server_error(sprintf(_('DB error inserting hashtag: %s'), $last_error->message));
- return;
- }
+ $this->saveTag($hashtag);
}
return true;
}
+ function saveTag($hashtag)
+ {
+ $hashtag = common_canonical_tag($hashtag);
+
+ $tag = new Notice_tag();
+ $tag->notice_id = $this->id;
+ $tag->tag = $hashtag;
+ $tag->created = $this->created;
+ $id = $tag->insert();
+
+ if (!$id) {
+ throw new ServerException(sprintf(_('DB error inserting hashtag: %s'),
+ $last_error->message));
+ return;
+ }
+ }
+
static function saveNew($profile_id, $content, $source=null, $is_local=1, $reply_to=null, $uri=null) {
$profile = Profile::staticGet($profile_id);
+ $final = common_shorten_links($content);
+
if (!$profile) {
common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
return _('Problem saving notice. Unknown user.');
@@ -125,7 +133,12 @@ class Notice extends Memcached_DataObject
return _('Too many notices too fast; take a breather and post again in a few minutes.');
}
- $banned = common_config('profile', 'banned');
+ if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
+ common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
+ return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.');
+ }
+
+ $banned = common_config('profile', 'banned');
if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) {
common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id).");
@@ -136,10 +149,12 @@ class Notice extends Memcached_DataObject
$notice->profile_id = $profile_id;
$blacklist = common_config('public', 'blacklist');
+ $autosource = common_config('public', 'autosource');
# Blacklisted are non-false, but not 1, either
- if ($blacklist && in_array($profile_id, $blacklist)) {
+ if (($blacklist && in_array($profile_id, $blacklist)) ||
+ ($source && $autosource && in_array($source, $autosource))) {
$notice->is_local = -1;
} else {
$notice->is_local = $is_local;
@@ -147,39 +162,44 @@ class Notice extends Memcached_DataObject
$notice->query('BEGIN');
- $notice->reply_to = $reply_to;
- $notice->created = common_sql_now();
- $notice->content = common_shorten_links($content);
- $notice->rendered = common_render_content($notice->content, $notice);
- $notice->source = $source;
- $notice->uri = $uri;
+ $notice->reply_to = $reply_to;
+ $notice->created = common_sql_now();
+ $notice->content = $final;
+ $notice->rendered = common_render_content($final, $notice);
+ $notice->source = $source;
+ $notice->uri = $uri;
- $id = $notice->insert();
+ if (Event::handle('StartNoticeSave', array(&$notice))) {
- if (!$id) {
- common_log_db_error($notice, 'INSERT', __FILE__);
- return _('Problem saving notice.');
- }
-
- # Update the URI after the notice is in the database
- if (!$uri) {
- $orig = clone($notice);
- $notice->uri = common_notice_uri($notice);
+ $id = $notice->insert();
- if (!$notice->update($orig)) {
- common_log_db_error($notice, 'UPDATE', __FILE__);
+ if (!$id) {
+ common_log_db_error($notice, 'INSERT', __FILE__);
return _('Problem saving notice.');
}
- }
- # XXX: do we need to change this for remote users?
+ # Update the URI after the notice is in the database
+ if (!$uri) {
+ $orig = clone($notice);
+ $notice->uri = common_notice_uri($notice);
+
+ if (!$notice->update($orig)) {
+ common_log_db_error($notice, 'UPDATE', __FILE__);
+ return _('Problem saving notice.');
+ }
+ }
+
+ # XXX: do we need to change this for remote users?
- $notice->saveReplies();
- $notice->saveTags();
- $notice->saveGroups();
+ $notice->saveReplies();
+ $notice->saveTags();
+ $notice->saveGroups();
- $notice->addToInboxes();
- $notice->query('COMMIT');
+ $notice->addToInboxes();
+ $notice->query('COMMIT');
+
+ Event::handle('EndNoticeSave', array($notice));
+ }
# Clear the cache for subscribed users, so they'll update at next request
# XXX: someone clever could prepend instead of clearing the cache
@@ -191,6 +211,32 @@ class Notice extends Memcached_DataObject
return $notice;
}
+ static function checkDupes($profile_id, $content) {
+ $profile = Profile::staticGet($profile_id);
+ if (!$profile) {
+ return false;
+ }
+ $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW);
+ if ($notice) {
+ $last = 0;
+ while ($notice->fetch()) {
+ if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
+ return true;
+ } else if ($notice->content == $content) {
+ return false;
+ }
+ }
+ }
+ # If we get here, oldest item in cache window is not
+ # old enough for dupe limit; do direct check against DB
+ $notice = new Notice();
+ $notice->profile_id = $profile_id;
+ $notice->content = $content;
+ $notice->whereAdd('now() - created < ' . common_config('notice', 'dupelimit'));
+ $cnt = $notice->count();
+ return ($cnt > 0);
+ }
+
static function checkEditThrottle($profile_id) {
$profile = Profile::staticGet($profile_id);
if (!$profile) {
@@ -572,7 +618,7 @@ class Notice extends Memcached_DataObject
$inbox = new Notice_inbox();
$UT = common_config('db','type')=='pgsql'?'"user"':'user';
$qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
- "SELECT $UT.id, " . $this->id . ', "' . $this->created . '" ' .
+ "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
"FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
'WHERE subscription.subscribed = ' . $this->profile_id . ' ' .
'AND NOT EXISTS (SELECT user_id, notice_id ' .
@@ -614,6 +660,15 @@ class Notice extends Memcached_DataObject
continue;
}
+ // we automatically add a tag for every group name, too
+
+ $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname),
+ 'notice_id' => $this->id));
+
+ if (is_null($tag)) {
+ $this->saveTag($nickname);
+ }
+
if ($profile->isMember($group)) {
$gi = new Group_inbox();
@@ -633,7 +688,7 @@ class Notice extends Memcached_DataObject
$inbox = new Notice_inbox();
$UT = common_config('db','type')=='pgsql'?'"user"':'user';
$qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
- "SELECT $UT.id, " . $this->id . ', "' . $this->created . '", 2 ' .
+ "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
"FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
'WHERE group_member.group_id = ' . $group->id . ' ' .
'AND NOT EXISTS (SELECT user_id, notice_id ' .
@@ -725,10 +780,19 @@ class Notice extends Memcached_DataObject
if (!$id) {
common_log_db_error($reply, 'INSERT', __FILE__);
return;
+ } else {
+ $replied[$recipient->id] = 1;
}
}
}
}
}
+
+ foreach (array_keys($replied) as $recipient) {
+ $user = User::staticGet('id', $recipient);
+ if ($user) {
+ mail_notify_attn($user, $this);
+ }
+ }
}
}
diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php
index 94f9296d6..f2247299a 100644
--- a/classes/Notice_tag.php
+++ b/classes/Notice_tag.php
@@ -19,7 +19,7 @@
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Notice_tag extends Memcached_DataObject
+class Notice_tag extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
@@ -35,18 +35,18 @@ class Notice_tag extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
-
+
static function getStream($tag, $offset=0, $limit=20) {
- $qry =
+ $qry =
'SELECT notice.* ' .
'FROM notice JOIN notice_tag ON notice.id = notice_tag.notice_id ' .
- 'WHERE notice_tag.tag = "%s" ';
+ "WHERE notice_tag.tag = '%s' ";
return Notice::getStream(sprintf($qry, $tag),
'notice_tag:notice_stream:' . common_keyize($tag),
$offset, $limit);
}
-
+
function blowCache()
{
$cache = common_memcache();
@@ -54,4 +54,9 @@ class Notice_tag extends Memcached_DataObject
$cache->delete(common_cache_key('notice_tag:notice_stream:' . $this->tag));
}
}
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('Notice_tag', $kv);
+ }
}
diff --git a/classes/Profile_tag.php b/classes/Profile_tag.php
index cb60cbaec..0a1ad9cd6 100644
--- a/classes/Profile_tag.php
+++ b/classes/Profile_tag.php
@@ -4,7 +4,7 @@
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Profile_tag extends Memcached_DataObject
+class Profile_tag extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
@@ -23,45 +23,46 @@ class Profile_tag extends Memcached_DataObject
###END_AUTOCODE
static function getTags($tagger, $tagged) {
-
+
$tags = array();
# XXX: store this in memcached
-
+
$profile_tag = new Profile_tag();
$profile_tag->tagger = $tagger;
$profile_tag->tagged = $tagged;
-
+
$profile_tag->find();
-
+
while ($profile_tag->fetch()) {
$tags[] = $profile_tag->tag;
}
-
+
$profile_tag->free();
-
+
return $tags;
}
-
+
static function setTags($tagger, $tagged, $newtags) {
-
+
+ $newtags = array_unique($newtags);
$oldtags = Profile_tag::getTags($tagger, $tagged);
-
+
# Delete stuff that's old that not in new
-
+
$to_delete = array_diff($oldtags, $newtags);
-
+
# Insert stuff that's in new and not in old
-
+
$to_insert = array_diff($newtags, $oldtags);
-
+
$profile_tag = new Profile_tag();
-
+
$profile_tag->tagger = $tagger;
$profile_tag->tagged = $tagged;
-
+
$profile_tag->query('BEGIN');
-
+
foreach ($to_delete as $deltag) {
$profile_tag->tag = $deltag;
$result = $profile_tag->delete();
@@ -70,7 +71,7 @@ class Profile_tag extends Memcached_DataObject
return false;
}
}
-
+
foreach ($to_insert as $instag) {
$profile_tag->tag = $instag;
$result = $profile_tag->insert();
@@ -79,12 +80,12 @@ class Profile_tag extends Memcached_DataObject
return false;
}
}
-
+
$profile_tag->query('COMMIT');
-
+
return true;
}
-
+
# Return profiles with a given tag
static function getTagged($tagger, $tag) {
$profile = new Profile();
diff --git a/classes/User.php b/classes/User.php
index a6a1b11b9..d9f30bec5 100644
--- a/classes/User.php
+++ b/classes/User.php
@@ -40,6 +40,7 @@ class User extends Memcached_DataObject
public $emailnotifyfav; // tinyint(1) default_1
public $emailnotifynudge; // tinyint(1) default_1
public $emailnotifymsg; // tinyint(1) default_1
+ public $emailnotifyattn; // tinyint(1) default_1
public $emailmicroid; // tinyint(1) default_1
public $language; // varchar(50)
public $timezone; // varchar(50)
@@ -62,8 +63,10 @@ class User extends Memcached_DataObject
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
/* Static get */
- function staticGet($k,$v=null)
- { return Memcached_DataObject::staticGet('User',$k,$v); }
+ function staticGet($k,$v=NULL)
+ {
+ return Memcached_DataObject::staticGet('User',$k,$v);
+ }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
@@ -118,7 +121,8 @@ class User extends Memcached_DataObject
static $blacklist = array('rss', 'xrds', 'doc', 'main',
'settings', 'notice', 'user',
'search', 'avatar', 'tag', 'tags',
- 'api', 'message', 'group', 'groups');
+ 'api', 'message', 'group', 'groups',
+ 'local');
$merged = array_merge($blacklist, common_config('nickname', 'blacklist'));
return !in_array($nickname, $merged);
}
@@ -180,16 +184,16 @@ class User extends Memcached_DataObject
$profile->nickname = $nickname;
$profile->profileurl = common_profile_url($nickname);
- if ($fullname) {
+ if (!empty($fullname)) {
$profile->fullname = $fullname;
}
- if ($homepage) {
+ if (!empty($homepage)) {
$profile->homepage = $homepage;
}
- if ($bio) {
+ if (!empty($bio)) {
$profile->bio = $bio;
}
- if ($location) {
+ if (!empty($location)) {
$profile->location = $location;
}
@@ -197,7 +201,7 @@ class User extends Memcached_DataObject
$id = $profile->insert();
- if (!$id) {
+ if (empty($id)) {
common_log_db_error($profile, 'INSERT', __FILE__);
return false;
}
@@ -207,13 +211,13 @@ class User extends Memcached_DataObject
$user->id = $id;
$user->nickname = $nickname;
- if ($password) { # may not have a password for OpenID users
+ if (!empty($password)) { # may not have a password for OpenID users
$user->password = common_munge_password($password, $id);
}
# Users who respond to invite email have proven their ownership of that address
- if ($code) {
+ if (!empty($code)) {
$invite = Invitation::staticGet($code);
if ($invite && $invite->address && $invite->address_type == 'email' && $invite->address == $email) {
$user->email = $invite->address;
@@ -250,7 +254,7 @@ class User extends Memcached_DataObject
return false;
}
- if ($email && !$user->email) {
+ if (!empty($email) && !$user->email) {
$confirm = new Confirm_address();
$confirm->code = common_confirmation_code(128);
@@ -265,7 +269,7 @@ class User extends Memcached_DataObject
}
}
- if ($code && $user->email) {
+ if (!empty($code) && $user->email) {
$user->emailChanged();
}
@@ -586,7 +590,7 @@ class User extends Memcached_DataObject
'JOIN profile_tag ON (profile_tag.tagged = subscription.subscriber ' .
'AND profile_tag.tagger = subscription.subscribed) ' .
'WHERE subscription.subscribed = %d ' .
- 'AND profile_tag.tag = "%s" ' .
+ "AND profile_tag.tag = '%s' " .
'AND subscription.subscribed != subscription.subscriber ' .
'ORDER BY subscription.created DESC ';
@@ -614,7 +618,7 @@ class User extends Memcached_DataObject
'JOIN profile_tag on (profile_tag.tagged = subscription.subscribed ' .
'AND profile_tag.tagger = subscription.subscriber) ' .
'WHERE subscription.subscriber = %d ' .
- 'AND profile_tag.tag = "%s" ' .
+ "AND profile_tag.tag = '%s' " .
'AND subscription.subscribed != subscription.subscriber ' .
'ORDER BY subscription.created DESC ';
diff --git a/classes/laconica.ini b/classes/laconica.ini
index 255122a97..529454d99 100755
--- a/classes/laconica.ini
+++ b/classes/laconica.ini
@@ -145,7 +145,7 @@ id = N
[nonce]
consumer_key = 130
-tok = 130
+tok = 2
nonce = 130
ts = 142
created = 142
@@ -153,8 +153,8 @@ modified = 384
[nonce__keys]
consumer_key = K
-tok = K
nonce = K
+ts = K
[notice]
id = 129
@@ -292,7 +292,8 @@ created = 142
modified = 384
[sms_carrier__keys]
-id = N
+id = K
+name = U
[subscription]
subscriber = 129
@@ -331,6 +332,7 @@ emailnotifysub = 17
emailnotifyfav = 17
emailnotifynudge = 17
emailnotifymsg = 17
+emailnotifyattn = 17
emailmicroid = 17
language = 2
timezone = 2
diff --git a/config.php.sample b/config.php.sample
index db1a21663..c2b27408c 100644
--- a/config.php.sample
+++ b/config.php.sample
@@ -18,6 +18,8 @@ $config['site']['server'] = 'localhost';
$config['site']['path'] = 'laconica';
#$config['site']['fancy'] = false;
#$config['site']['theme'] = 'default';
+#To enable the built-in mobile style sheet, defaults to false.
+#$config['site']['mobile'] = true;
#For contact email, defaults to $_SERVER["SERVER_ADMIN"]
#$config['site']['email'] = 'admin@example.net';
#Brought by...
@@ -32,6 +34,9 @@ $config['site']['path'] = 'laconica';
# If you want logging sent to a file instead of syslog
#$config['site']['logfile'] = '/tmp/laconica.log';
+# Enables extra log information, for example full details of PEAR DB errors
+#$config['site']['logdebug'] = true;
+
# This is a PEAR DB DSN, see http://pear.php.net/manual/en/package.database.db.intro-dsn.php
# Set it to match your actual database
@@ -107,6 +112,14 @@ $config['sphinx']['port'] = 3312;
#$config['public']['blacklist'][] = 123;
#$config['public']['blacklist'][] = 2307;
+#Mark certain notice sources as automatic and thus not
+#appropriate for public feed
+#$config['public]['autosource'][] = 'twitterfeed';
+#$config['public]['autosource'][] = 'rssdent';
+#$config['public]['autosource'][] = 'Ping.Fm';
+#$config['public]['autosource'][] = 'HelloTxt';
+#$config['public]['autosource'][] = 'Updating.Me';
+
#Do notice broadcasts offline
#If you use this, you must run the six offline daemons in the
#background. See the README for details.
@@ -139,6 +152,16 @@ $config['sphinx']['port'] = 3312;
#$config['profile']['banned'][] = 'hacker';
#$config['profile']['banned'][] = 12345;
-# config section for the built-in Facebook application
+# Config section for the built-in Facebook application
#$config['facebook']['apikey'] = 'APIKEY';
#$config['facebook']['secret'] = 'SECRET';
+
+# Add Google Analytics
+# require_once('plugins/GoogleAnalyticsPlugin.php');
+# $ga = new GoogleAnalyticsPlugin('your secret code');
+
+#Don't allow saying the same thing more than once per hour
+#$config['site']['dupelimit'] = 3600;
+#Don't enforce the dupe limit
+#$config['site']['dupelimit'] = -1;
+
diff --git a/db/carrier.sql b/db/carrier.sql
deleted file mode 100644
index 932f7c8bb..000000000
--- a/db/carrier.sql
+++ /dev/null
@@ -1,61 +0,0 @@
-insert into sms_carrier
- (name, email_pattern, created)
-values
- ('3 River Wireless', '%s@sms.3rivers.net', now()),
- ('7-11 Speakout', '%s@cingularme.com', now()),
- ('Airtel (Karnataka, India)', '%s@airtelkk.com', now()),
- ('Alaska Communications Systems', '%s@msg.acsalaska.com', now()),
- ('Alltel Wireless', '%s@message.alltel.com', now()),
- ('AT&T Wireless', '%s@txt.att.net', now()),
- ('Bell Mobility (Canada)', '%s@txt.bell.ca', now()),
- ('Boost Mobile', '%s@myboostmobile.com', now()),
- ('Cellular One (Dobson)', '%s@mobile.celloneusa.com', now()),
- ('Cincinnati Bell Wireless', '%s@gocbw.com', now()),
- ('Cingular (Postpaid)', '%s@cingularme.com', now()),
- ('Centennial Wireless', '%s@cwemail.com', now()),
- ('Cingular (GoPhone prepaid)', '%s@cingularme.com', now()),
- ('Claro (Nicaragua)', '%s@ideasclaro-ca.com', now()),
- ('Comcel', '%s@comcel.com.co', now()),
- ('Cricket', '%s@sms.mycricket.com', now()),
- ('CTI', '%s@sms.ctimovil.com.ar', now()),
- ('Emtel (Mauritius)', '%s@emtelworld.net', now()),
- ('Fido (Canada)', '%s@fido.ca', now()),
- ('General Communications Inc.', '%s@msg.gci.net', now()),
- ('Globalstar', '%s@msg.globalstarusa.com', now()),
- ('Helio', '%s@myhelio.com', now()),
- ('Illinois Valley Cellular', '%s@ivctext.com', now()),
- ('i wireless', '%s.iws@iwspcs.net', now()),
- ('Meteor (Ireland)', '%s@sms.mymeteor.ie', now()),
- ('Mero Mobile (Nepal)', '%s@sms.spicenepal.com', now()),
- ('MetroPCS', '%s@mymetropcs.com', now()),
- ('Movicom', '%s@movimensaje.com.ar', now()),
- ('Mobitel (Sri Lanka)', '%s@sms.mobitel.lk', now()),
- ('Movistar (Colombia)', '%s@movistar.com.co', now()),
- ('MTN (South Africa)', '%s@sms.co.za', now()),
- ('MTS (Canada)', '%s@text.mtsmobility.com', now()),
- ('Nextel (Argentina)', '%s@nextel.net.ar', now()),
- ('Orange (Poland)', '%s@orange.pl', now()),
- ('Orange (UK)', '%s@orange.net', now()),
- ('Personal (Argentina)', '%s@personal-net.com.ar', now()),
- ('Plus GSM (Poland)', '%s@text.plusgsm.pl', now()),
- ('President''s Choice (Canada)', '%s@txt.bell.ca', now()),
- ('Qwest', '%s@qwestmp.com', now()),
- ('Rogers (Canada)', '%s@pcs.rogers.com', now()),
- ('Sasktel (Canada)', '%s@sms.sasktel.com', now()),
- ('Setar Mobile email (Aruba)', '%s@mas.aw', now()),
- ('Solo Mobile', '%s@txt.bell.ca', now()),
- ('Sprint (PCS)', '%s@messaging.sprintpcs.com', now()),
- ('Sprint (Nextel)', '%s@page.nextel.com', now()),
- ('Suncom', '%s@tms.suncom.com', now()),
- ('T-Mobile', '%s@tmomail.net', now()),
- ('T-Mobile (Austria)', '%s@sms.t-mobile.at', now()),
- ('Telus Mobility (Canada)', '%s@msg.telus.com', now()),
- ('Thumb Cellular', '%s@sms.thumbcellular.com', now()),
- ('Tigo (Formerly Ola)', '%s@sms.tigo.com.co', now()),
- ('Unicel', '%s@utext.com', now()),
- ('US Cellular', '%s@email.uscc.net', now()),
- ('Verizon', '%s@vtext.com', now()),
- ('Virgin Mobile (Canada)', '%s@vmobile.ca', now()),
- ('Virgin Mobile (USA)', '%s@vmobl.com', now()),
- ('Vodafone NZ (txt ''R'' to 901 to enable first)', '%s@sms.vodafone.net.nz', now()),
- ('YCC', '%s@sms.ycc.ru', now());
diff --git a/db/foreign_services.sql b/db/foreign_services.sql
index 512d42513..557ede024 100644
--- a/db/foreign_services.sql
+++ b/db/foreign_services.sql
@@ -1,8 +1,5 @@
insert into foreign_service
(id, name, description, created)
values
- ('1','Twitter', 'Twitter Micro-blogging service', now());
-insert into foreign_service
- (id, name, description, created)
-values
- ('2','Facebook', 'Facebook', now());
+ ('1','Twitter', 'Twitter Micro-blogging service', now()),
+ ('2','Facebook', 'Facebook', now());
diff --git a/db/laconica.sql b/db/laconica.sql
index 012270b51..098fa4fd1 100644
--- a/db/laconica.sql
+++ b/db/laconica.sql
@@ -31,7 +31,7 @@ create table avatar (
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table sms_carrier (
- id integer auto_increment primary key comment 'primary key for SMS carrier',
+ id integer primary key comment 'primary key for SMS carrier',
name varchar(64) unique key comment 'name of the carrier',
email_pattern varchar(255) not null comment 'sprintf pattern for making an email address from a phone number',
created datetime not null comment 'date this record was created',
@@ -50,6 +50,7 @@ create table user (
emailnotifyfav tinyint default 1 comment 'Notify by email of favorites',
emailnotifynudge tinyint default 1 comment 'Notify by email of nudges',
emailnotifymsg tinyint default 1 comment 'Notify by email of direct messages',
+ emailnotifyattn tinyint default 1 comment 'Notify by email of @-replies',
emailmicroid tinyint default 1 comment 'whether to publish email microid',
language varchar(50) comment 'preferred language',
timezone varchar(50) comment 'timezone',
@@ -170,7 +171,7 @@ create table token (
tok char(32) not null comment 'identifying value',
secret char(32) not null comment 'secret value',
type tinyint not null default 0 comment 'request or access',
- state tinyint default 0 comment 'for requests; 0 = initial, 1 = authorized, 2 = used',
+ state tinyint default 0 comment 'for requests, 0 = initial, 1 = authorized, 2 = used',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
@@ -180,15 +181,14 @@ create table token (
create table nonce (
consumer_key varchar(255) not null comment 'unique identifier, root URL',
- tok char(32) not null comment 'identifying value',
+ tok char(32) null comment 'buggy old value, ignored',
nonce char(32) not null comment 'nonce',
ts datetime not null comment 'timestamp sent',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
- constraint primary key (consumer_key, tok, nonce),
- constraint foreign key (consumer_key, tok) references token (consumer_key, tok)
+ constraint primary key (consumer_key, ts, nonce)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
/* One-to-many relationship of user to openid_url */
@@ -258,7 +258,8 @@ create table notice_tag (
created datetime not null comment 'date this record was created',
constraint primary key (tag, notice_id),
- index notice_tag_created_idx (created)
+ index notice_tag_created_idx (created),
+ index notice_tag_notice_id_idx (notice_id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
/* Synching with foreign services */
@@ -342,7 +343,7 @@ create table notice_inbox (
user_id integer not null comment 'user receiving the message' references user (id),
notice_id integer not null comment 'notice received' references notice (id),
created datetime not null comment 'date the notice was created',
- source tinyint default 1 comment 'reason it is in the inbox; 1=subscription',
+ source tinyint default 1 comment 'reason it is in the inbox, 1=subscription',
constraint primary key (user_id, notice_id),
index notice_inbox_notice_id_idx (notice_id)
@@ -356,7 +357,8 @@ create table profile_tag (
constraint primary key (tagger, tagged, tag),
index profile_tag_modified_idx (modified),
- index profile_tag_tagger_tag_idx (tagger, tag)
+ index profile_tag_tagger_tag_idx (tagger, tag),
+ index profile_tag_tagged_idx (tagged)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table profile_block (
@@ -400,7 +402,9 @@ create table group_member (
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
- constraint primary key (group_id, profile_id)
+ constraint primary key (group_id, profile_id),
+ index group_member_profile_id_idx (profile_id),
+ index group_member_created_idx (created)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql
index 9882d091a..d9e0c6da7 100644
--- a/db/laconica_pg.sql
+++ b/db/laconica_pg.sql
@@ -8,7 +8,7 @@ create table profile (
homepage varchar(255) /* comment 'identifying URL' */,
bio varchar(140) /* comment 'descriptive biography' */,
location varchar(255) /* comment 'physical location' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
textsearch tsvector
@@ -23,7 +23,7 @@ create table avatar (
mediatype varchar(32) not null /* comment 'file type' */,
filename varchar(255) null /* comment 'local filename, if local' */,
url varchar(255) unique /* comment 'avatar location' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
primary key(profile_id, width, height)
@@ -34,7 +34,7 @@ create table sms_carrier (
id serial primary key /* comment 'primary key for SMS carrier' */,
name varchar(64) unique /* comment 'name of the carrier' */,
email_pattern varchar(255) not null /* comment 'sprintf pattern for making an email address from a phone number' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified ' */
);
@@ -50,6 +50,7 @@ create table "user" (
emailnotifyfav integer default 1 /* comment 'Notify by email of favorites' */,
emailnotifynudge integer default 1 /* comment 'Notify by email of nudges' */,
emailnotifymsg integer default 1 /* comment 'Notify by email of direct messages' */,
+ emailnotifyattn integer default 1 /* command 'Notify by email of @-replies' */,
emailmicroid integer default 1 /* comment 'whether to publish email microid' */,
language varchar(50) /* comment 'preferred language' */,
timezone varchar(50) /* comment 'timezone' */,
@@ -68,7 +69,7 @@ create table "user" (
autosubscribe integer default 0 /* comment 'automatically subscribe to users who subscribe to us' */,
urlshorteningservice varchar(50) default 'ur1.ca' /* comment 'service to use for auto-shortening URLs' */,
inboxed integer default 0 /* comment 'has an inbox been created for this user?' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -81,7 +82,7 @@ create table remote_profile (
uri varchar(255) unique /* comment 'universally unique identifier, usually a tag URI' */,
postnoticeurl varchar(255) /* comment 'URL we use for posting notices' */,
updateprofileurl varchar(255) /* comment 'URL we use for updates to this profile' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -92,7 +93,7 @@ create table subscription (
sms integer default 1 /* comment 'deliver sms messages' */,
token varchar(255) /* comment 'authorization token' */,
secret varchar(255) /* comment 'token secret' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
primary key (subscriber, subscribed)
@@ -108,7 +109,7 @@ create table notice (
content varchar(140) /* comment 'update content' */,
rendered text /* comment 'HTML version of the content' */,
url varchar(255) /* comment 'URL of any attachment (image, video, bookmark, whatever)' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
reply_to integer /* comment 'notice replied to (usually a guess)' */ references notice (id) ,
is_local integer default 0 /* comment 'notice was generated by a user' */,
@@ -123,7 +124,7 @@ create table notice_source (
code varchar(32) primary key not null /* comment 'source code' */,
name varchar(255) not null /* comment 'name of the source' */,
url varchar(255) not null /* comment 'url to link to' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -131,7 +132,7 @@ create table reply (
notice_id integer not null /* comment 'notice that is the reply' */ references notice (id) ,
profile_id integer not null /* comment 'profile replied to' */ references profile (id) ,
- modified timestamp not null default 'now' /* comment 'date this record was modified' */,
+ modified timestamp /* comment 'date this record was modified' */,
replied_id integer /* comment 'notice replied to (not used, see notice.reply_to)' */,
primary key (notice_id, profile_id)
@@ -145,7 +146,7 @@ create table fave (
notice_id integer not null /* comment 'notice that is the favorite' */ references notice (id),
user_id integer not null /* comment 'user who likes this notice' */ references "user" (id) ,
- modified timestamp not null /* comment 'date this record was modified' */,
+ modified timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was modified' */,
primary key (notice_id, user_id)
);
@@ -159,7 +160,7 @@ create table consumer (
consumer_key varchar(255) primary key /* comment 'unique identifier, root URL' */,
seed char(32) not null /* comment 'seed for new tokens by this consumer' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -170,7 +171,7 @@ create table token (
type integer not null default 0 /* comment 'request or access' */,
state integer default 0 /* comment 'for requests; 0 = initial, 1 = authorized, 2 = used' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
primary key (consumer_key, tok)
@@ -179,14 +180,13 @@ create table token (
create table nonce (
consumer_key varchar(255) not null /* comment 'unique identifier, root URL' */,
tok char(32) not null /* comment 'identifying value' */,
- nonce char(32) not null /* comment 'nonce' */,
- ts timestamp not null /* comment 'timestamp sent' */,
+ nonce char(32) null /* comment 'buggy old value, ignored */,
+ ts integer not null /* comment 'timestamp sent' values are epoch, and only used internally */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
- primary key (consumer_key, tok, nonce),
- foreign key (consumer_key, tok) references token (consumer_key, tok)
+ primary key (consumer_key, ts, nonce)
);
/* One-to-many relationship of user to openid_url */
@@ -195,7 +195,7 @@ create table user_openid (
canonical varchar(255) primary key /* comment 'Canonical true URL' */,
display varchar(255) not null unique /* comment 'URL for viewing, may be different from canonical' */,
user_id integer not null /* comment 'user owning this URL' */ references "user" (id) ,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -241,7 +241,7 @@ create table queue_item (
notice_id integer not null /* comment 'notice queued' */ references notice (id) ,
transport varchar(8) not null /* comment 'queue for what? "email", "jabber", "sms", "irc", ...' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
claimed timestamp /* comment 'date this item was claimed' */,
primary key (notice_id, transport)
@@ -253,7 +253,7 @@ create index queue_item_created_idx on queue_item using btree(created);
create table notice_tag (
tag varchar( 64 ) not null /* comment 'hash tag associated with this notice' */,
notice_id integer not null /* comment 'notice tagged' */ references notice (id) ,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
primary key (tag, notice_id)
);
@@ -265,7 +265,7 @@ create table foreign_service (
id int not null primary key /* comment 'numeric key for service' */,
name varchar(32) not null unique /* comment 'name of the service' */,
description varchar(255) /* comment 'description' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -274,7 +274,7 @@ create table foreign_user (
service int not null /* comment 'foreign key to service' */ references foreign_service(id) ,
uri varchar(255) not null unique /* comment 'identifying URI' */,
nickname varchar(255) /* comment 'nickname on foreign service' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
primary key (id, service)
@@ -288,8 +288,8 @@ create table foreign_link (
noticesync int not null default 1 /* comment 'notice synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies' */,
friendsync int not null default 2 /* comment 'friend synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming */,
profilesync int not null default 1 /* comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming' */,
- created timestamp not null /* comment 'date this record was created' */,
- modified timestamp not null /* comment 'date this record was modified' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
+ modified timestamp /* comment 'date this record was modified' */,
primary key (user_id,foreign_id,service)
);
@@ -299,7 +299,7 @@ create table foreign_subscription (
service int not null /* comment 'service where relationship happens' */ references foreign_service(id) ,
subscriber int not null /* comment 'subscriber on foreign service' */ ,
subscribed int not null /* comment 'subscribed user' */ ,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
primary key (service, subscriber, subscribed)
);
@@ -311,7 +311,7 @@ create table invitation (
user_id int not null /* comment 'who sent the invitation' */ references "user" (id),
address varchar(255) not null /* comment 'invitation sent to' */,
address_type varchar(8) not null /* comment 'address type ("email", "jabber", "sms") '*/,
- created timestamp not null /* comment 'date this record was created' */
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */
);
create index invitation_address_idx on invitation using btree(address,address_type);
@@ -326,7 +326,7 @@ create table message (
content varchar(140) /* comment 'message content' */,
rendered text /* comment 'HTML version of the content' */,
url varchar(255) /* comment 'URL of any attachment (image, video, bookmark, whatever)' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
source varchar(32) /* comment 'source of comment, like "web", "im", or "clientname"' */
@@ -339,7 +339,7 @@ create table notice_inbox (
user_id integer not null /* comment 'user receiving the message' */ references "user" (id),
notice_id integer not null /* comment 'notice received' */ references notice (id),
- created timestamp not null /* comment 'date the notice was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date the notice was created' */,
source integer default 1 /* comment 'reason it is in the inbox; 1=subscription' */,
primary key (user_id, notice_id)
@@ -382,7 +382,7 @@ create table user_group (
stream_logo varchar(255) /* comment 'stream-sized logo' */,
mini_logo varchar(255) /* comment 'mini logo' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */
);
@@ -394,7 +394,7 @@ create table group_member (
profile_id integer not null /* comment 'foreign key to profile table' */ references profile (id),
is_admin integer default 0 /* comment 'is this user an admin?' */,
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
primary key (group_id, profile_id)
@@ -405,7 +405,7 @@ create table related_group (
group_id integer not null /* comment 'foreign key to user_group' */ references user_group (id) ,
related_group_id integer not null /* comment 'foreign key to user_group' */ references user_group (id),
- created timestamp not null /* comment 'date this record was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
primary key (group_id, related_group_id)
@@ -414,7 +414,7 @@ create table related_group (
create table group_inbox (
group_id integer not null /* comment 'group receiving the message' references user_group (id) */,
notice_id integer not null /* comment 'notice received' references notice (id) */,
- created timestamp not null /* comment 'date the notice was created' */,
+ created timestamp not null default CURRENT_TIMESTAMP /* comment 'date the notice was created' */,
primary key (group_id, notice_id)
);
diff --git a/db/notice_source.sql b/db/notice_source.sql
new file mode 100644
index 000000000..ea04862e0
--- /dev/null
+++ b/db/notice_source.sql
@@ -0,0 +1,46 @@
+INSERT INTO notice_source
+ (code, name, url, created)
+VALUES
+ ('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()),
+ ('Facebook','Facebook','http://apps.facebook.com/identica/', now()),
+ ('Gwibber','Gwibber','http://launchpad.net/gwibber', now()),
+ ('HelloTxt','HelloTxt','http://hellotxt.com/', now()),
+ ('IdentiFox','IdentiFox','http://www.bitbucket.org/uncryptic/identifox/', now()),
+ ('LaTwit','LaTwit','http://latwit.mac65.com/', now()),
+ ('Nambu','Nambu','http://www.nambu.com/', now()),
+ ('Pikchur','Pikchur','http://www.pikchur.com/', now()),
+ ('Ping.fm','Ping.fm','http://ping.fm/', now()),
+ ('Twidge','Twidge','http://software.complete.org/twidge', now()),
+ ('Updating.Me','Updating.Me','http://updating.me/', now()),
+ ('betwittered','BeTwittered','http://www.32hours.com/betwitteredinfo/', now()),
+ ('bti','bti','http://gregkh.github.com/bti/', now()),
+ ('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()),
+ ('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()),
+ ('identichat','identichat','http://identichat.prosody.im/', now()),
+ ('identitwitch','IdentiTwitch','http://richfish.org/identitwitch/', now()),
+ ('mbpidgin','mbpidgin','http://code.google.com/p/microblog-purple/', now()),
+ ('moconica','Moconica','http://moconica.com/', now()),
+ ('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()),
+ ('posty','Posty','http://spreadingfunkyness.com/posty/', now()),
+ ('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()),
+ ('rssdent','rssdent','http://github.com/zcopley/rssdent/tree/master', now()),
+ ('rygh.no','rygh.no','http://rygh.no/', now()),
+ ('ryghsms','ryghsms','http://sms.rygh.no/', now()),
+ ('smob','SMOB','http://smob.sioc-project.org/', now()),
+ ('spaz','Spaz','http://funkatron.com/spaz', now()),
+ ('tarpipe','tarpipe','http://tarpipe.com/', now()),
+ ('tjunar','Tjunar','http://nederflash.nl/boek/titels/tjunar-air', now()),
+ ('tr.im','tr.im','http://tr.im/', now()),
+ ('tweenky','Tweenky','http://beta.tweenky.com/', now()),
+ ('twhirl','Twhirl','http://www.twhirl.org/', now()),
+ ('twibble','twibble','http://www.twibble.de/', now()),
+ ('twidge','Twidge','http://software.complete.org/twidge', now()),
+ ('twidroid','twidroid','http://www.twidroid.com/', now()),
+ ('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()),
+ ('twitterfeed','twitterfeed','http://twitterfeed.com/', now()),
+ ('twitterphoto','TwitterPhoto','http://richfish.org/twitterphoto/', now()),
+ ('twitterpm','Net::Twitter','http://search.cpan.org/dist/Net-Twitter/', now()),
+ ('twittertools','Twitter Tools','http://wordpress.org/extend/plugins/twitter-tools/', now()),
+ ('twitux','Twitux','http://live.gnome.org/DanielMorales/Twitux', now()),
+ ('twitvim','TwitVim','http://vim.sourceforge.net/scripts/script.php?script_id=2204', now()),
+ ('urfastr','urfastr','http://urfastr.net/', now());
diff --git a/db/sms_carrier.sql b/db/sms_carrier.sql
new file mode 100644
index 000000000..6879f2089
--- /dev/null
+++ b/db/sms_carrier.sql
@@ -0,0 +1,63 @@
+INSERT INTO sms_carrier
+ (id, name, email_pattern, created)
+VALUES
+ (100056, '3 River Wireless', '%s@sms.3rivers.net', now()),
+ (100057, '7-11 Speakout', '%s@cingularme.com', now()),
+ (100058, 'Airtel (Karnataka, India)', '%s@airtelkk.com', now()),
+ (100059, 'Alaska Communications Systems', '%s@msg.acsalaska.com', now()),
+ (100060, 'Alltel Wireless', '%s@message.alltel.com', now()),
+ (100061, 'AT&T Wireless', '%s@txt.att.net', now()),
+ (100062, 'Bell Mobility (Canada)', '%s@txt.bell.ca', now()),
+ (100063, 'Boost Mobile', '%s@myboostmobile.com', now()),
+ (100064, 'Cellular One (Dobson)', '%s@mobile.celloneusa.com', now()),
+ (100065, 'Cingular (Postpaid)', '%s@cingularme.com', now()),
+ (100066, 'Centennial Wireless', '%s@cwemail.com', now()),
+ (100067, 'Cingular (GoPhone prepaid)', '%s@cingularme.com', now()),
+ (100068, 'Claro (Nicaragua)', '%s@ideasclaro-ca.com', now()),
+ (100069, 'Comcel', '%s@comcel.com.co', now()),
+ (100070, 'Cricket', '%s@sms.mycricket.com', now()),
+ (100071, 'CTI', '%s@sms.ctimovil.com.ar', now()),
+ (100072, 'Emtel (Mauritius)', '%s@emtelworld.net', now()),
+ (100073, 'Fido (Canada)', '%s@fido.ca', now()),
+ (100074, 'General Communications Inc.', '%s@msg.gci.net', now()),
+ (100075, 'Globalstar', '%s@msg.globalstarusa.com', now()),
+ (100076, 'Helio', '%s@myhelio.com', now()),
+ (100077, 'Illinois Valley Cellular', '%s@ivctext.com', now()),
+ (100078, 'i wireless', '%s.iws@iwspcs.net', now()),
+ (100079, 'Meteor (Ireland)', '%s@sms.mymeteor.ie', now()),
+ (100080, 'Mero Mobile (Nepal)', '%s@sms.spicenepal.com', now()),
+ (100081, 'MetroPCS', '%s@mymetropcs.com', now()),
+ (100082, 'Movicom', '%s@movimensaje.com.ar', now()),
+ (100083, 'Mobitel (Sri Lanka)', '%s@sms.mobitel.lk', now()),
+ (100084, 'Movistar (Colombia)', '%s@movistar.com.co', now()),
+ (100085, 'MTN (South Africa)', '%s@sms.co.za', now()),
+ (100086, 'MTS (Canada)', '%s@text.mtsmobility.com', now()),
+ (100087, 'Nextel (Argentina)', '%s@nextel.net.ar', now()),
+ (100088, 'Orange (Poland)', '%s@orange.pl', now()),
+ (100089, 'Personal (Argentina)', '%s@personal-net.com.ar', now()),
+ (100090, 'Plus GSM (Poland)', '%s@text.plusgsm.pl', now()),
+ (100091, 'President\'s Choice (Canada)', '%s@txt.bell.ca', now()),
+ (100092, 'Qwest', '%s@qwestmp.com', now()),
+ (100093, 'Rogers (Canada)', '%s@pcs.rogers.com', now()),
+ (100094, 'Sasktel (Canada)', '%s@sms.sasktel.com', now()),
+ (100095, 'Setar Mobile email (Aruba)', '%s@mas.aw', now()),
+ (100096, 'Solo Mobile', '%s@txt.bell.ca', now()),
+ (100097, 'Sprint (PCS)', '%s@messaging.sprintpcs.com', now()),
+ (100098, 'Sprint (Nextel)', '%s@page.nextel.com', now()),
+ (100099, 'Suncom', '%s@tms.suncom.com', now()),
+ (100100, 'T-Mobile', '%s@tmomail.net', now()),
+ (100101, 'T-Mobile (Austria)', '%s@sms.t-mobile.at', now()),
+ (100102, 'Telus Mobility (Canada)', '%s@msg.telus.com', now()),
+ (100103, 'Thumb Cellular', '%s@sms.thumbcellular.com', now()),
+ (100104, 'Tigo (Formerly Ola)', '%s@sms.tigo.com.co', now()),
+ (100105, 'Unicel', '%s@utext.com', now()),
+ (100106, 'US Cellular', '%s@email.uscc.net', now()),
+ (100107, 'Verizon', '%s@vtext.com', now()),
+ (100108, 'Virgin Mobile (Canada)', '%s@vmobile.ca', now()),
+ (100109, 'Virgin Mobile (USA)', '%s@vmobl.com', now()),
+ (100110, 'YCC', '%s@sms.ycc.ru', now()),
+ (100111, 'Orange (UK)', '%s@orange.net', now()),
+ (100112, 'Cincinnati Bell Wireless', '%s@gocbw.com', now()),
+ (100113, 'T-Mobile Germany', '%s@t-mobile-sms.de', now()),
+ (100114, 'Vodafone Germany', '%s@vodafone-sms.de', now()),
+ (100115, 'E-Plus', '%s@smsmail.eplus.de', now());
diff --git a/doc/about b/doc-src/about
index 3036a51b9..3036a51b9 100644
--- a/doc/about
+++ b/doc-src/about
diff --git a/doc-src/badge b/doc-src/badge
new file mode 100644
index 000000000..1c368eb69
--- /dev/null
+++ b/doc-src/badge
@@ -0,0 +1,65 @@
+Install the %%site.name%% badge on you blog or web site to show the latest updates
+from you and your friends!
+
+<MTMarkdownOptions output='raw'>
+<script type="text/javascript" src="http://identi.ca/js/identica-badge.js">
+{
+ "user":"kentbrew",
+ "server":"identi.ca",
+ "headerText":" and friends"
+}
+</script>
+</MTMarkdownOptions>
+
+Things to try
+--------------
+
+* Click an avatar and the badge will refresh with that user's timeline
+* Click a nickname to open a user's profile in your browser
+* Click a notice's timestamp to view the notice in your browser
+* @-replies and #tags are live links
+
+## Installation instructions
+
+Copy and paste the following JavaScript into an HTML page where
+you want the badge to show up. Substitute your own ID in the user
+parameter.
+
+<pre>
+ &lt;script type=&quot;text/javascript&quot; src=&quot;http://identi.ca/js/identica-badge.js&quot;&gt;
+ {
+ &quot;user&quot;:&quot;kentbrew&quot;,
+ &quot;server&quot;:&quot;identi.ca&quot;,
+ &quot;headerText&quot;:&quot; and friends&quot;
+ }
+ &lt;/script&gt;
+
+</pre>
+
+
+
+Valid parameters for the badge:
+-------------------------------
+
+* user : defaults to 7000 (@kentbrew)
+* headerText : defaults to empty
+* height : defaults to 350px
+* width : defaults to 300px
+* background : defaults to #193441. If you set evenBackground, oddBackground,
+ and headerBackground, you won't see it at all.
+* border : defaults to 1px solid black
+* userColor : defaults to whatever link color is set to on your page
+* headerBackground : defaults to transparent
+* headerColor : defaults to white
+* evenBackground : defaults to #fff
+* oddBackground : defaults to #eee
+* thumbnailBorder : 1px solid black
+* thumbnailSize : defaults to 24px
+* padding : defaults to 3px
+* server : defaults to identi.ca
+
+Licence
+-------
+
+Identi.ca badge by [Kent Brewster](http://kentbrewster.com/identica-badge/).
+Licenced under [CC-BY-SA-3](http://kentbrewster.com/rights-and-permissions/).
diff --git a/doc/contact b/doc-src/contact
index a8efc456a..a8efc456a 100644
--- a/doc/contact
+++ b/doc-src/contact
diff --git a/doc/faq b/doc-src/faq
index 31582b9f0..31582b9f0 100644
--- a/doc/faq
+++ b/doc-src/faq
diff --git a/doc/groups b/doc-src/groups
index 645390e0c..645390e0c 100644
--- a/doc/groups
+++ b/doc-src/groups
diff --git a/doc/help b/doc-src/help
index a8cfccd2b..a8cfccd2b 100644
--- a/doc/help
+++ b/doc-src/help
diff --git a/doc/im b/doc-src/im
index da07f9fe7..da07f9fe7 100644
--- a/doc/im
+++ b/doc-src/im
diff --git a/doc/openid b/doc-src/openid
index c741e3674..c741e3674 100644
--- a/doc/openid
+++ b/doc-src/openid
diff --git a/doc/openmublog b/doc-src/openmublog
index 6e3abee42..6e3abee42 100644
--- a/doc/openmublog
+++ b/doc-src/openmublog
diff --git a/doc/privacy b/doc-src/privacy
index 90c7b3c7f..90c7b3c7f 100644
--- a/doc/privacy
+++ b/doc-src/privacy
diff --git a/doc/sms b/doc-src/sms
index 1beb49786..1beb49786 100644
--- a/doc/sms
+++ b/doc-src/sms
diff --git a/doc/source b/doc-src/source
index 83debbe53..83debbe53 100644
--- a/doc/source
+++ b/doc-src/source
diff --git a/doc/tags b/doc-src/tags
index 2ed352e70..2ed352e70 100644
--- a/doc/tags
+++ b/doc-src/tags
diff --git a/extlib/Net/URL.php b/extlib/Net/URL.php
new file mode 100644
index 000000000..3dcfef60d
--- /dev/null
+++ b/extlib/Net/URL.php
@@ -0,0 +1,485 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Copyright (c) 2002-2004, Richard Heyes |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | o Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | o Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution.|
+// | o The names of the authors may not be used to endorse or promote |
+// | products derived from this software without specific prior written |
+// | permission. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// | |
+// +-----------------------------------------------------------------------+
+// | Author: Richard Heyes <richard at php net> |
+// +-----------------------------------------------------------------------+
+//
+// $Id: URL.php,v 1.49 2007/06/28 14:43:07 davidc Exp $
+//
+// Net_URL Class
+
+
+class Net_URL
+{
+ var $options = array('encode_query_keys' => false);
+ /**
+ * Full url
+ * @var string
+ */
+ var $url;
+
+ /**
+ * Protocol
+ * @var string
+ */
+ var $protocol;
+
+ /**
+ * Username
+ * @var string
+ */
+ var $username;
+
+ /**
+ * Password
+ * @var string
+ */
+ var $password;
+
+ /**
+ * Host
+ * @var string
+ */
+ var $host;
+
+ /**
+ * Port
+ * @var integer
+ */
+ var $port;
+
+ /**
+ * Path
+ * @var string
+ */
+ var $path;
+
+ /**
+ * Query string
+ * @var array
+ */
+ var $querystring;
+
+ /**
+ * Anchor
+ * @var string
+ */
+ var $anchor;
+
+ /**
+ * Whether to use []
+ * @var bool
+ */
+ var $useBrackets;
+
+ /**
+ * PHP4 Constructor
+ *
+ * @see __construct()
+ */
+ function Net_URL($url = null, $useBrackets = true)
+ {
+ $this->__construct($url, $useBrackets);
+ }
+
+ /**
+ * PHP5 Constructor
+ *
+ * Parses the given url and stores the various parts
+ * Defaults are used in certain cases
+ *
+ * @param string $url Optional URL
+ * @param bool $useBrackets Whether to use square brackets when
+ * multiple querystrings with the same name
+ * exist
+ */
+ function __construct($url = null, $useBrackets = true)
+ {
+ $this->url = $url;
+ $this->useBrackets = $useBrackets;
+
+ $this->initialize();
+ }
+
+ function initialize()
+ {
+ $HTTP_SERVER_VARS = !empty($_SERVER) ? $_SERVER : $GLOBALS['HTTP_SERVER_VARS'];
+
+ $this->user = '';
+ $this->pass = '';
+ $this->host = '';
+ $this->port = 80;
+ $this->path = '';
+ $this->querystring = array();
+ $this->anchor = '';
+
+ // Only use defaults if not an absolute URL given
+ if (!preg_match('/^[a-z0-9]+:\/\//i', $this->url)) {
+ $this->protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http');
+
+ /**
+ * Figure out host/port
+ */
+ if (!empty($HTTP_SERVER_VARS['HTTP_HOST']) &&
+ preg_match('/^(.*)(:([0-9]+))?$/U', $HTTP_SERVER_VARS['HTTP_HOST'], $matches))
+ {
+ $host = $matches[1];
+ if (!empty($matches[3])) {
+ $port = $matches[3];
+ } else {
+ $port = $this->getStandardPort($this->protocol);
+ }
+ }
+
+ $this->user = '';
+ $this->pass = '';
+ $this->host = !empty($host) ? $host : (isset($HTTP_SERVER_VARS['SERVER_NAME']) ? $HTTP_SERVER_VARS['SERVER_NAME'] : 'localhost');
+ $this->port = !empty($port) ? $port : (isset($HTTP_SERVER_VARS['SERVER_PORT']) ? $HTTP_SERVER_VARS['SERVER_PORT'] : $this->getStandardPort($this->protocol));
+ $this->path = !empty($HTTP_SERVER_VARS['PHP_SELF']) ? $HTTP_SERVER_VARS['PHP_SELF'] : '/';
+ $this->querystring = isset($HTTP_SERVER_VARS['QUERY_STRING']) ? $this->_parseRawQuerystring($HTTP_SERVER_VARS['QUERY_STRING']) : null;
+ $this->anchor = '';
+ }
+
+ // Parse the url and store the various parts
+ if (!empty($this->url)) {
+ $urlinfo = parse_url($this->url);
+
+ // Default querystring
+ $this->querystring = array();
+
+ foreach ($urlinfo as $key => $value) {
+ switch ($key) {
+ case 'scheme':
+ $this->protocol = $value;
+ $this->port = $this->getStandardPort($value);
+ break;
+
+ case 'user':
+ case 'pass':
+ case 'host':
+ case 'port':
+ $this->$key = $value;
+ break;
+
+ case 'path':
+ if ($value{0} == '/') {
+ $this->path = $value;
+ } else {
+ $path = dirname($this->path) == DIRECTORY_SEPARATOR ? '' : dirname($this->path);
+ $this->path = sprintf('%s/%s', $path, $value);
+ }
+ break;
+
+ case 'query':
+ $this->querystring = $this->_parseRawQueryString($value);
+ break;
+
+ case 'fragment':
+ $this->anchor = $value;
+ break;
+ }
+ }
+ }
+ }
+ /**
+ * Returns full url
+ *
+ * @return string Full url
+ * @access public
+ */
+ function getURL()
+ {
+ $querystring = $this->getQueryString();
+
+ $this->url = $this->protocol . '://'
+ . $this->user . (!empty($this->pass) ? ':' : '')
+ . $this->pass . (!empty($this->user) ? '@' : '')
+ . $this->host . ($this->port == $this->getStandardPort($this->protocol) ? '' : ':' . $this->port)
+ . $this->path
+ . (!empty($querystring) ? '?' . $querystring : '')
+ . (!empty($this->anchor) ? '#' . $this->anchor : '');
+
+ return $this->url;
+ }
+
+ /**
+ * Adds or updates a querystring item (URL parameter).
+ * Automatically encodes parameters with rawurlencode() if $preencoded
+ * is false.
+ * You can pass an array to $value, it gets mapped via [] in the URL if
+ * $this->useBrackets is activated.
+ *
+ * @param string $name Name of item
+ * @param string $value Value of item
+ * @param bool $preencoded Whether value is urlencoded or not, default = not
+ * @access public
+ */
+ function addQueryString($name, $value, $preencoded = false)
+ {
+ if ($this->getOption('encode_query_keys')) {
+ $name = rawurlencode($name);
+ }
+
+ if ($preencoded) {
+ $this->querystring[$name] = $value;
+ } else {
+ $this->querystring[$name] = is_array($value) ? array_map('rawurlencode', $value): rawurlencode($value);
+ }
+ }
+
+ /**
+ * Removes a querystring item
+ *
+ * @param string $name Name of item
+ * @access public
+ */
+ function removeQueryString($name)
+ {
+ if ($this->getOption('encode_query_keys')) {
+ $name = rawurlencode($name);
+ }
+
+ if (isset($this->querystring[$name])) {
+ unset($this->querystring[$name]);
+ }
+ }
+
+ /**
+ * Sets the querystring to literally what you supply
+ *
+ * @param string $querystring The querystring data. Should be of the format foo=bar&x=y etc
+ * @access public
+ */
+ function addRawQueryString($querystring)
+ {
+ $this->querystring = $this->_parseRawQueryString($querystring);
+ }
+
+ /**
+ * Returns flat querystring
+ *
+ * @return string Querystring
+ * @access public
+ */
+ function getQueryString()
+ {
+ if (!empty($this->querystring)) {
+ foreach ($this->querystring as $name => $value) {
+ // Encode var name
+ $name = rawurlencode($name);
+
+ if (is_array($value)) {
+ foreach ($value as $k => $v) {
+ $querystring[] = $this->useBrackets ? sprintf('%s[%s]=%s', $name, $k, $v) : ($name . '=' . $v);
+ }
+ } elseif (!is_null($value)) {
+ $querystring[] = $name . '=' . $value;
+ } else {
+ $querystring[] = $name;
+ }
+ }
+ $querystring = implode(ini_get('arg_separator.output'), $querystring);
+ } else {
+ $querystring = '';
+ }
+
+ return $querystring;
+ }
+
+ /**
+ * Parses raw querystring and returns an array of it
+ *
+ * @param string $querystring The querystring to parse
+ * @return array An array of the querystring data
+ * @access private
+ */
+ function _parseRawQuerystring($querystring)
+ {
+ $parts = preg_split('/[' . preg_quote(ini_get('arg_separator.input'), '/') . ']/', $querystring, -1, PREG_SPLIT_NO_EMPTY);
+ $return = array();
+
+ foreach ($parts as $part) {
+ if (strpos($part, '=') !== false) {
+ $value = substr($part, strpos($part, '=') + 1);
+ $key = substr($part, 0, strpos($part, '='));
+ } else {
+ $value = null;
+ $key = $part;
+ }
+
+ if (!$this->getOption('encode_query_keys')) {
+ $key = rawurldecode($key);
+ }
+
+ if (preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) {
+ $key = $matches[1];
+ $idx = $matches[2];
+
+ // Ensure is an array
+ if (empty($return[$key]) || !is_array($return[$key])) {
+ $return[$key] = array();
+ }
+
+ // Add data
+ if ($idx === '') {
+ $return[$key][] = $value;
+ } else {
+ $return[$key][$idx] = $value;
+ }
+ } elseif (!$this->useBrackets AND !empty($return[$key])) {
+ $return[$key] = (array)$return[$key];
+ $return[$key][] = $value;
+ } else {
+ $return[$key] = $value;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Resolves //, ../ and ./ from a path and returns
+ * the result. Eg:
+ *
+ * /foo/bar/../boo.php => /foo/boo.php
+ * /foo/bar/../../boo.php => /boo.php
+ * /foo/bar/.././/boo.php => /foo/boo.php
+ *
+ * This method can also be called statically.
+ *
+ * @param string $path URL path to resolve
+ * @return string The result
+ */
+ function resolvePath($path)
+ {
+ $path = explode('/', str_replace('//', '/', $path));
+
+ for ($i=0; $i<count($path); $i++) {
+ if ($path[$i] == '.') {
+ unset($path[$i]);
+ $path = array_values($path);
+ $i--;
+
+ } elseif ($path[$i] == '..' AND ($i > 1 OR ($i == 1 AND $path[0] != '') ) ) {
+ unset($path[$i]);
+ unset($path[$i-1]);
+ $path = array_values($path);
+ $i -= 2;
+
+ } elseif ($path[$i] == '..' AND $i == 1 AND $path[0] == '') {
+ unset($path[$i]);
+ $path = array_values($path);
+ $i--;
+
+ } else {
+ continue;
+ }
+ }
+
+ return implode('/', $path);
+ }
+
+ /**
+ * Returns the standard port number for a protocol
+ *
+ * @param string $scheme The protocol to lookup
+ * @return integer Port number or NULL if no scheme matches
+ *
+ * @author Philippe Jausions <Philippe.Jausions@11abacus.com>
+ */
+ function getStandardPort($scheme)
+ {
+ switch (strtolower($scheme)) {
+ case 'http': return 80;
+ case 'https': return 443;
+ case 'ftp': return 21;
+ case 'imap': return 143;
+ case 'imaps': return 993;
+ case 'pop3': return 110;
+ case 'pop3s': return 995;
+ default: return null;
+ }
+ }
+
+ /**
+ * Forces the URL to a particular protocol
+ *
+ * @param string $protocol Protocol to force the URL to
+ * @param integer $port Optional port (standard port is used by default)
+ */
+ function setProtocol($protocol, $port = null)
+ {
+ $this->protocol = $protocol;
+ $this->port = is_null($port) ? $this->getStandardPort($protocol) : $port;
+ }
+
+ /**
+ * Set an option
+ *
+ * This function set an option
+ * to be used thorough the script.
+ *
+ * @access public
+ * @param string $optionName The optionname to set
+ * @param string $value The value of this option.
+ */
+ function setOption($optionName, $value)
+ {
+ if (!array_key_exists($optionName, $this->options)) {
+ return false;
+ }
+
+ $this->options[$optionName] = $value;
+ $this->initialize();
+ }
+
+ /**
+ * Get an option
+ *
+ * This function gets an option
+ * from the $this->options array
+ * and return it's value.
+ *
+ * @access public
+ * @param string $opionName The name of the option to retrieve
+ * @see $this->options
+ */
+ function getOption($optionName)
+ {
+ if (!isset($this->options[$optionName])) {
+ return false;
+ }
+
+ return $this->options[$optionName];
+ }
+
+}
+?>
diff --git a/extlib/Net/URL/Mapper.php b/extlib/Net/URL/Mapper.php
new file mode 100644
index 000000000..65e38818b
--- /dev/null
+++ b/extlib/Net/URL/Mapper.php
@@ -0,0 +1,324 @@
+<?php
+/**
+ * URL parser and mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Mapper.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+require_once 'Net/URL/Mapper/Path.php';
+require_once 'Net/URL/Mapper/Exception.php';
+
+/**
+ * URL parser and mapper class
+ *
+ * This class takes an URL and a configuration and returns formatted data
+ * about the request according to a configuration parameter
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @version Release: @package_version@
+ */
+class Net_URL_Mapper
+{
+ /**
+ * Array of Net_URL_Mapper instances
+ * @var array
+ */
+ private static $instances = array();
+
+ /**
+ * Mapped paths collection
+ * @var array
+ */
+ protected $paths = array();
+
+ /**
+ * Prefix used for url mapping
+ * @var string
+ */
+ protected $prefix = '';
+
+ /**
+ * Optional scriptname if mod_rewrite is not available
+ * @var string
+ */
+ protected $scriptname = '';
+
+ /**
+ * Mapper instance id
+ * @var string
+ */
+ protected $id = '__default__';
+
+ /**
+ * Class constructor
+ * Constructor is private, you should use getInstance() instead.
+ */
+ private function __construct() { }
+
+ /**
+ * Returns a singleton object corresponding to the requested instance id
+ * @param string Requested instance name
+ * @return Object Net_URL_Mapper Singleton
+ */
+ public static function getInstance($id = '__default__')
+ {
+ if (!isset(self::$instances[$id])) {
+ $m = new Net_URL_Mapper();
+ $m->id = $id;
+ self::$instances[$id] = $m;
+ }
+ return self::$instances[$id];
+ }
+
+ /**
+ * Returns the instance id
+ * @return string Mapper instance id
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Parses a path and creates a connection
+ * @param string The path to connect
+ * @param array Default values for path parts
+ * @param array Regular expressions for path parts
+ * @return object Net_URL_Mapper_Path
+ */
+ public function connect($path, $defaults = array(), $rules = array())
+ {
+ $pathObj = new Net_URL_Mapper_Path($path, $defaults, $rules);
+ $this->addPath($pathObj);
+ return $pathObj;
+ }
+
+ /**
+ * Set the url prefix if needed
+ *
+ * Example: using the prefix to differenciate mapper instances
+ * <code>
+ * $fr = Net_URL_Mapper::getInstance('fr');
+ * $fr->setPrefix('/fr');
+ * $en = Net_URL_Mapper::getInstance('en');
+ * $en->setPrefix('/en');
+ * </code>
+ *
+ * @param string URL prefix
+ */
+ public function setPrefix($prefix)
+ {
+ $this->prefix = '/'.trim($prefix, '/');
+ }
+
+ /**
+ * Set the scriptname if mod_rewrite not available
+ *
+ * Example: will match and generate url like
+ * - index.php/view/product/1
+ * <code>
+ * $m = Net_URL_Mapper::getInstance();
+ * $m->setScriptname('index.php');
+ * </code>
+ * @param string URL prefix
+ */
+ public function setScriptname($scriptname)
+ {
+ $this->scriptname = $scriptname;
+ }
+
+ /**
+ * Will attempt to match an url with a defined path
+ *
+ * If an url corresponds to a path, the resulting values are returned
+ * in an array. If none is found, null is returned. In case an url is
+ * matched but its content doesn't validate the path rules, an exception is
+ * thrown.
+ *
+ * @param string URL
+ * @return array|null array if match found, null otherwise
+ * @throws Net_URL_Mapper_InvalidException
+ */
+ public function match($url)
+ {
+ $nurl = '/'.trim($url, '/');
+
+ // Remove scriptname if needed
+
+ if (!empty($this->scriptname) &&
+ strpos($nurl, $this->scriptname) === 0) {
+ $nurl = substr($nurl, strlen($this->scriptname));
+ if (empty($nurl)) {
+ $nurl = '/';
+ }
+ }
+
+ // Remove prefix
+
+ if (!empty($this->prefix)) {
+ if (strpos($nurl, $this->prefix) !== 0) {
+ return null;
+ }
+ $nurl = substr($nurl, strlen($this->prefix));
+ if (empty($nurl)) {
+ $nurl = '/';
+ }
+ }
+
+ // Remove query string
+
+ if (($pos = strpos($nurl, '?')) !== false) {
+ $nurl = substr($nurl, 0, $pos);
+ }
+
+ $paths = array();
+ $values = null;
+
+ // Make a list of paths that conform to route format
+
+ foreach ($this->paths as $path) {
+ $regex = $path->getFormat();
+ if (preg_match($regex, $nurl)) {
+ $paths[] = $path;
+ }
+ }
+
+ // Make sure one of the paths found is valid
+
+ foreach ($paths as $path) {
+ $regex = $path->getRule();
+ if (preg_match($regex, $nurl, $matches)) {
+ $values = $path->getDefaults();
+ array_shift($matches);
+ $clean = array();
+ foreach ($matches as $k => $v) {
+ $v = trim($v, '/');
+ if (!is_int($k) && $v !== '') {
+ $values[$k] = $v;
+ }
+ }
+ break;
+ }
+ }
+
+ // A path conforms but does not validate
+
+ if (is_null($values) && !empty($paths)) {
+ $e = new Net_URL_Mapper_InvalidException('A path was found but is invalid.');
+ $e->setPath($paths[0]);
+ $e->setUrl($url);
+ throw $e;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Generate an url based on given parameters
+ *
+ * Will attempt to find a path definition that matches the given parameters and
+ * will generate an url based on this path.
+ *
+ * @param array Values to be used for the url generation
+ * @param array Key/value pairs for query string if needed
+ * @param string Anchor (fragment) if needed
+ * @return string|false String if a rule was found, false otherwise
+ */
+ public function generate($values = array(), $qstring = array(), $anchor = '')
+ {
+ // Use root path if any
+
+ if (empty($values) && isset($this->paths['/'])) {
+ return $this->scriptname.$this->prefix.$this->paths['/']->generate($values, $qstring, $anchor);
+ }
+
+ foreach ($this->paths as $path) {
+ $set = array();
+ foreach ($values as $k => $v) {
+ if ($path->hasKey($k, $v)) {
+ $set[$k] = $v;
+ }
+ }
+
+ if (count($set) == count($values) &&
+ count($set) <= $path->getMaxKeys()) {
+
+ $req = $path->getRequired();
+ if (count(array_intersect(array_keys($set), $req)) != count($req)) {
+ continue;
+ }
+ $gen = $path->generate($set, $qstring, $anchor);
+ return $this->scriptname.$this->prefix.$gen;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns defined paths
+ * @return array Array of paths
+ */
+ public function getPaths()
+ {
+ return $this->paths;
+ }
+
+ /**
+ * Reset all paths
+ * This is probably only useful for testing
+ */
+ public function reset()
+ {
+ $this->paths = array();
+ $this->prefix = '';
+ }
+
+ /**
+ * Add a new path to the mapper
+ * @param object Net_URL_Mapper_Path object
+ */
+ public function addPath(Net_URL_Mapper_Path $path)
+ {
+ $this->paths[$path->getPath()] = $path;
+ }
+
+}
+?> \ No newline at end of file
diff --git a/extlib/Net/URL/Mapper/Exception.php b/extlib/Net/URL/Mapper/Exception.php
new file mode 100644
index 000000000..ac3ad172b
--- /dev/null
+++ b/extlib/Net/URL/Mapper/Exception.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Exception classes for Net_URL_Mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Exception.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+/**
+ * Base class for exceptions in PEAR
+ */
+require_once 'PEAR/Exception.php';
+
+/**
+ * Base class for exceptions in Net_URL_Mapper package
+ *
+ * Such a base class is required by the Exception RFC:
+ * http://pear.php.net/pepr/pepr-proposal-show.php?id=132
+ * It will rarely be thrown directly, its specialized subclasses will be
+ * thrown most of the time.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @version Release: @package_version@
+ */
+class Net_URL_Mapper_Exception extends PEAR_Exception
+{
+}
+
+/**
+ * Exception thrown when a path is invalid
+ *
+ * A path can conform to a given structure, but contain invalid parameters.
+ * <code>
+ * $m = Net_URL_Mapper::getInstance();
+ * $m->connect('hi/:name', null, array('name'=>'[a-z]+'));
+ * $m->match('/hi/FOXY'); // Will throw the exception
+ * </code>
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @version Release: @package_version@
+ */
+class Net_URL_Mapper_InvalidException extends Net_URL_Mapper_Exception
+{
+ protected $path;
+ protected $url;
+
+ public function setPath($path)
+ {
+ $this->path = $path;
+ }
+
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ }
+
+ public function getUrl()
+ {
+ return $this->url;
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Net/URL/Mapper/Part.php b/extlib/Net/URL/Mapper/Part.php
new file mode 100644
index 000000000..2f15b2c16
--- /dev/null
+++ b/extlib/Net/URL/Mapper/Part.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * URL parser and mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Part.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+abstract class Net_URL_Mapper_Part
+{
+ protected $defaults;
+ protected $rule;
+ protected $public;
+ protected $type;
+ protected $required = false;
+
+ /**
+ * Part name if dynamic or content, generated from path
+ * @var string
+ */
+ public $content;
+
+ const DYNAMIC = 1;
+ const WILDCARD = 2;
+ const FIXED = 3;
+
+ public function __construct($content, $path)
+ {
+ $this->content = $content;
+ $this->path = $path;
+ }
+
+ public function setRule($rule)
+ {
+ $this->rule = $rule;
+ }
+
+ abstract public function getFormat();
+
+ abstract public function getRule();
+
+ public function addSlash($str)
+ {
+ $str = trim($str, '/');
+ if (($pos = strpos($this->path, '/')) !== false) {
+ if ($pos == 0) {
+ $str = '/'.$str;
+ } else {
+ $str .= '/';
+ }
+ }
+ return $str;
+ }
+
+ public function addSlashRegex($str)
+ {
+ $str = trim($str, '/');
+ if (($pos = strpos($this->path, '/')) !== false) {
+ if ($pos == 0) {
+ $str = '\/'.$str;
+ } else {
+ $str .= '\/';
+ }
+ }
+ if (!$this->isRequired()) {
+ $str = '('.$str.'|)';
+ }
+ return $str;
+ }
+
+ public function setDefaults($defaults)
+ {
+ $this->defaults = (string)$defaults;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function accept($visitor, $method = null)
+ {
+ $args = func_get_args();
+ $visitor->$method($this, $args);
+ }
+
+ public function setRequired($required)
+ {
+ $this->required = $required;
+ }
+
+ public function isRequired()
+ {
+ return $this->required;
+ }
+
+ abstract public function generate($value = null);
+
+ public function match($value)
+ {
+ $rule = $this->getRule();
+ return preg_match('/^'.$rule.'$/', $this->addSlash($value));
+ }
+
+}
+
+?> \ No newline at end of file
diff --git a/extlib/Net/URL/Mapper/Part/Dynamic.php b/extlib/Net/URL/Mapper/Part/Dynamic.php
new file mode 100644
index 000000000..349d87338
--- /dev/null
+++ b/extlib/Net/URL/Mapper/Part/Dynamic.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * URL parser and mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Dynamic.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+require_once 'Net/URL/Mapper/Part.php';
+
+class Net_URL_Mapper_Part_Dynamic extends Net_URL_Mapper_Part
+{
+
+ public function __construct($content, $path)
+ {
+ $this->type = Net_URL_Mapper_Part::DYNAMIC;
+ $this->setRequired(true);
+ parent::__construct($content, $path);
+ }
+
+ public function getFormat()
+ {
+ return $this->addSlashRegex('[^\/]+');
+ }
+
+ public function getRule()
+ {
+ if (!empty($this->rule)) {
+ return '(?P<'.$this->content.'>'.$this->addSlashRegex($this->rule).')';
+ }
+ return '(?P<'.$this->content.'>'.$this->addSlashRegex('[^\/]+').')';
+ }
+
+ public function generate($value = null)
+ {
+ if (is_array($value) && isset($value[$this->content])) {
+ $val = $value[$this->content];
+ } elseif (!is_array($value) && !is_null($value)) {
+ $val = $value;
+ } else {
+ $val = $this->defaults;
+ }
+ return $this->addSlash(urlencode($val));
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Net/URL/Mapper/Part/Fixed.php b/extlib/Net/URL/Mapper/Part/Fixed.php
new file mode 100644
index 000000000..b315b442d
--- /dev/null
+++ b/extlib/Net/URL/Mapper/Part/Fixed.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * URL parser and mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Fixed.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+require_once 'Net/URL/Mapper/Part.php';
+
+class Net_URL_Mapper_Part_Fixed extends Net_URL_Mapper_Part
+{
+
+ public function __construct($content, $path)
+ {
+ $this->type = Net_URL_Mapper_Part::FIXED;
+ parent::__construct($content, $path);
+ }
+
+ public function getFormat()
+ {
+ return $this->getRule();
+ }
+
+ public function getRule()
+ {
+ return preg_quote($this->path, '/');
+ }
+
+ public function generate($value = null)
+ {
+ return $this->path;
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Net/URL/Mapper/Part/Wildcard.php b/extlib/Net/URL/Mapper/Part/Wildcard.php
new file mode 100644
index 000000000..6085ff648
--- /dev/null
+++ b/extlib/Net/URL/Mapper/Part/Wildcard.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * URL parser and mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Wildcard.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+require_once 'Net/URL/Mapper/Part.php';
+
+class Net_URL_Mapper_Part_Wildcard extends Net_URL_Mapper_Part
+{
+
+ public function __construct($content, $path)
+ {
+ $this->type = Net_URL_Mapper_Part::WILDCARD;
+ $this->setRequired(true);
+ parent::__construct($content, $path);
+ }
+
+ public function getFormat()
+ {
+ return $this->addSlashRegex('.*');;
+ }
+
+ public function getRule()
+ {
+ return '(?P<'.$this->content.'>'.$this->addSlashRegex('.*').')';
+ }
+
+ public function generate($value = null)
+ {
+ if (is_array($value) && isset($value[$this->content])) {
+ $val = $value[$this->content];
+ } elseif (!is_array($value) && !is_null($value)) {
+ $val = $value;
+ } else {
+ $val = $this->defaults;
+ }
+ return $this->addSlash(str_replace(
+ array('%2F', '%23'),
+ array('/', '#'), urlencode($val)));
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Net/URL/Mapper/Path.php b/extlib/Net/URL/Mapper/Path.php
new file mode 100644
index 000000000..b541002c7
--- /dev/null
+++ b/extlib/Net/URL/Mapper/Path.php
@@ -0,0 +1,430 @@
+<?php
+/**
+ * URL parser and mapper
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category Net
+ * @package Net_URL_Mapper
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Path.php,v 1.1 2007/03/28 10:23:04 mansion Exp $
+ * @link http://pear.php.net/package/Net_URL_Mapper
+ */
+
+require_once 'Net/URL.php';
+require_once 'Net/URL/Mapper/Part/Dynamic.php';
+require_once 'Net/URL/Mapper/Part/Wildcard.php';
+require_once 'Net/URL/Mapper/Part/Fixed.php';
+
+class Net_URL_Mapper_Path
+{
+ private $path = '';
+ private $N = 0;
+ public $token;
+ public $value;
+ private $line = 1;
+ private $state = 1;
+
+
+ protected $alias;
+ protected $rules = array();
+ protected $defaults = array();
+ protected $parts = array();
+ protected $rule;
+ protected $format;
+ protected $minKeys;
+ protected $maxKeys;
+ protected $fixed = true;
+ protected $required;
+
+ public function __construct($path = '', $defaults = array(), $rules = array())
+ {
+ $this->path = '/'.trim(Net_URL::resolvePath($path), '/');
+ $this->setDefaults($defaults);
+ $this->setRules($rules);
+
+ try {
+ $this->parsePath();
+ } catch (Exception $e) {
+ // The path could not be parsed correctly, treat it as fixed
+ $this->fixed = true;
+ $part = self::createPart(Net_URL_Mapper_Part::FIXED, $this->path, $this->path);
+ $this->parts = array($part);
+ }
+ $this->getRequired();
+ }
+
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ protected function parsePath()
+ {
+ while ($this->yylex()) { }
+ }
+
+ /**
+ * Get the path alias
+ * Path aliases can be used instead of full path
+ * @return null|string
+ */
+ public function getAlias()
+ {
+ return $this->alias;
+ }
+
+ /**
+ * Set the path name
+ * @param string Set the path name
+ * @see getAlias()
+ */
+ public function setAlias($alias)
+ {
+ $this->alias = $alias;
+ return $this;
+ }
+
+ /**
+ * Get the path parts default values
+ * @return null|array
+ */
+ public function getDefaults()
+ {
+ return $this->defaults;
+ }
+
+ /**
+ * Set the path parts default values
+ * @param array Associative array with format partname => value
+ */
+ public function setDefaults($defaults)
+ {
+ if (is_array($defaults)) {
+ $this->defaults = $defaults;
+ } else {
+ $this->defaults = array();
+ }
+ }
+
+ /**
+ * Set the path parts default values
+ * @param array Associative array with format partname => value
+ */
+ public function setRules($rules)
+ {
+ if (is_array($rules)) {
+ $this->rules = $rules;
+ } else {
+ $this->rules = array();
+ }
+ }
+
+ /**
+ * Returns the regular expression used to match this path
+ * @return string PERL Regular expression
+ */
+ public function getRule()
+ {
+ if (is_null($this->rule)) {
+ $this->rule = '/^';
+ foreach ($this->parts as $path => $part) {
+ $this->rule .= $part->getRule();
+ }
+ $this->rule .= '$/';
+ }
+ return $this->rule;
+ }
+
+ public function getFormat()
+ {
+ if (is_null($this->format)) {
+ $this->format = '/^';
+ foreach ($this->parts as $path => $part) {
+ $this->format .= $part->getFormat();
+ }
+ $this->format .= '$/';
+ }
+ return $this->format;
+ }
+
+ protected function addPart($part)
+ {
+ if (array_key_exists($part->content, $this->defaults)) {
+ $part->setRequired(false);
+ $part->setDefaults($this->defaults[$part->content]);
+ }
+ if (isset($this->rules[$part->content])) {
+ $part->setRule($this->rules[$part->content]);
+ }
+ $this->rule = null;
+ if ($part->getType() != Net_URL_Mapper_Part::FIXED) {
+ $this->fixed = false;
+ $this->parts[$part->content] = $part;
+ } else {
+ $this->parts[] = $part;
+ }
+ return $part;
+ }
+
+ public static function createPart($type, $content, $path)
+ {
+ switch ($type) {
+ case Net_URL_Mapper_Part::DYNAMIC:
+ return new Net_URL_Mapper_Part_Dynamic($content, $path);
+ break;
+ case Net_URL_Mapper_Part::WILDCARD:
+ return new Net_URL_Mapper_Part_Wildcard($content, $path);
+ break;
+ default:
+ return new Net_URL_Mapper_Part_Fixed($content, $path);
+ }
+ }
+
+ /**
+ * Checks whether the path contains the given part by name
+ * If value parameter is given, the part also checks if the
+ * given value conforms to the part rule.
+ * @param string Part name
+ * @param mixed The value to check against
+ */
+ public function hasKey($partName, $value = null)
+ {
+ if (array_key_exists($partName, $this->parts)) {
+ if (!is_null($value) && $value !== false) {
+ return $this->parts[$partName]->match($value);
+ } else {
+ return true;
+ }
+ } elseif (array_key_exists($partName, $this->defaults) &&
+ $value == $this->defaults[$partName]) {
+ return true;
+ }
+ return false;
+ }
+
+ public function generate($values = array(), $qstring = array(), $anchor = '')
+ {
+ $path = '';
+ foreach ($this->parts as $part) {
+ $path .= $part->generate($values);
+ }
+ $path = '/'.trim(Net_URL::resolvePath($path), '/');
+ if (!empty($qstring)) {
+ $path .= '?'.http_build_query($qstring);
+ }
+ if (!empty($anchor)) {
+ $path .= '#'.ltrim($anchor, '#');
+ }
+ return $path;
+ }
+
+ public function getRequired()
+ {
+ if (!isset($this->required)) {
+ $req = array();
+ foreach ($this->parts as $part) {
+ if ($part->isRequired()) {
+ $req[] = $part->content;
+ }
+ }
+ $this->required = $req;
+ }
+ return $this->required;
+ }
+
+ public function getMaxKeys()
+ {
+ if (is_null($this->maxKeys)) {
+ $this->maxKeys = count($this->required);
+ $this->maxKeys += count($this->defaults);
+ }
+ return $this->maxKeys;
+ }
+
+
+
+
+ private $_yy_state = 1;
+ private $_yy_stack = array();
+
+ function yylex()
+ {
+ return $this->{'yylex' . $this->_yy_state}();
+ }
+
+ function yypushstate($state)
+ {
+ array_push($this->_yy_stack, $this->_yy_state);
+ $this->_yy_state = $state;
+ }
+
+ function yypopstate()
+ {
+ $this->_yy_state = array_pop($this->_yy_stack);
+ }
+
+ function yybegin($state)
+ {
+ $this->_yy_state = $state;
+ }
+
+
+
+ function yylex1()
+ {
+ $tokenMap = array (
+ 1 => 1,
+ 3 => 1,
+ 5 => 1,
+ 7 => 1,
+ 9 => 1,
+ );
+ if ($this->N >= strlen($this->path)) {
+ return false; // end of input
+ }
+ $yy_global_pattern = "/^(\/?:\/?\\(([a-zA-Z0-9_]+)\\))|^(\/?\\*\/?\\(([a-zA-Z0-9_]+)\\))|^(\/?:([a-zA-Z0-9_]+))|^(\/?\\*([a-zA-Z0-9_]+))|^(\/?([^\/:*]+))/";
+
+ do {
+ if (preg_match($yy_global_pattern, substr($this->path, $this->N), $yymatches)) {
+ $yysubmatches = $yymatches;
+ $yymatches = array_filter($yymatches, 'strlen'); // remove empty sub-patterns
+ if (!count($yymatches)) {
+ throw new Exception('Error: lexing failed because a rule matched' .
+ 'an empty string. Input "' . substr($this->path,
+ $this->N, 5) . '... state START');
+ }
+ next($yymatches); // skip global match
+ $this->token = key($yymatches); // token number
+ if ($tokenMap[$this->token]) {
+ // extract sub-patterns for passing to lex function
+ $yysubmatches = array_slice($yysubmatches, $this->token + 1,
+ $tokenMap[$this->token]);
+ } else {
+ $yysubmatches = array();
+ }
+ $this->value = current($yymatches); // token value
+ $r = $this->{'yy_r1_' . $this->token}($yysubmatches);
+ if ($r === null) {
+ $this->N += strlen($this->value);
+ $this->line += substr_count("\n", $this->value);
+ // accept this token
+ return true;
+ } elseif ($r === true) {
+ // we have changed state
+ // process this token in the new state
+ return $this->yylex();
+ } elseif ($r === false) {
+ $this->N += strlen($this->value);
+ $this->line += substr_count("\n", $this->value);
+ if ($this->N >= strlen($this->path)) {
+ return false; // end of input
+ }
+ // skip this token
+ continue;
+ } else { $yy_yymore_patterns = array(
+ 1 => "^(\/?\\*\/?\\(([a-zA-Z0-9_]+)\\))|^(\/?:([a-zA-Z0-9_]+))|^(\/?\\*([a-zA-Z0-9_]+))|^(\/?([^\/:*]+))",
+ 3 => "^(\/?:([a-zA-Z0-9_]+))|^(\/?\\*([a-zA-Z0-9_]+))|^(\/?([^\/:*]+))",
+ 5 => "^(\/?\\*([a-zA-Z0-9_]+))|^(\/?([^\/:*]+))",
+ 7 => "^(\/?([^\/:*]+))",
+ 9 => "",
+ );
+
+ // yymore is needed
+ do {
+ if (!strlen($yy_yymore_patterns[$this->token])) {
+ throw new Exception('cannot do yymore for the last token');
+ }
+ if (preg_match($yy_yymore_patterns[$this->token],
+ substr($this->path, $this->N), $yymatches)) {
+ $yymatches = array_filter($yymatches, 'strlen'); // remove empty sub-patterns
+ next($yymatches); // skip global match
+ $this->token = key($yymatches); // token number
+ $this->value = current($yymatches); // token value
+ $this->line = substr_count("\n", $this->value);
+ }
+ } while ($this->{'yy_r1_' . $this->token}() !== null);
+ // accept
+ $this->N += strlen($this->value);
+ $this->line += substr_count("\n", $this->value);
+ return true;
+ }
+ } else {
+ throw new Exception('Unexpected input at line' . $this->line .
+ ': ' . $this->path[$this->N]);
+ }
+ break;
+ } while (true);
+ } // end function
+
+
+ const START = 1;
+ function yy_r1_1($yy_subpatterns)
+ {
+
+ $c = $yy_subpatterns[0];
+ $part = self::createPart(Net_URL_Mapper_Part::DYNAMIC, $c, $this->value);
+ $this->addPart($part);
+ }
+ function yy_r1_3($yy_subpatterns)
+ {
+
+ $c = $yy_subpatterns[0];
+ $part = self::createPart(Net_URL_Mapper_Part::WILDCARD, $c, $this->value);
+ $this->addPart($part);
+ }
+ function yy_r1_5($yy_subpatterns)
+ {
+
+ $c = $yy_subpatterns[0];
+ $part = self::createPart(Net_URL_Mapper_Part::DYNAMIC, $c, $this->value);
+ $this->addPart($part);
+ }
+ function yy_r1_7($yy_subpatterns)
+ {
+
+ $c = $yy_subpatterns[0];
+ $part = self::createPart(Net_URL_Mapper_Part::WILDCARD, $c, $this->value);
+ $this->addPart($part);
+ }
+ function yy_r1_9($yy_subpatterns)
+ {
+
+ $c = $yy_subpatterns[0];
+ $part = self::createPart(Net_URL_Mapper_Part::FIXED, $c, $this->value);
+ $this->addPart($part);
+ }
+
+}
+
+?> \ No newline at end of file
diff --git a/extlib/OAuth.php b/extlib/OAuth.php
index 6dc6b3f35..029166175 100644
--- a/extlib/OAuth.php
+++ b/extlib/OAuth.php
@@ -16,6 +16,10 @@ class OAuthConsumer {/*{{{*/
$this->secret = $secret;
$this->callback_url = $callback_url;
}/*}}}*/
+
+ function __toString() {/*{{{*/
+ return "OAuthConsumer[key=$this->key,secret=$this->secret]";
+ }/*}}}*/
}/*}}}*/
class OAuthToken {/*{{{*/
@@ -37,8 +41,8 @@ class OAuthToken {/*{{{*/
* would respond to request_token and access_token calls with
*/
function to_string() {/*{{{*/
- return "oauth_token=" . OAuthUtil::urlencodeRFC3986($this->key) .
- "&oauth_token_secret=" . OAuthUtil::urlencodeRFC3986($this->secret);
+ return "oauth_token=" . OAuthUtil::urlencode_rfc3986($this->key) .
+ "&oauth_token_secret=" . OAuthUtil::urlencode_rfc3986($this->secret);
}/*}}}*/
function __toString() {/*{{{*/
@@ -67,7 +71,7 @@ class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/
($token) ? $token->secret : ""
);
- $key_parts = array_map(array('OAuthUtil','urlencodeRFC3986'), $key_parts);
+ $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
$key = implode('&', $key_parts);
return base64_encode( hash_hmac('sha1', $base_string, $key, true));
@@ -81,11 +85,11 @@ class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/
public function build_signature($request, $consumer, $token) {/*{{{*/
$sig = array(
- OAuthUtil::urlencodeRFC3986($consumer->secret)
+ OAuthUtil::urlencode_rfc3986($consumer->secret)
);
if ($token) {
- array_push($sig, OAuthUtil::urlencodeRFC3986($token->secret));
+ array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret));
} else {
array_push($sig, '');
}
@@ -94,7 +98,7 @@ class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/
// for debug purposes
$request->base_string = $raw;
- return OAuthUtil::urlencodeRFC3986($raw);
+ return OAuthUtil::urlencode_rfc3986($raw);
}/*}}}*/
}/*}}}*/
@@ -182,7 +186,7 @@ class OAuthRequest {/*{{{*/
*/
public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/
$scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https';
- @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT'] . $_SERVER['REQUEST_URI'];
@$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
$request_headers = OAuthRequest::get_headers();
@@ -192,27 +196,23 @@ class OAuthRequest {/*{{{*/
// do this
if ($parameters) {
$req = new OAuthRequest($http_method, $http_url, $parameters);
+ } else {
+ // collect request parameters from query string (GET) and post-data (POST) if appropriate (note: POST vars have priority)
+ $req_parameters = $_GET;
+ if ($http_method == "POST" && @strstr($request_headers["Content-Type"], "application/x-www-form-urlencoded") ) {
+ $req_parameters = array_merge($req_parameters, $_POST);
+ }
+
+ // next check for the auth header, we need to do some extra stuff
+ // if that is the case, namely suck in the parameters from GET or POST
+ // so that we can include them in the signature
+ if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
+ $header_parameters = OAuthRequest::split_header($request_headers['Authorization']);
+ $parameters = array_merge($req_parameters, $header_parameters);
+ $req = new OAuthRequest($http_method, $http_url, $parameters);
+ } else $req = new OAuthRequest($http_method, $http_url, $req_parameters);
}
- // next check for the auth header, we need to do some extra stuff
- // if that is the case, namely suck in the parameters from GET or POST
- // so that we can include them in the signature
- else if (@substr($request_headers['Authorization'], 0, 5) == "OAuth") {
- $header_parameters = OAuthRequest::split_header($request_headers['Authorization']);
- if ($http_method == "GET") {
- $req_parameters = $_GET;
- }
- else if ($http_method == "POST") {
- $req_parameters = $_POST;
- }
- $parameters = array_merge($header_parameters, $req_parameters);
- $req = new OAuthRequest($http_method, $http_url, $parameters);
- }
- else if ($http_method == "GET") {
- $req = new OAuthRequest($http_method, $http_url, $_GET);
- }
- else if ($http_method == "POST") {
- $req = new OAuthRequest($http_method, $http_url, $_POST);
- }
+
return $req;
}/*}}}*/
@@ -238,7 +238,7 @@ class OAuthRequest {/*{{{*/
}/*}}}*/
public function get_parameter($name) {/*{{{*/
- return $this->parameters[$name];
+ return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
}/*}}}*/
public function get_parameters() {/*{{{*/
@@ -267,12 +267,12 @@ class OAuthRequest {/*{{{*/
}
// Urlencode both keys and values
- $keys = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_keys($params));
- $values = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_values($params));
+ $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
+ $values = OAuthUtil::urlencode_rfc3986(array_values($params));
$params = array_combine($keys, $values);
// Sort by keys (natsort)
- uksort($params, 'strnatcmp');
+ uksort($params, 'strcmp');
// Generate key=value pairs
$pairs = array();
@@ -307,7 +307,7 @@ class OAuthRequest {/*{{{*/
$this->get_signable_parameters()
);
- $parts = array_map(array('OAuthUtil', 'urlencodeRFC3986'), $parts);
+ $parts = OAuthUtil::urlencode_rfc3986($parts);
return implode('&', $parts);
}/*}}}*/
@@ -351,11 +351,21 @@ class OAuthRequest {/*{{{*/
/**
* builds the data one would send in a POST request
+ *
+ * TODO(morten.fangel):
+ * this function might be easily replaced with http_build_query()
+ * and corrections for rfc3986 compatibility.. but not sure
*/
public function to_postdata() {/*{{{*/
$total = array();
foreach ($this->parameters as $k => $v) {
- $total[] = OAuthUtil::urlencodeRFC3986($k) . "=" . OAuthUtil::urlencodeRFC3986($v);
+ if (is_array($v)) {
+ foreach ($v as $va) {
+ $total[] = OAuthUtil::urlencode_rfc3986($k) . "[]=" . OAuthUtil::urlencode_rfc3986($va);
+ }
+ } else {
+ $total[] = OAuthUtil::urlencode_rfc3986($k) . "=" . OAuthUtil::urlencode_rfc3986($v);
+ }
}
$out = implode("&", $total);
return $out;
@@ -364,12 +374,13 @@ class OAuthRequest {/*{{{*/
/**
* builds the Authorization: header
*/
- public function to_header($realm="") {/*{{{*/
- $out ='"Authorization: OAuth realm="' . $realm . '",';
+ public function to_header() {/*{{{*/
+ $out ='Authorization: OAuth realm=""';
$total = array();
foreach ($this->parameters as $k => $v) {
if (substr($k, 0, 5) != "oauth") continue;
- $out .= ',' . OAuthUtil::urlencodeRFC3986($k) . '="' . OAuthUtil::urlencodeRFC3986($v) . '"';
+ if (is_array($v)) throw new OAuthException('Arrays not supported in headers');
+ $out .= ',' . OAuthUtil::urlencode_rfc3986($k) . '="' . OAuthUtil::urlencode_rfc3986($v) . '"';
}
return $out;
}/*}}}*/
@@ -412,24 +423,22 @@ class OAuthRequest {/*{{{*/
* parameters, has to do some unescaping
*/
private static function split_header($header) {/*{{{*/
- // remove 'OAuth ' at the start of a header
- $header = substr($header, 6);
-
- // error cases: commas in parameter values?
- $parts = explode(",", $header);
- $out = array();
- foreach ($parts as $param) {
- $param = ltrim($param);
- // skip the "realm" param, nobody ever uses it anyway
- if (substr($param, 0, 5) != "oauth") continue;
-
- $param_parts = explode("=", $param);
-
- // rawurldecode() used because urldecode() will turn a "+" in the
- // value into a space
- $out[$param_parts[0]] = rawurldecode(substr($param_parts[1], 1, -1));
+ $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
+ $offset = 0;
+ $params = array();
+ while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
+ $match = $matches[0];
+ $header_name = $matches[2][0];
+ $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
+ $params[$header_name] = OAuthUtil::urldecode_rfc3986( $header_content );
+ $offset = $match[1] + strlen($match[0]);
}
- return $out;
+
+ if (isset($params['realm'])) {
+ unset($params['realm']);
+ }
+
+ return $params;
}/*}}}*/
/**
@@ -506,6 +515,7 @@ class OAuthServer {/*{{{*/
// requires authorized request token
$token = $this->get_token($request, $consumer, "request");
+
$this->check_signature($request, $consumer, $token);
$new_token = $this->data_store->new_access_token($token, $consumer);
@@ -654,11 +664,11 @@ class OAuthDataStore {/*{{{*/
// implement me
}/*}}}*/
- function fetch_request_token($consumer) {/*{{{*/
+ function new_request_token($consumer) {/*{{{*/
// return a new token attached to this consumer
}/*}}}*/
- function fetch_access_token($token, $consumer) {/*{{{*/
+ function new_access_token($token, $consumer) {/*{{{*/
// return a new access token attached to this consumer
// for the user associated with this token if the request token
// is authorized
@@ -737,17 +747,22 @@ class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/
}/*}}}*/
class OAuthUtil {/*{{{*/
- public static function urlencodeRFC3986($string) {/*{{{*/
- return str_replace('+', ' ',
- str_replace('%7E', '~', rawurlencode($string)));
-
+ public static function urlencode_rfc3986($input) {/*{{{*/
+ if (is_array($input)) {
+ return array_map(array('OAuthUtil','urlencode_rfc3986'), $input);
+ } else if (is_scalar($input)) {
+ return str_replace('+', ' ',
+ str_replace('%7E', '~', rawurlencode($input)));
+ } else {
+ return '';
+ }
}/*}}}*/
// This decode function isn't taking into consideration the above
// modifications to the encoding process. However, this method doesn't
// seem to be used anywhere so leaving it as is.
- public static function urldecodeRFC3986($string) {/*{{{*/
+ public static function urldecode_rfc3986($string) {/*{{{*/
return rawurldecode($string);
}/*}}}*/
}/*}}}*/
diff --git a/htaccess.sample b/htaccess.sample
index 5f9827f96..634900dbf 100644
--- a/htaccess.sample
+++ b/htaccess.sample
@@ -4,165 +4,9 @@ RewriteEngine On
RewriteBase /mublog/
-RewriteRule ^$ index.php?action=public [L,QSA]
-RewriteRule ^rss$ index.php?action=publicrss [L,QSA]
-RewriteRule ^xrds$ index.php?action=publicxrds [L,QSA]
-RewriteRule ^featuredrss$ index.php?action=featuredrss [L,QSA]
-RewriteRule ^favoritedrss$ index.php?action=favoritedrss [L,QSA]
-RewriteRule ^opensearch/people$ index.php?action=opensearch&type=people [L,QSA]
-RewriteRule ^opensearch/notice$ index.php?action=opensearch&type=notice [L,QSA]
-
-RewriteRule ^doc/about$ index.php?action=doc&title=about [L,QSA]
-RewriteRule ^doc/contact$ index.php?action=doc&title=contact [L,QSA]
-RewriteRule ^doc/faq$ index.php?action=doc&title=faq [L,QSA]
-RewriteRule ^doc/help$ index.php?action=doc&title=help [L,QSA]
-RewriteRule ^doc/im$ index.php?action=doc&title=im [L,QSA]
-RewriteRule ^doc/openid$ index.php?action=doc&title=openid [L,QSA]
-RewriteRule ^doc/openmublog$ index.php?action=doc&title=openmublog [L,QSA]
-RewriteRule ^doc/privacy$ index.php?action=doc&title=privacy [L,QSA]
-RewriteRule ^doc/source$ index.php?action=doc&title=source [L,QSA]
-RewriteRule ^doc/tags$ index.php?action=doc&title=tags [L,QSA]
-RewriteRule ^doc/groups$ index.php?action=doc&title=groups [L,QSA]
-RewriteRule ^doc/sms$ index.php?action=doc&title=sms [L,QSA]
-
-RewriteRule ^facebook/$ index.php?action=facebookhome [L,QSA]
-RewriteRule ^facebook/index.php$ index.php?action=facebookhome [L,QSA]
-RewriteRule ^facebook/settings.php$ index.php?action=facebooksettings [L,QSA]
-RewriteRule ^facebook/invite.php$ index.php?action=facebookinvite [L,QSA]
-RewriteRule ^facebook/remove$ index.php?action=facebookremove [L,QSA]
-
-RewriteRule ^main/login$ index.php?action=login [L,QSA]
-RewriteRule ^main/logout$ index.php?action=logout [L,QSA]
-RewriteRule ^main/register/(.*)$ index.php?action=register&code=$1 [L,QSA]
-RewriteRule ^main/register$ index.php?action=register [L,QSA]
-RewriteRule ^main/openid$ index.php?action=openidlogin [L,QSA]
-RewriteRule ^main/remote$ index.php?action=remotesubscribe [L,QSA]
-
-RewriteRule ^main/subscribe$ index.php?action=subscribe [L,QSA]
-RewriteRule ^main/unsubscribe$ index.php?action=unsubscribe [L,QSA]
-RewriteRule ^main/confirmaddress$ index.php?action=confirmaddress [L,QSA]
-RewriteRule ^main/confirmaddress/(.*)$ index.php?action=confirmaddress&code=$1 [L,QSA]
-RewriteRule ^main/recoverpassword$ index.php?action=recoverpassword [L,QSA]
-RewriteRule ^main/recoverpassword/(.*)$ index.php?action=recoverpassword&code=$1 [L,QSA]
-RewriteRule ^main/invite$ index.php?action=invite [L,QSA]
-
-RewriteRule ^main/favor$ index.php?action=favor [L,QSA]
-RewriteRule ^main/disfavor$ index.php?action=disfavor [L,QSA]
-
-RewriteRule ^main/sup$ index.php?action=sup [L,QSA]
-
-RewriteRule ^main/tagother$ index.php?action=tagother [L,QSA]
-
-RewriteRule ^main/block$ index.php?action=block [L,QSA]
-
-RewriteRule ^settings/profile$ index.php?action=profilesettings [L,QSA]
-RewriteRule ^settings/avatar$ index.php?action=avatarsettings [L,QSA]
-RewriteRule ^settings/password$ index.php?action=passwordsettings [L,QSA]
-RewriteRule ^settings/openid$ index.php?action=openidsettings [L,QSA]
-RewriteRule ^settings/im$ index.php?action=imsettings [L,QSA]
-RewriteRule ^settings/email$ index.php?action=emailsettings [L,QSA]
-RewriteRule ^settings/sms$ index.php?action=smssettings [L,QSA]
-RewriteRule ^settings/twitter$ index.php?action=twittersettings [L,QSA]
-RewriteRule ^settings/other$ index.php?action=othersettings [L,QSA]
-
-RewriteRule ^search/group$ index.php?action=groupsearch [L,QSA]
-RewriteRule ^search/people$ index.php?action=peoplesearch [L,QSA]
-RewriteRule ^search/notice$ index.php?action=noticesearch [L,QSA]
-RewriteRule ^search/notice/rss$ index.php?action=noticesearchrss [L,QSA]
-
-RewriteRule ^notice/new$ index.php?action=newnotice [L,QSA]
-RewriteRule ^notice/(\d+)$ index.php?action=shownotice&notice=$1 [L,QSA]
-RewriteRule ^notice/delete/((\d+))?$ index.php?action=deletenotice&notice=$2 [L,QSA]
-RewriteRule ^notice/delete$ index.php?action=deletenotice [L,QSA]
-
-RewriteRule ^message/new$ index.php?action=newmessage [L,QSA]
-RewriteRule ^message/(\d+)$ index.php?action=showmessage&message=$1 [L,QSA]
-
-RewriteRule ^user/(\d+)$ index.php?action=userbyid&id=$1 [L,QSA]
-
-RewriteRule ^tags/?$ index.php?action=publictagcloud [L,QSA]
-RewriteRule ^tag/([a-zA-Z0-9]+)/rss$ index.php?action=tagrss&tag=$1 [L,QSA]
-RewriteRule ^tag(/(.*))?$ index.php?action=tag&tag=$2 [L,QSA]
-
-RewriteRule ^peopletag/([a-zA-Z0-9]+)$ index.php?action=peopletag&tag=$1 [L,QSA]
-
-RewriteRule ^featured/?$ index.php?action=featured [L,QSA]
-RewriteRule ^favorited/?$ index.php?action=favorited [L,QSA]
-
-RewriteRule ^group/new$ index.php?action=newgroup [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)/edit$ index.php?action=editgroup&nickname=$1 [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)/join$ index.php?action=joingroup&nickname=$1 [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)/leave$ index.php?action=leavegroup&nickname=$1 [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)/members$ index.php?action=groupmembers&nickname=$1 [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)/logo$ index.php?action=grouplogo&nickname=$1 [L,QSA]
-RewriteRule ^group/([0-9]+)/id$ index.php?action=groupbyid&id=$1 [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)/rss$ index.php?action=grouprss&nickname=$1 [L,QSA]
-RewriteRule ^group/([a-zA-Z0-9]+)$ index.php?action=showgroup&nickname=$1 [L,QSA]
-RewriteRule ^group$ index.php?action=groups [L,QSA]
-
-# Twitter-compatible API rewrites
-# XXX: Surely these can be refactored a little -- Zach
-RewriteRule ^api/statuses/public_timeline(.*)$ index.php?action=api&apiaction=statuses&method=public_timeline$1 [L,QSA]
-RewriteRule ^api/statuses/friends_timeline(.*)$ index.php?action=api&apiaction=statuses&method=friends_timeline$1 [L,QSA]
-RewriteRule ^api/statuses/user_timeline/(.*)$ index.php?action=api&apiaction=statuses&method=user_timeline&argument=$1 [L,QSA]
-RewriteRule ^api/statuses/user_timeline(.*)$ index.php?action=api&apiaction=statuses&method=user_timeline$1 [L,QSA]
-RewriteRule ^api/statuses/show/(.*)$ index.php?action=api&apiaction=statuses&method=show&argument=$1 [L,QSA]
-RewriteRule ^api/statuses/update(.*)$ index.php?action=api&apiaction=statuses&method=update$1 [L,QSA]
-RewriteRule ^api/statuses/replies(.*)$ index.php?action=api&apiaction=statuses&method=replies&argument=$1 [L,QSA]
-RewriteRule ^api/statuses/destroy/(.*)$ index.php?action=api&apiaction=statuses&method=destroy&argument=$1 [L,QSA]
-RewriteRule ^api/statuses/friends/(.*)$ index.php?action=api&apiaction=statuses&method=friends&argument=$1 [L,QSA]
-RewriteRule ^api/statuses/friends(.*)$ index.php?action=api&apiaction=statuses&method=friends$1 [L,QSA]
-RewriteRule ^api/statuses/followers/(.*)$ index.php?action=api&apiaction=statuses&method=followers&argument=$1 [L,QSA]
-RewriteRule ^api/statuses/followers(.*)$ index.php?action=api&apiaction=statuses&method=followers$1 [L,QSA]
-RewriteRule ^api/statuses/featured(.*)$ index.php?action=api&apiaction=statuses&method=featured$1 [L,QSA]
-RewriteRule ^api/users/show/(.*)$ index.php?action=api&apiaction=users&method=show&argument=$1 [L,QSA]
-RewriteRule ^api/users/show(.*)$ index.php?action=api&apiaction=users&method=show$1 [L,QSA]
-RewriteRule ^api/direct_messages/sent(.*)$ index.php?action=api&apiaction=direct_messages&method=sent$1 [L,QSA]
-RewriteRule ^api/direct_messages/destroy/(.*)$ index.php?action=api&apiaction=direct_messages&method=destroy&argument=$1 [L,QSA]
-RewriteRule ^api/direct_messages/new(.*)$ index.php?action=api&apiaction=direct_messages&method=create$1 [L,QSA]
-RewriteRule ^api/direct_messages(.*)$ index.php?action=api&apiaction=direct_messages&method=direct_messages$1 [L,QSA]
-RewriteRule ^api/friendships/create/(.*)$ index.php?action=api&apiaction=friendships&method=create&argument=$1 [L,QSA]
-RewriteRule ^api/friendships/destroy/(.*)$ index.php?action=api&apiaction=friendships&method=destroy&argument=$1 [L,QSA]
-RewriteRule ^api/friendships/exists(.*)$ index.php?action=api&apiaction=friendships&method=exists$1 [L,QSA]
-RewriteRule ^api/account/verify_credentials(.*)$ index.php?action=api&apiaction=account&method=verify_credentials$1 [L,QSA]
-RewriteRule ^api/account/end_session$ index.php?action=api&apiaction=account&method=end_session$1 [L,QSA]
-RewriteRule ^api/account/update_location(.*)$ index.php?action=api&apiaction=account&method=update_location$1 [L,QSA]
-RewriteRule ^api/account/update_delivery_device(.*)$ index.php?action=api&apiaction=account&method=update_delivery_device$1 [L,QSA]
-RewriteRule ^api/account/rate_limit_status(.*)$ index.php?action=api&apiaction=account&method=rate_limit_status$1 [L,QSA]
-RewriteRule ^api/favorites/create/(.*)$ index.php?action=api&apiaction=favorites&method=create&argument=$1 [L,QSA]
-RewriteRule ^api/favorites/destroy/(.*)$ index.php?action=api&apiaction=favorites&method=destroy&argument=$1 [L,QSA]
-RewriteRule ^api/favorites/(.*)$ index.php?action=api&apiaction=favorites&method=favorites&argument=$1 [L,QSA]
-RewriteRule ^api/favorites(.*)$ index.php?action=api&apiaction=favorites&method=favorites$1 [L,QSA]
-RewriteRule ^api/notifications/follow/(.*)$ index.php?action=api&apiaction=notifications&method=follow&argument=$1 [L,QSA]
-RewriteRule ^api/notifications/leave/(.*)$ index.php?action=api&apiaction=notifications&method=leave&argument=$1 [L,QSA]
-RewriteRule ^api/blocks/create/(.*)$ index.php?action=api&apiaction=blocks&method=create&argument=$1 [L,QSA]
-RewriteRule ^api/blocks/destroy/(.*)$ index.php?action=api&apiaction=blocks&method=destroy&argument=$1 [L,QSA]
-RewriteRule ^api/help/(.*)$ index.php?action=api&apiaction=help&method=$1 [L,QSA]
-RewriteRule ^api/laconica/version(.*)$ index.php?action=api&apiaction=laconica&method=version$1 [L,QSA]
-RewriteRule ^api/laconica/config(.*)$ index.php?action=api&apiaction=laconica&method=config$1 [L,QSA]
-RewriteRule ^api/laconica/wadl\.xml$ index.php?action=api&apiaction=laconica&method=wadl.xml [L,QSA]
-
-RewriteRule ^(\w+)/subscriptions$ index.php?action=subscriptions&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/subscriptions/([a-zA-Z0-9]+)$ index.php?action=subscriptions&nickname=$1&tag=$2 [L,QSA]
-RewriteRule ^(\w+)/subscribers/([a-zA-Z0-9]+)$ index.php?action=subscribers&nickname=$1&tag=$2 [L,QSA]
-RewriteRule ^(\w+)/subscribers$ index.php?action=subscribers&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/nudge$ index.php?action=nudge&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/xrds$ index.php?action=xrds&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/rss$ index.php?action=userrss&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/all$ index.php?action=all&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/all/rss$ index.php?action=allrss&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/foaf$ index.php?action=foaf&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/replies$ index.php?action=replies&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/replies/rss$ index.php?action=repliesrss&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/avatar/(original|96|48|24)$ index.php?action=avatarbynickname&nickname=$1&size=$2 [L,QSA]
-RewriteRule ^(\w+)/favorites$ index.php?action=showfavorites&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/favorites/rss$ index.php?action=favoritesrss&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/inbox$ index.php?action=inbox&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/outbox$ index.php?action=outbox&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/microsummary$ index.php?action=microsummary&nickname=$1 [L,QSA]
-RewriteRule ^(\w+)/groups$ index.php?action=usergroups&nickname=$1 [L,QSA]
-
-RewriteRule ^(\w+)$ index.php?action=showstream&nickname=$1 [L,QSA]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule (.*) index.php?p=$1 [L,QSA]
<FilesMatch "\.(ini)">
Order allow,deny
diff --git a/index.php b/index.php
index 387b642e2..dae1ae751 100644
--- a/index.php
+++ b/index.php
@@ -22,53 +22,144 @@ define('LACONICA', true);
require_once INSTALLDIR . '/lib/common.php';
-// get and cache current user
+$user = null;
+$action = null;
+
+function getPath($req)
+{
+ if ((common_config('site', 'fancy') || !array_key_exists('PATH_INFO', $_SERVER))
+ && array_key_exists('p', $req)) {
+ return $req['p'];
+ } else if (array_key_exists('PATH_INFO', $_SERVER)) {
+ return $_SERVER['PATH_INFO'];
+ } else {
+ return null;
+ }
+}
-$user = common_current_user();
+function handleError($error)
+{
+ if ($error->getCode() == DB_DATAOBJECT_ERROR_NODATA) {
+ return;
+ }
-// initialize language env
+ $logmsg = "PEAR error: " . $error->getMessage();
+ if(common_config('site', 'logdebug')) {
+ $logmsg .= " : ". $error->getDebugInfo();
+ }
+ common_log(LOG_ERR, $logmsg);
+ $msg = sprintf(_('The database for %s isn\'t responding correctly, '.
+ 'so the site won\'t work properly. '.
+ 'The site admins probably know about the problem, '.
+ 'but you can contact them at %s to make sure. '.
+ 'Otherwise, wait a few minutes and try again.'),
+ common_config('site', 'name'),
+ common_config('site', 'email'));
+
+ $dac = new DBErrorAction($msg, 500);
+ $dac->showPage();
+ exit(-1);
+}
-common_init_language();
+function main()
+{
+ global $user, $action;
-$action = $_REQUEST['action'];
+ if (!_have_config()) {
+ $msg = sprintf(_("No configuration file found. Try running ".
+ "the installation program first."));
+ $sac = new ServerErrorAction($msg);
+ $sac->showPage();
+ return;
+ }
-if (!$action || !preg_match('/^[a-zA-Z0-9_-]*$/', $action)) {
- common_redirect(common_local_url('public'));
-}
+ // For database errors
-// If the site is private, and they're not on one of the "public"
-// parts of the site, redirect to login
+ PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
-if (!$user && common_config('site', 'private') &&
- !in_array($action, array('login', 'openidlogin', 'finishopenidlogin',
- 'recoverpassword', 'api', 'doc', 'register'))) {
- common_redirect(common_local_url('login'));
-}
+ // XXX: we need a little more structure in this script
-$actionfile = INSTALLDIR."/actions/$action.php";
+ // get and cache current user
-if (file_exists($actionfile)) {
+ $user = common_current_user();
+
+ // initialize language env
+
+ common_init_language();
+
+ $path = getPath($_REQUEST);
+
+ $r = Router::get();
+
+ $args = $r->map($path);
+
+ if (!$args) {
+ $cac = new ClientErrorAction(_('Unknown page'), 404);
+ $cac->showPage();
+ return;
+ }
- include_once $actionfile;
+ $args = array_merge($args, $_REQUEST);
+
+ $action = $args['action'];
+
+ if (!$action || !preg_match('/^[a-zA-Z0-9_-]*$/', $action)) {
+ common_redirect(common_local_url('public'));
+ return;
+ }
+
+ // If the site is private, and they're not on one of the "public"
+ // parts of the site, redirect to login
+
+ if (!$user && common_config('site', 'private') &&
+ !in_array($action, array('login', 'openidlogin', 'finishopenidlogin',
+ 'recoverpassword', 'api', 'doc', 'register'))) {
+ common_redirect(common_local_url('login'));
+ return;
+ }
$action_class = ucfirst($action).'Action';
- $action_obj = new $action_class();
+ if (!class_exists($action_class)) {
+ $cac = new ClientErrorAction(_('Unknown action'), 404);
+ $cac->showPage();
+ } else {
+ $action_obj = new $action_class();
+
+ // XXX: find somewhere for this little block to live
- if ($config['db']['mirror'] && $action_obj->isReadOnly()) {
- if (is_array($config['db']['mirror'])) {
- // "load balancing", ha ha
- $k = array_rand($config['db']['mirror']);
+ if (common_config('db', 'mirror') && $action_obj->isReadOnly()) {
+ if (is_array(common_config('db', 'mirror'))) {
+ // "load balancing", ha ha
+ $k = array_rand($config['db']['mirror']);
- $mirror = $config['db']['mirror'][$k];
- } else {
- $mirror = $config['db']['mirror'];
+ $mirror = $config['db']['mirror'][$k];
+ } else {
+ $mirror = $config['db']['mirror'];
+ }
+ $config['db']['database'] = $mirror;
+ }
+
+ try {
+ if ($action_obj->prepare($args)) {
+ $action_obj->handle($args);
+ }
+ } catch (ClientException $cex) {
+ $cac = new ClientErrorAction($cex->getMessage(), $cex->getCode());
+ $cac->showPage();
+ } catch (ServerException $sex) { // snort snort guffaw
+ $sac = new ServerErrorAction($sex->getMessage(), $sex->getCode());
+ $sac->showPage();
+ } catch (Exception $ex) {
+ $sac = new ServerErrorAction($ex->getMessage());
+ $sac->showPage();
}
- $config['db']['database'] = $mirror;
- }
- if (call_user_func(array($action_obj, 'prepare'), $_REQUEST)) {
- call_user_func(array($action_obj, 'handle'), $_REQUEST);
}
-} else {
- common_user_error(_('Unknown action'));
-} \ No newline at end of file
+}
+
+main();
+
+// XXX: cleanup exit() calls or add an exit handler so
+// this always gets called
+
+Event::handle('CleanupPlugin');
diff --git a/install.php b/install.php
new file mode 100644
index 000000000..0240349bb
--- /dev/null
+++ b/install.php
@@ -0,0 +1,255 @@
+<?
+define('INSTALLDIR', dirname(__FILE__));
+
+function main()
+{
+ if (!checkPrereqs())
+ {
+ return;
+ }
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ handlePost();
+ } else {
+ showForm();
+ }
+}
+
+function checkPrereqs()
+{
+ if (file_exists(INSTALLDIR.'/config.php')) {
+ ?><p class="error">Config file &quot;config.php&quot; already exists.</p>
+ <?
+ return false;
+ }
+
+ if (version_compare(PHP_VERSION, '5.0.0', '<')) {
+ ?><p class="error">Require PHP version 5 or greater.</p><?
+ return false;
+ }
+
+ $reqs = array('gd', 'mysql', 'curl',
+ 'xmlwriter', 'mbstring',
+ 'gettext');
+
+ foreach ($reqs as $req) {
+ if (!checkExtension($req)) {
+ ?><p class="error">Cannot load required extension &quot;<?= $req ?>&quot;.</p><?
+ return false;
+ }
+ }
+
+ if (!is_writable(INSTALLDIR)) {
+ ?><p class="error">Cannot write config file to &quot;<?= INSTALLDIR ?>&quot;.</p>
+ <p>On your server, try this command:</p>
+ <blockquote>chmod a+w <?= INSTALLDIR ?></blockquote>
+ <?
+ return false;
+ }
+
+ if (!is_writable(INSTALLDIR.'/avatar/')) {
+ ?><p class="error">Cannot write avatar directory &quot;<?= INSTALLDIR ?>/avatar/&quot;.</p>
+ <p>On your server, try this command:</p>
+ <blockquote>chmod a+w <?= INSTALLDIR ?>/avatar/</blockquote>
+ <?
+ return false;
+ }
+
+ return true;
+}
+
+function checkExtension($name)
+{
+ if (!extension_loaded($name)) {
+ if (!dl($name.'.so')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function showForm()
+{
+?>
+<p>Enter your database connection information below to initialize the database.</p>
+<form method='post' action='install.php'>
+ <fieldset>
+ <ul class='form_data'>
+ <li>
+ <label for='sitename'>Site name</label>
+ <input type='text' id='sitename' name='sitename' />
+ <p>The name of your site</p>
+ </li>
+ <li>
+ <li>
+ <label for='host'>Hostname</label>
+ <input type='text' id='host' name='host' />
+ <p>Database hostname</p>
+ </li>
+ <li>
+ <label for='host'>Database</label>
+ <input type='text' id='database' name='database' />
+ <p>Database name</p>
+ </li>
+ <li>
+ <label for='username'>Username</label>
+ <input type='text' id='username' name='username' />
+ <p>Database username</p>
+ </li>
+ <li>
+ <label for='password'>Password</label>
+ <input type='password' id='password' name='password' />
+ <p>Database password</p>
+ </li>
+ </ul>
+ <input type='submit' name='submit' value='Submit'>
+ </fieldset>
+</form>
+<?
+}
+
+function updateStatus($status, $error=false)
+{
+?>
+ <li>
+<?
+ print $status;
+?>
+ </li>
+<?
+}
+
+function handlePost()
+{
+?>
+ <ul>
+<?
+ $host = $_POST['host'];
+ $database = $_POST['database'];
+ $username = $_POST['username'];
+ $password = $_POST['password'];
+ $sitename = $_POST['sitename'];
+
+ if (empty($host)) {
+ updateStatus("No hostname specified.", true);
+ showForm();
+ return;
+ }
+
+ if (empty($database)) {
+ updateStatus("No database specified.", true);
+ showForm();
+ return;
+ }
+
+ if (empty($username)) {
+ updateStatus("No username specified.", true);
+ showForm();
+ return;
+ }
+
+ if (empty($password)) {
+ updateStatus("No password specified.", true);
+ showForm();
+ return;
+ }
+
+ if (empty($sitename)) {
+ updateStatus("No sitename specified.", true);
+ showForm();
+ return;
+ }
+
+ updateStatus("Starting installation...");
+ updateStatus("Checking database...");
+ $conn = mysql_connect($host, $username, $password);
+ if (!$conn) {
+ updateStatus("Can't connect to server '$host' as '$username'.", true);
+ showForm();
+ return;
+ }
+ updateStatus("Changing to database...");
+ $res = mysql_select_db($database, $conn);
+ if (!$res) {
+ updateStatus("Can't change to database.", true);
+ showForm();
+ return;
+ }
+ updateStatus("Running database script...");
+ $res = runDbScript(INSTALLDIR.'/db/laconica.sql', $conn);
+ if ($res === false) {
+ updateStatus("Can't run database script.", true);
+ showForm();
+ return;
+ }
+ foreach (array('sms_carrier' => 'SMS carrier',
+ 'notice_source' => 'notice source',
+ 'foreign_services' => 'foreign service')
+ as $scr => $name) {
+ updateStatus(sprintf("Adding %s data to database...", $name));
+ $res = runDbScript(INSTALLDIR.'/db/'.$scr.'.sql', $conn);
+ if ($res === false) {
+ updateStatus(sprintf("Can't run %d script.", $name), true);
+ showForm();
+ return;
+ }
+ }
+ updateStatus("Writing config file...");
+ $sqlUrl = "mysqli://$username:$password@$host/$database";
+ $res = writeConf($sitename, $sqlUrl);
+ if (!$res) {
+ updateStatus("Can't write config file.", true);
+ showForm();
+ return;
+ }
+ updateStatus("Done!");
+?>
+ </ul>
+<?
+}
+
+function writeConf($sitename, $sqlUrl)
+{
+ $res = file_put_contents(INSTALLDIR.'/config.php',
+ "<?\n".
+ "\$config['site']['name'] = \"$sitename\";\n\n".
+ "\$config['db']['database'] = \"$sqlUrl\";\n\n");
+ return $res;
+}
+
+function runDbScript($filename, $conn)
+{
+ $sql = trim(file_get_contents($filename));
+ $stmts = explode(';', $sql);
+ foreach ($stmts as $stmt) {
+ $stmt = trim($stmt);
+ if (!mb_strlen($stmt)) {
+ continue;
+ }
+ $res = mysql_query($stmt, $conn);
+ if ($res === false) {
+ return $res;
+ }
+ }
+ return true;
+}
+
+?>
+<html>
+<head>
+ <title>Install Laconica</title>
+ <link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.7.1" media="screen, projection, tv"/>
+ <link rel="stylesheet" type="text/css" href="theme/base/css/modal.css?version=0.7.1" media="screen, projection, tv"/>
+ <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.7.1" media="screen, projection, tv"/>
+</head>
+<body>
+ <div id="wrap">
+ <div id="core">
+ <div id="content">
+ <h1>Install Laconica</h1>
+<? main() ?>
+ </div>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/js/flowplayer-3.0.5.min.js b/js/flowplayer-3.0.5.min.js
new file mode 100644
index 000000000..b1c33150a
--- /dev/null
+++ b/js/flowplayer-3.0.5.min.js
@@ -0,0 +1,24 @@
+/**
+ * flowplayer.js 3.0.5. The Flowplayer API
+ *
+ * Copyright 2009 Flowplayer Oy
+ *
+ * This file is part of Flowplayer.
+ *
+ * Flowplayer 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Flowplayer 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 Flowplayer. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Version: 3.0.5 - Tue Feb 03 2009 13:14:17 GMT-0000 (GMT+00:00)
+ */
+(function(){function log(args){console.log("$f.fireEvent",[].slice.call(args));}function clone(obj){if(!obj||typeof obj!='object'){return obj;}var temp=new obj.constructor();for(var key in obj){if(obj.hasOwnProperty(key)){temp[key]=clone(obj[key]);}}return temp;}function each(obj,fn){if(!obj){return;}var name,i=0,length=obj.length;if(length===undefined){for(name in obj){if(fn.call(obj[name],name,obj[name])===false){break;}}}else{for(var value=obj[0];i<length&&fn.call(value,i,value)!==false;value=obj[++i]){}}return obj;}function el(id){return document.getElementById(id);}function extend(to,from,skipFuncs){if(to&&from){each(from,function(name,value){if(!skipFuncs||typeof value!='function'){to[name]=value;}});}}function select(query){var index=query.indexOf(".");if(index!=-1){var tag=query.substring(0,index)||"*";var klass=query.substring(index+1,query.length);var els=[];each(document.getElementsByTagName(tag),function(){if(this.className&&this.className.indexOf(klass)!=-1){els.push(this);}});return els;}}function stopEvent(e){e=e||window.event;if(e.preventDefault){e.stopPropagation();e.preventDefault();}else{e.returnValue=false;e.cancelBubble=true;}return false;}function bind(to,evt,fn){to[evt]=to[evt]||[];to[evt].push(fn);}function makeId(){return"_"+(""+Math.random()).substring(2,10);}var Clip=function(json,index,player){var self=this;var cuepoints={};var listeners={};self.index=index;if(typeof json=='string'){json={url:json};}extend(this,json,true);each(("Begin*,Start,Pause*,Resume*,Seek*,Stop*,Finish*,LastSecond,Update,BufferFull,BufferEmpty,BufferStop").split(","),function(){var evt="on"+this;if(evt.indexOf("*")!=-1){evt=evt.substring(0,evt.length-1);var before="onBefore"+evt.substring(2);self[before]=function(fn){bind(listeners,before,fn);return self;};}self[evt]=function(fn){bind(listeners,evt,fn);return self;};if(index==-1){if(self[before]){player[before]=self[before];}if(self[evt]){player[evt]=self[evt];}}});extend(this,{onCuepoint:function(points,fn){if(arguments.length==1){cuepoints.embedded=[null,points];return self;}if(typeof points=='number'){points=[points];}var fnId=makeId();cuepoints[fnId]=[points,fn];if(player.isLoaded()){player._api().fp_addCuepoints(points,index,fnId);}return self;},update:function(json){extend(self,json);if(player.isLoaded()){player._api().fp_updateClip(json,index);}var conf=player.getConfig();var clip=(index==-1)?conf.clip:conf.playlist[index];extend(clip,json,true);},_fireEvent:function(evt,arg1,arg2,target){if(evt=='onLoad'){each(cuepoints,function(key,val){if(val[0]){player._api().fp_addCuepoints(val[0],index,key);}});return false;}target=target||self;if(evt=='onCuepoint'){var fn=cuepoints[arg1];if(fn){return fn[1].call(player,target,arg2);}}if(evt=='onStart'||evt=='onUpdate'){extend(target,arg1);if(!target.duration){target.duration=arg1.metaData.duration;}else{target.fullDuration=arg1.metaData.duration;}}var ret=true;each(listeners[evt],function(){ret=this.call(player,target,arg1,arg2);});return ret;}});if(json.onCuepoint){var arg=json.onCuepoint;self.onCuepoint.apply(self,typeof arg=='function'?[arg]:arg);delete json.onCuepoint;}each(json,function(key,val){if(typeof val=='function'){bind(listeners,key,val);delete json[key];}});if(index==-1){player.onCuepoint=this.onCuepoint;}};var Plugin=function(name,json,player,fn){var listeners={};var self=this;var hasMethods=false;if(fn){extend(listeners,fn);}each(json,function(key,val){if(typeof val=='function'){listeners[key]=val;delete json[key];}});extend(this,{animate:function(props,speed,fn){if(!props){return self;}if(typeof speed=='function'){fn=speed;speed=500;}if(typeof props=='string'){var key=props;props={};props[key]=speed;speed=500;}if(fn){var fnId=makeId();listeners[fnId]=fn;}if(speed===undefined){speed=500;}json=player._api().fp_animate(name,props,speed,fnId);return self;},css:function(props,val){if(val!==undefined){var css={};css[props]=val;props=css;}json=player._api().fp_css(name,props);extend(self,json);return self;},show:function(){this.display='block';player._api().fp_showPlugin(name);return self;},hide:function(){this.display='none';player._api().fp_hidePlugin(name);return self;},toggle:function(){this.display=player._api().fp_togglePlugin(name);return self;},fadeTo:function(o,speed,fn){if(typeof speed=='function'){fn=speed;speed=500;}if(fn){var fnId=makeId();listeners[fnId]=fn;}this.display=player._api().fp_fadeTo(name,o,speed,fnId);this.opacity=o;return self;},fadeIn:function(speed,fn){return self.fadeTo(1,speed,fn);},fadeOut:function(speed,fn){return self.fadeTo(0,speed,fn);},getName:function(){return name;},_fireEvent:function(evt,arg){if(evt=='onUpdate'){var json=player._api().fp_getPlugin(name);if(!json){return;}extend(self,json);delete self.methods;if(!hasMethods){each(json.methods,function(){var method=""+this;self[method]=function(){var a=[].slice.call(arguments);var ret=player._api().fp_invoke(name,method,a);return ret=='undefined'?self:ret;};});hasMethods=true;}}var fn=listeners[evt];if(fn){fn.call(self,arg);if(evt.substring(0,1)=="_"){delete listeners[evt];}}}});};function Player(wrapper,params,conf){var
+self=this,api=null,html,commonClip,playlist=[],plugins={},listeners={},playerId,apiId,playerIndex,activeIndex,swfHeight,wrapperHeight;extend(self,{id:function(){return playerId;},isLoaded:function(){return(api!==null);},getParent:function(){return wrapper;},hide:function(all){if(all){wrapper.style.height="0px";}if(api){api.style.height="0px";}return self;},show:function(){wrapper.style.height=wrapperHeight+"px";if(api){api.style.height=swfHeight+"px";}return self;},isHidden:function(){return api&&parseInt(api.style.height,10)===0;},load:function(fn){if(!api&&self._fireEvent("onBeforeLoad")!==false){each(players,function(){this.unload();});html=wrapper.innerHTML;flashembed(wrapper,params,{config:conf});if(fn){fn.cached=true;bind(listeners,"onLoad",fn);}}return self;},unload:function(){try{if(api&&api.fp_isFullscreen()){}}catch(error){return;}if(api&&html.replace(/\s/g,'')!==''&&!api.fp_isFullscreen()&&self._fireEvent("onBeforeUnload")!==false){api.fp_close();wrapper.innerHTML=html;self._fireEvent("onUnload");api=null;}return self;},getClip:function(index){if(index===undefined){index=activeIndex;}return playlist[index];},getCommonClip:function(){return commonClip;},getPlaylist:function(){return playlist;},getPlugin:function(name){var plugin=plugins[name];if(!plugin&&self.isLoaded()){var json=self._api().fp_getPlugin(name);if(json){plugin=new Plugin(name,json,self);plugins[name]=plugin;}}return plugin;},getScreen:function(){return self.getPlugin("screen");},getControls:function(){return self.getPlugin("controls");},getConfig:function(copy){return copy?clone(conf):conf;},getFlashParams:function(){return params;},loadPlugin:function(name,url,props,fn){if(typeof props=='function'){fn=props;props={};}var fnId=fn?makeId():"_";self._api().fp_loadPlugin(name,url,props,fnId);var arg={};arg[fnId]=fn;var p=new Plugin(name,null,self,arg);plugins[name]=p;return p;},getState:function(){return api?api.fp_getState():-1;},play:function(clip){function play(){if(clip!==undefined){self._api().fp_play(clip);}else{self._api().fp_play();}}if(api){play();}else{self.load(function(){play();});}return self;},getVersion:function(){var js="flowplayer.js 3.0.5";if(api){var ver=api.fp_getVersion();ver.push(js);return ver;}return js;},_api:function(){if(!api){throw"Flowplayer "+self.id()+" not loaded. Try moving your call to player's onLoad event";}return api;},_dump:function(){console.log(listeners);},setClip:function(clip){self.setPlaylist([clip]);},getIndex:function(){return playerIndex;}});each(("Click*,Load*,Unload*,Keypress*,Volume*,Mute*,Unmute*,PlaylistReplace,Fullscreen*,FullscreenExit,Error").split(","),function(){var name="on"+this;if(name.indexOf("*")!=-1){name=name.substring(0,name.length-1);var name2="onBefore"+name.substring(2);self[name2]=function(fn){bind(listeners,name2,fn);return self;};}self[name]=function(fn){bind(listeners,name,fn);return self;};});each(("pause,resume,mute,unmute,stop,toggle,seek,getStatus,getVolume,setVolume,getTime,isPaused,isPlaying,startBuffering,stopBuffering,isFullscreen,reset,close,setPlaylist").split(","),function(){var name=this;self[name]=function(arg){if(!api){return self;}var ret=(arg===undefined)?api["fp_"+name]():api["fp_"+name](arg);return ret=='undefined'?self:ret;};});self._fireEvent=function(evt,arg0,arg1,arg2){if(conf.debug){log(arguments);}if(!api&&evt=='onLoad'&&arg0=='player'){api=api||el(apiId);swfHeight=api.clientHeight;each(playlist,function(){this._fireEvent("onLoad");});each(plugins,function(name,p){p._fireEvent("onUpdate");});commonClip._fireEvent("onLoad");}if(evt=='onLoad'&&arg0!='player'){return;}if(evt=='onError'){if(typeof arg0=='string'||(typeof arg0=='number'&&typeof arg1=='number')){arg0=arg1;arg1=arg2;}}if(evt=='onContextMenu'){each(conf.contextMenu[arg0],function(key,fn){fn.call(self);});return;}if(evt=='onPluginEvent'){var name=arg0.name||arg0;var p=plugins[name];if(p){p._fireEvent("onUpdate",arg0);p._fireEvent(arg1);}return;}if(evt=='onPlaylistReplace'){playlist=[];var index=0;each(arg0,function(){playlist.push(new Clip(this,index++,self));});}var ret=true;if(typeof arg0=='number'&&arg0<playlist.length){activeIndex=arg0;var clip=playlist[arg0];if(clip){ret=clip._fireEvent(evt,arg1,arg2);}if(!clip||ret!==false){ret=commonClip._fireEvent(evt,arg1,arg2,clip);}}var i=0;each(listeners[evt],function(){ret=this.call(self,arg0,arg1);if(this.cached){listeners[evt].splice(i,1);}if(ret===false){return false;}i++;});return ret;};function init(){if($f(wrapper)){$f(wrapper).getParent().innerHTML="";playerIndex=$f(wrapper).getIndex();players[playerIndex]=self;}else{players.push(self);playerIndex=players.length-1;}wrapperHeight=parseInt(wrapper.style.height,10)||wrapper.clientHeight;if(typeof params=='string'){params={src:params};}playerId=wrapper.id||"fp"+makeId();apiId=params.id||playerId+"_api";params.id=apiId;conf.playerId=playerId;if(typeof conf=='string'){conf={clip:{url:conf}};}if(typeof conf.clip=='string'){conf.clip={url:conf.clip};}conf.clip=conf.clip||{};if(wrapper.getAttribute("href",2)&&!conf.clip.url){conf.clip.url=wrapper.getAttribute("href",2);}commonClip=new Clip(conf.clip,-1,self);conf.playlist=conf.playlist||[conf.clip];var index=0;each(conf.playlist,function(){var clip=this;if(typeof clip=='object'&&clip.length){clip=""+clip;}if(typeof clip=='string'){clip={url:clip};}each(conf.clip,function(key,val){if(conf.clip[key]!==undefined&&typeof val!='function'){clip[key]=val;}});conf.playlist[index]=clip;clip=new Clip(clip,index,self);playlist.push(clip);index++;});each(conf,function(key,val){if(typeof val=='function'){bind(listeners,key,val);delete conf[key];}});each(conf.plugins,function(name,val){if(val){plugins[name]=new Plugin(name,val,self);}});if(!conf.plugins||conf.plugins.controls===undefined){plugins.controls=new Plugin("controls",null,self);}params.bgcolor=params.bgcolor||"#000000";params.version=params.version||[9,0];params.expressInstall='http://www.flowplayer.org/swf/expressinstall.swf';function doClick(e){if(!self.isLoaded()&&self._fireEvent("onBeforeClick")!==false){self.load();}return stopEvent(e);}html=wrapper.innerHTML;if(html.replace(/\s/g,'')!==''){if(wrapper.addEventListener){wrapper.addEventListener("click",doClick,false);}else if(wrapper.attachEvent){wrapper.attachEvent("onclick",doClick);}}else{if(wrapper.addEventListener){wrapper.addEventListener("click",stopEvent,false);}self.load();}}if(typeof wrapper=='string'){flashembed.domReady(function(){var node=el(wrapper);if(!node){throw"Flowplayer cannot access element: "+wrapper;}else{wrapper=node;init();}});}else{init();}}var players=[];function Iterator(arr){this.length=arr.length;this.each=function(fn){each(arr,fn);};this.size=function(){return arr.length;};}window.flowplayer=window.$f=function(){var instance=null;var arg=arguments[0];if(!arguments.length){each(players,function(){if(this.isLoaded()){instance=this;return false;}});return instance||players[0];}if(arguments.length==1){if(typeof arg=='number'){return players[arg];}else{if(arg=='*'){return new Iterator(players);}each(players,function(){if(this.id()==arg.id||this.id()==arg||this.getParent()==arg){instance=this;return false;}});return instance;}}if(arguments.length>1){var swf=arguments[1];var conf=(arguments.length==3)?arguments[2]:{};if(typeof arg=='string'){if(arg.indexOf(".")!=-1){var instances=[];each(select(arg),function(){instances.push(new Player(this,clone(swf),clone(conf)));});return new Iterator(instances);}else{var node=el(arg);return new Player(node!==null?node:arg,swf,conf);}}else if(arg){return new Player(arg,swf,conf);}}return null;};extend(window.$f,{fireEvent:function(id,evt,a0,a1,a2){var p=$f(id);return p?p._fireEvent(evt,a0,a1,a2):null;},addPlugin:function(name,fn){Player.prototype[name]=fn;return $f;},each:each,extend:extend});if(document.all){window.onbeforeunload=function(){$f("*").each(function(){if(this.isLoaded()){this.close();}});};}if(typeof jQuery=='function'){jQuery.prototype.flowplayer=function(params,conf){if(!arguments.length||typeof arguments[0]=='number'){var arr=[];this.each(function(){var p=$f(this);if(p){arr.push(p);}});return arguments.length?arr[arguments[0]]:new Iterator(arr);}return this.each(function(){$f(this,clone(params),conf?clone(conf):{});});};}})();(function(){var jQ=typeof jQuery=='function';function isDomReady(){if(domReady.done){return false;}var d=document;if(d&&d.getElementsByTagName&&d.getElementById&&d.body){clearInterval(domReady.timer);domReady.timer=null;for(var i=0;i<domReady.ready.length;i++){domReady.ready[i].call();}domReady.ready=null;domReady.done=true;}}var domReady=jQ?jQuery:function(f){if(domReady.done){return f();}if(domReady.timer){domReady.ready.push(f);}else{domReady.ready=[f];domReady.timer=setInterval(isDomReady,13);}};function extend(to,from){if(from){for(key in from){if(from.hasOwnProperty(key)){to[key]=from[key];}}}return to;}function asString(obj){switch(typeOf(obj)){case'string':obj=obj.replace(new RegExp('(["\\\\])','g'),'\\$1');obj=obj.replace(/^\s?(\d+)%/,"$1pct");return'"'+obj+'"';case'array':return'['+map(obj,function(el){return asString(el);}).join(',')+']';case'function':return'"function()"';case'object':var str=[];for(var prop in obj){if(obj.hasOwnProperty(prop)){str.push('"'+prop+'":'+asString(obj[prop]));}}return'{'+str.join(',')+'}';}return String(obj).replace(/\s/g," ").replace(/\'/g,"\"");}function typeOf(obj){if(obj===null||obj===undefined){return false;}var type=typeof obj;return(type=='object'&&obj.push)?'array':type;}if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){};});}function map(arr,func){var newArr=[];for(var i in arr){if(arr.hasOwnProperty(i)){newArr[i]=func(arr[i]);}}return newArr;}function getHTML(p,c){var ie=document.all;var html='<object width="'+p.width+'" height="'+p.height+'"';if(ie&&!p.id){p.id="_"+(""+Math.random()).substring(9);}if(p.id){html+=' id="'+p.id+'"';}if(p.w3c||!ie){html+=' data="'+p.src+'" type="application/x-shockwave-flash"';}else{html+=' classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"';}html+='>';if(p.w3c||ie){html+='<param name="movie" value="'+p.src+'" />';}var e=extend({},p);e.width=e.height=e.id=e.w3c=e.src=null;for(var k in e){if(e[k]!==null){html+='<param name="'+k+'" value="'+e[k]+'" />';}}var vars="";if(c){for(var key in c){if(c[key]!==null){vars+=key+'='+(typeof c[key]=='object'?asString(c[key]):c[key])+'&';}}vars=vars.substring(0,vars.length-1);html+='<param name="flashvars" value=\''+vars+'\' />';}html+="</object>";return html;}function Flash(root,opts,flashvars){var version=flashembed.getVersion();extend(this,{getContainer:function(){return root;},getConf:function(){return conf;},getVersion:function(){return version;},getFlashvars:function(){return flashvars;},getApi:function(){return root.firstChild;},getHTML:function(){return getHTML(opts,flashvars);}});var required=opts.version;var express=opts.expressInstall;var ok=!required||flashembed.isSupported(required);if(ok){opts.onFail=opts.version=opts.expressInstall=null;root.innerHTML=getHTML(opts,flashvars);}else if(required&&express&&flashembed.isSupported([6,65])){extend(opts,{src:express});flashvars={MMredirectURL:location.href,MMplayerType:'PlugIn',MMdoctitle:document.title};root.innerHTML=getHTML(opts,flashvars);}else{if(root.innerHTML.replace(/\s/g,'')!==''){}else{root.innerHTML="<h2>Flash version "+required+" or greater is required</h2>"+"<h3>"+(version[0]>0?"Your version is "+version:"You have no flash plugin installed")+"</h3>"+"<p>Download latest version from <a href='http://www.adobe.com/go/getflashplayer'>here</a></p>";}}if(!ok&&opts.onFail){var ret=opts.onFail.call(this);if(typeof ret=='string'){root.innerHTML=ret;}}}window.flashembed=function(root,conf,flashvars){if(typeof root=='string'){var el=document.getElementById(root);if(el){root=el;}else{domReady(function(){flashembed(root,conf,flashvars);});return;}}if(!root){return;}var opts={width:'100%',height:'100%',allowfullscreen:true,allowscriptaccess:'always',quality:'high',version:null,onFail:null,expressInstall:null,w3c:false};if(typeof conf=='string'){conf={src:conf};}extend(opts,conf);return new Flash(root,opts,flashvars);};extend(window.flashembed,{getVersion:function(){var version=[0,0];if(navigator.plugins&&typeof navigator.plugins["Shockwave Flash"]=="object"){var _d=navigator.plugins["Shockwave Flash"].description;if(typeof _d!="undefined"){_d=_d.replace(/^.*\s+(\S+\s+\S+$)/,"$1");var _m=parseInt(_d.replace(/^(.*)\..*$/,"$1"),10);var _r=/r/.test(_d)?parseInt(_d.replace(/^.*r(.*)$/,"$1"),10):0;version=[_m,_r];}}else if(window.ActiveXObject){try{var _a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{_a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");version=[6,0];_a.AllowScriptAccess="always";}catch(ee){if(version[0]==6){return;}}try{_a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(eee){}}if(typeof _a=="object"){_d=_a.GetVariable("$version");if(typeof _d!="undefined"){_d=_d.replace(/^\S+\s+(.*)$/,"$1").split(",");version=[parseInt(_d[0],10),parseInt(_d[2],10)];}}}return version;},isSupported:function(version){var now=flashembed.getVersion();var ret=(now[0]>version[0])||(now[0]==version[0]&&now[1]>=version[1]);return ret;},domReady:domReady,asString:asString,getHTML:getHTML});if(jQ){jQuery.prototype.flashembed=function(conf,flashvars){return this.each(function(){flashembed(this,conf,flashvars);});};}})(); \ No newline at end of file
diff --git a/js/identica-badge.js b/js/identica-badge.js
index 5c586b5d6..869230b7a 100644
--- a/js/identica-badge.js
+++ b/js/identica-badge.js
@@ -1,4 +1,5 @@
// identica badge -- updated to work with the native API, 12-4-2008
+// Modified to point to Identi.ca, 2-20-2009 by Zach
// copyright Kent Brewster 2008
// see http://kentbrewster.com/identica-badge for info
( function() {
@@ -127,7 +128,7 @@
var a = document.createElement('A');
a.innerHTML = 'get this';
a.target = '_blank';
- a.href = 'http://kentbrewster.com/identica-badge';
+ a.href = 'http://identica/doc/badge';
$.s.f.appendChild(a);
$.s.appendChild($.s.f);
$.f.getUser();
diff --git a/js/jcrop/jquery.Jcrop.go.js b/js/jcrop/jquery.Jcrop.go.js
index b2737407b..a0399d540 100644
--- a/js/jcrop/jquery.Jcrop.go.js
+++ b/js/jcrop/jquery.Jcrop.go.js
@@ -37,10 +37,3 @@
$('#avatar_crop_w').val(c.w);
$('#avatar_crop_h').val(c.h);
};
-
- function checkCoords() {
- if (parseInt($('#avatar_crop_w').val())) return true;
- alert('Please select a crop region then press submit.');
- return false;
- };
-
diff --git a/js/jquery.js b/js/jquery.js
index fc06ace27..94e9c1755 100644
--- a/js/jquery.js
+++ b/js/jquery.js
@@ -1,13 +1,13 @@
/*!
- * jQuery JavaScript Library v1.3
+ * jQuery JavaScript Library v1.3.1
* http://jquery.com/
*
* Copyright (c) 2009 John Resig
* Dual licensed under the MIT and GPL licenses.
* http://docs.jquery.com/License
*
- * Date: 2009-01-13 12:50:31 -0500 (Tue, 13 Jan 2009)
- * Revision: 6104
+ * Date: 2009-01-21 20:42:16 -0500 (Wed, 21 Jan 2009)
+ * Revision: 6158
*/
(function(){
@@ -60,20 +60,16 @@ jQuery.fn = jQuery.prototype = {
else {
var elem = document.getElementById( match[3] );
- // Make sure an element was located
- if ( elem ){
- // Handle the case where IE and Opera return items
- // by name instead of ID
- if ( elem.id != match[3] )
- return jQuery().find( selector );
-
- // Otherwise, we inject the element directly into the jQuery object
- var ret = jQuery( elem );
- ret.context = document;
- ret.selector = selector;
- return ret;
- }
- selector = [];
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem && elem.id != match[3] )
+ return jQuery().find( selector );
+
+ // Otherwise, we inject the element directly into the jQuery object
+ var ret = jQuery( elem || [] );
+ ret.context = document;
+ ret.selector = selector;
+ return ret;
}
// HANDLE: $(expr, [context])
@@ -99,7 +95,7 @@ jQuery.fn = jQuery.prototype = {
selector: "",
// The current version of jQuery being used
- jquery: "1.3",
+ jquery: "1.3.1",
// The number of elements contained in the matched element set
size: function() {
@@ -634,8 +630,8 @@ jQuery.extend({
// check if an element is in a (or is an) XML document
isXMLDoc: function( elem ) {
- return elem.documentElement && !elem.body ||
- elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
+ return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+ !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument );
},
// Evalulates a script in a global context
@@ -725,7 +721,7 @@ jQuery.extend({
// internal only, use hasClass("class")
has: function( elem, className ) {
- return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+ return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
}
},
@@ -999,9 +995,11 @@ jQuery.extend({
var attributeNode = elem.getAttributeNode( "tabIndex" );
return attributeNode && attributeNode.specified
? attributeNode.value
- : elem.nodeName.match(/^(a|area|button|input|object|select|textarea)$/i)
+ : elem.nodeName.match(/(button|input|object|select|textarea)/i)
? 0
- : undefined;
+ : elem.nodeName.match(/^(a|area)$/i) && elem.href
+ ? 0
+ : undefined;
}
return elem[ name ];
@@ -1397,14 +1395,14 @@ jQuery.fn.extend({
});
}
});/*!
- * Sizzle CSS Selector Engine - v0.9.1
+ * Sizzle CSS Selector Engine - v0.9.3
* Copyright 2009, The Dojo Foundation
* Released under the MIT, BSD, and GPL Licenses.
* More information: http://sizzlejs.com/
*/
(function(){
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|[^[\]]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]+['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,
done = 0,
toString = Object.prototype.toString;
@@ -1433,40 +1431,27 @@ var Sizzle = function(selector, context, results, seed) {
}
}
- if ( parts.length > 1 && Expr.match.POS.exec( selector ) ) {
+ if ( parts.length > 1 && origPOS.exec( selector ) ) {
if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
- var later = "", match;
-
- // Position selectors must be done after the filter
- while ( (match = Expr.match.POS.exec( selector )) ) {
- later += match[0];
- selector = selector.replace( Expr.match.POS, "" );
- }
-
- set = Sizzle.filter( later, Sizzle( /\s$/.test(selector) ? selector + "*" : selector, context ) );
+ set = posProcess( parts[0] + parts[1], context );
} else {
set = Expr.relative[ parts[0] ] ?
[ context ] :
Sizzle( parts.shift(), context );
while ( parts.length ) {
- var tmpSet = [];
-
selector = parts.shift();
+
if ( Expr.relative[ selector ] )
selector += parts.shift();
- for ( var i = 0, l = set.length; i < l; i++ ) {
- Sizzle( selector, set[i], tmpSet );
- }
-
- set = tmpSet;
+ set = posProcess( selector, set );
}
}
} else {
var ret = seed ?
{ expr: parts.pop(), set: makeArray(seed) } :
- Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context );
+ Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context, isXML(context) );
set = Sizzle.filter( ret.expr, ret.set );
if ( parts.length > 0 ) {
@@ -1531,7 +1516,7 @@ Sizzle.matches = function(expr, set){
return Sizzle(expr, null, null, set);
};
-Sizzle.find = function(expr, context){
+Sizzle.find = function(expr, context, isXML){
var set, match;
if ( !expr ) {
@@ -1546,7 +1531,7 @@ Sizzle.find = function(expr, context){
if ( left.substr( left.length - 1 ) !== "\\" ) {
match[1] = (match[1] || "").replace(/\\/g, "");
- set = Expr.find[ type ]( match, context );
+ set = Expr.find[ type ]( match, context, isXML );
if ( set != null ) {
expr = expr.replace( Expr.match[ type ], "" );
break;
@@ -1568,7 +1553,7 @@ Sizzle.filter = function(expr, set, inplace, not){
while ( expr && set.length ) {
for ( var type in Expr.filter ) {
if ( (match = Expr.match[ type ].exec( expr )) != null ) {
- var filter = Expr.filter[ type ], goodArray = null, goodPos = 0, found, item;
+ var filter = Expr.filter[ type ], found, item;
anyFound = false;
if ( curLoop == result ) {
@@ -1582,26 +1567,13 @@ Sizzle.filter = function(expr, set, inplace, not){
anyFound = found = true;
} else if ( match === true ) {
continue;
- } else if ( match[0] === true ) {
- goodArray = [];
- var last = null, elem;
- for ( var i = 0; (elem = curLoop[i]) !== undefined; i++ ) {
- if ( elem && last !== elem ) {
- goodArray.push( elem );
- last = elem;
- }
- }
}
}
if ( match ) {
- for ( var i = 0; (item = curLoop[i]) !== undefined; i++ ) {
+ for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
if ( item ) {
- if ( goodArray && item != goodArray[goodPos] ) {
- goodPos++;
- }
-
- found = filter( item, match, goodPos, goodArray );
+ found = filter( item, match, i, curLoop );
var pass = not ^ !!found;
if ( inplace && found != null ) {
@@ -1739,14 +1711,16 @@ var Expr = Sizzle.selectors = {
}
},
find: {
- ID: function(match, context){
- if ( context.getElementById ) {
+ ID: function(match, context, isXML){
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
var m = context.getElementById(match[1]);
return m ? [m] : [];
}
},
- NAME: function(match, context){
- return context.getElementsByName ? context.getElementsByName(match[1]) : null;
+ NAME: function(match, context, isXML){
+ if ( typeof context.getElementsByName !== "undefined" && !isXML ) {
+ return context.getElementsByName(match[1]);
+ }
},
TAG: function(match, context){
return context.getElementsByTagName(match[1]);
@@ -1756,12 +1730,15 @@ var Expr = Sizzle.selectors = {
CLASS: function(match, curLoop, inplace, result, not){
match = " " + match[1].replace(/\\/g, "") + " ";
- for ( var i = 0; curLoop[i]; i++ ) {
- if ( not ^ (" " + curLoop[i].className + " ").indexOf(match) >= 0 ) {
- if ( !inplace )
- result.push( curLoop[i] );
- } else if ( inplace ) {
- curLoop[i] = false;
+ var elem;
+ for ( var i = 0; (elem = curLoop[i]) != null; i++ ) {
+ if ( elem ) {
+ if ( not ^ (" " + elem.className + " ").indexOf(match) >= 0 ) {
+ if ( !inplace )
+ result.push( elem );
+ } else if ( inplace ) {
+ curLoop[i] = false;
+ }
}
}
@@ -1771,8 +1748,8 @@ var Expr = Sizzle.selectors = {
return match[1].replace(/\\/g, "");
},
TAG: function(match, curLoop){
- for ( var i = 0; !curLoop[i]; i++ ){}
- return isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
+ for ( var i = 0; curLoop[i] === false; i++ ){}
+ return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
},
CHILD: function(match){
if ( match[1] == "nth" ) {
@@ -1792,7 +1769,7 @@ var Expr = Sizzle.selectors = {
return match;
},
ATTR: function(match){
- var name = match[1];
+ var name = match[1].replace(/\\/g, "");
if ( Expr.attrMap[name] ) {
match[1] = Expr.attrMap[name];
@@ -1916,7 +1893,7 @@ var Expr = Sizzle.selectors = {
CHILD: function(elem, match){
var type = match[1], parent = elem.parentNode;
- var doneName = "child" + parent.childNodes.length;
+ var doneName = match[0];
if ( parent && (!parent[ doneName ] || !elem.nodeIndex) ) {
var count = 1;
@@ -1985,7 +1962,7 @@ var Expr = Sizzle.selectors = {
ATTR: function(elem, match){
var result = Expr.attrHandle[ match[1] ] ? Expr.attrHandle[ match[1] ]( elem ) : elem[ match[1] ] || elem.getAttribute( match[1] ), value = result + "", type = match[2], check = match[4];
return result == null ?
- false :
+ type === "!=" :
type === "=" ?
value === check :
type === "*=" ?
@@ -2014,6 +1991,8 @@ var Expr = Sizzle.selectors = {
}
};
+var origPOS = Expr.match.POS;
+
for ( var type in Expr.match ) {
Expr.match[ type ] = RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
}
@@ -2072,15 +2051,15 @@ try {
// The workaround has to do additional checks after a getElementById
// Which slows things down for other browsers (hence the branching)
if ( !!document.getElementById( id ) ) {
- Expr.find.ID = function(match, context){
- if ( context.getElementById ) {
+ Expr.find.ID = function(match, context, isXML){
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
var m = context.getElementById(match[1]);
- return m ? m.id === match[1] || m.getAttributeNode && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
+ return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
}
};
Expr.filter.ID = function(elem, match){
- var node = elem.getAttributeNode && elem.getAttributeNode("id");
+ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
return elem.nodeType === 1 && node && node.nodeValue === match;
};
}
@@ -2120,7 +2099,7 @@ try {
// Check to see if an attribute returns normalized href attributes
div.innerHTML = "<a href='#'></a>";
- if ( div.firstChild.getAttribute("href") !== "#" ) {
+ if ( div.firstChild && div.firstChild.getAttribute("href") !== "#" ) {
Expr.attrHandle.href = function(elem){
return elem.getAttribute("href", 2);
};
@@ -2128,12 +2107,21 @@ try {
})();
if ( document.querySelectorAll ) (function(){
- var oldSizzle = Sizzle;
+ var oldSizzle = Sizzle, div = document.createElement("div");
+ div.innerHTML = "<p class='TEST'></p>";
+
+ // Safari can't handle uppercase or unicode characters when
+ // in quirks mode.
+ if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+ return;
+ }
Sizzle = function(query, context, extra, seed){
context = context || document;
- if ( !seed && context.nodeType === 9 ) {
+ // Only use querySelectorAll on non-XML documents
+ // (ID selectors don't work in non-HTML documents)
+ if ( !seed && context.nodeType === 9 && !isXML(context) ) {
try {
return makeArray( context.querySelectorAll(query), extra );
} catch(e){}
@@ -2148,7 +2136,7 @@ if ( document.querySelectorAll ) (function(){
Sizzle.matches = oldSizzle.matches;
})();
-if ( document.documentElement.getElementsByClassName ) {
+if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) {
Expr.order.splice(1, 0, "CLASS");
Expr.find.CLASS = function(match, context) {
return context.getElementsByClassName(match[1]);
@@ -2229,8 +2217,28 @@ var contains = document.compareDocumentPosition ? function(a, b){
};
var isXML = function(elem){
- return elem.documentElement && !elem.body ||
- elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
+ return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+ !!elem.ownerDocument && isXML( elem.ownerDocument );
+};
+
+var posProcess = function(selector, context){
+ var tmpSet = [], later = "", match,
+ root = context.nodeType ? [context] : context;
+
+ // Position selectors must be done after the filter
+ // And so must :not(positional) so we move all PSEUDOs to the end
+ while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
+ later += match[0];
+ selector = selector.replace( Expr.match.PSEUDO, "" );
+ }
+
+ selector = Expr.relative[selector] ? selector + "*" : selector;
+
+ for ( var i = 0, l = root.length; i < l; i++ ) {
+ Sizzle( selector, root[i], tmpSet );
+ }
+
+ return Sizzle.filter( later, tmpSet );
};
// EXPOSE
@@ -2681,13 +2689,13 @@ jQuery.Event = function( src ){
if( src && src.type ){
this.originalEvent = src;
this.type = src.type;
- this.timeStamp = src.timeStamp;
// Event type
}else
this.type = src;
- if( !this.timeStamp )
- this.timeStamp = now();
+ // timeStamp is buggy for some events on Firefox(#3843)
+ // So we won't rely on the native value
+ this.timeStamp = now();
// Mark it as fixed
this[expando] = true;
@@ -2876,9 +2884,8 @@ function liveHandler( event ){
});
jQuery.each(elems, function(){
- if ( !event.isImmediatePropagationStopped() &&
- this.fn.call(this.elem, event, this.fn.data) === false )
- stop = false;
+ if ( this.fn.call(this.elem, event, this.fn.data) === false )
+ stop = false;
});
return stop;
@@ -2942,7 +2949,7 @@ function bindReady(){
// If IE and not an iframe
// continually check to see if the document is ready
- if ( document.documentElement.doScroll && !window.frameElement ) (function(){
+ if ( document.documentElement.doScroll && typeof window.frameElement === "undefined" ) (function(){
if ( jQuery.isReady ) return;
try {
@@ -3477,6 +3484,9 @@ jQuery.extend({
// Fire the complete handlers
complete();
+ if ( isTimeout )
+ xhr.abort();
+
// Stop memory leaks
if ( s.async )
xhr = null;
@@ -3491,14 +3501,8 @@ jQuery.extend({
if ( s.timeout > 0 )
setTimeout(function(){
// Check to see if the request is still happening
- if ( xhr ) {
- if( !requestDone )
- onreadystatechange( "timeout" );
-
- // Cancel the request
- if ( xhr )
- xhr.abort();
- }
+ if ( xhr && !requestDone )
+ onreadystatechange( "timeout" );
}, s.timeout);
}
@@ -3637,6 +3641,7 @@ jQuery.extend({
});
var elemdisplay = {},
+ timerId,
fxAttrs = [
// height animations
[ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
@@ -3859,7 +3864,6 @@ jQuery.extend({
},
timers: [],
- timerId: null,
fx: function( elem, options, prop ){
this.options = options;
@@ -3911,10 +3915,8 @@ jQuery.fx.prototype = {
t.elem = this.elem;
- jQuery.timers.push(t);
-
- if ( t() && jQuery.timerId == null ) {
- jQuery.timerId = setInterval(function(){
+ if ( t() && jQuery.timers.push(t) == 1 ) {
+ timerId = setInterval(function(){
var timers = jQuery.timers;
for ( var i = 0; i < timers.length; i++ )
@@ -3922,8 +3924,7 @@ jQuery.fx.prototype = {
timers.splice(i--, 1);
if ( !timers.length ) {
- clearInterval( jQuery.timerId );
- jQuery.timerId = null;
+ clearInterval( timerId );
}
}, 13);
}
@@ -3989,11 +3990,10 @@ jQuery.fx.prototype = {
if ( this.options.hide || this.options.show )
for ( var p in this.options.curAnim )
jQuery.attr(this.elem.style, p, this.options.orig[p]);
- }
-
- if ( done )
+
// Execute the complete function
this.options.complete.call( this.elem );
+ }
return false;
} else {
@@ -4087,7 +4087,7 @@ jQuery.offset = {
initialize: function() {
if ( this.initialized ) return;
var body = document.body, container = document.createElement('div'), innerDiv, checkDiv, table, td, rules, prop, bodyMarginTop = body.style.marginTop,
- html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"cellpadding="0"cellspacing="0"><tr><td></td></tr></table>';
+ html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';
rules = { position: 'absolute', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px', visibility: 'hidden' };
for ( prop in rules ) container.style[prop] = rules[prop];
diff --git a/js/jquery.min.js b/js/jquery.min.js
index 396646c84..c327fae81 100644
--- a/js/jquery.min.js
+++ b/js/jquery.min.js
@@ -1,19 +1,19 @@
/*
- * jQuery JavaScript Library v1.3
+ * jQuery JavaScript Library v1.3.1
* http://jquery.com/
*
* Copyright (c) 2009 John Resig
* Dual licensed under the MIT and GPL licenses.
* http://docs.jquery.com/License
*
- * Date: 2009-01-13 12:50:31 -0500 (Tue, 13 Jan 2009)
- * Revision: 6104
+ * Date: 2009-01-21 20:42:16 -0500 (Wed, 21 Jan 2009)
+ * Revision: 6158
*/
-(function(){var l=this,g,x=l.jQuery,o=l.$,n=l.jQuery=l.$=function(D,E){return new n.fn.init(D,E)},C=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;n.fn=n.prototype={init:function(D,G){D=D||document;if(D.nodeType){this[0]=D;this.length=1;this.context=D;return this}if(typeof D==="string"){var F=C.exec(D);if(F&&(F[1]||!G)){if(F[1]){D=n.clean([F[1]],G)}else{var H=document.getElementById(F[3]);if(H){if(H.id!=F[3]){return n().find(D)}var E=n(H);E.context=document;E.selector=D;return E}D=[]}}else{return n(G).find(D)}}else{if(n.isFunction(D)){return n(document).ready(D)}}if(D.selector&&D.context){this.selector=D.selector;this.context=D.context}return this.setArray(n.makeArray(D))},selector:"",jquery:"1.3",size:function(){return this.length},get:function(D){return D===g?n.makeArray(this):this[D]},pushStack:function(E,G,D){var F=n(E);F.prevObject=this;F.context=this.context;if(G==="find"){F.selector=this.selector+(this.selector?" ":"")+D}else{if(G){F.selector=this.selector+"."+G+"("+D+")"}}return F},setArray:function(D){this.length=0;Array.prototype.push.apply(this,D);return this},each:function(E,D){return n.each(this,E,D)},index:function(D){return n.inArray(D&&D.jquery?D[0]:D,this)},attr:function(E,G,F){var D=E;if(typeof E==="string"){if(G===g){return this[0]&&n[F||"attr"](this[0],E)}else{D={};D[E]=G}}return this.each(function(H){for(E in D){n.attr(F?this.style:this,E,n.prop(this,D[E],F,H,E))}})},css:function(D,E){if((D=="width"||D=="height")&&parseFloat(E)<0){E=g}return this.attr(D,E,"curCSS")},text:function(E){if(typeof E!=="object"&&E!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(E))}var D="";n.each(E||this,function(){n.each(this.childNodes,function(){if(this.nodeType!=8){D+=this.nodeType!=1?this.nodeValue:n.fn.text([this])}})});return D},wrapAll:function(D){if(this[0]){var E=n(D,this[0].ownerDocument).clone();if(this[0].parentNode){E.insertBefore(this[0])}E.map(function(){var F=this;while(F.firstChild){F=F.firstChild}return F}).append(this)}return this},wrapInner:function(D){return this.each(function(){n(this).contents().wrapAll(D)})},wrap:function(D){return this.each(function(){n(this).wrapAll(D)})},append:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.appendChild(D)}})},prepend:function(){return this.domManip(arguments,true,function(D){if(this.nodeType==1){this.insertBefore(D,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this)})},after:function(){return this.domManip(arguments,false,function(D){this.parentNode.insertBefore(D,this.nextSibling)})},end:function(){return this.prevObject||n([])},push:[].push,find:function(D){if(this.length===1&&!/,/.test(D)){var F=this.pushStack([],"find",D);F.length=0;n.find(D,this[0],F);return F}else{var E=n.map(this,function(G){return n.find(D,G)});return this.pushStack(/[^+>] [^+>]/.test(D)?n.unique(E):E,"find",D)}},clone:function(E){var D=this.map(function(){if(!n.support.noCloneEvent&&!n.isXMLDoc(this)){var H=this.cloneNode(true),G=document.createElement("div");G.appendChild(H);return n.clean([G.innerHTML])[0]}else{return this.cloneNode(true)}});var F=D.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(E===true){this.find("*").andSelf().each(function(H){if(this.nodeType==3){return}var G=n.data(this,"events");for(var J in G){for(var I in G[J]){n.event.add(F[H],J,G[J][I],G[J][I].data)}}})}return D},filter:function(D){return this.pushStack(n.isFunction(D)&&n.grep(this,function(F,E){return D.call(F,E)})||n.multiFilter(D,n.grep(this,function(E){return E.nodeType===1})),"filter",D)},closest:function(D){var E=n.expr.match.POS.test(D)?n(D):null;return this.map(function(){var F=this;while(F&&F.ownerDocument){if(E?E.index(F)>-1:n(F).is(D)){return F}F=F.parentNode}})},not:function(D){if(typeof D==="string"){if(f.test(D)){return this.pushStack(n.multiFilter(D,this,true),"not",D)}else{D=n.multiFilter(D,this)}}var E=D.length&&D[D.length-1]!==g&&!D.nodeType;return this.filter(function(){return E?n.inArray(this,D)<0:this!=D})},add:function(D){return this.pushStack(n.unique(n.merge(this.get(),typeof D==="string"?n(D):n.makeArray(D))))},is:function(D){return !!D&&n.multiFilter(D,this).length>0},hasClass:function(D){return !!D&&this.is("."+D)},val:function(J){if(J===g){var D=this[0];if(D){if(n.nodeName(D,"option")){return(D.attributes.value||{}).specified?D.value:D.text}if(n.nodeName(D,"select")){var H=D.selectedIndex,K=[],L=D.options,G=D.type=="select-one";if(H<0){return null}for(var E=G?H:0,I=G?H+1:L.length;E<I;E++){var F=L[E];if(F.selected){J=n(F).val();if(G){return J}K.push(J)}}return K}return(D.value||"").replace(/\r/g,"")}return g}if(typeof J==="number"){J+=""}return this.each(function(){if(this.nodeType!=1){return}if(n.isArray(J)&&/radio|checkbox/.test(this.type)){this.checked=(n.inArray(this.value,J)>=0||n.inArray(this.name,J)>=0)}else{if(n.nodeName(this,"select")){var M=n.makeArray(J);n("option",this).each(function(){this.selected=(n.inArray(this.value,M)>=0||n.inArray(this.text,M)>=0)});if(!M.length){this.selectedIndex=-1}}else{this.value=J}}})},html:function(D){return D===g?(this[0]?this[0].innerHTML:null):this.empty().append(D)},replaceWith:function(D){return this.after(D).remove()},eq:function(D){return this.slice(D,+D+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(D){return this.pushStack(n.map(this,function(F,E){return D.call(F,E,F)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=n.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild,D=this.length>1?I.cloneNode(true):I;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),G>0?D.cloneNode(true):I)}}if(F){n.each(F,y)}}return this;function K(N,O){return M&&n.nodeName(N,"table")&&n.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};n.fn.init.prototype=n.fn;function y(D,E){if(E.src){n.ajax({url:E.src,async:false,dataType:"script"})}else{n.globalEval(E.text||E.textContent||E.innerHTML||"")}if(E.parentNode){E.parentNode.removeChild(E)}}function e(){return +new Date}n.extend=n.fn.extend=function(){var I=arguments[0]||{},G=1,H=arguments.length,D=false,F;if(typeof I==="boolean"){D=I;I=arguments[1]||{};G=2}if(typeof I!=="object"&&!n.isFunction(I)){I={}}if(H==G){I=this;--G}for(;G<H;G++){if((F=arguments[G])!=null){for(var E in F){var J=I[E],K=F[E];if(I===K){continue}if(D&&K&&typeof K==="object"&&!K.nodeType){I[E]=n.extend(D,J||(K.length!=null?[]:{}),K)}else{if(K!==g){I[E]=K}}}}}return I};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,p=document.defaultView||{},r=Object.prototype.toString;n.extend({noConflict:function(D){l.$=o;if(D){l.jQuery=x}return n},isFunction:function(D){return r.call(D)==="[object Function]"},isArray:function(D){return r.call(D)==="[object Array]"},isXMLDoc:function(D){return D.documentElement&&!D.body||D.tagName&&D.ownerDocument&&!D.ownerDocument.body},globalEval:function(F){F=n.trim(F);if(F){var E=document.getElementsByTagName("head")[0]||document.documentElement,D=document.createElement("script");D.type="text/javascript";if(n.support.scriptEval){D.appendChild(document.createTextNode(F))}else{D.text=F}E.insertBefore(D,E.firstChild);E.removeChild(D)}},nodeName:function(E,D){return E.nodeName&&E.nodeName.toUpperCase()==D.toUpperCase()},each:function(F,J,E){var D,G=0,H=F.length;if(E){if(H===g){for(D in F){if(J.apply(F[D],E)===false){break}}}else{for(;G<H;){if(J.apply(F[G++],E)===false){break}}}}else{if(H===g){for(D in F){if(J.call(F[D],D,F[D])===false){break}}}else{for(var I=F[0];G<H&&J.call(I,G,I)!==false;I=F[++G]){}}}return F},prop:function(G,H,F,E,D){if(n.isFunction(H)){H=H.call(G,E)}return typeof H==="number"&&F=="curCSS"&&!b.test(D)?H+"px":H},className:{add:function(D,E){n.each((E||"").split(/\s+/),function(F,G){if(D.nodeType==1&&!n.className.has(D.className,G)){D.className+=(D.className?" ":"")+G}})},remove:function(D,E){if(D.nodeType==1){D.className=E!==g?n.grep(D.className.split(/\s+/),function(F){return !n.className.has(E,F)}).join(" "):""}},has:function(E,D){return n.inArray(D,(E.className||E).toString().split(/\s+/))>-1}},swap:function(G,F,H){var D={};for(var E in F){D[E]=G.style[E];G.style[E]=F[E]}H.call(G);for(var E in F){G.style[E]=D[E]}},css:function(F,D,H){if(D=="width"||D=="height"){var J,E={position:"absolute",visibility:"hidden",display:"block"},I=D=="width"?["Left","Right"]:["Top","Bottom"];function G(){J=D=="width"?F.offsetWidth:F.offsetHeight;var L=0,K=0;n.each(I,function(){L+=parseFloat(n.curCSS(F,"padding"+this,true))||0;K+=parseFloat(n.curCSS(F,"border"+this+"Width",true))||0});J-=Math.round(L+K)}if(n(F).is(":visible")){G()}else{n.swap(F,E,G)}return Math.max(0,J)}return n.curCSS(F,D,H)},curCSS:function(H,E,F){var K,D=H.style;if(E=="opacity"&&!n.support.opacity){K=n.attr(D,"opacity");return K==""?"1":K}if(E.match(/float/i)){E=v}if(!F&&D&&D[E]){K=D[E]}else{if(p.getComputedStyle){if(E.match(/float/i)){E="float"}E=E.replace(/([A-Z])/g,"-$1").toLowerCase();var L=p.getComputedStyle(H,null);if(L){K=L.getPropertyValue(E)}if(E=="opacity"&&K==""){K="1"}}else{if(H.currentStyle){var I=E.replace(/\-(\w)/g,function(M,N){return N.toUpperCase()});K=H.currentStyle[E]||H.currentStyle[I];if(!/^\d+(px)?$/i.test(K)&&/^\d/.test(K)){var G=D.left,J=H.runtimeStyle.left;H.runtimeStyle.left=H.currentStyle.left;D.left=K||0;K=D.pixelLeft+"px";D.left=G;H.runtimeStyle.left=J}}}}return K},clean:function(E,J,H){J=J||document;if(typeof J.createElement==="undefined"){J=J.ownerDocument||J[0]&&J[0].ownerDocument||document}if(!H&&E.length===1&&typeof E[0]==="string"){var G=/^<(\w+)\s*\/?>$/.exec(E[0]);if(G){return[J.createElement(G[1])]}}var F=[],D=[],K=J.createElement("div");n.each(E,function(O,Q){if(typeof Q==="number"){Q+=""}if(!Q){return}if(typeof Q==="string"){Q=Q.replace(/(<(\w+)[^>]*?)\/>/g,function(S,T,R){return R.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?S:T+"></"+R+">"});var N=n.trim(Q).toLowerCase();var P=!N.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!N.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||N.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!N.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!N.indexOf("<td")||!N.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!N.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!n.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];K.innerHTML=P[1]+Q+P[2];while(P[0]--){K=K.lastChild}if(!n.support.tbody){var M=!N.indexOf("<table")&&N.indexOf("<tbody")<0?K.firstChild&&K.firstChild.childNodes:P[1]=="<table>"&&N.indexOf("<tbody")<0?K.childNodes:[];for(var L=M.length-1;L>=0;--L){if(n.nodeName(M[L],"tbody")&&!M[L].childNodes.length){M[L].parentNode.removeChild(M[L])}}}if(!n.support.leadingWhitespace&&/^\s/.test(Q)){K.insertBefore(J.createTextNode(Q.match(/^\s*/)[0]),K.firstChild)}Q=n.makeArray(K.childNodes)}if(Q.nodeType){F.push(Q)}else{F=n.merge(F,Q)}});if(H){for(var I=0;F[I];I++){if(n.nodeName(F[I],"script")&&(!F[I].type||F[I].type.toLowerCase()==="text/javascript")){D.push(F[I].parentNode?F[I].parentNode.removeChild(F[I]):F[I])}else{if(F[I].nodeType===1){F.splice.apply(F,[I+1,0].concat(n.makeArray(F[I].getElementsByTagName("script"))))}H.appendChild(F[I])}}return D}return F},attr:function(I,F,J){if(!I||I.nodeType==3||I.nodeType==8){return g}var G=!n.isXMLDoc(I),K=J!==g;F=G&&n.props[F]||F;if(I.tagName){var E=/href|src|style/.test(F);if(F=="selected"&&I.parentNode){I.parentNode.selectedIndex}if(F in I&&G&&!E){if(K){if(F=="type"&&n.nodeName(I,"input")&&I.parentNode){throw"type property can't be changed"}I[F]=J}if(n.nodeName(I,"form")&&I.getAttributeNode(F)){return I.getAttributeNode(F).nodeValue}if(F=="tabIndex"){var H=I.getAttributeNode("tabIndex");return H&&H.specified?H.value:I.nodeName.match(/^(a|area|button|input|object|select|textarea)$/i)?0:g}return I[F]}if(!n.support.style&&G&&F=="style"){return n.attr(I.style,"cssText",J)}if(K){I.setAttribute(F,""+J)}var D=!n.support.hrefNormalized&&G&&E?I.getAttribute(F,2):I.getAttribute(F);return D===null?g:D}if(!n.support.opacity&&F=="opacity"){if(K){I.zoom=1;I.filter=(I.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(J)+""=="NaN"?"":"alpha(opacity="+J*100+")")}return I.filter&&I.filter.indexOf("opacity=")>=0?(parseFloat(I.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}F=F.replace(/-([a-z])/ig,function(L,M){return M.toUpperCase()});if(K){I[F]=J}return I[F]},trim:function(D){return(D||"").replace(/^\s+|\s+$/g,"")},makeArray:function(F){var D=[];if(F!=null){var E=F.length;if(E==null||typeof F==="string"||n.isFunction(F)||F.setInterval){D[0]=F}else{while(E){D[--E]=F[E]}}}return D},inArray:function(F,G){for(var D=0,E=G.length;D<E;D++){if(G[D]===F){return D}}return -1},merge:function(G,D){var E=0,F,H=G.length;if(!n.support.getAll){while((F=D[E++])!=null){if(F.nodeType!=8){G[H++]=F}}}else{while((F=D[E++])!=null){G[H++]=F}}return G},unique:function(J){var E=[],D={};try{for(var F=0,G=J.length;F<G;F++){var I=n.data(J[F]);if(!D[I]){D[I]=true;E.push(J[F])}}}catch(H){E=J}return E},grep:function(E,I,D){var F=[];for(var G=0,H=E.length;G<H;G++){if(!D!=!I(E[G],G)){F.push(E[G])}}return F},map:function(D,I){var E=[];for(var F=0,G=D.length;F<G;F++){var H=I(D[F],F);if(H!=null){E[E.length]=H}}return E.concat.apply([],E)}});var B=navigator.userAgent.toLowerCase();n.browser={version:(B.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(B),opera:/opera/.test(B),msie:/msie/.test(B)&&!/opera/.test(B),mozilla:/mozilla/.test(B)&&!/(compatible|webkit)/.test(B)};n.each({parent:function(D){return D.parentNode},parents:function(D){return n.dir(D,"parentNode")},next:function(D){return n.nth(D,2,"nextSibling")},prev:function(D){return n.nth(D,2,"previousSibling")},nextAll:function(D){return n.dir(D,"nextSibling")},prevAll:function(D){return n.dir(D,"previousSibling")},siblings:function(D){return n.sibling(D.parentNode.firstChild,D)},children:function(D){return n.sibling(D.firstChild)},contents:function(D){return n.nodeName(D,"iframe")?D.contentDocument||D.contentWindow.document:n.makeArray(D.childNodes)}},function(D,E){n.fn[D]=function(F){var G=n.map(this,E);if(F&&typeof F=="string"){G=n.multiFilter(F,G)}return this.pushStack(n.unique(G),D,F)}});n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(D,E){n.fn[D]=function(){var F=arguments;return this.each(function(){for(var G=0,H=F.length;G<H;G++){n(F[G])[E](this)}})}});n.each({removeAttr:function(D){n.attr(this,D,"");if(this.nodeType==1){this.removeAttribute(D)}},addClass:function(D){n.className.add(this,D)},removeClass:function(D){n.className.remove(this,D)},toggleClass:function(E,D){if(typeof D!=="boolean"){D=!n.className.has(this,E)}n.className[D?"add":"remove"](this,E)},remove:function(D){if(!D||n.filter(D,[this]).length){n("*",this).add([this]).each(function(){n.event.remove(this);n.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){n(">*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(D,E){n.fn[D]=function(){return this.each(E,arguments)}});function j(D,E){return D[0]&&parseInt(n.curCSS(D[0],E,true),10)||0}var h="jQuery"+e(),u=0,z={};n.extend({cache:{},data:function(E,D,F){E=E==l?z:E;var G=E[h];if(!G){G=E[h]=++u}if(D&&!n.cache[G]){n.cache[G]={}}if(F!==g){n.cache[G][D]=F}return D?n.cache[G][D]:G},removeData:function(E,D){E=E==l?z:E;var G=E[h];if(D){if(n.cache[G]){delete n.cache[G][D];D="";for(D in n.cache[G]){break}if(!D){n.removeData(E)}}}else{try{delete E[h]}catch(F){if(E.removeAttribute){E.removeAttribute(h)}}delete n.cache[G]}},queue:function(E,D,G){if(E){D=(D||"fx")+"queue";var F=n.data(E,D);if(!F||n.isArray(G)){F=n.data(E,D,n.makeArray(G))}else{if(G){F.push(G)}}}return F},dequeue:function(G,F){var D=n.queue(G,F),E=D.shift();if(!F||F==="fx"){E=D[0]}if(E!==g){E.call(G)}}});n.fn.extend({data:function(D,F){var G=D.split(".");G[1]=G[1]?"."+G[1]:"";if(F===g){var E=this.triggerHandler("getData"+G[1]+"!",[G[0]]);if(E===g&&this.length){E=n.data(this[0],D)}return E===g&&G[1]?this.data(G[0]):E}else{return this.trigger("setData"+G[1]+"!",[G[0],F]).each(function(){n.data(this,D,F)})}},removeData:function(D){return this.each(function(){n.removeData(this,D)})},queue:function(D,E){if(typeof D!=="string"){E=D;D="fx"}if(E===g){return n.queue(this[0],D)}return this.each(function(){var F=n.queue(this,D,E);if(D=="fx"&&F.length==1){F[0].call(this)}})},dequeue:function(D){return this.each(function(){n.dequeue(this,D)})}});
+(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.makeArray(E))},selector:"",jquery:"1.3.1",size:function(){return this.length},get:function(E){return E===g?o.makeArray(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,find:function(E){if(this.length===1&&!/,/.test(E)){var G=this.pushStack([],"find",E);G.length=0;o.find(E,this[0],G);return G}else{var F=o.map(this,function(H){return o.find(E,H)});return this.pushStack(/[^+>] [^+>]/.test(E)?o.unique(F):F,"find",E)}},clone:function(F){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.cloneNode(true),H=document.createElement("div");H.appendChild(I);return o.clean([H.innerHTML])[0]}else{return this.cloneNode(true)}});var G=E.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(F===true){this.find("*").andSelf().each(function(I){if(this.nodeType==3){return}var H=o.data(this,"events");for(var K in H){for(var J in H[K]){o.event.add(G[I],K,H[K][J],H[K][J].data)}}})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var F=o.expr.match.POS.test(E)?o(E):null;return this.map(function(){var G=this;while(G&&G.ownerDocument){if(F?F.index(G)>-1:o(G).is(E)){return G}G=G.parentNode}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F<J;F++){var G=M[F];if(G.selected){K=o(G).val();if(H){return K}L.push(K)}}return L}return(E.value||"").replace(/\r/g,"")}return g}if(typeof K==="number"){K+=""}return this.each(function(){if(this.nodeType!=1){return}if(o.isArray(K)&&/radio|checkbox/.test(this.type)){this.checked=(o.inArray(this.value,K)>=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML:null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(K,N,M){if(this[0]){var J=(this[0].ownerDocument||this[0]).createDocumentFragment(),G=o.clean(K,(this[0].ownerDocument||this[0]),J),I=J.firstChild,E=this.length>1?J.cloneNode(true):J;if(I){for(var H=0,F=this.length;H<F;H++){M.call(L(this[H],I),H>0?E.cloneNode(true):J)}}if(G){o.each(G,z)}}return this;function L(O,P){return N&&o.nodeName(O,"table")&&o.nodeName(P,"tr")?(O.getElementsByTagName("tbody")[0]||O.appendChild(O.ownerDocument.createElement("tbody"))):O}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H<I;H++){if((G=arguments[H])!=null){for(var F in G){var K=J[F],L=G[F];if(J===L){continue}if(E&&L&&typeof L==="object"&&!L.nodeType){J[F]=o.extend(E,K||(L.length!=null?[]:{}),L)}else{if(L!==g){J[F]=L}}}}}return J};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,q=document.defaultView||{},s=Object.prototype.toString;o.extend({noConflict:function(E){l.$=p;if(E){l.jQuery=y}return o},isFunction:function(E){return s.call(E)==="[object Function]"},isArray:function(E){return s.call(E)==="[object Array]"},isXMLDoc:function(E){return E.nodeType===9&&E.documentElement.nodeName!=="HTML"||!!E.ownerDocument&&o.isXMLDoc(E.ownerDocument)},globalEval:function(G){G=o.trim(G);if(G){var F=document.getElementsByTagName("head")[0]||document.documentElement,E=document.createElement("script");E.type="text/javascript";if(o.support.scriptEval){E.appendChild(document.createTextNode(G))}else{E.text=G}F.insertBefore(E,F.firstChild);F.removeChild(E)}},nodeName:function(F,E){return F.nodeName&&F.nodeName.toUpperCase()==E.toUpperCase()},each:function(G,K,F){var E,H=0,I=G.length;if(F){if(I===g){for(E in G){if(K.apply(G[E],F)===false){break}}}else{for(;H<I;){if(K.apply(G[H++],F)===false){break}}}}else{if(I===g){for(E in G){if(K.call(G[E],E,G[E])===false){break}}}else{for(var J=G[0];H<I&&K.call(J,H,J)!==false;J=G[++H]){}}}return G},prop:function(H,I,G,F,E){if(o.isFunction(I)){I=I.call(H,F)}return typeof I==="number"&&G=="curCSS"&&!b.test(E)?I+"px":I},className:{add:function(E,F){o.each((F||"").split(/\s+/),function(G,H){if(E.nodeType==1&&!o.className.has(E.className,H)){E.className+=(E.className?" ":"")+H}})},remove:function(E,F){if(E.nodeType==1){E.className=F!==g?o.grep(E.className.split(/\s+/),function(G){return !o.className.has(F,G)}).join(" "):""}},has:function(F,E){return F&&o.inArray(E,(F.className||F).toString().split(/\s+/))>-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(G,E,I){if(E=="width"||E=="height"){var K,F={position:"absolute",visibility:"hidden",display:"block"},J=E=="width"?["Left","Right"]:["Top","Bottom"];function H(){K=E=="width"?G.offsetWidth:G.offsetHeight;var M=0,L=0;o.each(J,function(){M+=parseFloat(o.curCSS(G,"padding"+this,true))||0;L+=parseFloat(o.curCSS(G,"border"+this+"Width",true))||0});K-=Math.round(M+L)}if(o(G).is(":visible")){H()}else{o.swap(G,F,H)}return Math.max(0,K)}return o.curCSS(G,E,I)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,R){if(typeof R==="number"){R+=""}if(!R){return}if(typeof R==="string"){R=R.replace(/(<(\w+)[^>]*?)\/>/g,function(T,U,S){return S.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?T:U+"></"+S+">"});var O=o.trim(R).toLowerCase();var Q=!O.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!O.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!O.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!O.indexOf("<td")||!O.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!O.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!o.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];L.innerHTML=Q[1]+R+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var N=!O.indexOf("<table")&&O.indexOf("<tbody")<0?L.firstChild&&L.firstChild.childNodes:Q[1]=="<table>"&&O.indexOf("<tbody")<0?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(R)){L.insertBefore(K.createTextNode(R.match(/^\s*/)[0]),L.firstChild)}R=o.makeArray(L.childNodes)}if(R.nodeType){G.push(R)}else{G=o.merge(G,R)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E<F;E++){if(H[E]===G){return E}}return -1},merge:function(H,E){var F=0,G,I=H.length;if(!o.support.getAll){while((G=E[F++])!=null){if(G.nodeType!=8){H[I++]=G}}}else{while((G=E[F++])!=null){H[I++]=G}}return H},unique:function(K){var F=[],E={};try{for(var G=0,H=K.length;G<H;G++){var J=o.data(K[G]);if(!E[J]){E[J]=true;F.push(K[G])}}}catch(I){F=K}return F},grep:function(F,J,E){var G=[];for(var H=0,I=F.length;H<I;H++){if(!E!=!J(F[H],H)){G.push(F[H])}}return G},map:function(E,J){var F=[];for(var G=0,H=E.length;G<H;G++){var I=J(E[G],G);if(I!=null){F[F.length]=I}}return F.concat.apply([],F)}});var C=navigator.userAgent.toLowerCase();o.browser={version:(C.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(C),opera:/opera/.test(C),msie:/msie/.test(C)&&!/opera/.test(C),mozilla:/mozilla/.test(C)&&!/(compatible|webkit)/.test(C)};o.each({parent:function(E){return E.parentNode},parents:function(E){return o.dir(E,"parentNode")},next:function(E){return o.nth(E,2,"nextSibling")},prev:function(E){return o.nth(E,2,"previousSibling")},nextAll:function(E){return o.dir(E,"nextSibling")},prevAll:function(E){return o.dir(E,"previousSibling")},siblings:function(E){return o.sibling(E.parentNode.firstChild,E)},children:function(E){return o.sibling(E.firstChild)},contents:function(E){return o.nodeName(E,"iframe")?E.contentDocument||E.contentWindow.document:o.makeArray(E.childNodes)}},function(E,F){o.fn[E]=function(G){var H=o.map(this,F);if(G&&typeof G=="string"){H=o.multiFilter(G,H)}return this.pushStack(o.unique(H),E,G)}});o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(E,F){o.fn[E]=function(){var G=arguments;return this.each(function(){for(var H=0,I=G.length;H<I;H++){o(G[H])[F](this)}})}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(">*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}});
/*
- * Sizzle CSS Selector Engine - v0.9.1
+ * Sizzle CSS Selector Engine - v0.9.3
* Copyright 2009, The Dojo Foundation
* Released under the MIT, BSD, and GPL Licenses.
* More information: http://sizzlejs.com/
*/
-(function(){var N=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|[^[\]]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,I=0,F=Object.prototype.toString;var E=function(ae,S,aa,V){aa=aa||[];S=S||document;if(S.nodeType!==1&&S.nodeType!==9){return[]}if(!ae||typeof ae!=="string"){return aa}var ab=[],ac,Y,ah,ag,Z,R,Q=true;N.lastIndex=0;while((ac=N.exec(ae))!==null){ab.push(ac[1]);if(ac[2]){R=RegExp.rightContext;break}}if(ab.length>1&&G.match.POS.exec(ae)){if(ab.length===2&&G.relative[ab[0]]){var U="",X;while((X=G.match.POS.exec(ae))){U+=X[0];ae=ae.replace(G.match.POS,"")}Y=E.filter(U,E(/\s$/.test(ae)?ae+"*":ae,S))}else{Y=G.relative[ab[0]]?[S]:E(ab.shift(),S);while(ab.length){var P=[];ae=ab.shift();if(G.relative[ae]){ae+=ab.shift()}for(var af=0,ad=Y.length;af<ad;af++){E(ae,Y[af],P)}Y=P}}}else{var ai=V?{expr:ab.pop(),set:D(V)}:E.find(ab.pop(),ab.length===1&&S.parentNode?S.parentNode:S);Y=E.filter(ai.expr,ai.set);if(ab.length>0){ah=D(Y)}else{Q=false}while(ab.length){var T=ab.pop(),W=T;if(!G.relative[T]){T=""}else{W=ab.pop()}if(W==null){W=S}G.relative[T](ah,W,M(S))}}if(!ah){ah=Y}if(!ah){throw"Syntax error, unrecognized expression: "+(T||ae)}if(F.call(ah)==="[object Array]"){if(!Q){aa.push.apply(aa,ah)}else{if(S.nodeType===1){for(var af=0;ah[af]!=null;af++){if(ah[af]&&(ah[af]===true||ah[af].nodeType===1&&H(S,ah[af]))){aa.push(Y[af])}}}else{for(var af=0;ah[af]!=null;af++){if(ah[af]&&ah[af].nodeType===1){aa.push(Y[af])}}}}}else{D(ah,aa)}if(R){E(R,S,aa,V)}return aa};E.matches=function(P,Q){return E(P,null,null,Q)};E.find=function(V,S){var W,Q;if(!V){return[]}for(var R=0,P=G.order.length;R<P;R++){var T=G.order[R],Q;if((Q=G.match[T].exec(V))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){Q[1]=(Q[1]||"").replace(/\\/g,"");W=G.find[T](Q,S);if(W!=null){V=V.replace(G.match[T],"");break}}}}if(!W){W=S.getElementsByTagName("*")}return{set:W,expr:V}};E.filter=function(S,ac,ad,T){var Q=S,Y=[],ah=ac,V,ab;while(S&&ac.length){for(var U in G.filter){if((V=G.match[U].exec(S))!=null){var Z=G.filter[U],R=null,X=0,aa,ag;ab=false;if(ah==Y){Y=[]}if(G.preFilter[U]){V=G.preFilter[U](V,ah,ad,Y,T);if(!V){ab=aa=true}else{if(V===true){continue}else{if(V[0]===true){R=[];var W=null,af;for(var ae=0;(af=ah[ae])!==g;ae++){if(af&&W!==af){R.push(af);W=af}}}}}}if(V){for(var ae=0;(ag=ah[ae])!==g;ae++){if(ag){if(R&&ag!=R[X]){X++}aa=Z(ag,V,X,R);var P=T^!!aa;if(ad&&aa!=null){if(P){ab=true}else{ah[ae]=false}}else{if(P){Y.push(ag);ab=true}}}}}if(aa!==g){if(!ad){ah=Y}S=S.replace(G.match[U],"");if(!ab){return[]}break}}}S=S.replace(/\s*,\s*/,"");if(S==Q){if(ab==null){throw"Syntax error, unrecognized expression: "+S}else{break}}Q=S}return ah};var G=E.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(P){return P.getAttribute("href")}},relative:{"+":function(T,Q){for(var R=0,P=T.length;R<P;R++){var S=T[R];if(S){var U=S.previousSibling;while(U&&U.nodeType!==1){U=U.previousSibling}T[R]=typeof Q==="string"?U||false:U===Q}}if(typeof Q==="string"){E.filter(Q,T,true)}},">":function(U,Q,V){if(typeof Q==="string"&&!/\W/.test(Q)){Q=V?Q:Q.toUpperCase();for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){var S=T.parentNode;U[R]=S.nodeName===Q?S:false}}}else{for(var R=0,P=U.length;R<P;R++){var T=U[R];if(T){U[R]=typeof Q==="string"?T.parentNode:T.parentNode===Q}}if(typeof Q==="string"){E.filter(Q,U,true)}}},"":function(S,Q,U){var R="done"+(I++),P=O;if(!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("parentNode",Q,R,S,T,U)},"~":function(S,Q,U){var R="done"+(I++),P=O;if(typeof Q==="string"&&!Q.match(/\W/)){var T=Q=U?Q:Q.toUpperCase();P=L}P("previousSibling",Q,R,S,T,U)}},find:{ID:function(Q,R){if(R.getElementById){var P=R.getElementById(Q[1]);return P?[P]:[]}},NAME:function(P,Q){return Q.getElementsByName?Q.getElementsByName(P[1]):null},TAG:function(P,Q){return Q.getElementsByTagName(P[1])}},preFilter:{CLASS:function(S,Q,R,P,U){S=" "+S[1].replace(/\\/g,"")+" ";for(var T=0;Q[T];T++){if(U^(" "+Q[T].className+" ").indexOf(S)>=0){if(!R){P.push(Q[T])}}else{if(R){Q[T]=false}}}return false},ID:function(P){return P[1].replace(/\\/g,"")},TAG:function(Q,P){for(var R=0;!P[R];R++){}return M(P[R])?Q[1]:Q[1].toUpperCase()},CHILD:function(P){if(P[1]=="nth"){var Q=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(P[2]=="even"&&"2n"||P[2]=="odd"&&"2n+1"||!/\D/.test(P[2])&&"0n+"+P[2]||P[2]);P[2]=(Q[1]+(Q[2]||1))-0;P[3]=Q[3]-0}P[0]="done"+(I++);return P},ATTR:function(Q){var P=Q[1];if(G.attrMap[P]){Q[1]=G.attrMap[P]}if(Q[2]==="~="){Q[4]=" "+Q[4]+" "}return Q},PSEUDO:function(T,Q,R,P,U){if(T[1]==="not"){if(T[3].match(N).length>1){T[3]=E(T[3],null,null,Q)}else{var S=E.filter(T[3],Q,R,true^U);if(!R){P.push.apply(P,S)}return false}}else{if(G.match.POS.test(T[0])){return true}}return T},POS:function(P){P.unshift(true);return P}},filters:{enabled:function(P){return P.disabled===false&&P.type!=="hidden"},disabled:function(P){return P.disabled===true},checked:function(P){return P.checked===true},selected:function(P){P.parentNode.selectedIndex;return P.selected===true},parent:function(P){return !!P.firstChild},empty:function(P){return !P.firstChild},has:function(R,Q,P){return !!E(P[3],R).length},header:function(P){return/h\d/i.test(P.nodeName)},text:function(P){return"text"===P.type},radio:function(P){return"radio"===P.type},checkbox:function(P){return"checkbox"===P.type},file:function(P){return"file"===P.type},password:function(P){return"password"===P.type},submit:function(P){return"submit"===P.type},image:function(P){return"image"===P.type},reset:function(P){return"reset"===P.type},button:function(P){return"button"===P.type||P.nodeName.toUpperCase()==="BUTTON"},input:function(P){return/input|select|textarea|button/i.test(P.nodeName)}},setFilters:{first:function(Q,P){return P===0},last:function(R,Q,P,S){return Q===S.length-1},even:function(Q,P){return P%2===0},odd:function(Q,P){return P%2===1},lt:function(R,Q,P){return Q<P[3]-0},gt:function(R,Q,P){return Q>P[3]-0},nth:function(R,Q,P){return P[3]-0==Q},eq:function(R,Q,P){return P[3]-0==Q}},filter:{CHILD:function(P,S){var V=S[1],W=P.parentNode;var U="child"+W.childNodes.length;if(W&&(!W[U]||!P.nodeIndex)){var T=1;for(var Q=W.firstChild;Q;Q=Q.nextSibling){if(Q.nodeType==1){Q.nodeIndex=T++}}W[U]=T-1}if(V=="first"){return P.nodeIndex==1}else{if(V=="last"){return P.nodeIndex==W[U]}else{if(V=="only"){return W[U]==1}else{if(V=="nth"){var Y=false,R=S[2],X=S[3];if(R==1&&X==0){return true}if(R==0){if(P.nodeIndex==X){Y=true}}else{if((P.nodeIndex-X)%R==0&&(P.nodeIndex-X)/R>=0){Y=true}}return Y}}}}},PSEUDO:function(V,R,S,W){var Q=R[1],T=G.filters[Q];if(T){return T(V,S,R,W)}else{if(Q==="contains"){return(V.textContent||V.innerText||"").indexOf(R[3])>=0}else{if(Q==="not"){var U=R[3];for(var S=0,P=U.length;S<P;S++){if(U[S]===V){return false}}return true}}}},ID:function(Q,P){return Q.nodeType===1&&Q.getAttribute("id")===P},TAG:function(Q,P){return(P==="*"&&Q.nodeType===1)||Q.nodeName===P},CLASS:function(Q,P){return P.test(Q.className)},ATTR:function(T,R){var P=G.attrHandle[R[1]]?G.attrHandle[R[1]](T):T[R[1]]||T.getAttribute(R[1]),U=P+"",S=R[2],Q=R[4];return P==null?false:S==="="?U===Q:S==="*="?U.indexOf(Q)>=0:S==="~="?(" "+U+" ").indexOf(Q)>=0:!R[4]?P:S==="!="?U!=Q:S==="^="?U.indexOf(Q)===0:S==="$="?U.substr(U.length-Q.length)===Q:S==="|="?U===Q||U.substr(0,Q.length+1)===Q+"-":false},POS:function(T,Q,R,U){var P=Q[2],S=G.setFilters[P];if(S){return S(T,R,Q,U)}}}};for(var K in G.match){G.match[K]=RegExp(G.match[K].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var D=function(Q,P){Q=Array.prototype.slice.call(Q);if(P){P.push.apply(P,Q);return P}return Q};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(J){D=function(T,S){var Q=S||[];if(F.call(T)==="[object Array]"){Array.prototype.push.apply(Q,T)}else{if(typeof T.length==="number"){for(var R=0,P=T.length;R<P;R++){Q.push(T[R])}}else{for(var R=0;T[R];R++){Q.push(T[R])}}}return Q}}(function(){var Q=document.createElement("form"),R="script"+(new Date).getTime();Q.innerHTML="<input name='"+R+"'/>";var P=document.documentElement;P.insertBefore(Q,P.firstChild);if(!!document.getElementById(R)){G.find.ID=function(T,U){if(U.getElementById){var S=U.getElementById(T[1]);return S?S.id===T[1]||S.getAttributeNode&&S.getAttributeNode("id").nodeValue===T[1]?[S]:g:[]}};G.filter.ID=function(U,S){var T=U.getAttributeNode&&U.getAttributeNode("id");return U.nodeType===1&&T&&T.nodeValue===S}}P.removeChild(Q)})();(function(){var P=document.createElement("div");P.appendChild(document.createComment(""));if(P.getElementsByTagName("*").length>0){G.find.TAG=function(Q,U){var T=U.getElementsByTagName(Q[1]);if(Q[1]==="*"){var S=[];for(var R=0;T[R];R++){if(T[R].nodeType===1){S.push(T[R])}}T=S}return T}}P.innerHTML="<a href='#'></a>";if(P.firstChild.getAttribute("href")!=="#"){G.attrHandle.href=function(Q){return Q.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var P=E;E=function(T,S,Q,R){S=S||document;if(!R&&S.nodeType===9){try{return D(S.querySelectorAll(T),Q)}catch(U){}}return P(T,S,Q,R)};E.find=P.find;E.filter=P.filter;E.selectors=P.selectors;E.matches=P.matches})()}if(document.documentElement.getElementsByClassName){G.order.splice(1,0,"CLASS");G.find.CLASS=function(P,Q){return Q.getElementsByClassName(P[1])}}function L(Q,W,V,Z,X,Y){for(var T=0,R=Z.length;T<R;T++){var P=Z[T];if(P){P=P[Q];var U=false;while(P&&P.nodeType){var S=P[V];if(S){U=Z[S];break}if(P.nodeType===1&&!Y){P[V]=T}if(P.nodeName===W){U=P;break}P=P[Q]}Z[T]=U}}}function O(Q,V,U,Y,W,X){for(var S=0,R=Y.length;S<R;S++){var P=Y[S];if(P){P=P[Q];var T=false;while(P&&P.nodeType){if(P[U]){T=Y[P[U]];break}if(P.nodeType===1){if(!X){P[U]=S}if(typeof V!=="string"){if(P===V){T=true;break}}else{if(E.filter(V,[P]).length>0){T=P;break}}}P=P[Q]}Y[S]=T}}}var H=document.compareDocumentPosition?function(Q,P){return Q.compareDocumentPosition(P)&16}:function(Q,P){return Q!==P&&(Q.contains?Q.contains(P):true)};var M=function(P){return P.documentElement&&!P.body||P.tagName&&P.ownerDocument&&!P.ownerDocument.body};n.find=E;n.filter=E.filter;n.expr=E.selectors;n.expr[":"]=n.expr.filters;E.selectors.filters.hidden=function(P){return"hidden"===P.type||n.css(P,"display")==="none"||n.css(P,"visibility")==="hidden"};E.selectors.filters.visible=function(P){return"hidden"!==P.type&&n.css(P,"display")!=="none"&&n.css(P,"visibility")!=="hidden"};E.selectors.filters.animated=function(P){return n.grep(n.timers,function(Q){return P===Q.elem}).length};n.multiFilter=function(R,P,Q){if(Q){R=":not("+R+")"}return E.matches(R,P)};n.dir=function(R,Q){var P=[],S=R[Q];while(S&&S!=document){if(S.nodeType==1){P.push(S)}S=S[Q]}return P};n.nth=function(T,P,R,S){P=P||1;var Q=0;for(;T;T=T[R]){if(T.nodeType==1&&++Q==P){break}}return T};n.sibling=function(R,Q){var P=[];for(;R;R=R.nextSibling){if(R.nodeType==1&&R!=Q){P.push(R)}}return P};return;l.Sizzle=E})();n.event={add:function(H,E,G,J){if(H.nodeType==3||H.nodeType==8){return}if(H.setInterval&&H!=l){H=l}if(!G.guid){G.guid=this.guid++}if(J!==g){var F=G;G=this.proxy(F);G.data=J}var D=n.data(H,"events")||n.data(H,"events",{}),I=n.data(H,"handle")||n.data(H,"handle",function(){return typeof n!=="undefined"&&!n.event.triggered?n.event.handle.apply(arguments.callee.elem,arguments):g});I.elem=H;n.each(E.split(/\s+/),function(L,M){var N=M.split(".");M=N.shift();G.type=N.slice().sort().join(".");var K=D[M];if(n.event.specialAll[M]){n.event.specialAll[M].setup.call(H,J,N)}if(!K){K=D[M]={};if(!n.event.special[M]||n.event.special[M].setup.call(H,J,N)===false){if(H.addEventListener){H.addEventListener(M,I,false)}else{if(H.attachEvent){H.attachEvent("on"+M,I)}}}}K[G.guid]=G;n.event.global[M]=true});H=null},guid:1,global:{},remove:function(J,G,I){if(J.nodeType==3||J.nodeType==8){return}var F=n.data(J,"events"),E,D;if(F){if(G===g||(typeof G==="string"&&G.charAt(0)==".")){for(var H in F){this.remove(J,H+(G||""))}}else{if(G.type){I=G.handler;G=G.type}n.each(G.split(/\s+/),function(L,N){var P=N.split(".");N=P.shift();var M=RegExp("(^|\\.)"+P.slice().sort().join(".*\\.")+"(\\.|$)");if(F[N]){if(I){delete F[N][I.guid]}else{for(var O in F[N]){if(M.test(F[N][O].type)){delete F[N][O]}}}if(n.event.specialAll[N]){n.event.specialAll[N].teardown.call(J,P)}for(E in F[N]){break}if(!E){if(!n.event.special[N]||n.event.special[N].teardown.call(J,P)===false){if(J.removeEventListener){J.removeEventListener(N,n.data(J,"handle"),false)}else{if(J.detachEvent){J.detachEvent("on"+N,n.data(J,"handle"))}}}E=null;delete F[N]}}})}for(E in F){break}if(!E){var K=n.data(J,"handle");if(K){K.elem=null}n.removeData(J,"events");n.removeData(J,"handle")}}},trigger:function(H,J,G,D){var F=H.type||H;if(!D){H=typeof H==="object"?H[h]?H:n.extend(n.Event(F),H):n.Event(F);if(F.indexOf("!")>=0){H.type=F=F.slice(0,-1);H.exclusive=true}if(!G){H.stopPropagation();if(this.global[F]){n.each(n.cache,function(){if(this.events&&this.events[F]){n.event.trigger(H,J,this.handle.elem)}})}}if(!G||G.nodeType==3||G.nodeType==8){return g}H.result=g;H.target=G;J=n.makeArray(J);J.unshift(H)}H.currentTarget=G;var I=n.data(G,"handle");if(I){I.apply(G,J)}if((!G[F]||(n.nodeName(G,"a")&&F=="click"))&&G["on"+F]&&G["on"+F].apply(G,J)===false){H.result=false}if(!D&&G[F]&&!H.isDefaultPrevented()&&!(n.nodeName(G,"a")&&F=="click")){this.triggered=true;try{G[F]()}catch(K){}}this.triggered=false;if(!H.isPropagationStopped()){var E=G.parentNode||G.ownerDocument;if(E){n.event.trigger(H,J,E,true)}}},handle:function(J){var I,D;J=arguments[0]=n.event.fix(J||l.event);var K=J.type.split(".");J.type=K.shift();I=!K.length&&!J.exclusive;var H=RegExp("(^|\\.)"+K.slice().sort().join(".*\\.")+"(\\.|$)");D=(n.data(this,"events")||{})[J.type];for(var F in D){var G=D[F];if(I||H.test(G.type)){J.handler=G;J.data=G.data;var E=G.apply(this,arguments);if(E!==g){J.result=E;if(E===false){J.preventDefault();J.stopPropagation()}}if(J.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(G){if(G[h]){return G}var E=G;G=n.Event(E);for(var F=this.props.length,I;F;){I=this.props[--F];G[I]=E[I]}if(!G.target){G.target=G.srcElement||document}if(G.target.nodeType==3){G.target=G.target.parentNode}if(!G.relatedTarget&&G.fromElement){G.relatedTarget=G.fromElement==G.target?G.toElement:G.fromElement}if(G.pageX==null&&G.clientX!=null){var H=document.documentElement,D=document.body;G.pageX=G.clientX+(H&&H.scrollLeft||D&&D.scrollLeft||0)-(H.clientLeft||0);G.pageY=G.clientY+(H&&H.scrollTop||D&&D.scrollTop||0)-(H.clientTop||0)}if(!G.which&&((G.charCode||G.charCode===0)?G.charCode:G.keyCode)){G.which=G.charCode||G.keyCode}if(!G.metaKey&&G.ctrlKey){G.metaKey=G.ctrlKey}if(!G.which&&G.button){G.which=(G.button&1?1:(G.button&2?3:(G.button&4?2:0)))}return G},proxy:function(E,D){D=D||function(){return E.apply(this,arguments)};D.guid=E.guid=E.guid||D.guid||this.guid++;return D},special:{ready:{setup:A,teardown:function(){}}},specialAll:{live:{setup:function(D,E){n.event.add(this,E[0],c)},teardown:function(F){if(F.length){var D=0,E=RegExp("(^|\\.)"+F[0]+"(\\.|$)");n.each((n.data(this,"events").live||{}),function(){if(E.test(this.type)){D++}});if(D<1){n.event.remove(this,F[0],c)}}}}}};n.Event=function(D){if(!this.preventDefault){return new n.Event(D)}if(D&&D.type){this.originalEvent=D;this.type=D.type;this.timeStamp=D.timeStamp}else{this.type=D}if(!this.timeStamp){this.timeStamp=e()}this[h]=true};function k(){return false}function t(){return true}n.Event.prototype={preventDefault:function(){this.isDefaultPrevented=t;var D=this.originalEvent;if(!D){return}if(D.preventDefault){D.preventDefault()}D.returnValue=false},stopPropagation:function(){this.isPropagationStopped=t;var D=this.originalEvent;if(!D){return}if(D.stopPropagation){D.stopPropagation()}D.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=t;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(E){var D=E.relatedTarget;while(D&&D!=this){try{D=D.parentNode}catch(F){D=this}}if(D!=this){E.type=E.data;n.event.handle.apply(this,arguments)}};n.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(E,D){n.event.special[D]={setup:function(){n.event.add(this,E,a,D)},teardown:function(){n.event.remove(this,E,a)}}});n.fn.extend({bind:function(E,F,D){return E=="unload"?this.one(E,F,D):this.each(function(){n.event.add(this,E,D||F,D&&F)})},one:function(F,G,E){var D=n.event.proxy(E||G,function(H){n(this).unbind(H,D);return(E||G).apply(this,arguments)});return this.each(function(){n.event.add(this,F,D,E&&G)})},unbind:function(E,D){return this.each(function(){n.event.remove(this,E,D)})},trigger:function(D,E){return this.each(function(){n.event.trigger(D,E,this)})},triggerHandler:function(D,F){if(this[0]){var E=n.Event(D);E.preventDefault();E.stopPropagation();n.event.trigger(E,F,this[0]);return E.result}},toggle:function(F){var D=arguments,E=1;while(E<D.length){n.event.proxy(F,D[E++])}return this.click(n.event.proxy(F,function(G){this.lastToggle=(this.lastToggle||0)%E;G.preventDefault();return D[this.lastToggle++].apply(this,arguments)||false}))},hover:function(D,E){return this.mouseenter(D).mouseleave(E)},ready:function(D){A();if(n.isReady){D.call(document,n)}else{n.readyList.push(D)}return this},live:function(F,E){var D=n.event.proxy(E);D.guid+=this.selector+F;n(document).bind(i(F,this.selector),this.selector,D);return this},die:function(E,D){n(document).unbind(i(E,this.selector),D?{guid:D.guid+this.selector+E}:null);return this}});function c(G){var D=RegExp("(^|\\.)"+G.type+"(\\.|$)"),F=true,E=[];n.each(n.data(this,"events").live||[],function(H,I){if(D.test(I.type)){var J=n(G.target).closest(I.data)[0];if(J){E.push({elem:J,fn:I})}}});n.each(E,function(){if(!G.isImmediatePropagationStopped()&&this.fn.call(this.elem,G,this.fn.data)===false){F=false}});return F}function i(E,D){return["live",E,D.replace(/\./g,"`").replace(/ /g,"|")].join(".")}n.extend({isReady:false,readyList:[],ready:function(){if(!n.isReady){n.isReady=true;if(n.readyList){n.each(n.readyList,function(){this.call(document,n)});n.readyList=null}n(document).triggerHandler("ready")}}});var w=false;function A(){if(w){return}w=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);n.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);n.ready()}});if(document.documentElement.doScroll&&!l.frameElement){(function(){if(n.isReady){return}try{document.documentElement.doScroll("left")}catch(D){setTimeout(arguments.callee,0);return}n.ready()})()}}}n.event.add(l,"load",n.ready)}n.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(E,D){n.fn[D]=function(F){return F?this.bind(D,F):this.trigger(D)}});n(l).bind("unload",function(){for(var D in n.cache){if(D!=1&&n.cache[D].handle){n.event.remove(n.cache[D].handle.elem)}}});(function(){n.support={};var E=document.documentElement,F=document.createElement("script"),J=document.createElement("div"),I="script"+(new Date).getTime();J.style.display="none";J.innerHTML=' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var G=J.getElementsByTagName("*"),D=J.getElementsByTagName("a")[0];if(!G||!G.length||!D){return}n.support={leadingWhitespace:J.firstChild.nodeType==3,tbody:!J.getElementsByTagName("tbody").length,objectAll:!!J.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!J.getElementsByTagName("link").length,style:/red/.test(D.getAttribute("style")),hrefNormalized:D.getAttribute("href")==="/a",opacity:D.style.opacity==="0.5",cssFloat:!!D.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};F.type="text/javascript";try{F.appendChild(document.createTextNode("window."+I+"=1;"))}catch(H){}E.insertBefore(F,E.firstChild);if(l[I]){n.support.scriptEval=true;delete l[I]}E.removeChild(F);if(J.attachEvent&&J.fireEvent){J.attachEvent("onclick",function(){n.support.noCloneEvent=false;J.detachEvent("onclick",arguments.callee)});J.cloneNode(true).fireEvent("onclick")}n(function(){var K=document.createElement("div");K.style.width="1px";K.style.paddingLeft="1px";document.body.appendChild(K);n.boxModel=n.support.boxModel=K.offsetWidth===2;document.body.removeChild(K)})})();var v=n.support.cssFloat?"cssFloat":"styleFloat";n.props={"for":"htmlFor","class":"className","float":v,cssFloat:v,styleFloat:v,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};n.fn.extend({_load:n.fn.load,load:function(F,I,J){if(typeof F!=="string"){return this._load(F)}var H=F.indexOf(" ");if(H>=0){var D=F.slice(H,F.length);F=F.slice(0,H)}var G="GET";if(I){if(n.isFunction(I)){J=I;I=null}else{if(typeof I==="object"){I=n.param(I);G="POST"}}}var E=this;n.ajax({url:F,type:G,dataType:"html",data:I,complete:function(L,K){if(K=="success"||K=="notmodified"){E.html(D?n("<div/>").append(L.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(D):L.responseText)}if(J){E.each(J,[L.responseText,K,L])}}});return this},serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?n.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(D,E){var F=n(this).val();return F==null?null:n.isArray(F)?n.map(F,function(H,G){return{name:E.name,value:H}}):{name:E.name,value:F}}).get()}});n.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(D,E){n.fn[E]=function(F){return this.bind(E,F)}});var q=e();n.extend({get:function(D,F,G,E){if(n.isFunction(F)){G=F;F=null}return n.ajax({type:"GET",url:D,data:F,success:G,dataType:E})},getScript:function(D,E){return n.get(D,null,E,"script")},getJSON:function(D,E,F){return n.get(D,E,F,"json")},post:function(D,F,G,E){if(n.isFunction(F)){G=F;F={}}return n.ajax({type:"POST",url:D,data:F,success:G,dataType:E})},ajaxSetup:function(D){n.extend(n.ajaxSettings,D)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(L){L=n.extend(true,L,n.extend(true,{},n.ajaxSettings,L));var V,E=/=\?(&|$)/g,Q,U,F=L.type.toUpperCase();if(L.data&&L.processData&&typeof L.data!=="string"){L.data=n.param(L.data)}if(L.dataType=="jsonp"){if(F=="GET"){if(!L.url.match(E)){L.url+=(L.url.match(/\?/)?"&":"?")+(L.jsonp||"callback")+"=?"}}else{if(!L.data||!L.data.match(E)){L.data=(L.data?L.data+"&":"")+(L.jsonp||"callback")+"=?"}}L.dataType="json"}if(L.dataType=="json"&&(L.data&&L.data.match(E)||L.url.match(E))){V="jsonp"+q++;if(L.data){L.data=(L.data+"").replace(E,"="+V+"$1")}L.url=L.url.replace(E,"="+V+"$1");L.dataType="script";l[V]=function(W){U=W;H();K();l[V]=g;try{delete l[V]}catch(X){}if(G){G.removeChild(S)}}}if(L.dataType=="script"&&L.cache==null){L.cache=false}if(L.cache===false&&F=="GET"){var D=e();var T=L.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+D+"$2");L.url=T+((T==L.url)?(L.url.match(/\?/)?"&":"?")+"_="+D:"")}if(L.data&&F=="GET"){L.url+=(L.url.match(/\?/)?"&":"?")+L.data;L.data=null}if(L.global&&!n.active++){n.event.trigger("ajaxStart")}var P=/^(\w+:)?\/\/([^\/?#]+)/.exec(L.url);if(L.dataType=="script"&&F=="GET"&&P&&(P[1]&&P[1]!=location.protocol||P[2]!=location.host)){var G=document.getElementsByTagName("head")[0];var S=document.createElement("script");S.src=L.url;if(L.scriptCharset){S.charset=L.scriptCharset}if(!V){var N=false;S.onload=S.onreadystatechange=function(){if(!N&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){N=true;H();K();G.removeChild(S)}}}G.appendChild(S);return g}var J=false;var I=L.xhr();if(L.username){I.open(F,L.url,L.async,L.username,L.password)}else{I.open(F,L.url,L.async)}try{if(L.data){I.setRequestHeader("Content-Type",L.contentType)}if(L.ifModified){I.setRequestHeader("If-Modified-Since",n.lastModified[L.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}I.setRequestHeader("X-Requested-With","XMLHttpRequest");I.setRequestHeader("Accept",L.dataType&&L.accepts[L.dataType]?L.accepts[L.dataType]+", */*":L.accepts._default)}catch(R){}if(L.beforeSend&&L.beforeSend(I,L)===false){if(L.global&&!--n.active){n.event.trigger("ajaxStop")}I.abort();return false}if(L.global){n.event.trigger("ajaxSend",[I,L])}var M=function(W){if(I.readyState==0){if(O){clearInterval(O);O=null;if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}}else{if(!J&&I&&(I.readyState==4||W=="timeout")){J=true;if(O){clearInterval(O);O=null}Q=W=="timeout"?"timeout":!n.httpSuccess(I)?"error":L.ifModified&&n.httpNotModified(I,L.url)?"notmodified":"success";if(Q=="success"){try{U=n.httpData(I,L.dataType,L)}catch(Y){Q="parsererror"}}if(Q=="success"){var X;try{X=I.getResponseHeader("Last-Modified")}catch(Y){}if(L.ifModified&&X){n.lastModified[L.url]=X}if(!V){H()}}else{n.handleError(L,I,Q)}K();if(L.async){I=null}}}};if(L.async){var O=setInterval(M,13);if(L.timeout>0){setTimeout(function(){if(I){if(!J){M("timeout")}if(I){I.abort()}}},L.timeout)}}try{I.send(L.data)}catch(R){n.handleError(L,I,null,R)}if(!L.async){M()}function H(){if(L.success){L.success(U,Q)}if(L.global){n.event.trigger("ajaxSuccess",[I,L])}}function K(){if(L.complete){L.complete(I,Q)}if(L.global){n.event.trigger("ajaxComplete",[I,L])}if(L.global&&!--n.active){n.event.trigger("ajaxStop")}}return I},handleError:function(E,G,D,F){if(E.error){E.error(G,D,F)}if(E.global){n.event.trigger("ajaxError",[G,E,F])}},active:0,httpSuccess:function(E){try{return !E.status&&location.protocol=="file:"||(E.status>=200&&E.status<300)||E.status==304||E.status==1223}catch(D){}return false},httpNotModified:function(F,D){try{var G=F.getResponseHeader("Last-Modified");return F.status==304||G==n.lastModified[D]}catch(E){}return false},httpData:function(I,G,F){var E=I.getResponseHeader("content-type"),D=G=="xml"||!G&&E&&E.indexOf("xml")>=0,H=D?I.responseXML:I.responseText;if(D&&H.documentElement.tagName=="parsererror"){throw"parsererror"}if(F&&F.dataFilter){H=F.dataFilter(H,G)}if(typeof H==="string"){if(G=="script"){n.globalEval(H)}if(G=="json"){H=l["eval"]("("+H+")")}}return H},param:function(D){var F=[];function G(H,I){F[F.length]=encodeURIComponent(H)+"="+encodeURIComponent(I)}if(n.isArray(D)||D.jquery){n.each(D,function(){G(this.name,this.value)})}else{for(var E in D){if(n.isArray(D[E])){n.each(D[E],function(){G(E,this)})}else{G(E,n.isFunction(D[E])?D[E]():D[E])}}}return F.join("&").replace(/%20/g,"+")}});var m={},d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function s(E,D){var F={};n.each(d.concat.apply([],d.slice(0,D)),function(){F[this]=E});return F}n.fn.extend({show:function(I,K){if(I){return this.animate(s("show",3),I,K)}else{for(var G=0,E=this.length;G<E;G++){var D=n.data(this[G],"olddisplay");this[G].style.display=D||"";if(n.css(this[G],"display")==="none"){var F=this[G].tagName,J;if(m[F]){J=m[F]}else{var H=n("<"+F+" />").appendTo("body");J=H.css("display");if(J==="none"){J="block"}H.remove();m[F]=J}this[G].style.display=n.data(this[G],"olddisplay",J)}}return this}},hide:function(G,H){if(G){return this.animate(s("hide",3),G,H)}else{for(var F=0,E=this.length;F<E;F++){var D=n.data(this[F],"olddisplay");if(!D&&D!=="none"){n.data(this[F],"olddisplay",n.css(this[F],"display"))}this[F].style.display="none"}return this}},_toggle:n.fn.toggle,toggle:function(F,E){var D=typeof F==="boolean";return n.isFunction(F)&&n.isFunction(E)?this._toggle.apply(this,arguments):F==null||D?this.each(function(){var G=D?F:n(this).is(":hidden");n(this)[G?"show":"hide"]()}):this.animate(s("toggle",3),F,E)},fadeTo:function(D,F,E){return this.animate({opacity:F},D,E)},animate:function(H,E,G,F){var D=n.speed(E,G,F);return this[D.queue===false?"each":"queue"](function(){var J=n.extend({},D),L,K=this.nodeType==1&&n(this).is(":hidden"),I=this;for(L in H){if(H[L]=="hide"&&K||H[L]=="show"&&!K){return J.complete.call(this)}if((L=="height"||L=="width")&&this.style){J.display=n.css(this,"display");J.overflow=this.style.overflow}}if(J.overflow!=null){this.style.overflow="hidden"}J.curAnim=n.extend({},H);n.each(H,function(N,R){var Q=new n.fx(I,J,N);if(/toggle|show|hide/.test(R)){Q[R=="toggle"?K?"show":"hide":R](H)}else{var P=R.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),S=Q.cur(true)||0;if(P){var M=parseFloat(P[2]),O=P[3]||"px";if(O!="px"){I.style[N]=(M||1)+O;S=((M||1)/Q.cur(true))*S;I.style[N]=S+O}if(P[1]){M=((P[1]=="-="?-1:1)*M)+S}Q.custom(S,M,O)}else{Q.custom(S,R,"")}}});return true})},stop:function(E,D){var F=n.timers;if(E){this.queue([])}this.each(function(){for(var G=F.length-1;G>=0;G--){if(F[G].elem==this){if(D){F[G](true)}F.splice(G,1)}}});if(!D){this.dequeue()}return this}});n.each({slideDown:s("show",1),slideUp:s("hide",1),slideToggle:s("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(D,E){n.fn[D]=function(F,G){return this.animate(E,F,G)}});n.extend({speed:function(F,G,E){var D=typeof F==="object"?F:{complete:E||!E&&G||n.isFunction(F)&&F,duration:F,easing:E&&G||G&&!n.isFunction(G)&&G};D.duration=n.fx.off?0:typeof D.duration==="number"?D.duration:n.fx.speeds[D.duration]||n.fx.speeds._default;D.old=D.complete;D.complete=function(){if(D.queue!==false){n(this).dequeue()}if(n.isFunction(D.old)){D.old.call(this)}};return D},easing:{linear:function(F,G,D,E){return D+E*F},swing:function(F,G,D,E){return((-Math.cos(F*Math.PI)/2)+0.5)*E+D}},timers:[],timerId:null,fx:function(E,D,F){this.options=D;this.elem=E;this.prop=F;if(!D.orig){D.orig={}}}});n.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(n.fx.step[this.prop]||n.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(E){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var D=parseFloat(n.css(this.elem,this.prop,E));return D&&D>-10000?D:parseFloat(n.curCSS(this.elem,this.prop))||0},custom:function(H,G,F){this.startTime=e();this.start=H;this.end=G;this.unit=F||this.unit||"px";this.now=this.start;this.pos=this.state=0;var D=this;function E(I){return D.step(I)}E.elem=this.elem;n.timers.push(E);if(E()&&n.timerId==null){n.timerId=setInterval(function(){var J=n.timers;for(var I=0;I<J.length;I++){if(!J[I]()){J.splice(I--,1)}}if(!J.length){clearInterval(n.timerId);n.timerId=null}},13)}},show:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());n(this.elem).show()},hide:function(){this.options.orig[this.prop]=n.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(G){var F=e();if(G||F>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var D=true;for(var E in this.options.curAnim){if(this.options.curAnim[E]!==true){D=false}}if(D){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(n.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){n(this.elem).hide()}if(this.options.hide||this.options.show){for(var H in this.options.curAnim){n.attr(this.elem.style,H,this.options.orig[H])}}}if(D){this.options.complete.call(this.elem)}return false}else{var I=F-this.startTime;this.state=I/this.options.duration;this.pos=n.easing[this.options.easing||(n.easing.swing?"swing":"linear")](this.state,I,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};n.extend(n.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(D){n.attr(D.elem.style,"opacity",D.now)},_default:function(D){if(D.elem.style&&D.elem.style[D.prop]!=null){D.elem.style[D.prop]=D.now+D.unit}else{D.elem[D.prop]=D.now}}}});if(document.documentElement.getBoundingClientRect){n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}var F=this[0].getBoundingClientRect(),I=this[0].ownerDocument,E=I.body,D=I.documentElement,K=D.clientTop||E.clientTop||0,J=D.clientLeft||E.clientLeft||0,H=F.top+(self.pageYOffset||n.boxModel&&D.scrollTop||E.scrollTop)-K,G=F.left+(self.pageXOffset||n.boxModel&&D.scrollLeft||E.scrollLeft)-J;return{top:H,left:G}}}else{n.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return n.offset.bodyOffset(this[0])}n.offset.initialized||n.offset.initialize();var I=this[0],F=I.offsetParent,E=I,N=I.ownerDocument,L,G=N.documentElement,J=N.body,K=N.defaultView,D=K.getComputedStyle(I,null),M=I.offsetTop,H=I.offsetLeft;while((I=I.parentNode)&&I!==J&&I!==G){L=K.getComputedStyle(I,null);M-=I.scrollTop,H-=I.scrollLeft;if(I===F){M+=I.offsetTop,H+=I.offsetLeft;if(n.offset.doesNotAddBorder&&!(n.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(I.tagName))){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}E=F,F=I.offsetParent}if(n.offset.subtractsBorderForOverflowNotVisible&&L.overflow!=="visible"){M+=parseInt(L.borderTopWidth,10)||0,H+=parseInt(L.borderLeftWidth,10)||0}D=L}if(D.position==="relative"||D.position==="static"){M+=J.offsetTop,H+=J.offsetLeft}if(D.position==="fixed"){M+=Math.max(G.scrollTop,J.scrollTop),H+=Math.max(G.scrollLeft,J.scrollLeft)}return{top:M,left:H}}}n.offset={initialize:function(){if(this.initialized){return}var K=document.body,E=document.createElement("div"),G,F,M,H,L,D,I=K.style.marginTop,J='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"cellpadding="0"cellspacing="0"><tr><td></td></tr></table>';L={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(D in L){E.style[D]=L[D]}E.innerHTML=J;K.insertBefore(E,K.firstChild);G=E.firstChild,F=G.firstChild,H=G.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(F.offsetTop!==5);this.doesAddBorderForTableAndCells=(H.offsetTop===5);G.style.overflow="hidden",G.style.position="relative";this.subtractsBorderForOverflowNotVisible=(F.offsetTop===-5);K.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(K.offsetTop===0);K.style.marginTop=I;K.removeChild(E);this.initialized=true},bodyOffset:function(D){n.offset.initialized||n.offset.initialize();var F=D.offsetTop,E=D.offsetLeft;if(n.offset.doesNotIncludeMarginInBodyOffset){F+=parseInt(n.curCSS(D,"marginTop",true),10)||0,E+=parseInt(n.curCSS(D,"marginLeft",true),10)||0}return{top:F,left:E}}};n.fn.extend({position:function(){var H=0,G=0,E;if(this[0]){var F=this.offsetParent(),I=this.offset(),D=/^body|html$/i.test(F[0].tagName)?{top:0,left:0}:F.offset();I.top-=j(this,"marginTop");I.left-=j(this,"marginLeft");D.top+=j(F,"borderTopWidth");D.left+=j(F,"borderLeftWidth");E={top:I.top-D.top,left:I.left-D.left}}return E},offsetParent:function(){var D=this[0].offsetParent||document.body;while(D&&(!/^body|html$/i.test(D.tagName)&&n.css(D,"position")=="static")){D=D.offsetParent}return n(D)}});n.each(["Left","Top"],function(E,D){var F="scroll"+D;n.fn[F]=function(G){if(!this[0]){return null}return G!==g?this.each(function(){this==l||this==document?l.scrollTo(!E?G:n(l).scrollLeft(),E?G:n(l).scrollTop()):this[F]=G}):this[0]==l||this[0]==document?self[E?"pageYOffset":"pageXOffset"]||n.boxModel&&document.documentElement[F]||document.body[F]:this[0][F]}});n.each(["Height","Width"],function(G,E){var D=G?"Left":"Top",F=G?"Right":"Bottom";n.fn["inner"+E]=function(){return this[E.toLowerCase()]()+j(this,"padding"+D)+j(this,"padding"+F)};n.fn["outer"+E]=function(I){return this["inner"+E]()+j(this,"border"+D+"Width")+j(this,"border"+F+"Width")+(I?j(this,"margin"+D)+j(this,"margin"+F):0)};var H=E.toLowerCase();n.fn[H]=function(I){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+E]||document.body["client"+E]:this[0]==document?Math.max(document.documentElement["client"+E],document.body["scroll"+E],document.documentElement["scroll"+E],document.body["offset"+E],document.documentElement["offset"+E]):I===g?(this.length?n.css(this[0],H):null):this.css(H,typeof I==="string"?I:I+"px")}})})(); \ No newline at end of file
+(function(){var Q=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]+['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,K=0,G=Object.prototype.toString;var F=function(X,T,aa,ab){aa=aa||[];T=T||document;if(T.nodeType!==1&&T.nodeType!==9){return[]}if(!X||typeof X!=="string"){return aa}var Y=[],V,ae,ah,S,ac,U,W=true;Q.lastIndex=0;while((V=Q.exec(X))!==null){Y.push(V[1]);if(V[2]){U=RegExp.rightContext;break}}if(Y.length>1&&L.exec(X)){if(Y.length===2&&H.relative[Y[0]]){ae=I(Y[0]+Y[1],T)}else{ae=H.relative[Y[0]]?[T]:F(Y.shift(),T);while(Y.length){X=Y.shift();if(H.relative[X]){X+=Y.shift()}ae=I(X,ae)}}}else{var ad=ab?{expr:Y.pop(),set:E(ab)}:F.find(Y.pop(),Y.length===1&&T.parentNode?T.parentNode:T,P(T));ae=F.filter(ad.expr,ad.set);if(Y.length>0){ah=E(ae)}else{W=false}while(Y.length){var ag=Y.pop(),af=ag;if(!H.relative[ag]){ag=""}else{af=Y.pop()}if(af==null){af=T}H.relative[ag](ah,af,P(T))}}if(!ah){ah=ae}if(!ah){throw"Syntax error, unrecognized expression: "+(ag||X)}if(G.call(ah)==="[object Array]"){if(!W){aa.push.apply(aa,ah)}else{if(T.nodeType===1){for(var Z=0;ah[Z]!=null;Z++){if(ah[Z]&&(ah[Z]===true||ah[Z].nodeType===1&&J(T,ah[Z]))){aa.push(ae[Z])}}}else{for(var Z=0;ah[Z]!=null;Z++){if(ah[Z]&&ah[Z].nodeType===1){aa.push(ae[Z])}}}}}else{E(ah,aa)}if(U){F(U,T,aa,ab)}return aa};F.matches=function(S,T){return F(S,null,null,T)};F.find=function(Z,S,aa){var Y,W;if(!Z){return[]}for(var V=0,U=H.order.length;V<U;V++){var X=H.order[V],W;if((W=H.match[X].exec(Z))){var T=RegExp.leftContext;if(T.substr(T.length-1)!=="\\"){W[1]=(W[1]||"").replace(/\\/g,"");Y=H.find[X](W,S,aa);if(Y!=null){Z=Z.replace(H.match[X],"");break}}}}if(!Y){Y=S.getElementsByTagName("*")}return{set:Y,expr:Z}};F.filter=function(ab,aa,ae,V){var U=ab,ag=[],Y=aa,X,S;while(ab&&aa.length){for(var Z in H.filter){if((X=H.match[Z].exec(ab))!=null){var T=H.filter[Z],af,ad;S=false;if(Y==ag){ag=[]}if(H.preFilter[Z]){X=H.preFilter[Z](X,Y,ae,ag,V);if(!X){S=af=true}else{if(X===true){continue}}}if(X){for(var W=0;(ad=Y[W])!=null;W++){if(ad){af=T(ad,X,W,Y);var ac=V^!!af;if(ae&&af!=null){if(ac){S=true}else{Y[W]=false}}else{if(ac){ag.push(ad);S=true}}}}}if(af!==g){if(!ae){Y=ag}ab=ab.replace(H.match[Z],"");if(!S){return[]}break}}}ab=ab.replace(/\s*,\s*/,"");if(ab==U){if(S==null){throw"Syntax error, unrecognized expression: "+ab}else{break}}U=ab}return Y};var H=F.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(S){return S.getAttribute("href")}},relative:{"+":function(W,T){for(var U=0,S=W.length;U<S;U++){var V=W[U];if(V){var X=V.previousSibling;while(X&&X.nodeType!==1){X=X.previousSibling}W[U]=typeof T==="string"?X||false:X===T}}if(typeof T==="string"){F.filter(T,W,true)}},">":function(X,T,Y){if(typeof T==="string"&&!/\W/.test(T)){T=Y?T:T.toUpperCase();for(var U=0,S=X.length;U<S;U++){var W=X[U];if(W){var V=W.parentNode;X[U]=V.nodeName===T?V:false}}}else{for(var U=0,S=X.length;U<S;U++){var W=X[U];if(W){X[U]=typeof T==="string"?W.parentNode:W.parentNode===T}}if(typeof T==="string"){F.filter(T,X,true)}}},"":function(V,T,X){var U="done"+(K++),S=R;if(!T.match(/\W/)){var W=T=X?T:T.toUpperCase();S=O}S("parentNode",T,U,V,W,X)},"~":function(V,T,X){var U="done"+(K++),S=R;if(typeof T==="string"&&!T.match(/\W/)){var W=T=X?T:T.toUpperCase();S=O}S("previousSibling",T,U,V,W,X)}},find:{ID:function(T,U,V){if(typeof U.getElementById!=="undefined"&&!V){var S=U.getElementById(T[1]);return S?[S]:[]}},NAME:function(S,T,U){if(typeof T.getElementsByName!=="undefined"&&!U){return T.getElementsByName(S[1])}},TAG:function(S,T){return T.getElementsByTagName(S[1])}},preFilter:{CLASS:function(V,T,U,S,Y){V=" "+V[1].replace(/\\/g,"")+" ";var X;for(var W=0;(X=T[W])!=null;W++){if(X){if(Y^(" "+X.className+" ").indexOf(V)>=0){if(!U){S.push(X)}}else{if(U){T[W]=false}}}}return false},ID:function(S){return S[1].replace(/\\/g,"")},TAG:function(T,S){for(var U=0;S[U]===false;U++){}return S[U]&&P(S[U])?T[1]:T[1].toUpperCase()},CHILD:function(S){if(S[1]=="nth"){var T=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(S[2]=="even"&&"2n"||S[2]=="odd"&&"2n+1"||!/\D/.test(S[2])&&"0n+"+S[2]||S[2]);S[2]=(T[1]+(T[2]||1))-0;S[3]=T[3]-0}S[0]="done"+(K++);return S},ATTR:function(T){var S=T[1].replace(/\\/g,"");if(H.attrMap[S]){T[1]=H.attrMap[S]}if(T[2]==="~="){T[4]=" "+T[4]+" "}return T},PSEUDO:function(W,T,U,S,X){if(W[1]==="not"){if(W[3].match(Q).length>1){W[3]=F(W[3],null,null,T)}else{var V=F.filter(W[3],T,U,true^X);if(!U){S.push.apply(S,V)}return false}}else{if(H.match.POS.test(W[0])){return true}}return W},POS:function(S){S.unshift(true);return S}},filters:{enabled:function(S){return S.disabled===false&&S.type!=="hidden"},disabled:function(S){return S.disabled===true},checked:function(S){return S.checked===true},selected:function(S){S.parentNode.selectedIndex;return S.selected===true},parent:function(S){return !!S.firstChild},empty:function(S){return !S.firstChild},has:function(U,T,S){return !!F(S[3],U).length},header:function(S){return/h\d/i.test(S.nodeName)},text:function(S){return"text"===S.type},radio:function(S){return"radio"===S.type},checkbox:function(S){return"checkbox"===S.type},file:function(S){return"file"===S.type},password:function(S){return"password"===S.type},submit:function(S){return"submit"===S.type},image:function(S){return"image"===S.type},reset:function(S){return"reset"===S.type},button:function(S){return"button"===S.type||S.nodeName.toUpperCase()==="BUTTON"},input:function(S){return/input|select|textarea|button/i.test(S.nodeName)}},setFilters:{first:function(T,S){return S===0},last:function(U,T,S,V){return T===V.length-1},even:function(T,S){return S%2===0},odd:function(T,S){return S%2===1},lt:function(U,T,S){return T<S[3]-0},gt:function(U,T,S){return T>S[3]-0},nth:function(U,T,S){return S[3]-0==T},eq:function(U,T,S){return S[3]-0==T}},filter:{CHILD:function(S,V){var Y=V[1],Z=S.parentNode;var X=V[0];if(Z&&(!Z[X]||!S.nodeIndex)){var W=1;for(var T=Z.firstChild;T;T=T.nextSibling){if(T.nodeType==1){T.nodeIndex=W++}}Z[X]=W-1}if(Y=="first"){return S.nodeIndex==1}else{if(Y=="last"){return S.nodeIndex==Z[X]}else{if(Y=="only"){return Z[X]==1}else{if(Y=="nth"){var ab=false,U=V[2],aa=V[3];if(U==1&&aa==0){return true}if(U==0){if(S.nodeIndex==aa){ab=true}}else{if((S.nodeIndex-aa)%U==0&&(S.nodeIndex-aa)/U>=0){ab=true}}return ab}}}}},PSEUDO:function(Y,U,V,Z){var T=U[1],W=H.filters[T];if(W){return W(Y,V,U,Z)}else{if(T==="contains"){return(Y.textContent||Y.innerText||"").indexOf(U[3])>=0}else{if(T==="not"){var X=U[3];for(var V=0,S=X.length;V<S;V++){if(X[V]===Y){return false}}return true}}}},ID:function(T,S){return T.nodeType===1&&T.getAttribute("id")===S},TAG:function(T,S){return(S==="*"&&T.nodeType===1)||T.nodeName===S},CLASS:function(T,S){return S.test(T.className)},ATTR:function(W,U){var S=H.attrHandle[U[1]]?H.attrHandle[U[1]](W):W[U[1]]||W.getAttribute(U[1]),X=S+"",V=U[2],T=U[4];return S==null?V==="!=":V==="="?X===T:V==="*="?X.indexOf(T)>=0:V==="~="?(" "+X+" ").indexOf(T)>=0:!U[4]?S:V==="!="?X!=T:V==="^="?X.indexOf(T)===0:V==="$="?X.substr(X.length-T.length)===T:V==="|="?X===T||X.substr(0,T.length+1)===T+"-":false},POS:function(W,T,U,X){var S=T[2],V=H.setFilters[S];if(V){return V(W,U,T,X)}}}};var L=H.match.POS;for(var N in H.match){H.match[N]=RegExp(H.match[N].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(T,S){T=Array.prototype.slice.call(T);if(S){S.push.apply(S,T);return S}return T};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(M){E=function(W,V){var T=V||[];if(G.call(W)==="[object Array]"){Array.prototype.push.apply(T,W)}else{if(typeof W.length==="number"){for(var U=0,S=W.length;U<S;U++){T.push(W[U])}}else{for(var U=0;W[U];U++){T.push(W[U])}}}return T}}(function(){var T=document.createElement("form"),U="script"+(new Date).getTime();T.innerHTML="<input name='"+U+"'/>";var S=document.documentElement;S.insertBefore(T,S.firstChild);if(!!document.getElementById(U)){H.find.ID=function(W,X,Y){if(typeof X.getElementById!=="undefined"&&!Y){var V=X.getElementById(W[1]);return V?V.id===W[1]||typeof V.getAttributeNode!=="undefined"&&V.getAttributeNode("id").nodeValue===W[1]?[V]:g:[]}};H.filter.ID=function(X,V){var W=typeof X.getAttributeNode!=="undefined"&&X.getAttributeNode("id");return X.nodeType===1&&W&&W.nodeValue===V}}S.removeChild(T)})();(function(){var S=document.createElement("div");S.appendChild(document.createComment(""));if(S.getElementsByTagName("*").length>0){H.find.TAG=function(T,X){var W=X.getElementsByTagName(T[1]);if(T[1]==="*"){var V=[];for(var U=0;W[U];U++){if(W[U].nodeType===1){V.push(W[U])}}W=V}return W}}S.innerHTML="<a href='#'></a>";if(S.firstChild&&S.firstChild.getAttribute("href")!=="#"){H.attrHandle.href=function(T){return T.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var S=F,T=document.createElement("div");T.innerHTML="<p class='TEST'></p>";if(T.querySelectorAll&&T.querySelectorAll(".TEST").length===0){return}F=function(X,W,U,V){W=W||document;if(!V&&W.nodeType===9&&!P(W)){try{return E(W.querySelectorAll(X),U)}catch(Y){}}return S(X,W,U,V)};F.find=S.find;F.filter=S.filter;F.selectors=S.selectors;F.matches=S.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){H.order.splice(1,0,"CLASS");H.find.CLASS=function(S,T){return T.getElementsByClassName(S[1])}}function O(T,Z,Y,ac,aa,ab){for(var W=0,U=ac.length;W<U;W++){var S=ac[W];if(S){S=S[T];var X=false;while(S&&S.nodeType){var V=S[Y];if(V){X=ac[V];break}if(S.nodeType===1&&!ab){S[Y]=W}if(S.nodeName===Z){X=S;break}S=S[T]}ac[W]=X}}}function R(T,Y,X,ab,Z,aa){for(var V=0,U=ab.length;V<U;V++){var S=ab[V];if(S){S=S[T];var W=false;while(S&&S.nodeType){if(S[X]){W=ab[S[X]];break}if(S.nodeType===1){if(!aa){S[X]=V}if(typeof Y!=="string"){if(S===Y){W=true;break}}else{if(F.filter(Y,[S]).length>0){W=S;break}}}S=S[T]}ab[V]=W}}}var J=document.compareDocumentPosition?function(T,S){return T.compareDocumentPosition(S)&16}:function(T,S){return T!==S&&(T.contains?T.contains(S):true)};var P=function(S){return S.nodeType===9&&S.documentElement.nodeName!=="HTML"||!!S.ownerDocument&&P(S.ownerDocument)};var I=function(S,Z){var V=[],W="",X,U=Z.nodeType?[Z]:Z;while((X=H.match.PSEUDO.exec(S))){W+=X[0];S=S.replace(H.match.PSEUDO,"")}S=H.relative[S]?S+"*":S;for(var Y=0,T=U.length;Y<T;Y++){F(S,U[Y],V)}return F.filter(W,V)};o.find=F;o.filter=F.filter;o.expr=F.selectors;o.expr[":"]=o.expr.filters;F.selectors.filters.hidden=function(S){return"hidden"===S.type||o.css(S,"display")==="none"||o.css(S,"visibility")==="hidden"};F.selectors.filters.visible=function(S){return"hidden"!==S.type&&o.css(S,"display")!=="none"&&o.css(S,"visibility")!=="hidden"};F.selectors.filters.animated=function(S){return o.grep(o.timers,function(T){return S===T.elem}).length};o.multiFilter=function(U,S,T){if(T){U=":not("+U+")"}return F.matches(U,S)};o.dir=function(U,T){var S=[],V=U[T];while(V&&V!=document){if(V.nodeType==1){S.push(V)}V=V[T]}return S};o.nth=function(W,S,U,V){S=S||1;var T=0;for(;W;W=W[U]){if(W.nodeType==1&&++T==S){break}}return W};o.sibling=function(U,T){var S=[];for(;U;U=U.nextSibling){if(U.nodeType==1&&U!=T){S.push(U)}}return S};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F<E.length){o.event.proxy(G,E[F++])}return this.click(o.event.proxy(G,function(H){this.lastToggle=(this.lastToggle||0)%F;H.preventDefault();return E[this.lastToggle++].apply(this,arguments)||false}))},hover:function(E,F){return this.mouseenter(E).mouseleave(F)},ready:function(E){B();if(o.isReady){E.call(document,o)}else{o.readyList.push(E)}return this},live:function(G,F){var E=o.event.proxy(F);E.guid+=this.selector+G;o(document).bind(i(G,this.selector),this.selector,E);return this},die:function(F,E){o(document).unbind(i(F,this.selector),E?{guid:E.guid+this.selector+F}:null);return this}});function c(H){var E=RegExp("(^|\\.)"+H.type+"(\\.|$)"),G=true,F=[];o.each(o.data(this,"events").live||[],function(I,J){if(E.test(J.type)){var K=o(H.target).closest(J.data)[0];if(K){F.push({elem:K,fn:J})}}});o.each(F,function(){if(this.fn.call(this.elem,H,this.fn.data)===false){G=false}});return G}function i(F,E){return["live",F,E.replace(/\./g,"`").replace(/ /g,"|")].join(".")}o.extend({isReady:false,readyList:[],ready:function(){if(!o.isReady){o.isReady=true;if(o.readyList){o.each(o.readyList,function(){this.call(document,o)});o.readyList=null}o(document).triggerHandler("ready")}}});var x=false;function B(){if(x){return}x=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);o.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);o.ready()}});if(document.documentElement.doScroll&&typeof l.frameElement==="undefined"){(function(){if(o.isReady){return}try{document.documentElement.doScroll("left")}catch(E){setTimeout(arguments.callee,0);return}o.ready()})()}}}o.event.add(l,"load",o.ready)}o.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(F,E){o.fn[E]=function(G){return G?this.bind(E,G):this.trigger(E)}});o(l).bind("unload",function(){for(var E in o.cache){if(E!=1&&o.cache[E].handle){o.event.remove(o.cache[E].handle.elem)}}});(function(){o.support={};var F=document.documentElement,G=document.createElement("script"),K=document.createElement("div"),J="script"+(new Date).getTime();K.style.display="none";K.innerHTML=' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,style:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var L=document.createElement("div");L.style.width="1px";L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L)})})();var w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("<div/>").append(M.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H<F;H++){var E=o.data(this[H],"olddisplay");this[H].style.display=E||"";if(o.css(this[H],"display")==="none"){var G=this[H].tagName,K;if(m[G]){K=m[G]}else{var I=o("<"+G+" />").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}this[H].style.display=o.data(this[H],"olddisplay",K)}}return this}},hide:function(H,I){if(H){return this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G<F;G++){var E=o.data(this[G],"olddisplay");if(!E&&E!=="none"){o.data(this[G],"olddisplay",o.css(this[G],"display"))}this[G].style.display="none"}return this}},_toggle:o.fn.toggle,toggle:function(G,F){var E=typeof G==="boolean";return o.isFunction(G)&&o.isFunction(F)?this._toggle.apply(this,arguments):G==null||E?this.each(function(){var H=E?G:o(this).is(":hidden");o(this)[H?"show":"hide"]()}):this.animate(t("toggle",3),G,F)},fadeTo:function(E,G,F){return this.animate({opacity:G},E,F)},animate:function(I,F,H,G){var E=o.speed(F,H,G);return this[E.queue===false?"each":"queue"](function(){var K=o.extend({},E),M,L=this.nodeType==1&&o(this).is(":hidden"),J=this;for(M in I){if(I[M]=="hide"&&L||I[M]=="show"&&!L){return K.complete.call(this)}if((M=="height"||M=="width")&&this.style){K.display=o.css(this,"display");K.overflow=this.style.overflow}}if(K.overflow!=null){this.style.overflow="hidden"}K.curAnim=o.extend({},I);o.each(I,function(O,S){var R=new o.fx(J,K,O);if(/toggle|show|hide/.test(S)){R[S=="toggle"?L?"show":"hide":S](I)}else{var Q=S.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),T=R.cur(true)||0;if(Q){var N=parseFloat(Q[2]),P=Q[3]||"px";if(P!="px"){J.style[O]=(N||1)+P;T=((N||1)/R.cur(true))*T;J.style[O]=T+P}if(Q[1]){N=((Q[1]=="-="?-1:1)*N)+T}R.custom(T,N,P)}else{R.custom(T,S,"")}}});return true})},stop:function(F,E){var G=o.timers;if(F){this.queue([])}this.each(function(){for(var H=G.length-1;H>=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)==1){n=setInterval(function(){var K=o.timers;for(var J=0;J<K.length;J++){if(!K[J]()){K.splice(J--,1)}}if(!K.length){clearInterval(n)}},13)}},show:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());o(this.elem).show()},hide:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(H){var G=e();if(H||G>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(H,F){var E=H?"Left":"Top",G=H?"Right":"Bottom";o.fn["inner"+F]=function(){return this[F.toLowerCase()]()+j(this,"padding"+E)+j(this,"padding"+G)};o.fn["outer"+F]=function(J){return this["inner"+F]()+j(this,"border"+E+"Width")+j(this,"border"+G+"Width")+(J?j(this,"margin"+E)+j(this,"margin"+G):0)};var I=F.toLowerCase();o.fn[I]=function(J){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+F]||document.body["client"+F]:this[0]==document?Math.max(document.documentElement["client"+F],document.body["scroll"+F],document.documentElement["scroll"+F],document.body["offset"+F],document.documentElement["offset"+F]):J===g?(this.length?o.css(this[0],I):null):this.css(I,typeof J==="string"?J:J+"px")}})})(); \ No newline at end of file
diff --git a/js/jquery.simplemodal-1.2.2.pack.js b/js/jquery.simplemodal-1.2.2.pack.js
new file mode 100644
index 000000000..b5ad5c23a
--- /dev/null
+++ b/js/jquery.simplemodal-1.2.2.pack.js
@@ -0,0 +1,8 @@
+/*
+ * SimpleModal 1.2.2 - jQuery Plugin
+ * http://www.ericmmartin.com/projects/simplemodal/
+ * Copyright (c) 2008 Eric Martin
+ * Dual licensed under the MIT and GPL licenses
+ * Revision: $Id: jquery.simplemodal.js 181 2008-12-16 16:51:44Z emartin24 $
+ */
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(g($){m f=$.Q.1Q&&1a($.Q.1D)==6&&!10[\'2g\'],1f=$.Q.1Q&&!$.2a,w=[];$.y=g(a,b){I $.y.12.1n(a,b)};$.y.D=g(){$.y.12.D()};$.1P.y=g(a){I $.y.12.1n(3,a)};$.y.1O={V:29,1J:\'r-H\',1B:{},1z:\'r-n\',20:{},1Z:{},v:2t,D:1o,1T:\'<a 2j="2h" 2f="2e"></a>\',X:\'r-D\',l:F,1g:K,1e:F,1d:F,1c:F};$.y.12={7:F,4:{},1n:g(a,b){8(3.4.j){I K}3.7=$.U({},$.y.1O,b);3.v=3.7.v;3.1w=K;8(J a==\'27\'){a=a 25 1A?a:$(a);8(a.1v().1v().23()>0){3.4.T=a.1v();8(!3.7.1g){3.4.21=a.2x(1o)}}}q 8(J a==\'2w\'||J a==\'1r\'){a=$(\'<1q/>\').2s(a)}q{2r(\'2q 2p: 2o j 2l: \'+J a);I K}3.4.j=a.11(\'r-j\').E(3.7.1Z);a=F;3.1S();3.1R();8($.1m(3.7.1d)){3.7.1d.1l(3,[3.4])}I 3},1S:g(){w=3.1k();8(f){3.4.x=$(\'<x 2d="2c:K;"/>\').E($.U(3.7.2b,{1j:\'1i\',V:0,l:\'1h\',A:w[0],z:w[1],v:3.7.v,L:0,B:0})).O(\'u\')}3.4.H=$(\'<1q/>\').1N(\'1M\',3.7.1J).11(\'r-H\').E($.U(3.7.1B,{1j:\'1i\',V:3.7.V/1b,A:w[0],z:w[1],l:\'1h\',B:0,L:0,v:3.7.v+1})).O(\'u\');3.4.n=$(\'<1q/>\').1N(\'1M\',3.7.1z).11(\'r-n\').E($.U(3.7.20,{1j:\'1i\',l:\'1h\',v:3.7.v+2})).1K(3.7.D?$(3.7.1T).11(3.7.X):\'\').O(\'u\');3.19();8(f||1f){3.18()}3.4.n.1K(3.4.j.1I())},1H:g(){m a=3;$(\'.\'+3.7.X).1G(\'1L.r\',g(e){e.28();a.D()});$(10).1G(\'1F.r\',g(){w=a.1k();a.19();8(f||1f){a.18()}q{a.4.x&&a.4.x.E({A:w[0],z:w[1]});a.4.H.E({A:w[0],z:w[1]})}})},1E:g(){$(\'.\'+3.7.X).1C(\'1L.r\');$(10).1C(\'1F.r\')},18:g(){m p=3.7.l;$.26([3.4.x||F,3.4.H,3.4.n],g(i,e){8(e){m a=\'k.u.17\',N=\'k.u.1W\',16=\'k.u.24\',S=\'k.u.1y\',R=\'k.u.1x\',15=\'k.u.22\',1t=\'k.P.17\',1s=\'k.P.1W\',C=\'k.P.1y\',G=\'k.P.1x\',s=e[0].2v;s.l=\'2u\';8(i<2){s.14(\'A\');s.14(\'z\');s.Z(\'A\',\'\'+16+\' > \'+a+\' ? \'+16+\' : \'+a+\' + "o"\');s.Z(\'z\',\'\'+15+\' > \'+N+\' ? \'+15+\' : \'+N+\' + "o"\')}q{m b,W;8(p&&p.1Y==1X){8(p[0]){m c=J p[0]==\'1r\'?p[0].1V():p[0].13(/o/,\'\');b=c.1U(\'%\')==-1?c+\' + (t = \'+G+\' ? \'+G+\' : \'+R+\') + "o"\':1a(c.13(/%/,\'\'))+\' * ((\'+1t+\' || \'+a+\') / 1b) + (t = \'+G+\' ? \'+G+\' : \'+R+\') + "o"\'}8(p[1]){m d=J p[1]==\'1r\'?p[1].1V():p[1].13(/o/,\'\');W=d.1U(\'%\')==-1?d+\' + (t = \'+C+\' ? \'+C+\' : \'+S+\') + "o"\':1a(d.13(/%/,\'\'))+\' * ((\'+1s+\' || \'+N+\') / 1b) + (t = \'+C+\' ? \'+C+\' : \'+S+\') + "o"\'}}q{b=\'(\'+1t+\' || \'+a+\') / 2 - (3.2n / 2) + (t = \'+G+\' ? \'+G+\' : \'+R+\') + "o"\';W=\'(\'+1s+\' || \'+N+\') / 2 - (3.2m / 2) + (t = \'+C+\' ? \'+C+\' : \'+S+\') + "o"\'}s.14(\'L\');s.14(\'B\');s.Z(\'L\',b);s.Z(\'B\',W)}}})},1k:g(){m a=$(10);m h=$.Q.2k&&$.Q.1D>\'9.5\'&&$.1P.2i<=\'1.2.6\'?k.P[\'17\']:a.A();I[h,a.z()]},19:g(){m a,B,1u=(w[0]/2)-((3.4.n.A()||3.4.j.A())/2),1p=(w[1]/2)-((3.4.n.z()||3.4.j.z())/2);8(3.7.l&&3.7.l.1Y==1X){a=3.7.l[0]||1u;B=3.7.l[1]||1p}q{a=1u;B=1p}3.4.n.E({B:B,L:a})},1R:g(){3.4.x&&3.4.x.Y();8($.1m(3.7.1e)){3.7.1e.1l(3,[3.4])}q{3.4.H.Y();3.4.n.Y();3.4.j.Y()}3.1H()},D:g(){8(!3.4.j){I K}8($.1m(3.7.1c)&&!3.1w){3.1w=1o;3.7.1c.1l(3,[3.4])}q{8(3.4.T){8(3.7.1g){3.4.j.1I().O(3.4.T)}q{3.4.j.M();3.4.21.O(3.4.T)}}q{3.4.j.M()}3.4.n.M();3.4.H.M();3.4.x&&3.4.x.M();3.4={}}3.1E()}}})(1A);',62,158,'|||this|dialog|||opts|if||||||||function|||data|document|position|var|container|px||else|simplemodal|||body|zIndex||iframe|modal|width|height|left|sl|close|css|null|st|overlay|return|typeof|false|top|remove|bcw|appendTo|documentElement|browser|bst|bsl|parentNode|extend|opacity|le|closeClass|show|setExpression|window|addClass|impl|replace|removeExpression|bsw|bsh|clientHeight|fixIE|setPosition|parseInt|100|onClose|onShow|onOpen|ieQuirks|persist|fixed|none|display|getDimensions|apply|isFunction|init|true|vCenter|div|number|cw|ch|hCenter|parent|occb|scrollTop|scrollLeft|containerId|jQuery|overlayCss|unbind|version|unbindEvents|resize|bind|bindEvents|hide|overlayId|append|click|id|attr|defaults|fn|msie|open|create|closeHTML|indexOf|toString|clientWidth|Array|constructor|dataCss|containerCss|orig|scrollWidth|size|scrollHeight|instanceof|each|object|preventDefault|50|boxModel|iframeCss|javascript|src|Close|title|XMLHttpRequest|modalCloseImg|jquery|class|opera|type|offsetWidth|offsetHeight|Unsupported|Error|SimpleModal|alert|html|1000|absolute|style|string|clone'.split('|'),0,{})) \ No newline at end of file
diff --git a/js/video.js b/js/video.js
new file mode 100644
index 000000000..936a6312e
--- /dev/null
+++ b/js/video.js
@@ -0,0 +1,9 @@
+$('document').ready(function() {
+ $('a.media, a.mediamp3').append(' <sup>[PLAY]</sup>');
+ $('a.mediamp3').html('').css('display', 'block').css('width', '224px').css('height','24px').flowplayer('../bin/flowplayer-3.0.5.swf');
+ $('a.media').click(function() {
+ $('<a id="p1i"></a>').attr('href', $(this).attr('href')).flowplayer('../bin/flowplayer-3.0.5.swf').modal({'closeHTML':'<a class="modalCloseImg" title="Close"><img src="x.png" /></a>'});
+ return false;
+ });
+});
+
diff --git a/lib/action.php b/lib/action.php
index c4172ada1..812df635e 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -93,7 +93,10 @@ class Action extends HTMLOutputter // lawsuit
*/
function showPage()
{
- $this->startHTML();
+ if (Event::handle('StartShowHTML', array($this))) {
+ $this->startHTML();
+ Event::handle('EndShowHTML', array($this));
+ }
$this->showHead();
$this->showBody();
$this->endHTML();
@@ -151,25 +154,50 @@ class Action extends HTMLOutputter // lawsuit
*/
function showStylesheets()
{
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
- 'media' => 'screen, projection, tv'));
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
- 'media' => 'screen, projection, tv'));
- $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
- foreach (array(6,7) as $ver) {
- if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
- // Yes, IE people should be put in jail.
- $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ if (Event::handle('StartShowStyles', array($this))) {
+ if (Event::handle('StartShowLaconicaStyles', array($this))) {
+
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/modal.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ if (common_config('site', 'mobile')) {
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
+ // TODO: "handheld" CSS for other mobile devices
+ 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
+ }
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'print'));
+ Event::handle('EndShowLaconicaStyles', array($this));
+ }
+ if (Event::handle('StartShowUAStyles', array($this))) {
+ $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ foreach (array(6,7) as $ver) {
+ if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
+ // Yes, IE people should be put in jail.
+ $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ }
+ }
+ $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
+ Event::handle('EndShowUAStyles', array($this));
}
+ Event::handle('EndShowStyles', array($this));
}
- $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
}
/**
@@ -179,18 +207,43 @@ class Action extends HTMLOutputter // lawsuit
*/
function showScripts()
{
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/jquery.min.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/jquery.form.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/xbImportNode.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
- ' ');
+ if (Event::handle('StartShowScripts', array($this))) {
+ if (Event::handle('StartShowJQueryScripts', array($this))) {
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.min.js')),
+ ' ');
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.form.js')),
+ ' ');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.simplemodal-1.2.2.pack.js')),
+ ' ');
+
+ Event::handle('EndShowJQueryScripts', array($this));
+ }
+ if (Event::handle('StartShowLaconicaScripts', array($this))) {
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/xbImportNode.js')),
+ ' ');
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
+ ' ');
+ // Frame-busting code to avoid clickjacking attacks.
+ $this->element('script', array('type' => 'text/javascript'),
+ 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/flowplayer-3.0.5.min.js')),
+ ' ');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/video.js')),
+ ' ');
+ Event::handle('EndShowLaconicaScripts', array($this));
+ }
+ Event::handle('EndShowScripts', array($this));
+ }
}
/**
@@ -216,9 +269,19 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function showFeeds()
{
- // does nothing by default
+ $feeds = $this->getFeeds();
+
+ if ($feeds) {
+ foreach ($feeds as $feed) {
+ $this->element('link', array('rel' => $feed->rel(),
+ 'href' => $feed->url,
+ 'type' => $feed->mimeType(),
+ 'title' => $feed->title));
+ }
+ }
}
/**
@@ -256,9 +319,15 @@ class Action extends HTMLOutputter // lawsuit
{
$this->elementStart('body', array('id' => $this->trimmed('action')));
$this->elementStart('div', array('id' => 'wrap'));
- $this->showHeader();
+ if (Event::handle('StartShowHeader', array($this))) {
+ $this->showHeader();
+ Event::handle('EndShowHeader', array($this));
+ }
$this->showCore();
- $this->showFooter();
+ if (Event::handle('StartShowFooter', array($this))) {
+ $this->showFooter();
+ Event::handle('EndShowFooter', array($this));
+ }
$this->elementEnd('div');
$this->elementEnd('body');
}
@@ -312,42 +381,46 @@ class Action extends HTMLOutputter // lawsuit
*/
function showPrimaryNav()
{
+ $user = common_current_user();
+
$this->elementStart('dl', array('id' => 'site_nav_global_primary'));
$this->element('dt', null, _('Primary site navigation'));
$this->elementStart('dd');
- $user = common_current_user();
$this->elementStart('ul', array('class' => 'nav'));
- if ($user) {
- $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
- _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
- }
- $this->menuItem(common_local_url('peoplesearch'),
- _('Search'), _('Search for people or text'), false, 'nav_search');
- if ($user) {
- $this->menuItem(common_local_url('profilesettings'),
- _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
-
- if (common_config('xmpp', 'enabled')) {
- $this->menuItem(common_local_url('imsettings'),
- _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
- } else {
- $this->menuItem(common_local_url('smssettings'),
- _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+ if (Event::handle('StartPrimaryNav', array($this))) {
+ if ($user) {
+ $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
+ _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
}
- $this->menuItem(common_local_url('logout'),
- _('Logout'), _('Logout from the site'), false, 'nav_logout');
- } else {
- $this->menuItem(common_local_url('login'),
- _('Login'), _('Login to the site'), false, 'nav_login');
- if (!common_config('site', 'closed')) {
- $this->menuItem(common_local_url('register'),
- _('Register'), _('Create an account'), false, 'nav_register');
+ $this->menuItem(common_local_url('peoplesearch'),
+ _('Search'), _('Search for people or text'), false, 'nav_search');
+ if ($user) {
+ $this->menuItem(common_local_url('profilesettings'),
+ _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
+
+ if (common_config('xmpp', 'enabled')) {
+ $this->menuItem(common_local_url('imsettings'),
+ _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
+ } else {
+ $this->menuItem(common_local_url('smssettings'),
+ _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+ }
+ $this->menuItem(common_local_url('logout'),
+ _('Logout'), _('Logout from the site'), false, 'nav_logout');
+ } else {
+ $this->menuItem(common_local_url('login'),
+ _('Login'), _('Login to the site'), false, 'nav_login');
+ if (!common_config('site', 'closed')) {
+ $this->menuItem(common_local_url('register'),
+ _('Register'), _('Create an account'), false, 'nav_register');
+ }
+ $this->menuItem(common_local_url('openidlogin'),
+ _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
}
- $this->menuItem(common_local_url('openidlogin'),
- _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
+ $this->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'), _('Help me!'), false, 'nav_help');
+ Event::handle('EndPrimaryNav', array($this));
}
- $this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'), _('Help me!'), false, 'nav_help');
$this->elementEnd('ul');
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -408,8 +481,14 @@ class Action extends HTMLOutputter // lawsuit
function showCore()
{
$this->elementStart('div', array('id' => 'core'));
- $this->showLocalNavBlock();
- $this->showContentBlock();
+ if (Event::handle('StartShowLocalNavBlock', array($this))) {
+ $this->showLocalNavBlock();
+ Event::handle('EndShowLocalNavBlock', array($this));
+ }
+ if (Event::handle('StartShowContentBlock', array($this))) {
+ $this->showContentBlock();
+ Event::handle('EndShowContentBlock', array($this));
+ }
$this->showAside();
$this->elementEnd('div');
}
@@ -511,27 +590,32 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function showAside()
{
$this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
$this->showExportData();
- $this->showSections();
+ if (Event::handle('StartShowSections', array($this))) {
+ $this->showSections();
+ Event::handle('EndShowSections', array($this));
+ }
$this->elementEnd('div');
}
/**
* Show export data feeds.
*
- * MAY overload if there are feeds
- *
- * @return nothing
+ * @return void
*/
+
function showExportData()
{
- // is there structure to this?
- // list of (visible!) feed links
- // can we reuse list of feeds from showFeeds() ?
+ $feeds = $this->getFeeds();
+ if ($feeds) {
+ $fl = new FeedList($this);
+ $fl->show($feeds);
+ }
}
/**
@@ -570,18 +654,23 @@ class Action extends HTMLOutputter // lawsuit
$this->element('dt', null, _('Secondary site navigation'));
$this->elementStart('dd', null);
$this->elementStart('ul', array('class' => 'nav'));
- $this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'));
- $this->menuItem(common_local_url('doc', array('title' => 'about')),
- _('About'));
- $this->menuItem(common_local_url('doc', array('title' => 'faq')),
- _('FAQ'));
- $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
- _('Privacy'));
- $this->menuItem(common_local_url('doc', array('title' => 'source')),
- _('Source'));
- $this->menuItem(common_local_url('doc', array('title' => 'contact')),
- _('Contact'));
+ if (Event::handle('StartSecondaryNav', array($this))) {
+ $this->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'));
+ $this->menuItem(common_local_url('doc', array('title' => 'about')),
+ _('About'));
+ $this->menuItem(common_local_url('doc', array('title' => 'faq')),
+ _('FAQ'));
+ $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
+ _('Privacy'));
+ $this->menuItem(common_local_url('doc', array('title' => 'source')),
+ _('Source'));
+ $this->menuItem(common_local_url('doc', array('title' => 'contact')),
+ _('Contact'));
+ $this->menuItem(common_local_url('doc', array('title' => 'badge')),
+ _('Badge'));
+ Event::handle('EndSecondaryNav', array($this));
+ }
$this->elementEnd('ul');
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -734,8 +823,10 @@ class Action extends HTMLOutputter // lawsuit
if ($if_modified_since) {
$ims = strtotime($if_modified_since);
if ($lm <= $ims) {
- if (!$etag ||
- $this->_hasEtag($etag, $_SERVER['HTTP_IF_NONE_MATCH'])) {
+ $if_none_match = $_SERVER['HTTP_IF_NONE_MATCH'];
+ if (!$if_none_match ||
+ !$etag ||
+ $this->_hasEtag($etag, $if_none_match)) {
header('HTTP/1.1 304 Not Modified');
// Better way to do this?
exit(0);
@@ -753,9 +844,11 @@ class Action extends HTMLOutputter // lawsuit
*
* @return boolean
*/
+
function _hasEtag($etag, $if_none_match)
{
- return ($if_none_match) && in_array($etag, explode(',', $if_none_match));
+ $etags = explode(',', $if_none_match);
+ return in_array($etag, $etags) || in_array('*', $etags);
}
/**
@@ -789,11 +882,12 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function serverError($msg, $code=500)
{
$action = $this->trimmed('action');
common_debug("Server error '$code' on '$action': $msg", __FILE__);
- common_server_error($msg, $code);
+ throw new ServerException($msg, $code);
}
/**
@@ -804,11 +898,12 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function clientError($msg, $code=400)
{
$action = $this->trimmed('action');
common_debug("User error '$code' on '$action': $msg", __FILE__);
- common_user_error($msg, $code);
+ throw new ClientException($msg, $code);
}
/**
@@ -881,17 +976,17 @@ class Action extends HTMLOutputter // lawsuit
}
if ($have_before) {
$pargs = array('page' => $page-1);
- $newargs = $args ? array_merge($args, $pargs) : $pargs;
$this->elementStart('li', array('class' => 'nav_prev'));
- $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'prev'),
+ $this->element('a', array('href' => common_local_url($action, $args, $pargs),
+ 'rel' => 'prev'),
_('After'));
$this->elementEnd('li');
}
if ($have_after) {
$pargs = array('page' => $page+1);
- $newargs = $args ? array_merge($args, $pargs) : $pargs;
$this->elementStart('li', array('class' => 'nav_next'));
- $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'next'),
+ $this->element('a', array('href' => common_local_url($action, $args, $pargs),
+ 'rel' => 'next'),
_('Before'));
$this->elementEnd('li');
}
@@ -902,4 +997,17 @@ class Action extends HTMLOutputter // lawsuit
$this->elementEnd('div');
}
}
+
+ /**
+ * An array of feeds for this action.
+ *
+ * Returns an array of potential feeds for this action.
+ *
+ * @return array Feed object to show in head and links
+ */
+
+ function getFeeds()
+ {
+ return null;
+ }
}
diff --git a/classes/Channel.php b/lib/channel.php
index fdeff21fc..f1e205546 100644
--- a/classes/Channel.php
+++ b/lib/channel.php
@@ -21,7 +21,6 @@ if (!defined('LACONICA')) { exit(1); }
class Channel
{
-
function on($user)
{
return false;
diff --git a/lib/clienterroraction.php b/lib/clienterroraction.php
index 5019dc06d..0c48414d5 100644
--- a/lib/clienterroraction.php
+++ b/lib/clienterroraction.php
@@ -49,7 +49,7 @@ class ClientErrorAction extends ErrorAction
function __construct($message='Error', $code=400)
{
parent::__construct($message, $code);
-
+
$this->status = array(400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
@@ -72,7 +72,7 @@ class ClientErrorAction extends ErrorAction
}
// XXX: Should these error actions even be invokable via URI?
-
+
function handle($args)
{
parent::handle($args);
@@ -84,11 +84,16 @@ class ClientErrorAction extends ErrorAction
}
$this->message = $this->trimmed('message');
-
+
if (!$this->message) {
- $this->message = "Client Error $this->code";
- }
+ $this->message = "Client Error $this->code";
+ }
$this->showPage();
}
+
+ function title()
+ {
+ return $this->status[$this->code];
+ }
}
diff --git a/lib/clientexception.php b/lib/clientexception.php
new file mode 100644
index 000000000..3020d7f50
--- /dev/null
+++ b/lib/clientexception.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * class for a client exception (user error)
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Class for client exceptions
+ *
+ * Subclass of PHP Exception for user errors.
+ *
+ * @category Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class ClientException extends Exception
+{
+ public function __construct($message = null, $code = 400) {
+ parent::__construct($message, $code);
+ }
+
+ // custom string representation of object
+ public function __toString() {
+ return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
+ }
+}
diff --git a/classes/Command.php b/lib/command.php
index eacbdacb3..507990a0b 100644
--- a/classes/Command.php
+++ b/lib/command.php
@@ -19,18 +19,18 @@
if (!defined('LACONICA')) { exit(1); }
-require_once(INSTALLDIR.'/classes/Channel.php');
+require_once(INSTALLDIR.'/lib/channel.php');
class Command
{
-
+
var $user = null;
-
+
function __construct($user=null)
{
$this->user = $user;
}
-
+
function execute($channel)
{
return false;
@@ -109,7 +109,7 @@ class StatsCommand extends Command
$notices = new Notice();
$notices->profile_id = $this->user->id;
$notice_count = (int) $notices->count();
-
+
$channel->output($this->user, sprintf(_("Subscriptions: %1\$s\n".
"Subscribers: %2\$s\n".
"Notices: %3\$s"),
@@ -121,21 +121,21 @@ class StatsCommand extends Command
class FavCommand extends Command
{
-
+
var $other = null;
-
+
function __construct($user, $other)
{
parent::__construct($user);
$this->other = $other;
}
-
+
function execute($channel)
{
-
- $recipient =
+
+ $recipient =
common_relative_profile($this->user, common_canonical_nickname($this->other));
-
+
if (!$recipient) {
$channel->error($this->user, _('No such user.'));
return;
@@ -145,7 +145,7 @@ class FavCommand extends Command
$channel->error($this->user, _('User has no last notice'));
return;
}
-
+
$fave = Fave::addNew($this->user, $notice);
if (!$fave) {
@@ -154,15 +154,15 @@ class FavCommand extends Command
}
$other = User::staticGet('id', $recipient->id);
-
+
if ($other && $other->id != $user->id) {
if ($other->email && $other->emailnotifyfav) {
mail_notify_fave($other, $this->user, $notice);
}
}
-
+
$this->user->blowFavesCache();
-
+
$channel->output($this->user, _('Notice marked as fave.'));
}
}
@@ -175,17 +175,17 @@ class WhoisCommand extends Command
parent::__construct($user);
$this->other = $other;
}
-
+
function execute($channel)
{
- $recipient =
+ $recipient =
common_relative_profile($this->user, common_canonical_nickname($this->other));
-
+
if (!$recipient) {
$channel->error($this->user, _('No such user.'));
return;
}
-
+
$whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname,
$recipient->profileurl);
if ($recipient->fullname) {
@@ -214,7 +214,7 @@ class MessageCommand extends Command
$this->other = $other;
$this->text = $text;
}
-
+
function execute($channel)
{
$other = User::staticGet('nickname', common_canonical_nickname($this->other));
@@ -229,7 +229,7 @@ class MessageCommand extends Command
return;
}
}
-
+
if (!$other) {
$channel->error($this->user, _('No such user.'));
return;
@@ -251,19 +251,19 @@ class MessageCommand extends Command
class GetCommand extends Command
{
-
+
var $other = null;
-
+
function __construct($user, $other)
{
parent::__construct($user);
$this->other = $other;
}
-
+
function execute($channel)
{
$target_nickname = common_canonical_nickname($this->other);
-
+
$target =
common_relative_profile($this->user, $target_nickname);
@@ -277,32 +277,32 @@ class GetCommand extends Command
return;
}
$notice_content = $notice->content;
-
+
$channel->output($this->user, $target_nickname . ": " . $notice_content);
}
}
class SubCommand extends Command
{
-
+
var $other = null;
-
+
function __construct($user, $other)
{
parent::__construct($user);
$this->other = $other;
}
-
+
function execute($channel)
{
-
+
if (!$this->other) {
$channel->error($this->user, _('Specify the name of the user to subscribe to'));
return;
}
-
+
$result = subs_subscribe_user($this->user, $this->other);
-
+
if ($result == 'true') {
$channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other));
} else {
@@ -315,7 +315,7 @@ class UnsubCommand extends Command
{
var $other = null;
-
+
function __construct($user, $other)
{
parent::__construct($user);
@@ -328,9 +328,9 @@ class UnsubCommand extends Command
$channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
return;
}
-
+
$result=subs_unsubscribe_user($this->user, $this->other);
-
+
if ($result) {
$channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other));
} else {
@@ -369,7 +369,7 @@ class OnCommand extends Command
parent::__construct($user);
$this->other = $other;
}
-
+
function execute($channel)
{
if ($other) {
@@ -406,7 +406,7 @@ class HelpCommand extends Command
"unsub <nickname> - same as 'leave'\n".
"last <nickname> - same as 'get'\n".
"on <nickname> - not yet implemented.\n".
- "off <nickname> - not yet implemented.\n".
+ "off <nickname> - not yet implemented.\n".
"nudge <nickname> - not yet implemented.\n".
"invite <phone number> - not yet implemented.\n".
"track <word> - not yet implemented.\n".
diff --git a/classes/CommandInterpreter.php b/lib/commandinterpreter.php
index 0679f5462..49c733c03 100644
--- a/classes/CommandInterpreter.php
+++ b/lib/commandinterpreter.php
@@ -19,11 +19,10 @@
if (!defined('LACONICA')) { exit(1); }
-require_once(INSTALLDIR.'/classes/Command.php');
+require_once INSTALLDIR.'/lib/command.php';
class CommandInterpreter
{
-
function handle_command($user, $text)
{
# XXX: localise
diff --git a/lib/common.php b/lib/common.php
index 5b4e3c40c..917fdeafa 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -49,6 +49,12 @@ require_once('DB/DataObject/Cast.php'); # for dates
require_once(INSTALLDIR.'/lib/language.php');
+// This gets included before the config file, so that admin code and plugins
+// can use it
+
+require_once(INSTALLDIR.'/lib/event.php');
+require_once(INSTALLDIR.'/lib/plugin.php');
+
// try to figure out where we are
$_server = array_key_exists('SERVER_NAME', $_SERVER) ?
@@ -67,6 +73,7 @@ $config =
'theme' => 'default',
'path' => $_path,
'logfile' => null,
+ 'logdebug' => false,
'fancy' => false,
'locale_path' => INSTALLDIR.'/locale',
'language' => 'en_US',
@@ -78,7 +85,8 @@ $config =
'broughtbyurl' => null,
'closed' => false,
'inviteonly' => false,
- 'private' => false),
+ 'private' => false,
+ 'dupelimit' => 60), # default for same person saying the same thing
'syslog' =>
array('appname' => 'laconica', # for syslog
'priority' => 'debug'), # XXX: currently ignored
@@ -100,7 +108,8 @@ $config =
array('server' => null),
'public' =>
array('localonly' => true,
- 'blacklist' => array()),
+ 'blacklist' => array(),
+ 'autosource' => array()),
'theme' =>
array('server' => null),
'throttle' =>
@@ -136,6 +145,8 @@ $config =
array('enabled' => false,
'server' => 'localhost',
'port' => 11211),
+ 'ping' =>
+ array('notify' => array()),
'inboxes' =>
array('enabled' => true), # on by default for new sites
);
@@ -171,12 +182,33 @@ if (strlen($_path) > 0) {
$_config_files[] = INSTALLDIR.'/config.php';
+$_have_a_config = false;
+
foreach ($_config_files as $_config_file) {
if (file_exists($_config_file)) {
include_once($_config_file);
+ $_have_a_config = true;
}
}
+function _have_config()
+{
+ global $_have_a_config;
+ return $_have_a_config;
+}
+
+// XXX: Throw a conniption if database not installed
+
+// Fixup for laconica.ini
+
+$_db_name = substr($config['db']['database'], strrpos($config['db']['database'], '/') + 1);
+
+if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db'])) {
+ $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/laconica.ini';
+}
+
+// XXX: how many of these could be auto-loaded on use?
+
require_once('Validate.php');
require_once('markdown.php');
@@ -188,6 +220,9 @@ require_once(INSTALLDIR.'/lib/subs.php');
require_once(INSTALLDIR.'/lib/Shorturl_api.php');
require_once(INSTALLDIR.'/lib/twitter.php');
+require_once(INSTALLDIR.'/lib/clientexception.php');
+require_once(INSTALLDIR.'/lib/serverexception.php');
+
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
@@ -200,5 +235,12 @@ function __autoload($class)
require_once(INSTALLDIR.'/classes/' . $class . '.php');
} else if (file_exists(INSTALLDIR.'/lib/' . strtolower($class) . '.php')) {
require_once(INSTALLDIR.'/lib/' . strtolower($class) . '.php');
+ } else if (mb_substr($class, -6) == 'Action' &&
+ file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php')) {
+ require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php');
}
}
+
+// Give plugins a chance to initialize in a fully-prepared environment
+
+Event::handle('InitializePlugin');
diff --git a/lib/dberroraction.php b/lib/dberroraction.php
new file mode 100644
index 000000000..0dc92490c
--- /dev/null
+++ b/lib/dberroraction.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * DB error action.
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/servererroraction.php';
+
+/**
+ * Class for displaying DB Errors
+ *
+ * This only occurs if there's been a DB_DataObject_Error that's
+ * reported through PEAR, so we try to avoid doing anything that connects
+ * to the DB, so we don't trigger it again.
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ */
+
+class DBErrorAction extends ServerErrorAction
+{
+ function __construct($message='Error', $code=500)
+ {
+ parent::__construct($message, $code);
+ }
+
+ function title()
+ {
+ return _('Database error');
+ }
+
+ function getLanguage()
+ {
+ // Don't try to figure out user's language; just show the page
+ return common_config('site', 'language');
+ }
+
+ function showPrimaryNav()
+ {
+ // don't show primary nav
+ }
+}
diff --git a/lib/event.php b/lib/event.php
new file mode 100644
index 000000000..d815ae54b
--- /dev/null
+++ b/lib/event.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * utilities for defining and running event handlers
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Event
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Class for events
+ *
+ * This "class" two static functions for managing events in the Laconica code.
+ *
+ * @category Event
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @todo Define a system for using Event instances
+ */
+
+class Event {
+
+ /* Global array of hooks, mapping eventname => array of callables */
+
+ protected static $_handlers = array();
+
+ /**
+ * Add an event handler
+ *
+ * To run some code at a particular point in Laconica processing.
+ * Named events include receiving an XMPP message, adding a new notice,
+ * or showing part of an HTML page.
+ *
+ * The arguments to the handler vary by the event. Handlers can return
+ * two possible values: false means that the event has been replaced by
+ * the handler completely, and no default processing should be done.
+ * Non-false means successful handling, and that the default processing
+ * should succeed. (Note that this only makes sense for some events.)
+ *
+ * Handlers can also abort processing by throwing an exception; these will
+ * be caught by the closest code and displayed as errors.
+ *
+ * @param string $name Name of the event
+ * @param callable $handler Code to run
+ *
+ * @return void
+ */
+
+ public static function addHandler($name, $handler) {
+ if (array_key_exists($name, Event::$_handlers)) {
+ Event::$_handlers[$name][] = $handler;
+ } else {
+ Event::$_handlers[$name] = array($handler);
+ }
+ }
+
+ /**
+ * Handle an event
+ *
+ * Events are any point in the code that we want to expose for admins
+ * or third-party developers to use.
+ *
+ * We pass in an array of arguments (including references, for stuff
+ * that can be changed), and each assigned handler gets run with those
+ * arguments. Exceptions can be thrown to indicate an error.
+ *
+ * @param string $name Name of the event that's happening
+ * @param array $args Arguments for handlers
+ *
+ * @return boolean flag saying whether to continue processing, based
+ * on results of handlers.
+ */
+
+ public static function handle($name, $args=array()) {
+ $result = null;
+ if (array_key_exists($name, Event::$_handlers)) {
+ foreach (Event::$_handlers[$name] as $handler) {
+ $result = call_user_func_array($handler, $args);
+ if ($result === false) {
+ break;
+ }
+ }
+ }
+ return ($result !== false);
+ }
+}
diff --git a/lib/facebookutil.php b/lib/facebookutil.php
index beab51366..ec3987273 100644
--- a/lib/facebookutil.php
+++ b/lib/facebookutil.php
@@ -25,21 +25,6 @@ define("FACEBOOK_SERVICE", 2); // Facebook is foreign_service ID 2
define("FACEBOOK_NOTICE_PREFIX", 1);
define("FACEBOOK_PROMPTED_UPDATE_PREF", 2);
-// Gets all the notices from users with a Facebook link since a given ID
-function getFacebookNotices($since)
-{
- $qry = 'SELECT notice.* ' .
- 'FROM notice ' .
- 'JOIN foreign_link ' .
- 'WHERE notice.profile_id = foreign_link.user_id ' .
- 'AND foreign_link.service = 2';
-
- // XXX: What should the limit be?
- //static function getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since) {
-
- return Notice::getStreamDirect($qry, 0, 1000, 0, 0, null, $since);
-}
-
function getFacebook()
{
$apikey = common_config('facebook', 'apikey');
@@ -52,3 +37,97 @@ function updateProfileBox($facebook, $flink, $notice) {
$fbaction->updateProfileBox($notice);
}
+function isFacebookBound($notice, $flink) {
+
+ // If the user does not want to broadcast to Facebook, move along
+ if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
+ common_log(LOG_INFO, "Skipping notice $notice->id " .
+ 'because user has FOREIGN_NOTICE_SEND bit off.');
+ return false;
+ }
+
+ $success = false;
+
+ // If it's not a reply, or if the user WANTS to send @-replies...
+ if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+
+ $success = true;
+
+ // The two condition below are deal breakers:
+
+ // Avoid a loop
+ if ($notice->source == 'Facebook') {
+ common_log(LOG_INFO, "Skipping notice $notice->id because its " .
+ 'source is Facebook.');
+ $success = false;
+ }
+
+ $facebook = getFacebook();
+ $fbuid = $flink->foreign_id;
+
+ try {
+
+ // Check to see if the user has given the FB app status update perms
+ $result = $facebook->api_client->
+ users_hasAppPermission('status_update', $fbuid);
+
+ if ($result != 1) {
+ $user = $flink->getUser();
+ $msg = "Can't send notice $notice->id to Facebook " .
+ "because user $user->nickname hasn't given the " .
+ 'Facebook app \'status_update\' permission.';
+ common_log(LOG_INFO, $msg);
+ $success = false;
+ }
+
+ } catch(FacebookRestClientException $e){
+ common_log(LOG_ERR, $e->getMessage());
+ $success = false;
+ }
+
+ }
+
+ return $success;
+
+}
+
+
+function facebookBroadcastNotice($notice)
+{
+ $facebook = getFacebook();
+ $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE);
+ $fbuid = $flink->foreign_id;
+
+ if (isFacebookBound($notice, $flink)) {
+
+ $status = null;
+
+ // Get the status 'verb' (prefix) the user has set
+ try {
+ $prefix = $facebook->api_client->
+ data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
+
+ $status = "$prefix $notice->content";
+
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+ }
+
+ // Okay, we're good to go!
+
+ try {
+ $facebook->api_client->users_setStatus($status, $fbuid, false, true);
+ updateProfileBox($facebook, $flink, $notice);
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+
+ // Should we remove flink if this fails?
+ }
+
+ }
+
+ return true;
+}
diff --git a/lib/featureduserssection.php b/lib/featureduserssection.php
index 2935d8363..aed94b1a5 100644
--- a/lib/featureduserssection.php
+++ b/lib/featureduserssection.php
@@ -86,4 +86,9 @@ class FeaturedUsersSection extends ProfileSection
{
return 'featured_users';
}
+
+ function moreUrl()
+ {
+ return common_local_url('featured');
+ }
}
diff --git a/lib/feed.php b/lib/feed.php
new file mode 100644
index 000000000..466926844
--- /dev/null
+++ b/lib/feed.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Data structure for info about syndication feeds (RSS 1.0, RSS 2.0, Atom)
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Data structure for feeds
+ *
+ * This structure is a helpful container for shipping around information about syndication feeds.
+ *
+ * @category Feed
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class Feed
+{
+ const RSS1 = 1;
+ const RSS2 = 2;
+ const ATOM = 3;
+ const FOAF = 4;
+
+ var $type = null;
+ var $url = null;
+ var $title = null;
+
+ function __construct($type, $url, $title)
+ {
+ $this->type = $type;
+ $this->url = $url;
+ $this->title = $title;
+ }
+
+ function mimeType()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ return 'application/rdf+xml';
+ case Feed::RSS2:
+ return 'application/rss+xml';
+ case Feed::ATOM:
+ return 'application/atom+xml';
+ case Feed::FOAF:
+ return 'application/rdf+xml';
+ default:
+ return null;
+ }
+ }
+
+ function typeName()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ return _('RSS 1.0');
+ case Feed::RSS2:
+ return _('RSS 2.0');
+ case Feed::ATOM:
+ return _('Atom');
+ case Feed::FOAF:
+ return _('FOAF');
+ default:
+ return null;
+ }
+ }
+
+ function rel()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ case Feed::RSS2:
+ case Feed::ATOM:
+ return 'alternate';
+ case Feed::FOAF:
+ return 'meta';
+ default:
+ return null;
+ }
+ }
+}
diff --git a/lib/feedlist.php b/lib/feedlist.php
index 47d909e96..927e43c33 100644
--- a/lib/feedlist.php
+++ b/lib/feedlist.php
@@ -50,7 +50,7 @@ if (!defined('LACONICA')) {
class FeedList extends Widget
{
var $action = null;
-
+
function __construct($action=null)
{
parent::__construct($action);
@@ -64,8 +64,8 @@ class FeedList extends Widget
$this->out->element('h2', null, _('Export data'));
$this->out->elementStart('ul', array('class' => 'xoxo'));
- foreach ($feeds as $key => $value) {
- $this->feedItem($feeds[$key]);
+ foreach ($feeds as $feed) {
+ $this->feedItem($feed);
}
$this->out->elementEnd('ul');
@@ -74,85 +74,27 @@ class FeedList extends Widget
function feedItem($feed)
{
- $nickname = $this->action->trimmed('nickname');
-
- switch($feed['item']) {
- case 'notices': default:
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's ".$feed['version']." notice feed";
- $feed['textContent'] = "RSS";
- break;
-
- case 'allrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for $nickname and friends";
- $feed['textContent'] = "RSS";
- break;
-
- case 'repliesrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for replies to $nickname";
- $feed['textContent'] = "RSS";
- break;
-
- case 'publicrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Public timeline ".$feed['version']." feed";
- $feed['textContent'] = "RSS";
- break;
+ $classname = null;
- case 'publicatom':
- $feed_classname = "atom";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Public timeline ".$feed['version']." feed";
- $feed['textContent'] = "Atom";
+ switch ($feed->type) {
+ case Feed::RSS1:
+ case Feed::RSS2:
+ $classname = 'rss';
break;
-
- case 'tagrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for this tag";
- $feed['textContent'] = "RSS";
+ case Feed::ATOM:
+ $classname = 'atom';
break;
-
- case 'favoritedrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Favorited ".$feed['version']." feed";
- $feed['textContent'] = "RSS";
- break;
-
- case 'foaf':
- $feed_classname = "foaf";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's FOAF file";
- $feed['textContent'] = "FOAF";
- break;
-
- case 'favoritesrss':
- $feed_classname = "favorites";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Feed for favorites of $nickname";
- $feed['textContent'] = "RSS";
- break;
-
- case 'usertimeline':
- $feed_classname = "atom";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's ".$feed['version']." notice feed";
- $feed['textContent'] = "Atom";
+ case Feed::FOAF:
+ $classname = 'foaf';
break;
}
+
$this->out->elementStart('li');
- $this->out->element('a', array('href' => $feed['href'],
- 'class' => $feed_classname,
- 'type' => $feed_mimetype,
- 'title' => $feed_title),
- $feed['textContent']);
+ $this->out->element('a', array('href' => $feed->url,
+ 'class' => $classname,
+ 'type' => $feed->mimeType(),
+ 'title' => $feed->title),
+ $feed->typeName());
$this->out->elementEnd('li');
}
}
diff --git a/lib/grouplist.php b/lib/grouplist.php
index 8f3b0abd8..8e2637fec 100644
--- a/lib/grouplist.php
+++ b/lib/grouplist.php
@@ -124,7 +124,7 @@ class GroupList extends Widget
if ($this->group->location) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
- $this->out->elementStart('dd', 'location');
+ $this->out->elementStart('dd', 'label');
$this->out->raw($this->highlight($this->group->location));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
@@ -151,7 +151,7 @@ class GroupList extends Widget
# If we're on a list with an owner (subscriptions or subscribers)...
- if ($user && $user->id == $this->owner->id) {
+ if (!empty($user) && !empty($this->owner) && $user->id == $this->owner->id) {
$this->showOwnerControls();
}
diff --git a/lib/groupsbymemberssection.php b/lib/groupsbymemberssection.php
index 4fa07a244..963e21f15 100644
--- a/lib/groupsbymemberssection.php
+++ b/lib/groupsbymemberssection.php
@@ -48,7 +48,7 @@ class GroupsByMembersSection extends GroupSection
$qry = 'SELECT user_group.*, count(*) as value ' .
'FROM user_group JOIN group_member '.
'ON user_group.id = group_member.group_id ' .
- 'GROUP BY user_group.id ' .
+ 'GROUP BY user_group.id,user_group.nickname,user_group.fullname,user_group.homepage,user_group.description,user_group.location,user_group.original_logo,user_group.homepage_logo,user_group.stream_logo,user_group.mini_logo,user_group.created,user_group.modified ' .
'ORDER BY value DESC ';
$limit = GROUPS_PER_SECTION;
diff --git a/lib/groupsbypostssection.php b/lib/groupsbypostssection.php
index a5e33a93d..325b4033f 100644
--- a/lib/groupsbypostssection.php
+++ b/lib/groupsbypostssection.php
@@ -48,7 +48,7 @@ class GroupsByPostsSection extends GroupSection
$qry = 'SELECT user_group.*, count(*) as value ' .
'FROM user_group JOIN group_inbox '.
'ON user_group.id = group_inbox.group_id ' .
- 'GROUP BY user_group.id ' .
+ 'GROUP BY user_group.id,user_group.nickname,user_group.fullname,user_group.homepage,user_group.description,user_group.location,user_group.original_logo,user_group.homepage_logo,user_group.stream_logo,user_group.mini_logo,user_group.created,user_group.modified ' .
'ORDER BY value DESC ';
$limit = GROUPS_PER_SECTION;
diff --git a/lib/grouptagcloudsection.php b/lib/grouptagcloudsection.php
index f05be85cb..5d68af28b 100644
--- a/lib/grouptagcloudsection.php
+++ b/lib/grouptagcloudsection.php
@@ -58,8 +58,14 @@ class GroupTagCloudSection extends TagCloudSection
function getTags()
{
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - notice_tag.created)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))';
+ }
+
$qry = 'SELECT notice_tag.tag, '.
- 'sum(exp(-(now() - notice_tag.created)/%s)) as weight ' .
+ $weightexpr . ' as weight ' .
'FROM notice_tag JOIN notice ' .
'ON notice_tag.notice_id = notice.id ' .
'JOIN group_inbox on group_inbox.notice_id = notice.id ' .
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index 7780b1c19..06603ac05 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -101,29 +101,32 @@ class HTMLOutputter extends XMLOutputter
$type = common_negotiate_type($cp, $sp);
if (!$type) {
- common_user_error(_('This page is not available in a '.
- 'media type you accept'), 406);
- exit(0);
+ throw new ClientException(_('This page is not available in a '.
+ 'media type you accept'), 406);
}
}
header('Content-Type: '.$type);
-
+
$this->extraHeaders();
$this->startXML('html',
'-//W3C//DTD XHTML 1.0 Strict//EN',
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
- // FIXME: correct language for interface
-
- $language = common_language();
+ $language = $this->getLanguage();
$this->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
'xml:lang' => $language,
'lang' => $language));
}
+ function getLanguage()
+ {
+ // FIXME: correct language for interface
+ return common_language();
+ }
+
/**
* Ends an HTML document
*
@@ -134,7 +137,7 @@ class HTMLOutputter extends XMLOutputter
$this->elementEnd('html');
$this->endXML();
}
-
+
/**
* To specify additional HTTP headers for the action
*
@@ -255,7 +258,7 @@ class HTMLOutputter extends XMLOutputter
foreach ($content as $value => $option) {
if ($value == $selected) {
$this->element('option', array('value' => $value,
- 'selected' => $value),
+ 'selected' => 'selected'),
$option);
} else {
$this->element('option', array('value' => $value), $option);
diff --git a/lib/imagefile.php b/lib/imagefile.php
index db344db8f..0c93b257e 100644
--- a/lib/imagefile.php
+++ b/lib/imagefile.php
@@ -68,17 +68,17 @@ class ImageFile
static function fromUpload($param='upload')
{
switch ($_FILES[$param]['error']) {
- case UPLOAD_ERR_OK: // success, jump out
+ case UPLOAD_ERR_OK: // success, jump out
break;
- case UPLOAD_ERR_INI_SIZE:
- case UPLOAD_ERR_FORM_SIZE:
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'), $this->maxFileSize()));
return;
- case UPLOAD_ERR_PARTIAL:
+ case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[$param]['tmp_name']);
throw new Exception(_('Partial upload.'));
return;
- default:
+ default:
throw new Exception(_('System error uploading file.'));
return;
}
@@ -113,6 +113,23 @@ class ImageFile
return;
}
+ // Don't crop/scale if it isn't necessary
+ if ($size === $this->width
+ && $size === $this->height
+ && $x === 0
+ && $y === 0
+ && $w === $this->width
+ && $h === $this->height) {
+
+ $outname = Avatar::filename($this->id,
+ image_type_to_extension($this->type),
+ $size,
+ common_timestamp());
+ $outpath = Avatar::path($outname);
+ @copy($this->filepath, $outpath);
+ return $outname;
+ }
+
switch ($this->type) {
case IMAGETYPE_GIF:
$image_src = imagecreatefromgif($this->filepath);
@@ -154,9 +171,9 @@ class ImageFile
imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $size, $size, $w, $h);
$outname = Avatar::filename($this->id,
- image_type_to_extension($this->type),
- $size,
- common_timestamp());
+ image_type_to_extension($this->type),
+ $size,
+ common_timestamp());
$outpath = Avatar::path($outname);
@@ -165,7 +182,7 @@ class ImageFile
imagegif($image_dest, $outpath);
break;
case IMAGETYPE_JPEG:
- imagejpeg($image_dest, $outpath);
+ imagejpeg($image_dest, $outpath, 100);
break;
case IMAGETYPE_PNG:
imagepng($image_dest, $outpath);
@@ -175,6 +192,9 @@ class ImageFile
return;
}
+ imagedestroy($image_src);
+ imagedestroy($image_dest);
+
return $outname;
}
@@ -209,12 +229,12 @@ class ImageFile
$num = substr($str, 0, -1);
switch(strtoupper($unit)){
- case 'G':
- $num *= 1024;
- case 'M':
- $num *= 1024;
- case 'K':
- $num *= 1024;
+ case 'G':
+ $num *= 1024;
+ case 'M':
+ $num *= 1024;
+ case 'K':
+ $num *= 1024;
}
return $num;
diff --git a/lib/jabber.php b/lib/jabber.php
index f41d984d6..3fbb3e1ab 100644
--- a/lib/jabber.php
+++ b/lib/jabber.php
@@ -114,7 +114,7 @@ function jabber_connect($resource=null)
try {
$conn->connect(true); // true = persistent connection
} catch (XMPPHP_Exception $e) {
- common_log(LOG_ERROR, $e->getMessage());
+ common_log(LOG_ERR, $e->getMessage());
return false;
}
@@ -186,6 +186,11 @@ function jabber_format_entry($profile, $notice)
$entry .= "<id>". $notice->uri . "</id>\n";
$entry .= "<published>".common_date_w3dtf($notice->created)."</published>\n";
$entry .= "<updated>".common_date_w3dtf($notice->modified)."</updated>\n";
+ if ($notice->reply_to) {
+ $replyurl = common_local_url('shownotice',
+ array('notice' => $notice->reply_to));
+ $entry .= "<link rel='related' href='" . $replyurl . "'/>\n";
+ }
$entry .= "</entry>\n";
$html = "\n<html xmlns='http://jabber.org/protocol/xhtml-im'>\n";
diff --git a/lib/jsonsearchresultslist.php b/lib/jsonsearchresultslist.php
new file mode 100644
index 000000000..0cdcf0c51
--- /dev/null
+++ b/lib/jsonsearchresultslist.php
@@ -0,0 +1,270 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * widget for displaying a list of notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * widget-like class for showing JSON search results
+ *
+ * @category Search
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ */
+
+class JSONSearchResultsList
+{
+ protected $notice; // protected attrs invisible to json_encode()
+ protected $rpp;
+
+ // The below attributes are carefully named so the JSON output from
+ // this obj matches the output from search.twitter.com
+
+ var $results;
+ var $since_id;
+ var $max_id;
+ var $refresh_url;
+ var $results_per_page;
+ var $completed_in;
+ var $page;
+ var $query;
+
+ /**
+ * constructor
+ *
+ * @param Notice $notice stream of notices from DB_DataObject
+ * @param string $query the original search query
+ * @param int $rpp the number of results to display per page
+ * @param int $page a page offset
+ * @param int $since_id only display notices newer than this
+ */
+
+ function __construct($notice, $query, $rpp, $page, $since_id = 0)
+ {
+ $this->notice = $notice;
+ $this->query = urlencode($query);
+ $this->results_per_page = $rpp;
+ $this->rpp = $rpp;
+ $this->page = $page;
+ $this->since_id = $since_id;
+ $this->results = array();
+ }
+
+ /**
+ * show the list of search results
+ *
+ * @return int $count of the search results listed.
+ */
+
+ function show()
+ {
+ $cnt = 0;
+
+ $time_start = microtime(true);
+
+ while ($this->notice->fetch() && $cnt <= $this->rpp) {
+ $cnt++;
+
+ // XXX: Hmmm. this depends on desc sort order
+ if (!$this->max_id) {
+ $this->max_id = (int)$this->notice->id;
+ }
+
+ if ($cnt > $this->rpp) {
+ break;
+ }
+
+ $item = new ResultItem($this->notice);
+ array_push($this->results, $item);
+ }
+
+ $time_end = microtime(true);
+ $this->completed_in = $time_end - $time_start;
+
+ // Set other attrs
+
+ $this->refresh_url = '?since_id=' . $this->max_id .
+ '&q=' . $this->query;
+
+ // pagination stuff
+
+ if ($cnt > $this->rpp) {
+ $this->next_page = '?page=' . ($this->page + 1) .
+ '&max_id=' . $this->max_id;
+ if ($this->rpp != 15) {
+ $this->next_page .= '&rpp=' . $this->rpp;
+ }
+ $this->next_page .= '&q=' . $this->query;
+ }
+
+ if ($this->page > 1) {
+ $this->previous_page = '?page=' . ($this->page - 1) .
+ '&max_id=' . $this->max_id;
+ if ($this->rpp != 15) {
+ $this->previous_page .= '&rpp=' . $this->rpp;
+ }
+ $this->previous_page .= '&q=' . $this->query;
+ }
+
+ print json_encode($this);
+
+ return $cnt;
+ }
+}
+
+/**
+ * widget for displaying a single JSON search result
+ *
+ * @category UI
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @see JSONSearchResultsList
+ */
+
+class ResultItem
+{
+ /** The notice this item is based on. */
+
+ protected $notice; // protected attrs invisible to json_encode()
+
+ /** The profile associated with the notice. */
+
+ protected $profile;
+
+ // The below attributes are carefully named so the JSON output from
+ // this obj matches the output from search.twitter.com
+
+ var $text;
+ var $to_user_id;
+ var $to_user;
+ var $from_user;
+ var $id;
+ var $from_user_id;
+ var $iso_language_code;
+ var $source;
+ var $profile_image_url;
+ var $created_at;
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($notice)
+ {
+ $this->notice = $notice;
+ $this->profile = $notice->getProfile();
+ $this->buildResult();
+ }
+
+ /**
+ * Build a search result object
+ *
+ * This populates the the result in preparation for JSON encoding.
+ *
+ * @return void
+ */
+
+ function buildResult()
+ {
+ $this->text = $this->notice->content;
+ $replier_profile = null;
+
+ if ($this->notice->reply_to) {
+ $reply = Notice::staticGet(intval($notice->reply_to));
+ if ($reply) {
+ $replier_profile = $reply->getProfile();
+ }
+ }
+
+ $this->to_user_id = ($replier_profile) ?
+ intval($replier_profile->id) : null;
+ $this->to_user = ($replier_profile) ?
+ $replier_profile->nickname : null;
+
+ $this->from_user = $this->profile->nickname;
+ $this->id = $this->notice->id;
+ $this->from_user_id = $this->profile->id;
+
+ $user = User::staticGet('id', $this->profile->id);
+
+ $this->iso_language_code = $this->user->language;
+
+ $this->source = $this->getSourceLink($this->notice->source);
+
+ $avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE);
+
+ $this->profile_image_url = ($avatar) ?
+ $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE);
+
+ $this->created_at = date('r', $this->notice->created);
+ }
+
+ /**
+ * Show the source of the notice
+ *
+ * Either the name (and link) of the API client that posted the notice,
+ * or one of other other channels.
+ *
+ * @param string $source the source of the Notice
+ *
+ * @return string a fully rendered source of the Notice
+ */
+
+ function getSourceLink($source)
+ {
+ $source_name = _($source);
+ switch ($source) {
+ case 'web':
+ case 'xmpp':
+ case 'mail':
+ case 'omb':
+ case 'api':
+ break;
+ default:
+ $ns = Notice_source::staticGet($source);
+ if ($ns) {
+ $source_name = '<a href="' . $ns->url . '">' . $ns->name . '</a>';
+ }
+ break;
+ }
+
+ return $source_name;
+ }
+
+}
diff --git a/lib/language.php b/lib/language.php
index a73b73f28..79e9030ae 100644
--- a/lib/language.php
+++ b/lib/language.php
@@ -94,40 +94,43 @@ function get_nice_language_list()
* Get a list of all languages that are enabled in the default config
*
* This should ONLY be called when setting up the default config in common.php.
- * Any other attempt to get a list of lanugages should instead call
+ * Any other attempt to get a list of languages should instead call
* common_config('site','languages')
*
* @return array mapping of language codes to language info
*/
function get_all_languages() {
return array(
- 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'),
- 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'),
- 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'),
- 'de' => array('q' => 0.5, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'),
- 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'),
- 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'),
- 'en-gb' => array('q' => 0.3, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
- 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'),
- 'es' => array('q' => 0.5, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'),
- 'fr-fr' => array('q' => 0.2, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'),
- 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'ltr'),
- 'it' => array('q' => 0.9, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'rtl'),
- 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'),
-# 'ko' => array('q' => 0, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'),
- 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'),
- 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (bokmal)', 'direction' => 'ltr'),
- 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'),
- 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'),
-# 'pt' => array('q' => 0, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'),
- 'pt-br' => array('q' => 0.7, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'),
- 'ru' => array('q' => 0.1, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'),
- 'sv' => array('q' => 0.9, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'),
- 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'),
- 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'),
- 'uk' => array('q' => 0.7, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'),
- 'vi' => array('q' => 0.7, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'),
- 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'),
- 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_hant', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'),
+ 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'),
+ 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'),
+ 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'),
+ 'de' => array('q' => 0.5, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'),
+ 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'),
+ 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'),
+ 'en-gb' => array('q' => 0.3, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
+ 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'),
+ 'es' => array('q' => 0.5, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'),
+ 'fi' => array('q' => 0.5, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'),
+ 'fr-fr' => array('q' => 0.2, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'),
+ 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'rtl'),
+ 'it' => array('q' => 0.9, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'ltr'),
+ 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'),
+# 'ko' => array('q' => 0, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'),
+ 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'),
+ 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'),
+ 'no' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'),
+ 'nn' => array('q' => 0.1, 'lang' => 'nn_NO', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'),
+ 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'),
+ 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'),
+# 'pt' => array('q' => 0, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'),
+ 'pt-br' => array('q' => 0.7, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'),
+ 'ru' => array('q' => 0.1, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'),
+ 'sv' => array('q' => 0.9, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'),
+ 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'),
+ 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'),
+ 'uk' => array('q' => 0.7, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'),
+ 'vi' => array('q' => 0.7, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'),
+ 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'),
+ 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_hant', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'),
);
}
diff --git a/lib/mail.php b/lib/mail.php
index b424d579f..9fa86de5c 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -246,7 +246,7 @@ function mail_subscribe_notify_profile($listenee, $other)
"\n".'Faithfully yours,'."\n".'%7$s.'."\n\n".
"----\n".
"Change your email address or ".
- "notification options at ".'%8$s\n'),
+ "notification options at ".'%8$s' ."\n"),
$long_name,
common_config('site', 'name'),
$other->profileurl,
@@ -573,3 +573,53 @@ function mail_notify_fave($other, $user, $notice)
common_init_locale();
mail_to_user($other, $subject, $body);
}
+
+/**
+ * notify a user that they have received an "attn:" message AKA "@-reply"
+ *
+ * @param User $user The user who recevied the notice
+ * @param Notice $notice The notice that was sent
+ *
+ * @return void
+ */
+
+function mail_notify_attn($user, $notice)
+{
+ if (!$user->email || !$user->emailnotifyattn) {
+ return;
+ }
+
+ $sender = $notice->getProfile();
+
+ $bestname = $sender->getBestName();
+
+ common_init_locale($user->language);
+
+ $subject = sprintf(_('%s sent a notice to your attention'), $bestname);
+
+ $body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
+ "The notice is here:\n\n".
+ "\t%3\$s\n\n" .
+ "It reads:\n\n".
+ "\t%4\$s\n\n" .
+ "You can reply back here:\n\n".
+ "\t%5\$s\n\n" .
+ "The list of all @-replies for you here:\n\n" .
+ "%6\$s\n\n" .
+ "Faithfully yours,\n" .
+ "%2\$s\n\n" .
+ "P.S. You can turn off these email notifications here: %7\$s\n"),
+ $bestname,
+ common_config('site', 'name'),
+ common_local_url('shownotice',
+ array('notice' => $notice->id)),
+ $notice->content,
+ common_local_url('newnotice',
+ array('replyto' => $sender->nickname)),
+ common_local_url('replies',
+ array('nickname' => $user->nickname)),
+ common_local_url('emailsettings'));
+
+ common_init_locale();
+ mail_to_user($user, $subject, $body);
+}
diff --git a/lib/noticesection.php b/lib/noticesection.php
index 97b517529..94c2738ef 100644
--- a/lib/noticesection.php
+++ b/lib/noticesection.php
@@ -73,6 +73,11 @@ class NoticeSection extends Section
function showNotice($notice)
{
$profile = $notice->getProfile();
+ if (empty($profile)) {
+ common_log(LOG_WARNING, sprintf("Notice %d has no profile",
+ $notice->id));
+ return;
+ }
$this->out->elementStart('li', 'hentry notice');
$this->out->elementStart('div', 'entry-title');
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
@@ -96,7 +101,7 @@ class NoticeSection extends Section
$this->out->elementStart('p', 'entry-content');
$this->out->raw($notice->rendered);
$this->out->elementEnd('p');
- if ($notice->value) {
+ if (!empty($notice->value)) {
$this->out->elementStart('p');
$this->out->text($notice->value);
$this->out->elementEnd('p');
diff --git a/lib/oauthstore.php b/lib/oauthstore.php
index 7ad3be20e..183164e17 100644
--- a/lib/oauthstore.php
+++ b/lib/oauthstore.php
@@ -54,16 +54,21 @@ class LaconicaOAuthDataStore extends OAuthDataStore
}
}
+ // http://oauth.net/core/1.0/#nonce
+ // "The Consumer SHALL then generate a Nonce value that is unique for
+ // all requests with that timestamp."
+
+ // XXX: It's not clear why the token is here
+
function lookup_nonce($consumer, $token, $nonce, $timestamp)
{
$n = new Nonce();
$n->consumer_key = $consumer->key;
- $n->tok = $token->key;
+ $n->ts = $timestamp;
$n->nonce = $nonce;
if ($n->find(true)) {
return true;
} else {
- $n->timestamp = $timestamp;
$n->created = DB_DataObject_Cast::dateTime();
$n->insert();
return false;
diff --git a/lib/omb.php b/lib/omb.php
index f2dbef5ba..befcf4666 100644
--- a/lib/omb.php
+++ b/lib/omb.php
@@ -206,7 +206,7 @@ function omb_post_notice_keys($notice, $postnoticeurl, $tk, $secret)
$result = $fetcher->post($req->get_normalized_http_url(),
$req->to_postdata(),
- array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
+ array('User-Agent: Laconica/' . LACONICA_VERSION));
common_debug('Got HTTP result "'.print_r($result,true).'"', __FILE__);
@@ -239,7 +239,7 @@ function omb_broadcast_profile($profile)
while ($sub->fetch()) {
$rp = Remote_profile::staticGet('id', $sub->subscriber);
if ($rp) {
- if (!$updated[$rp->updateprofileurl]) {
+ if (!array_key_exists($rp->updateprofileurl, $updated)) {
if (omb_update_profile($profile, $rp, $sub)) {
$updated[$rp->updateprofileurl] = true;
}
@@ -291,11 +291,13 @@ function omb_update_profile($profile, $remote_profile, $subscription)
common_debug('postdata = '.$req->to_postdata(), __FILE__);
$result = $fetcher->post($req->get_normalized_http_url(),
$req->to_postdata(),
- array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
+ array('User-Agent: Laconica/' . LACONICA_VERSION));
common_debug('Got HTTP result "'.print_r($result,true).'"', __FILE__);
- if ($result->status == 403) { # not authorized, don't send again
+ if (empty($result) || $result) {
+ common_debug("Unable to contact " . $req->get_normalized_http_url());
+ } else if ($result->status == 403) { # not authorized, don't send again
common_debug('403 result, deleting subscription', __FILE__);
$subscription->delete();
return false;
diff --git a/lib/openid.php b/lib/openid.php
index 860573702..3aa488b6d 100644
--- a/lib/openid.php
+++ b/lib/openid.php
@@ -64,6 +64,9 @@ function oid_set_last($openid_url)
function oid_get_last()
{
+ if (empty($_COOKIE[OPENID_COOKIE_KEY])) {
+ return null;
+ }
$openid_url = $_COOKIE[OPENID_COOKIE_KEY];
if ($openid_url && strlen($openid_url) > 0) {
return $openid_url;
@@ -157,7 +160,7 @@ function oid_authenticate($openid_url, $returnto, $immediate=false)
$auth_request->addExtension($sreg_request);
}
- $trust_root = common_local_url('public');
+ $trust_root = common_path('');
$process_url = common_local_url($returnto);
if ($auth_request->shouldSendRedirect()) {
diff --git a/lib/peoplesearchresults.php b/lib/peoplesearchresults.php
new file mode 100644
index 000000000..f8ab7cf3b
--- /dev/null
+++ b/lib/peoplesearchresults.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * People search results class
+ *
+ * PHP version 5
+ *
+ * @category Widget
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Robin Millette <millette@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/profilelist.php';
+
+/**
+ * People search results class
+ *
+ * Derivative of ProfileList with specialization for highlighting search terms.
+ *
+ * @category Widget
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Robin Millette <millette@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * @see PeoplesearchAction
+ */
+
+class PeopleSearchResults extends ProfileList
+{
+ var $terms = null;
+ var $pattern = null;
+
+ function __construct($profile, $terms, $action)
+ {
+ parent::__construct($profile, $terms, $action);
+ $this->terms = array_map('preg_quote',
+ array_map('htmlspecialchars', $terms));
+ $this->pattern = '/('.implode('|',$terms).')/i';
+ }
+
+ function highlight($text)
+ {
+ return preg_replace($this->pattern, '<strong>\\1</strong>', htmlspecialchars($text));
+ }
+
+ function isReadOnly()
+ {
+ return true;
+ }
+}
+
diff --git a/lib/personaltagcloudsection.php b/lib/personaltagcloudsection.php
index 0882822db..978153a84 100644
--- a/lib/personaltagcloudsection.php
+++ b/lib/personaltagcloudsection.php
@@ -58,8 +58,14 @@ class PersonalTagCloudSection extends TagCloudSection
function getTags()
{
- $qry = 'SELECT notice_tag.tag, '.
- 'sum(exp(-(now() - notice_tag.created)/%s)) as weight ' .
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - notice_tag.created)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))';
+ }
+
+ $qry = 'SELECT notice_tag.tag, '.
+ $weightexpr . ' as weight ' .
'FROM notice_tag JOIN notice ' .
'ON notice_tag.notice_id = notice.id ' .
'WHERE notice.profile_id = %d ' .
diff --git a/lib/ping.php b/lib/ping.php
new file mode 100644
index 000000000..32c0b9806
--- /dev/null
+++ b/lib/ping.php
@@ -0,0 +1,79 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+function ping_broadcast_notice($notice) {
+ if (!$notice->is_local) {
+ return;
+ }
+
+ # Array of servers, URL => type
+ $notify = common_config('ping', 'notify');
+ $profile = $notice->getProfile();
+ $tags = ping_notice_tags($notice);
+
+ foreach ($notify as $notify_url => $type) {
+ switch ($type) {
+ case 'xmlrpc':
+ case 'extended':
+ $req = xmlrpc_encode_request('weblogUpdates.ping',
+ array($profile->nickname, # site name
+ common_local_url('showstream',
+ array('nickname' => $profile->nickname)),
+ common_local_url('shownotice',
+ array('notice' => $notice->id)),
+ common_local_url('userrss',
+ array('nickname' => $profile->nickname)),
+ $tags));
+
+ # We re-use this tool's fetcher, since it's pretty good
+
+ $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
+
+ if (!$fetcher) {
+ common_log(LOG_WARNING, 'Failed to initialize Yadis fetcher.', __FILE__);
+ return false;
+ }
+
+ $result = $fetcher->post($notify_url,
+ $req);
+
+ case 'get':
+ case 'post':
+ default:
+ common_log(LOG_WARNING, 'Unknown notify type for ' . $notify_url . ': ' . $type);
+ }
+ }
+}
+
+function ping_notice_tags($notice) {
+ $tag = new Notice_tag();
+ $tag->notice_id = $notice->id;
+ $tags = array();
+ if ($tag->find()) {
+ while ($tag->fetch()) {
+ $tags[] = $tag->tag;
+ }
+ $tag->free();
+ unset($tag);
+ return implode('|', $tags);
+ }
+ return NULL;
+} \ No newline at end of file
diff --git a/lib/plugin.php b/lib/plugin.php
new file mode 100644
index 000000000..7b2436e54
--- /dev/null
+++ b/lib/plugin.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Utility class for plugins
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Base class for plugins
+ *
+ * A base class for Laconica plugins. Mostly a light wrapper around
+ * the Event framework.
+ *
+ * Subclasses of Plugin will automatically handle an event if they define
+ * a method called "onEventName". (Well, OK -- only if they call parent::__construct()
+ * in their constructors.)
+ *
+ * They will also automatically handle the InitializePlugin and CleanupPlugin with the
+ * initialize() and cleanup() methods, respectively.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Event
+ */
+
+class Plugin
+{
+ function __construct()
+ {
+ Event::addHandler('InitializePlugin', array($this, 'initialize'));
+ Event::addHandler('CleanupPlugin', array($this, 'cleanup'));
+
+ foreach (get_class_methods($this) as $method) {
+ if (mb_substr($method, 0, 2) == 'on') {
+ Event::addHandler(mb_substr($method, 2), array($this, $method));
+ }
+ }
+ }
+
+ function initialize()
+ {
+ return true;
+ }
+
+ function cleanup()
+ {
+ return true;
+ }
+}
diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php
index 5734d8001..0505f0fa9 100644
--- a/lib/popularnoticesection.php
+++ b/lib/popularnoticesection.php
@@ -31,8 +31,6 @@ if (!defined('LACONICA')) {
exit(1);
}
-define('NOTICES_PER_SECTION', 5);
-
/**
* Base class for sections showing lists of notices
*
@@ -50,10 +48,18 @@ class PopularNoticeSection extends NoticeSection
{
function getNotices()
{
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - fave.modified) / %s))';
+ }
+
$qry = 'SELECT notice.*, '.
- 'sum(exp(-(now() - fave.modified) / %s)) as weight ' .
+ $weightexpr . ' as weight ' .
'FROM notice JOIN fave ON notice.id = fave.notice_id ' .
- 'GROUP BY fave.notice_id ' .
+ 'GROUP BY notice.id,notice.profile_id,notice.content,notice.uri,' .
+ 'notice.rendered,notice.url,notice.created,notice.modified,' .
+ 'notice.reply_to,notice.is_local,notice.source ' .
'ORDER BY weight DESC';
$offset = 0;
@@ -80,4 +86,9 @@ class PopularNoticeSection extends NoticeSection
{
return 'popular_notices';
}
+
+ function moreUrl()
+ {
+ return common_local_url('favorited');
+ }
}
diff --git a/lib/profilelist.php b/lib/profilelist.php
index 4d924b039..c2040fbc2 100644
--- a/lib/profilelist.php
+++ b/lib/profilelist.php
@@ -34,8 +34,6 @@ if (!defined('LACONICA')) {
require_once INSTALLDIR.'/lib/widget.php';
-define('PROFILES_PER_PAGE', 20);
-
/**
* Widget to show a list of profiles
*
@@ -123,7 +121,7 @@ class ProfileList extends Widget
if ($this->profile->location) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
- $this->out->elementStart('dd', 'location');
+ $this->out->elementStart('dd', 'label');
$this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
diff --git a/lib/publicgroupnav.php b/lib/publicgroupnav.php
index d72475e20..485d25e20 100644
--- a/lib/publicgroupnav.php
+++ b/lib/publicgroupnav.php
@@ -39,6 +39,7 @@ require_once INSTALLDIR.'/lib/widget.php';
* @category Output
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*
@@ -73,23 +74,26 @@ class PublicGroupNav extends Widget
$this->action->elementStart('ul', array('class' => 'nav'));
- $this->out->menuItem(common_local_url('public'), _('Public'),
- _('Public timeline'), $action_name == 'public', 'nav_timeline_public');
+ if (Event::handle('StartPublicGroupNav', array($this))) {
+ $this->out->menuItem(common_local_url('public'), _('Public'),
+ _('Public timeline'), $action_name == 'public', 'nav_timeline_public');
- $this->out->menuItem(common_local_url('groups'), _('Groups'),
- _('User groups'), $action_name == 'groups', 'nav_groups');
+ $this->out->menuItem(common_local_url('groups'), _('Groups'),
+ _('User groups'), $action_name == 'groups', 'nav_groups');
- $this->out->menuItem(common_local_url('publictagcloud'), _('Recent tags'),
- _('Recent tags'), $action_name == 'publictagcloud', 'nav_recent-tags');
+ $this->out->menuItem(common_local_url('publictagcloud'), _('Recent tags'),
+ _('Recent tags'), $action_name == 'publictagcloud', 'nav_recent-tags');
- if (count(common_config('nickname', 'featured')) > 0) {
- $this->out->menuItem(common_local_url('featured'), _('Featured'),
- _('Featured users'), $action_name == 'featured', 'nav_featured');
- }
+ if (count(common_config('nickname', 'featured')) > 0) {
+ $this->out->menuItem(common_local_url('featured'), _('Featured'),
+ _('Featured users'), $action_name == 'featured', 'nav_featured');
+ }
- $this->out->menuItem(common_local_url('favorited'), _('Popular'),
- _("Popular notices"), $action_name == 'favorited', 'nav_timeline_favorited');
+ $this->out->menuItem(common_local_url('favorited'), _('Popular'),
+ _("Popular notices"), $action_name == 'favorited', 'nav_timeline_favorited');
+ Event::handle('EndPublicGroupNav', array($this));
+ }
$this->action->elementEnd('ul');
}
}
diff --git a/lib/router.php b/lib/router.php
new file mode 100644
index 000000000..52051fdb5
--- /dev/null
+++ b/lib/router.php
@@ -0,0 +1,434 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * URL routing utilities
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category URL
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once 'Net/URL/Mapper.php';
+
+/**
+ * URL Router
+ *
+ * Cheap wrapper around Net_URL_Mapper
+ *
+ * @category URL
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class Router
+{
+ var $m = null;
+ static $inst = null;
+ static $bare = array('requesttoken', 'accesstoken', 'userauthorization',
+ 'postnotice', 'updateprofile', 'finishremotesubscribe',
+ 'finishopenidlogin', 'finishaddopenid');
+
+ static function get()
+ {
+ if (!Router::$inst) {
+ Router::$inst = new Router();
+ }
+ return Router::$inst;
+ }
+
+ function __construct()
+ {
+ if (!$this->m) {
+ $this->m = $this->initialize();
+ }
+ }
+
+ function initialize() {
+
+ $m = Net_URL_Mapper::getInstance();
+
+ // In the "root"
+
+ $m->connect('', array('action' => 'public'));
+ $m->connect('rss', array('action' => 'publicrss'));
+ $m->connect('xrds', array('action' => 'publicxrds'));
+ $m->connect('featuredrss', array('action' => 'featuredrss'));
+ $m->connect('favoritedrss', array('action' => 'favoritedrss'));
+ $m->connect('opensearch/people', array('action' => 'opensearch',
+ 'type' => 'people'));
+ $m->connect('opensearch/notice', array('action' => 'opensearch',
+ 'type' => 'notice'));
+
+ // docs
+
+ $m->connect('doc/:title', array('action' => 'doc'));
+
+ // facebook
+
+ $m->connect('facebook', array('action' => 'facebookhome'));
+ $m->connect('facebook/index.php', array('action' => 'facebookhome'));
+ $m->connect('facebook/settings.php', array('action' => 'facebooksettings'));
+ $m->connect('facebook/invite.php', array('action' => 'facebookinvite'));
+ $m->connect('facebook/remove', array('action' => 'facebookremove'));
+
+ // main stuff is repetitive
+
+ $main = array('login', 'logout', 'register', 'subscribe',
+ 'unsubscribe', 'confirmaddress', 'recoverpassword',
+ 'invite', 'favor', 'disfavor', 'sup',
+ 'block');
+
+ foreach ($main as $a) {
+ $m->connect('main/'.$a, array('action' => $a));
+ }
+
+ $m->connect('main/tagother/:id', array('action' => 'tagother'));
+
+ // these take a code
+
+ foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
+ $m->connect('main/'.$c.'/:code', array('action' => $c));
+ }
+
+ // exceptional
+
+ $m->connect('main/openid', array('action' => 'openidlogin'));
+ $m->connect('main/remote', array('action' => 'remotesubscribe'));
+ $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+'));
+
+ foreach (Router::$bare as $action) {
+ $m->connect('index.php?action=' . $action, array('action' => $action));
+ }
+
+ // settings
+
+ foreach (array('profile', 'avatar', 'password', 'openid', 'im',
+ 'email', 'sms', 'twitter', 'other') as $s) {
+ $m->connect('settings/'.$s, array('action' => $s.'settings'));
+ }
+
+ // search
+
+ foreach (array('group', 'people', 'notice') as $s) {
+ $m->connect('search/'.$s, array('action' => $s.'search'));
+ $m->connect('search/'.$s.'?q=:q', array('action' => $s.'search'), array('q' => '.+'));
+ }
+
+ $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
+
+ // notice
+
+ $m->connect('notice/new', array('action' => 'newnotice'));
+ $m->connect('notice/new?replyto=:replyto',
+ array('action' => 'newnotice'),
+ array('replyto' => '[A-Za-z0-9_-]+'));
+ $m->connect('notice/:notice',
+ array('action' => 'shownotice'),
+ array('notice' => '[0-9]+'));
+ $m->connect('notice/delete', array('action' => 'deletenotice'));
+ $m->connect('notice/delete/:notice',
+ array('action' => 'deletenotice'),
+ array('notice' => '[0-9]+'));
+
+ $m->connect('message/new', array('action' => 'newmessage'));
+ $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '[A-Za-z0-9_-]'));
+ $m->connect('message/:message',
+ array('action' => 'showmessage'),
+ array('message' => '[0-9]+'));
+
+ $m->connect('user/:id',
+ array('action' => 'userbyid'),
+ array('id' => '[0-9]+'));
+
+ $m->connect('tags/', array('action' => 'publictagcloud'));
+ $m->connect('tag/', array('action' => 'publictagcloud'));
+ $m->connect('tags', array('action' => 'publictagcloud'));
+ $m->connect('tag', array('action' => 'publictagcloud'));
+ $m->connect('tag/:tag/rss',
+ array('action' => 'tagrss'),
+ array('tag' => '[a-zA-Z0-9]+'));
+ $m->connect('tag/:tag',
+ array('action' => 'tag'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('peopletag/:tag',
+ array('action' => 'peopletag'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('featured/', array('action' => 'featured'));
+ $m->connect('featured', array('action' => 'featured'));
+ $m->connect('favorited/', array('action' => 'favorited'));
+ $m->connect('favorited', array('action' => 'favorited'));
+
+ // groups
+
+ $m->connect('group/new', array('action' => 'newgroup'));
+
+ foreach (array('edit', 'join', 'leave') as $v) {
+ $m->connect('group/:nickname/'.$v,
+ array('action' => $v.'group'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+ }
+
+ foreach (array('members', 'logo', 'rss') as $n) {
+ $m->connect('group/:nickname/'.$n,
+ array('action' => 'group'.$n),
+ array('nickname' => '[a-zA-Z0-9]+'));
+ }
+
+ $m->connect('group/:id/id',
+ array('action' => 'groupbyid'),
+ array('id' => '[0-9]+'));
+
+ $m->connect('group/:nickname',
+ array('action' => 'showgroup'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+
+ $m->connect('group/', array('action' => 'groups'));
+ $m->connect('group', array('action' => 'groups'));
+ $m->connect('groups/', array('action' => 'groups'));
+ $m->connect('groups', array('action' => 'groups'));
+
+ // Twitter-compatible API
+
+ // statuses API
+
+ $m->connect('api/statuses/:method',
+ array('action' => 'api',
+ 'apiaction' => 'statuses'),
+ array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+
+ $m->connect('api/statuses/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses'),
+ array('method' => '(user_timeline|friends_timeline|show|destroy|friends|followers)'));
+
+ // users
+
+ $m->connect('api/users/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'users'),
+ array('method' => 'show(\.(xml|json))?'));
+
+ $m->connect('api/users/:method',
+ array('action' => 'api',
+ 'apiaction' => 'users'),
+ array('method' => 'show(\.(xml|json))?'));
+
+ // direct messages
+
+ foreach (array('xml', 'json') as $e) {
+ $m->connect('api/direct_messages/new.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages',
+ 'method' => 'create.'.$e));
+ }
+
+ foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+ $m->connect('api/direct_messages.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages',
+ 'method' => 'direct_messages.'.$e));
+ }
+
+ foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+ $m->connect('api/direct_message/sent.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages',
+ 'method' => 'sent.'.$e));
+ }
+
+ $m->connect('api/direct_messages/destroy/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages'));
+
+ // friendships
+
+ $m->connect('api/friendships/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'friendships'),
+ array('method' => '(create|destroy)'));
+
+ $m->connect('api/friendships/:method',
+ array('action' => 'api',
+ 'apiaction' => 'friendships'),
+ array('method' => 'exists(\.(xml|json|rss|atom))'));
+
+ // Social graph
+
+ $m->connect('api/friends/ids/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'friendsIDs'));
+
+ foreach (array('xml', 'json') as $e) {
+ $m->connect('api/friends/ids.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'friendsIDs.'.$e));
+ }
+
+ $m->connect('api/followers/ids/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'followersIDs'));
+
+ foreach (array('xml', 'json') as $e) {
+ $m->connect('api/followers/ids.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'followersIDs.'.$e));
+ }
+
+ // account
+
+ $m->connect('api/account/:method',
+ array('action' => 'api',
+ 'apiaction' => 'account'));
+
+ // favorites
+
+ $m->connect('api/favorites/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'));
+
+ $m->connect('api/favorites/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites',
+ 'method' => 'favorites'));
+
+ foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+ $m->connect('api/favorites.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'favorites',
+ 'method' => 'favorites.'.$e));
+ }
+
+ // notifications
+
+ $m->connect('api/notifications/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'));
+
+ // blocks
+
+ $m->connect('api/blocks/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'blocks'));
+
+ // help
+
+ $m->connect('api/help/:method',
+ array('action' => 'api',
+ 'apiaction' => 'help'));
+
+ // laconica
+
+ $m->connect('api/laconica/:method',
+ array('action' => 'api',
+ 'apiaction' => 'laconica'));
+
+ // search
+ $m->connect('api/search.atom', array('action' => 'twitapisearchatom'));
+ $m->connect('api/search.json', array('action' => 'twitapisearchjson'));
+ $m->connect('api/trends.json', array('action' => 'twitapitrends'));
+
+ // user stuff
+
+ foreach (array('subscriptions', 'subscribers',
+ 'nudge', 'xrds', 'all', 'foaf',
+ 'replies', 'inbox', 'outbox', 'microsummary') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => $a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('subscriptions', 'subscribers') as $a) {
+ $m->connect(':nickname/'.$a.'/:tag',
+ array('action' => $a),
+ array('tag' => '[a-zA-Z0-9]+',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('rss', 'groups') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => 'user'.$a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('all', 'replies', 'favorites') as $a) {
+ $m->connect(':nickname/'.$a.'/rss',
+ array('action' => $a.'rss'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ $m->connect(':nickname/favorites',
+ array('action' => 'showfavorites'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ $m->connect(':nickname/avatar/:size',
+ array('action' => 'avatarbynickname'),
+ array('size' => '(original|96|48|24)',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ $m->connect(':nickname',
+ array('action' => 'showstream'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ Event::handle('RouterInitialized', array($m));
+
+ return $m;
+ }
+
+ function map($path)
+ {
+ try {
+ $match = $this->m->match($path);
+ } catch (Net_URL_Mapper_InvalidException $e) {
+ common_log(LOG_ERR, "Problem getting route for $path - " .
+ $e->getMessage());
+ $cac = new ClientErrorAction("Page not found.", 404);
+ $cac->showPage();
+ }
+
+ return $match;
+ }
+
+ function build($action, $args=null, $params=null, $fragment=null)
+ {
+ $action_arg = array('action' => $action);
+
+ if ($args) {
+ $args = array_merge($action_arg, $args);
+ } else {
+ $args = $action_arg;
+ }
+
+ return $this->m->generate($args, $params, $fragment);
+ }
+}
diff --git a/lib/rssaction.php b/lib/rssaction.php
index 131e8ac65..66c2d9e8c 100644
--- a/lib/rssaction.php
+++ b/lib/rssaction.php
@@ -38,6 +38,7 @@ class Rss10Action extends Action
var $creators = array();
var $limit = DEFAULT_RSS_LIMIT;
+ var $notices = null;
/**
* Constructor
@@ -93,6 +94,9 @@ class Rss10Action extends Action
function handle($args)
{
+ // Get the list of notices
+ $this->notices = $this->getNotices();
+ // Parent handling, including cache check
parent::handle($args);
$this->showRss($this->limit);
}
@@ -258,5 +262,25 @@ class Rss10Action extends Action
{
$this->elementEnd('rdf:RDF');
}
+
+ /**
+ * When was this page last modified?
+ *
+ */
+
+ function lastModified()
+ {
+ if (empty($this->notices)) {
+ return null;
+ }
+
+ if (count($this->notices) == 0) {
+ return null;
+ }
+
+ // FIXME: doesn't handle modified profiles, avatars, deleted notices
+
+ return strtotime($this->notices[0]->created);
+ }
}
diff --git a/lib/searchaction.php b/lib/searchaction.php
index fdfb8dc5a..df6876445 100644
--- a/lib/searchaction.php
+++ b/lib/searchaction.php
@@ -79,10 +79,11 @@ class SearchAction extends Action
function showTop($arr=null)
{
+ $error = null;
if ($arr) {
$error = $arr[1];
}
- if ($error) {
+ if (!empty($error)) {
$this->element('p', 'error', $error);
} else {
$instr = $this->getInstructions();
diff --git a/lib/section.php b/lib/section.php
index 0c32ddcf8..d14575086 100644
--- a/lib/section.php
+++ b/lib/section.php
@@ -103,6 +103,6 @@ class Section extends Widget
function moreTitle()
{
- return null;
+ return _('More...');
}
}
diff --git a/lib/servererroraction.php b/lib/servererroraction.php
index 80a3fdd7b..595dcf147 100644
--- a/lib/servererroraction.php
+++ b/lib/servererroraction.php
@@ -42,7 +42,7 @@ require_once INSTALLDIR.'/lib/error.php';
* says that 500 errors should be treated similarly to 400 errors, and
* it's easier to give an HTML response. Maybe we can customize these
* to display some funny animal cartoons. If not, we can probably role
- * these classes up into a single class.
+ * these classes up into a single class.
*
* See: http://tools.ietf.org/html/rfc2616#section-10
*
@@ -57,19 +57,19 @@ class ServerErrorAction extends ErrorAction
function __construct($message='Error', $code=500)
{
parent::__construct($message, $code);
-
+
$this->status = array(500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported');
-
+
$this->default = 500;
}
// XXX: Should these error actions even be invokable via URI?
-
+
function handle($args)
{
parent::handle($args);
@@ -81,12 +81,16 @@ class ServerErrorAction extends ErrorAction
}
$this->message = $this->trimmed('message');
-
+
if (!$this->message) {
- $this->message = "Server Error $this->code";
- }
+ $this->message = "Server Error $this->code";
+ }
$this->showPage();
}
-
+
+ function title()
+ {
+ return $this->status[$this->code];
+ }
}
diff --git a/lib/serverexception.php b/lib/serverexception.php
new file mode 100644
index 000000000..b8ed6846e
--- /dev/null
+++ b/lib/serverexception.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * class for a server exception (user error)
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Class for server exceptions
+ *
+ * Subclass of PHP Exception for server errors. The user typically can't fix these.
+ *
+ * @category Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class ServerException extends Exception
+{
+ public function __construct($message = null, $code = 400) {
+ parent::__construct($message, $code);
+ }
+
+ public function __toString() {
+ return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
+ }
+}
diff --git a/lib/twitter.php b/lib/twitter.php
index 197298549..deb6fd276 100644
--- a/lib/twitter.php
+++ b/lib/twitter.php
@@ -19,6 +19,8 @@
if (!defined('LACONICA')) { exit(1); }
+define("TWITTER_SERVICE", 1); // Twitter is foreign_service ID 1
+
function get_twitter_data($uri, $screen_name, $password)
{
@@ -28,14 +30,13 @@ function get_twitter_data($uri, $screen_name, $password)
CURLOPT_FAILONERROR => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
- # CURLOPT_USERAGENT => "identi.ca",
+ CURLOPT_USERAGENT => "Laconica",
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
# Twitter is strict about accepting invalid "Expect" headers
CURLOPT_HTTPHEADER => array('Expect:')
);
-
$ch = curl_init($uri);
curl_setopt_array($ch, $options);
$data = curl_exec($ch);
@@ -95,7 +96,7 @@ function add_twitter_user($twitter_id, $screen_name)
$fuser->nickname = $screen_name;
$fuser->uri = 'http://twitter.com/' . $screen_name;
$fuser->id = $twitter_id;
- $fuser->service = 1; // Twitter
+ $fuser->service = TWITTER_SERVICE; // Twitter
$fuser->created = common_sql_now();
$result = $fuser->insert();
@@ -206,3 +207,93 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password)
return true;
}
+function is_twitter_bound($notice, $flink) {
+
+ // Check to see if notice should go to Twitter
+ if (($flink->noticesync & FOREIGN_NOTICE_SEND)) {
+
+ // If it's not a Twitter-style reply, or if the user WANTS to send replies.
+ if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function broadcast_twitter($notice)
+{
+ global $config;
+ $success = true;
+
+ $flink = Foreign_link::getByUserID($notice->profile_id,
+ TWITTER_SERVICE);
+
+ // XXX: Not sure WHERE to check whether a notice should go to
+ // Twitter. Should we even put in the queue if it shouldn't? --Zach
+ if (is_twitter_bound($notice, $flink)) {
+
+ $fuser = $flink->getForeignUser();
+ $twitter_user = $fuser->nickname;
+ $twitter_password = $flink->credentials;
+ $uri = 'http://www.twitter.com/statuses/update.json';
+
+ // XXX: Hack to get around PHP cURL's use of @ being a a meta character
+ $statustxt = preg_replace('/^@/', ' @', $notice->content);
+
+ $options = array(
+ CURLOPT_USERPWD => "$twitter_user:$twitter_password",
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS =>
+ array(
+ 'status' => $statustxt,
+ 'source' => $config['integration']['source']
+ ),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FAILONERROR => true,
+ CURLOPT_HEADER => false,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_USERAGENT => "Laconica",
+ CURLOPT_CONNECTTIMEOUT => 120, // XXX: How long should this be?
+ CURLOPT_TIMEOUT => 120,
+
+ # Twitter is strict about accepting invalid "Expect" headers
+ CURLOPT_HTTPHEADER => array('Expect:')
+ );
+
+ $ch = curl_init($uri);
+ curl_setopt_array($ch, $options);
+ $data = curl_exec($ch);
+ $errmsg = curl_error($ch);
+
+ if ($errmsg) {
+ common_debug("cURL error: $errmsg - " .
+ "trying to send notice for $twitter_user.",
+ __FILE__);
+ $success = false;
+ }
+
+ curl_close($ch);
+
+ if (!$data) {
+ common_debug("No data returned by Twitter's " .
+ "API trying to send update for $twitter_user",
+ __FILE__);
+ $success = false;
+ }
+
+ // Twitter should return a status
+ $status = json_decode($data);
+
+ if (!$status->id) {
+ common_debug("Unexpected data returned by Twitter " .
+ " API trying to send update for $twitter_user",
+ __FILE__);
+ $success = false;
+ }
+ }
+
+ return $success;
+}
+
diff --git a/lib/twitterapi.php b/lib/twitterapi.php
index a4d183fcd..1de169a0b 100644
--- a/lib/twitterapi.php
+++ b/lib/twitterapi.php
@@ -24,11 +24,33 @@ class TwitterapiAction extends Action
var $auth_user;
+ /**
+ * Initialization.
+ *
+ * @param array $args Web and URL arguments
+ *
+ * @return boolean false if user doesn't exist
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ return true;
+ }
+
+ /**
+ * Handle a request
+ *
+ * @param array $args Arguments from $_REQUEST
+ *
+ * @return void
+ */
+
function handle($args)
{
parent::handle($args);
}
-
+
function twitter_user_array($profile, $get_notice=false)
{
@@ -60,20 +82,34 @@ class TwitterapiAction extends Action
function twitter_status_array($notice, $include_user=true)
{
-
$profile = $notice->getProfile();
$twitter_status = array();
$twitter_status['text'] = $notice->content;
$twitter_status['truncated'] = 'false'; # Not possible on Laconica
$twitter_status['created_at'] = $this->date_twitter($notice->created);
- $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? intval($notice->reply_to) : null;
+ $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
+ intval($notice->reply_to) : null;
$twitter_status['source'] = $this->source_link($notice->source);
$twitter_status['id'] = intval($notice->id);
- $twitter_status['in_reply_to_user_id'] = ($notice->reply_to) ? $this->replier_by_reply(intval($notice->reply_to)) : null;
+
+ $replier_profile = null;
+
+ if ($notice->reply_to) {
+ $reply = Notice::staticGet(intval($notice->reply_to));
+ if ($reply) {
+ $replier_profile = $reply->getProfile();
+ }
+ }
+
+ $twitter_status['in_reply_to_user_id'] =
+ ($replier_profile) ? intval($replier_profile->id) : null;
+ $twitter_status['in_reply_to_screen_name'] =
+ ($replier_profile) ? $replier_profile->nickname : null;
if (isset($this->auth_user)) {
- $twitter_status['favorited'] = ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
+ $twitter_status['favorited'] =
+ ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
} else {
$twitter_status['favorited'] = 'false';
}
@@ -137,7 +173,6 @@ class TwitterapiAction extends Action
function twitter_dmsg_array($message)
{
-
$twitter_dm = array();
$from_profile = $message->getFrom();
@@ -387,22 +422,6 @@ class TwitterapiAction extends Action
return date("D M d G:i:s O Y", $t);
}
- function replier_by_reply($reply_id)
- {
- $notice = Notice::staticGet($reply_id);
- if ($notice) {
- $profile = $notice->getProfile();
- if ($profile) {
- return intval($profile->id);
- } else {
- common_debug('Can\'t find a profile for notice: ' . $notice->id, __FILE__);
- }
- } else {
- common_debug("Can't get notice: $reply_id", __FILE__);
- }
- return null;
- }
-
// XXX: Candidate for a general utility method somewhere?
function count_subscriptions($profile)
{
diff --git a/lib/util.php b/lib/util.php
index 7ce4e229e..ca8376f28 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -81,7 +81,7 @@ function common_language()
// If there is a user logged in and they've set a language preference
// then return that one...
- if (common_logged_in()) {
+ if (_have_config() && common_logged_in()) {
$user = common_current_user();
$user_language = $user->language;
if ($user_language)
@@ -315,6 +315,10 @@ function common_current_user()
{
global $_cur;
+ if (!_have_config()) {
+ return null;
+ }
+
if ($_cur === false) {
if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
@@ -394,32 +398,32 @@ function common_render_text($text)
function common_replace_urls_callback($text, $callback) {
// Start off with a regex
- $regex = '#
- (?:
- (?:
- (?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|xmpp|irc)://
- |
- (?:mailto|aim|tel):
- )
- [^.\s]+\.[^\s]+
- |
- (?:[^.\s/:]+\.)+
- (?:museum|travel|[a-z]{2,4})
- (?:[:/][^\s]*)?
- )
- #ix';
+ $regex = '#'.
+ '(?:'.
+ '(?:'.
+ '(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|xmpp|irc)://'.
+ '|'.
+ '(?:mailto|aim|tel):'.
+ ')'.
+ '[^.\s]+\.[^\s]+'.
+ '|'.
+ '(?:[^.\s/:]+\.)+'.
+ '(?:museum|travel|[a-z]{2,4})'.
+ '(?:[:/][^\s]*)?'.
+ ')'.
+ '#ix';
preg_match_all($regex, $text, $matches);
// Then clean up what the regex left behind
$offset = 0;
- foreach($matches[0] as $url) {
- $url = htmlspecialchars_decode($url);
+ foreach($matches[0] as $orig_url) {
+ $url = htmlspecialchars_decode($orig_url);
// Make sure we didn't pick up an email address
if (preg_match('#^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$#i', $url)) continue;
- // Remove trailing punctuation
- $url = rtrim($url, '.?!,;:\'"`');
+ // Remove surrounding punctuation
+ $url = trim($url, '.?!,;:\'"`([<');
// Remove surrounding parens and the like
preg_match('/[)\]>]+$/', $url, $trailing);
@@ -446,7 +450,7 @@ function common_replace_urls_callback($text, $callback) {
// If the first part wasn't cap'd but the last part was, we captured too much
if ((!$prev_part && $last_part)) {
- $url = substr_replace($url, '', mb_strpos($url, '.'.$url_parts[2], 0));
+ $url = mb_substr($url, 0 , mb_strpos($url, '.'.$url_parts['2'], 0));
}
// Capture the new TLD
@@ -456,6 +460,12 @@ function common_replace_urls_callback($text, $callback) {
if (!in_array($url_parts[2], $tlds)) continue;
+ // Make sure we didn't capture a hash tag
+ if (strpos($url, '#') === 0) continue;
+
+ // Put the url back the way we found it.
+ $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
+
// Call user specified func
$modified_url = $callback($url);
@@ -469,16 +479,25 @@ function common_replace_urls_callback($text, $callback) {
}
function common_linkify($url) {
+ // It comes in special'd, so we unspecial it before passing to the stringifying
+ // functions
+ $ext = pathinfo($url, PATHINFO_EXTENSION);
+ $url = htmlspecialchars_decode($url);
+ $video_ext = array('mp4', 'flv', 'avi', 'mpg', 'mp3', 'ogg');
$display = $url;
- $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url:$url;
+ $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url;
+
+ $attrs = array('href' => $url, 'rel' => 'external');
+
+ if (in_array($ext, $video_ext)) {
+ $attrs['class'] = 'media';
+ }
if ($longurl = common_longurl($url)) {
- $longurl = htmlentities($longurl, ENT_QUOTES, 'UTF-8');
- $title = "title=\"$longurl\"";
+ $attrs['title'] = $longurl;
}
- else $title = '';
- return "<a href=\"$url\" $title class=\"extlink\">$display</a>";
+ return XMLStringer::estring('a', $attrs, $display);
}
function common_longurl($short_url)
@@ -579,7 +598,13 @@ function common_tag_link($tag)
{
$canonical = common_canonical_tag($tag);
$url = common_local_url('tag', array('tag' => $canonical));
- return '<span class="tag"><a href="' . htmlspecialchars($url) . '" rel="tag">' . htmlspecialchars($tag) . '</a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'tag');
+ $xs->element('a', array('href' => $url,
+ 'rel' => 'tag'),
+ $tag);
+ $xs->elementEnd('span');
+ return $xs->getString();
}
function common_canonical_tag($tag)
@@ -597,7 +622,20 @@ function common_at_link($sender_id, $nickname)
$sender = Profile::staticGet($sender_id);
$recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
if ($recipient) {
- return '<span class="vcard"><a href="'.htmlspecialchars($recipient->profileurl).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
+ $user = User::staticGet('id', $recipient->id);
+ if ($user) {
+ $url = common_local_url('userbyid', array('id' => $user->id));
+ } else {
+ $url = $recipient->profileurl;
+ }
+ $xs = new XMLStringer(false);
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', array('href' => $url,
+ 'class' => 'url'));
+ $xs->element('span', 'fn nickname', $nickname);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $nickname;
}
@@ -608,7 +646,14 @@ function common_group_link($sender_id, $nickname)
$sender = Profile::staticGet($sender_id);
$group = User_group::staticGet('nickname', common_canonical_nickname($nickname));
if ($group && $sender->isMember($group)) {
- return '<span class="vcard"><a href="'.htmlspecialchars($group->permalink()).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', array('href' => $group->permalink(),
+ 'class' => 'url'));
+ $xs->element('span', 'fn nickname', $nickname);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $nickname;
}
@@ -625,7 +670,13 @@ function common_at_hash_link($sender_id, $tag)
$url = common_local_url('subscriptions',
array('nickname' => $user->nickname,
'tag' => $tag));
- return '<span class="tag"><a href="'.htmlspecialchars($url).'" rel="tag">'.$tag.'</a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'tag');
+ $xs->element('a', array('href' => $url,
+ 'rel' => $tag),
+ $tag);
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $tag;
}
@@ -667,275 +718,21 @@ function common_relative_profile($sender, $nickname, $dt=null)
return null;
}
-function common_local_url($action, $args=null, $fragment=null)
+function common_local_url($action, $args=null, $params=null, $fragment=null)
{
- $url = null;
+ $r = Router::get();
+ $path = $r->build($action, $args, $params, $fragment);
+
if (common_config('site','fancy')) {
- $url = common_fancy_url($action, $args);
+ $url = common_path(mb_substr($path, 1));
} else {
- $url = common_simple_url($action, $args);
- }
- if (!is_null($fragment)) {
- $url .= '#'.$fragment;
- }
- return $url;
-}
-
-function common_fancy_url($action, $args=null)
-{
- switch (strtolower($action)) {
- case 'public':
- if ($args && isset($args['page'])) {
- return common_path('?page=' . $args['page']);
- } else {
- return common_path('');
- }
- case 'featured':
- if ($args && isset($args['page'])) {
- return common_path('featured?page=' . $args['page']);
- } else {
- return common_path('featured');
- }
- case 'favorited':
- if ($args && isset($args['page'])) {
- return common_path('favorited?page=' . $args['page']);
- } else {
- return common_path('favorited');
- }
- case 'publicrss':
- return common_path('rss');
- case 'publicatom':
- return common_path("api/statuses/public_timeline.atom");
- case 'publicxrds':
- return common_path('xrds');
- case 'tagrss':
- return common_path('tag/' . $args['tag'] . '/rss');
- case 'featuredrss':
- return common_path('featuredrss');
- case 'favoritedrss':
- return common_path('favoritedrss');
- case 'opensearch':
- if ($args && $args['type']) {
- return common_path('opensearch/'.$args['type']);
- } else {
- return common_path('opensearch/people');
- }
- case 'doc':
- return common_path('doc/'.$args['title']);
- case 'block':
- case 'login':
- case 'logout':
- case 'subscribe':
- case 'unsubscribe':
- case 'invite':
- return common_path('main/'.$action);
- case 'tagother':
- return common_path('main/tagother?id='.$args['id']);
- case 'register':
- if ($args && $args['code']) {
- return common_path('main/register/'.$args['code']);
- } else {
- return common_path('main/register');
- }
- case 'remotesubscribe':
- if ($args && $args['nickname']) {
- return common_path('main/remote?nickname=' . $args['nickname']);
- } else {
- return common_path('main/remote');
- }
- case 'nudge':
- return common_path($args['nickname'].'/nudge');
- case 'openidlogin':
- return common_path('main/openid');
- case 'profilesettings':
- return common_path('settings/profile');
- case 'passwordsettings':
- return common_path('settings/password');
- case 'emailsettings':
- return common_path('settings/email');
- case 'openidsettings':
- return common_path('settings/openid');
- case 'smssettings':
- return common_path('settings/sms');
- case 'twittersettings':
- return common_path('settings/twitter');
- case 'othersettings':
- return common_path('settings/other');
- case 'deleteprofile':
- return common_path('settings/delete');
- case 'newnotice':
- if ($args && $args['replyto']) {
- return common_path('notice/new?replyto='.$args['replyto']);
- } else {
- return common_path('notice/new');
- }
- case 'shownotice':
- return common_path('notice/'.$args['notice']);
- case 'deletenotice':
- if ($args && $args['notice']) {
- return common_path('notice/delete/'.$args['notice']);
- } else {
- return common_path('notice/delete');
- }
- case 'microsummary':
- case 'xrds':
- case 'foaf':
- return common_path($args['nickname'].'/'.$action);
- case 'all':
- case 'replies':
- case 'inbox':
- case 'outbox':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'/'.$action.'?page=' . $args['page']);
- } else {
- return common_path($args['nickname'].'/'.$action);
- }
- case 'subscriptions':
- case 'subscribers':
- $nickname = $args['nickname'];
- unset($args['nickname']);
- if (isset($args['tag'])) {
- $tag = $args['tag'];
- unset($args['tag']);
- }
- $params = http_build_query($args);
- if ($params) {
- return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : '') . '?' . $params);
- } else {
- return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : ''));
- }
- case 'allrss':
- return common_path($args['nickname'].'/all/rss');
- case 'repliesrss':
- return common_path($args['nickname'].'/replies/rss');
- case 'userrss':
- if (isset($args['limit']))
- return common_path($args['nickname'].'/rss?limit=' . $args['limit']);
- return common_path($args['nickname'].'/rss');
- case 'showstream':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'?page=' . $args['page']);
- } else {
- return common_path($args['nickname']);
- }
-
- case 'usertimeline':
- return common_path("api/statuses/user_timeline/".$args['nickname'].".atom");
- case 'confirmaddress':
- return common_path('main/confirmaddress/'.$args['code']);
- case 'userbyid':
- return common_path('user/'.$args['id']);
- case 'recoverpassword':
- $path = 'main/recoverpassword';
- if ($args['code']) {
- $path .= '/' . $args['code'];
- }
- return common_path($path);
- case 'imsettings':
- return common_path('settings/im');
- case 'avatarsettings':
- return common_path('settings/avatar');
- case 'groupsearch':
- return common_path('search/group' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'peoplesearch':
- return common_path('search/people' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'noticesearch':
- return common_path('search/notice' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'noticesearchrss':
- return common_path('search/notice/rss' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'avatarbynickname':
- return common_path($args['nickname'].'/avatar/'.$args['size']);
- case 'tag':
- $path = 'tag/' . $args['tag'];
- unset($args['tag']);
- return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'publictagcloud':
- return common_path('tags');
- case 'peopletag':
- $path = 'peopletag/' . $args['tag'];
- unset($args['tag']);
- return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'tags':
- return common_path('tags' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'favor':
- return common_path('main/favor');
- case 'disfavor':
- return common_path('main/disfavor');
- case 'showfavorites':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'/favorites?page=' . $args['page']);
- } else {
- return common_path($args['nickname'].'/favorites');
- }
- case 'favoritesrss':
- return common_path($args['nickname'].'/favorites/rss');
- case 'showmessage':
- return common_path('message/' . $args['message']);
- case 'newmessage':
- return common_path('message/new' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'api':
- // XXX: do fancy URLs for all the API methods
- switch (strtolower($args['apiaction'])) {
- case 'statuses':
- switch (strtolower($args['method'])) {
- case 'user_timeline.rss':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.rss');
- case 'user_timeline.atom':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.atom');
- case 'user_timeline.json':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.json');
- case 'user_timeline.xml':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.xml');
- default: return common_simple_url($action, $args);
- }
- default: return common_simple_url($action, $args);
- }
- case 'sup':
- if ($args && isset($args['seconds'])) {
- return common_path('main/sup?seconds='.$args['seconds']);
+ if (mb_strpos($path, '/index.php') === 0) {
+ $url = common_path(mb_substr($path, 1));
} else {
- return common_path('main/sup');
- }
- case 'newgroup':
- return common_path('group/new');
- case 'showgroup':
- return common_path('group/'.$args['nickname'] . (($args['page']) ? ('?page=' . $args['page']) : ''));
- case 'editgroup':
- return common_path('group/'.$args['nickname'].'/edit');
- case 'joingroup':
- return common_path('group/'.$args['nickname'].'/join');
- case 'leavegroup':
- return common_path('group/'.$args['nickname'].'/leave');
- case 'groupbyid':
- return common_path('group/'.$args['id'].'/id');
- case 'grouprss':
- return common_path('group/'.$args['nickname'].'/rss');
- case 'groupmembers':
- return common_path('group/'.$args['nickname'].'/members' . (($args['page']) ? ('?page=' . $args['page']) : ''));
- case 'grouplogo':
- return common_path('group/'.$args['nickname'].'/logo');
- case 'usergroups':
- $nickname = $args['nickname'];
- unset($args['nickname']);
- return common_path($nickname.'/groups' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'groups':
- return common_path('group' . (($args) ? ('?' . http_build_query($args)) : ''));
- default:
- return common_simple_url($action, $args);
- }
-}
-
-function common_simple_url($action, $args=null)
-{
- global $config;
- /* XXX: pretty URLs */
- $extra = '';
- if ($args) {
- foreach ($args as $key => $value) {
- $extra .= "&${key}=${value}";
+ $url = common_path('index.php'.$path);
}
}
- return common_path("index.php?action=${action}${extra}");
+ return $url;
}
function common_path($relative)
@@ -1046,24 +843,6 @@ function common_redirect($url, $code=307)
function common_broadcast_notice($notice, $remote=false)
{
-
- // Check to see if notice should go to Twitter
- $flink = Foreign_link::getByUserID($notice->profile_id, 1); // 1 == Twitter
- if (($flink->noticesync & FOREIGN_NOTICE_SEND) == FOREIGN_NOTICE_SEND) {
-
- // If it's not a Twitter-style reply, or if the user WANTS to send replies...
-
- if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
- (($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) == FOREIGN_NOTICE_SEND_REPLY)) {
-
- $result = common_twitter_broadcast($notice, $flink);
-
- if (!$result) {
- common_debug('Unable to send notice: ' . $notice->id . ' to Twitter.', __FILE__);
- }
- }
- }
-
if (common_config('queue', 'enabled')) {
// Do it later!
return common_enqueue_notice($notice);
@@ -1072,73 +851,11 @@ function common_broadcast_notice($notice, $remote=false)
}
}
-function common_twitter_broadcast($notice, $flink)
-{
- global $config;
- $success = true;
- $fuser = $flink->getForeignUser();
- $twitter_user = $fuser->nickname;
- $twitter_password = $flink->credentials;
- $uri = 'http://www.twitter.com/statuses/update.json';
-
- // XXX: Hack to get around PHP cURL's use of @ being a a meta character
- $statustxt = preg_replace('/^@/', ' @', $notice->content);
-
- $options = array(
- CURLOPT_USERPWD => "$twitter_user:$twitter_password",
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => array(
- 'status' => $statustxt,
- 'source' => $config['integration']['source']
- ),
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_FAILONERROR => true,
- CURLOPT_HEADER => false,
- CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_USERAGENT => "Laconica",
- CURLOPT_CONNECTTIMEOUT => 120, // XXX: Scary!!!! How long should this be?
- CURLOPT_TIMEOUT => 120,
-
- # Twitter is strict about accepting invalid "Expect" headers
- CURLOPT_HTTPHEADER => array('Expect:')
- );
-
- $ch = curl_init($uri);
- curl_setopt_array($ch, $options);
- $data = curl_exec($ch);
- $errmsg = curl_error($ch);
-
- if ($errmsg) {
- common_debug("cURL error: $errmsg - trying to send notice for $twitter_user.",
- __FILE__);
- $success = false;
- }
-
- curl_close($ch);
-
- if (!$data) {
- common_debug("No data returned by Twitter's API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
- }
-
- // Twitter should return a status
- $status = json_decode($data);
-
- if (!$status->id) {
- common_debug("Unexpected data returned by Twitter API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
- }
-
- return $success;
-}
-
// Stick the notice on the queue
function common_enqueue_notice($notice)
{
- foreach (array('jabber', 'omb', 'sms', 'public') as $transport) {
+ foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook', 'ping') as $transport) {
$qi = new Queue_item();
$qi->notice_id = $notice->id;
$qi->transport = $transport;
@@ -1185,6 +902,15 @@ function common_real_broadcast($notice, $remote=false)
common_log(LOG_ERR, 'Error in public broadcast for notice ' . $notice->id);
}
}
+ if ($success) {
+ $success = broadcast_twitter($notice);
+ if (!$success) {
+ common_log(LOG_ERR, 'Error in Twitter broadcast for notice ' . $notice->id);
+ }
+ }
+
+ // XXX: Do a real-time FB broadcast here?
+
// XXX: broadcast notices to other IM
return $success;
}
diff --git a/lib/xmlstringer.php b/lib/xmlstringer.php
new file mode 100644
index 000000000..951b13b67
--- /dev/null
+++ b/lib/xmlstringer.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Generator for in-memory XML
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Create in-memory XML
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @see Action
+ * @see HTMLOutputter
+ */
+
+class XMLStringer extends XMLOutputter
+{
+ function __construct($indent=false)
+ {
+ $this->xw = new XMLWriter();
+ $this->xw->openMemory();
+ $this->xw->setIndent($indent);
+ }
+
+ function getString()
+ {
+ return $this->xw->outputMemory();
+ }
+
+ // utility for quickly creating XML-strings
+
+ static function estring($tag, $attrs=null, $content=null)
+ {
+ $xs = new XMLStringer();
+ $xs->element($tag, $attrs, $content);
+ return $xs->getString();
+ }
+} \ No newline at end of file
diff --git a/local/.gitignore b/local/.gitignore
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/local/.gitignore
diff --git a/plugins/BlogspamNetPlugin.php b/plugins/BlogspamNetPlugin.php
new file mode 100644
index 000000000..d9372bcd5
--- /dev/null
+++ b/plugins/BlogspamNetPlugin.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to check submitted notices with blogspam.net
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+define('BLOGSPAMNETPLUGIN_VERSION', '0.1');
+
+/**
+ * Plugin to check submitted notices with blogspam.net
+ *
+ * When new notices are saved, we check their text with blogspam.net (or
+ * a compatible service).
+ *
+ * Blogspam.net is supposed to catch blog comment spam, and I found that
+ * some of its tests (min/max size, bayesian match) gave a lot of false positives.
+ * So, I've turned those tests off by default. This may not get as many
+ * hits, but it's better than nothing.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Event
+ */
+
+class BlogspamNetPlugin extends Plugin
+{
+ var $baseUrl = 'http://test.blogspam.net:8888/';
+
+ function __construct($url=null)
+ {
+ parent::__construct();
+ if ($url) {
+ $this->baseUrl = $url;
+ }
+ }
+
+ function onStartNoticeSave($notice)
+ {
+ $args = $this->testArgs($notice);
+ common_debug("Blogspamnet args = " . print_r($args, TRUE));
+ $request = xmlrpc_encode_request('testComment', array($args));
+ $context = stream_context_create(array('http' => array('method' => "POST",
+ 'header' =>
+ "Content-Type: text/xml\r\n".
+ "User-Agent: " . $this->userAgent(),
+ 'content' => $request)));
+ $file = file_get_contents($this->baseUrl, false, $context);
+ $response = xmlrpc_decode($file);
+ if (xmlrpc_is_fault($response)) {
+ throw new ServerException("$response[faultString] ($response[faultCode])", 500);
+ } else {
+ common_debug("Blogspamnet results = " . $response);
+ if (preg_match('/^ERROR(:(.*))?$/', $response, $match)) {
+ throw new ServerException(sprintf(_("Error from %s: %s"), $this->baseUrl, $match[2]), 500);
+ } else if (preg_match('/^SPAM(:(.*))?$/', $response, $match)) {
+ throw new ClientException(sprintf(_("Spam checker results: %s"), $match[2]), 400);
+ } else if (preg_match('/^OK$/', $response)) {
+ // don't do anything
+ } else {
+ throw new ServerException(sprintf(_("Unexpected response from %s: %s"), $this->baseUrl, $response), 500);
+ }
+ }
+ return true;
+ }
+
+ function testArgs($notice)
+ {
+ $args = array();
+ $args['comment'] = $notice->content;
+ $args['ip'] = $this->getClientIP();
+
+ if (isset($_SERVER) && array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
+ $args['agent'] = $_SERVER['HTTP_USER_AGENT'];
+ }
+
+ $profile = $notice->getProfile();
+
+ if ($profile && $profile->homepage) {
+ $args['link'] = $profile->homepage;
+ }
+
+ if ($profile && $profile->fullname) {
+ $args['name'] = $profile->fullname;
+ } else {
+ $args['name'] = $profile->nickname;
+ }
+
+ $args['site'] = common_root_url();
+ $args['version'] = $this->userAgent();
+
+ $args['options'] = "max-size=140,min-size=0,min-words=0,exclude=bayasian";
+
+ return $args;
+ }
+
+ function getClientIP()
+ {
+ if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ // Note: order matters here; use proxy-forwarded stuff first
+ foreach (array('HTTP_X_FORWARDED_FOR', 'CLIENT-IP', 'REMOTE_ADDR') as $k) {
+ if (isset($_SERVER[$k])) {
+ return $_SERVER[$k];
+ }
+ }
+ }
+ return '127.0.0.1';
+ }
+
+ function userAgent()
+ {
+ return 'BlogspamNetPlugin/'.BLOGSPAMNETPLUGIN_VERSION . ' Laconica/' . LACONICA_VERSION;
+ }
+}
diff --git a/plugins/GoogleAnalyticsPlugin.php b/plugins/GoogleAnalyticsPlugin.php
new file mode 100644
index 000000000..1ecbb664e
--- /dev/null
+++ b/plugins/GoogleAnalyticsPlugin.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to use Google Analytics
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Plugin to use Google Analytics
+ *
+ * This plugin will spoot out the correct JavaScript spell to invoke Google Analytics on a page.
+ *
+ * Note that Google Analytics is not compatible with the Franklin Street Statement; consider using
+ * Piwik (http://www.piwik.org/) instead!
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Event
+ */
+
+class GoogleAnalyticsPlugin extends Plugin
+{
+ var $code = null;
+
+ function __construct($code=null)
+ {
+ $this->code = $code;
+ parent::__construct();
+ }
+
+ function onEndShowScripts($action)
+ {
+ $js1 = 'var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");'.
+ 'document.write(unescape("%3Cscript src=\'" + gaJsHost + "google-analytics.com/ga.js\' type=\'text/javascript\'%3E%3C/script%3E"));';
+ $js2 = sprintf('try{'.
+ 'var pageTracker = _gat._getTracker("%s");'.
+ 'pageTracker._trackPageview();'.
+ '} catch(err) {}',
+ $this->code);
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw($js1);
+ $action->elementEnd('script');
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw($js2);
+ $action->elementEnd('script');
+ }
+}
diff --git a/scripts/SearchMonkey-Om3.0.txt b/scripts/SearchMonkey-Om3.0.txt
new file mode 100644
index 000000000..45b782d0b
--- /dev/null
+++ b/scripts/SearchMonkey-Om3.0.txt
@@ -0,0 +1 @@
+BE9KrPPSJAPljm0ykS59yJQmWBFY9RPVJFhZsiF_5Wcf_tkGwf4FP2Ncjs7qGfHFCb2pv.eDr5y0zhKrF4s3ugs89DbLaA.hrbRJwglsNym5TDwDycrGw3TvjfCVmHBx4VzOZ2QzUyXBIs0T30paT6PYQfARKBdMkieKbbQ1tfl.f.ul35_kcpoXZt_lTDWFDaia2RM41uDLHJyVbCcRkqfHCLabgNeOq_MGFXGA6DjnKQx7TNyQe.2N6IyVd1quVapHn6jUOsWNkahehPMHtO72yvPpugS0UHCKBqcd.UCcbIhmtzLnKoBQAH2AJqUrmfg1XRwqFvTo6y9Z5XmDQK2hRnv97InV5he1AMIqNUotAcIrYjq5Tn42whYsznnMYhMY44UqZGoJI_ZwsSvnH6Je.AhKU3hBW9Tsmggpxgnhx_o2vhyNw2QAgPJng0FKxaevCmPnFtLntwluhxLbiTo2IiIotP4VjIVKs76hHzAsmXYuvS01OU.XB43Gmcw5yP9OUIoh5fEA1ANxOb.ba8aVxu2FdvedbOECgO6gvr.kYilrdxwwnVbHwVP6esrOiYE32dRs0_tWdgfvJfyloLjj_M5stZLEBcVfoUSQHsxX6jW5jrs4BTEHhKrxlOzdBt8f8T2E6eA9x2h7B8zK4eRtC2NBrcj1jzYCVWUT7IdbN05NZBjGYvxtmX1KRW91QwdFXgytfOINFkDk0scNCGGt4OYDzKLJ7sRBy0iKhHNfTC3noUhWf1372uXQ_yiWDWB7cTfxF9meAU3TWrTS7DjGTkwLvCVUJ43QyCxBQd1jWU6sp_VvytDeHx7cqrkNsa3JRN9dht3POqq_mVL0c5MX_XRpV8O.tPSGmfUPUrR5qnV8Z739D6PGWkOQzfzTTv6vGkf4jj1xyDEfUqr767_yL7gHeI2VlbdD2ammzwEhRK9f8ME8FbrZTTgX1OD0.v1cFULt1lew.rtCOtKf4F2MBbi90edst.TpOoUh_TvFkgKf0zgteNo2JjzrmCO.uviLCM2weGksARL70mdU6W9N932YWr6E8HbUc02S8ifSnbMpiUTHwNzMErsg7BSyRVJngGLDreBkBEIjXkNoApJR7kMPMaYVwtvjU.8wLmyFLZ.PHqqcrADTr7R50hL3UKj1mLsL19gQL3cP9J7_zJJM4Q1GDj2dPWBLhJMosUVhW8XBFPhW0aEdmgqhpsAJ_qv52xfjBeEPyPmKHu1p.1G2QEmOqFm_swbVAPHnra5zd8x4OOlHGFeL3qPLW9_LXN46chs6cOpUdYaWRzT2YuOUHHE8RDV9Mev22p7WjrbgC8hY3wK55Cpw_fCekaHcJX2jU3FPl09httjI1i5yGU04yD5MpOQF5EadbSlfjDEg8H.GoOFZcyKKDnqj_8SYdrXWq4aTqAnWgvBPh.bznvmevDgEpH.9gtzLcao4Dzmk.K0PnRgvkzyaN5IGk60TiD4Fk0u3f4bqKaPCOIKCTajbNGIasyND0J5ROtAa_IlXljCpzeEYPhSASSFV6fWmlqHFiGa2cKlLNr157MAUVQg52dJRRyid1YDcHK8ZuMc4BX8QA0olb2CDbUHKxxwv0zbIsgUTL8gvKIAPwBfF0cPfV9v6Phs1rPFXnysxdnunXnr6r6ZCN00rZeA7XbH68f.UjW0.ERGvB72kMnB8fBhfEgD22Y0ZFkwxI00UVAm5yiwkjYx86EdCpkMLBVDR1kWictX04pFLzxZK8iRflPBy74Nx2SFO19qJFnQmKEV2IN2wlrH6z14902LEI2Vuh_dVxNphr1k5mJ9x2VNGmvol.vvHmaLCufHIrOMRkAFMxeP37Mnzyk6NpxAQbdV0KwC.YTfoK9Y5knYAherlh7x8NqELX6XqutF2Lm11dikoK8Yu8u3Rkhoaef00PISV0LOb0E.D1LRjFFGzUCt5Ezcq68Tt0rFNEF1gm5Xl9Rygln_67F0KMgacpKTJ2PBIqcteyRZoRohFBRUTLkKn784KacSBsWFkogD5n7blv943wWeZFvUcKsFyYZny1WLvGEQX01kbjyaDCLDxiEQWSvlDuD.2wOra0JO5.Bo2PpPK5tbNexZDVx3k1yb48ev2UC4J4IlD6oK1KYe46Y.7NX9kDYCcdRiQOVnLJ4PSbPhDW0hcE2SNtxmFdpAf2xQOiekXY35lkuNxvebVhKuoFWxUgQmFEfreJQsC5qf_dunxGquD6u3SXnLbTg8aNWZqtR74dr3s7C5kwBK6eBGwJfkAx8Z2JKwLr5QHdISfa0wLl7aIejz5Mg_tfeATLIwW8SiIQdJiZ992Bx1k_50XQVeHOdyEc8J1Tjnkrji_ZY9PFn3AZc94wXD8erpANuJIbDIdLUp1idqF_TDtmqSK2qQgz26IkewcZnSIQnm98qxkHf9DAIbE6_GUP0ZeWqLQrn0CdhejhOPsKsovcDI7zBHMQABquMeBh9YC1.QCO9br6YmYEAV30n5IsZgl5PRiRDjkhXv8VaE.fnRNndSy7BX6kmxSmDWQfY0FvbUBWBQm7tfU3Mrk2Ojm4ALIYUZnJxl.5xj6VvD.2ZX3ug_1zo7mAiAYG_F939DMccfgCVFUcdtbF9Q9YFeC2bVMfgX5gA75nBCu5R8KopeWfTjdqZBtJoT6HxFmCSLQqIdjrxD7X2RapCI0QoDsp9xyzRZJ5NF5ygVBJqtNn2lAwFENasqFvGRSb35B.sjTQoHW5WBoBjFy5tNF1vWYJENfKRjWn5Pr0FvFdQfBKqeiWrLtStAnGrd49frviyK5VtYa.eSuRyX32SGb7.Pxo0Hf0bhER4uvPBktf3ec5UjL_xRI3eQ__F7tRiGu2tRYIUYcUkhLKAGYoDx2_gdMOXcow5W3KwtuVNtZ4UMRmuB3ccyBUzDTRWNLFISq56JXU46_v7ANub44kTLDfGuJExu.0NaOuW6isndAIYslJlcCs6NJ6j_6Ag55G.UhQFfzDKVOEt.L \ No newline at end of file
diff --git a/scripts/SearchMonkey-yQP.0.txt b/scripts/SearchMonkey-yQP.0.txt
new file mode 100644
index 000000000..e42a04e16
--- /dev/null
+++ b/scripts/SearchMonkey-yQP.0.txt
@@ -0,0 +1 @@
+bzN6aQzSJBM8jb6JDpNRvtfEMpxOC6r2ffo5VJf0ah_Q3kXs8gwvDzGy0KGHkwDDijfnJAwhb.BIcFihUZVWe.vAy2xZZN8XUMxHwc16HwCsgDyOC9sH5WzOV0dvJcVZ.9ipAgI7RuXsAvZdJv90N89iOh1WmD6QwWyzyvnu.FjQDrQH1DlGwFs3ZNzY5lsd5uYhtJ3puBXrzSmGcR68_7OEw4QzAV9SyPSHSskXF56cpH8pUey8WiQieM4X_uuKsMjQfWEUdqNb6teXDplvPDRqHwP5rC6X1_oHBiocfgWiBfUfKOXE1g5J.JtqXYIBee8ROyx3sVIc7V2I0eotZCCiCJ1k74ru2OGC9UhX__ZESGrp9b0JZGZFO6w97IL1Y3BTgVXNox8L3FcFjl11s7YcYBhc_3e3WbJA4pZzczPzd0ouZiCjUcBCZXNu6fnM6XerBbsVj584ZbPdjQ6A5TcrED8dTcFdgfMWlMu7bHsE7e0QIJYmc2g5NWeur2ovAOPVxhELrvnJ6X6Z.06vAmwdJcl4hyYRVqA.9QynHlH2dezXS5y0eKXFUzg.tHNqZboEGutLL316mZQYvwZwATZFc8iO3EuiNV3MWbiCrLX.n3nbXp82A4v7Bv.PVJoeOTmiPmq8vuxts8nUEPf7gA3j6.DmlDvfZQNx9.WNmNFps0B08hzDMvKX98.ft9.GJlftPCYCk2qHRtWm3tjB2R9O6mF6XYvOLUK7aQ4rz5oCxHwmrF.n56G07MS5GS1pjV3ByUO88PI1i_rPbim_Up0jIO5q7PqXudyFE.nG0Dc8yaExZk7PryBfSHk2yMQVXcBjy3XQBVmrIwPwKs.YUFC1qLSThfA8UNckik2xuDZKvf29xpGvHlMbH67UV5HhY5CTeqlgvapRThmfrexqaBTTPPqR.Sy3Pvr9vHX.0UC6kNCTlxW3CExrx9EipRKUvRVClNLEG6M9hm5MQkdfgmd2Dmj5DmjcrZkWOqnNfLn_mtYdoD3nK4zCk.NZwXTFlx5FVr5bOzyncalNdb77qLEH9E9R8d2KUA4Axljx29kycILhZCvy3Qz22Vz_M72lVFKQAFnGlERRS7NLu2rB00e.MNeH9aB01uj.y9UHpfjTAwOJSFEBf7yKRCkVzqFpGAQo7txlC3NRgSyI3kQtW0WeWiNzghBv8c_5iXWkXqFyRfVZUCw2qqow3BF6SDtkhXAr8d.JrprGdG67U66ZD3JB10OLmmAaDiiCr81eVOkrsz3ONdigZOlP7RrYtodAORnheCZ10SAYbaru0sKmsJx4sB41QKn79dieWs5oihI5Rr3jUzPjfYgav5jeEfUYz5qHTtmps4vVJjt9s4zueRiT8AYzg.Xv.wdC1dvBr_kpGxwrkMEZJ0QfZkH.TDWBSWyxeG9gtuKd1gy9CGQTXlLvonJWHwK9gwWZwRKIanV4gnMfWz4pvhGixzEaiRU6g9giKYHpVKe.w0cwPfSDIhbBYqrlvuxkSTXfuFNyTpHM0Mgc6PMNnH6GooeLVK3IO2M8dRdDbL7EHykIJWe85pm5BxSqFTcWXn1nGuVWQV.F.J1XYJj1KheT725MVRlrE3sH.MgY6m4hKCUXT7mIgJ6xsfAQ8dWn7V1fCpqoIYub55iw_dg8yfZ7Yfgwj9bqN81lvTWQzBMLKYpG6pOh37JsTikpXa9kTSJfbJm0_GYZv_Bu10D1WqiO0UCvJXrlqkl5F610fkXkhwRXvmIQL2Mv_R71zSKqMr1g5RmnNifUv43jpXoOWmOKscg59pv8eOyb2JG26BtyVS2XOTDw6pHpHKuksqRKt62i1z83uv_ve5rO0zIKeMnWGlvXKHn8ld3fqGyJvqZ7wqYpnV_LTE3p73XCLEeniClXyk5Q_Icu1PgakYh3GIz8RVBjbjgrZIsvvkEp0Kgeo6oFY0wnboIwDlaUVHIue1nPkuAIsVk9i_IJWeqoRV0eO0wUGWgQ5.HT9RzcKdFVe2kl84RKuQkvoibiCfGlfTXpOfLOZtwsfl2TuNgZtP5oG1Wfsgi6g2a1N8B2WHYN8kZuG8pMnc4NQDIoO8BDnz89lDt7Y9JCslIM3O2Y25EGsylNKamKPJZr.ZSvvTVJG5sxftLxvF8vc8h5_pJD9Akj.84CjqQyfsnzFqc_.EqBaRVSy35wHHrdWTBsvjcobtSed8wXs.5En6HdubdATcgthozHWMEMsUdA29XkenPic2cR.8bQ5VdGZ2_YkJIXrxgt_XoEFFB8tgy061cghyL2fc2fHMQm4KBEc1vjkZjwRo9adWbg9dzAl782AwKC.C72bw-- \ No newline at end of file
diff --git a/scripts/facebookqueuehandler.php b/scripts/facebookqueuehandler.php
new file mode 100755
index 000000000..c6859cb21
--- /dev/null
+++ b/scripts/facebookqueuehandler.php
@@ -0,0 +1,71 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+# Abort if called from a web server
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/facebookutil.php');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class FacebookQueueHandler extends QueueHandler
+{
+
+ function transport()
+ {
+ return 'facebook';
+ }
+
+ function start()
+ {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice)
+ {
+ return facebookBroadcastNotice($notice);
+ }
+
+ function finish()
+ {
+ }
+
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new FacebookQueueHandler($id);
+
+$handler->runOnce();
diff --git a/scripts/laconica.spec b/scripts/laconica.spec
new file mode 100644
index 000000000..5f0ed5fa9
--- /dev/null
+++ b/scripts/laconica.spec
@@ -0,0 +1,87 @@
+BuildRequires: php-pear
+BuildRequires: httpd-devel
+
+Name: laconica
+Version: 0.7.2
+Release: 1%{?dist}
+License: GAGPL v3 or later
+Source: laconica-0.7.2.tar.gz
+Group: Applications/Internet
+Summary: Laconica, the Open Source microblogging platform
+BuildArch: noarch
+
+Requires: httpd
+Requires: php >= 5
+Requires: php-pear-Mail-Mime
+Requires: php-curl
+Requires: php-mysql
+Requires: php-mbstring
+Requires: php-gettext
+Requires: php-xml
+Requires: php-gd
+
+BuildRoot: %{_tmppath}/%{name}-%{version}-build
+
+%define apache_serverroot %(/usr/sbin/apxs -q DATADIR)
+%define apache_sysconfdir %(/usr/sbin/apxs -q SYSCONFDIR)
+%define wwwpath %{apache_serverroot}/%{name}
+%define confpath %{_sysconfdir}/%{name}
+
+%description
+From the ABOUT file: Laconica (pronounced "luh-KAWN-ih-kuh") is a Free
+and Open Source microblogging platform. It helps people in a
+community, company or group to exchange short (140 character) messages
+over the Web. Users can choose which people to "follow" and receive
+only their friends' or colleagues' status messages. It provides a
+similar service to sites like Twitter, Jaiku, and Plurk.
+
+
+%prep
+%setup -q
+
+%build
+
+
+%install
+mkdir -p %{buildroot}%{wwwpath}
+cp -a * %{buildroot}%{wwwpath}
+
+mkdir -p %{buildroot}%{_datadir}/laconica
+cp -a db %{buildroot}%{_datadir}/laconica/db
+
+mkdir -p %{buildroot}%{_sysconfdir}/httpd/conf.d
+cat > %{buildroot}%{_sysconfdir}/httpd/conf.d/laconica.conf <<"EOF"
+Alias /laconica/ "/var/www/laconica/"
+
+<Directory "/var/www/laconica">
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Order allow,deny
+ Allow from all
+</Directory>
+EOF
+
+%clean
+rm -rf %buildroot
+
+%files
+%defattr(-,root,root)
+%dir %{wwwpath}
+%{wwwpath}/*
+%{_datadir}/laconica/*
+%attr(-,apache,apache) %dir %{_datadir}/laconica/avatar
+%doc COPYING README doc-src/*
+%config(noreplace) %{_sysconfdir}/httpd/conf.d/laconica.conf
+
+%changelog
+* Wed Mar 03 2009 Zach Copley <zach@controlyourself.ca> - 0.7.2
+- Changed version number to 0.7.2.
+
+* Sat Feb 28 2009 Ken Sedgwick <ken@bonsai.com> - 0.7.1-1
+- Modified RPM for Fedora.
+
+* Thu Feb 13 2009 tuukka.pasanen@ilmi.fi
+- packaged laconica version 0.7.1
+
+* Wed Feb 04 2009 tuukka.pasanen@ilmi.fi
+- packaged laconica version 0.7.0 using the buildservice spec file wizard
diff --git a/scripts/pingqueuehandler.php b/scripts/pingqueuehandler.php
new file mode 100644
index 000000000..55a266e4a
--- /dev/null
+++ b/scripts/pingqueuehandler.php
@@ -0,0 +1,64 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+# Abort if called from a web server
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/ping.php');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class PingQueueHandler extends QueueHandler {
+
+ function transport() {
+ return 'ping';
+ }
+
+ function start() {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice) {
+ return ping_broadcast_notice($notice);
+ }
+
+ function finish() {
+ }
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : NULL;
+
+$handler = new PingQueueHandler($id);
+
+$handler->runOnce();
diff --git a/scripts/sitemap.php b/scripts/sitemap.php
index 51a9bbd75..39eb859bb 100755
--- a/scripts/sitemap.php
+++ b/scripts/sitemap.php
@@ -61,7 +61,8 @@ function standard_map()
)
);
- $docs = array('about', 'faq', 'contact', 'im', 'openid', 'openmublog', 'privacy', 'source');
+ $docs = array('about', 'faq', 'contact', 'im', 'openid', 'openmublog',
+ 'privacy', 'source', 'badge');
foreach($docs as $title) {
$standard_map_urls .= url(
diff --git a/scripts/startdaemons.sh b/scripts/startdaemons.sh
index 685bd938f..c3729761d 100755
--- a/scripts/startdaemons.sh
+++ b/scripts/startdaemons.sh
@@ -23,7 +23,8 @@
DIR=`dirname $0`
for f in xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php \
- xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php; do
+ xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php \
+ twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php; do
echo -n "Starting $f...";
php $DIR/$f
diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh
index 08e1d4714..fd4406d41 100755
--- a/scripts/stopdaemons.sh
+++ b/scripts/stopdaemons.sh
@@ -24,7 +24,7 @@ SDIR=`dirname $0`
DIR=`php $SDIR/getpiddir.php`
for f in jabberhandler ombhandler publichandler smshandler \
- xmppconfirmhandler xmppdaemon; do
+ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler ; do
FILES="$DIR/$f.*.pid"
for ff in "$FILES" ; do
diff --git a/scripts/twitterqueuehandler.php b/scripts/twitterqueuehandler.php
new file mode 100755
index 000000000..7da4f1e20
--- /dev/null
+++ b/scripts/twitterqueuehandler.php
@@ -0,0 +1,71 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+# Abort if called from a web server
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/twitter.php');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class TwitterQueueHandler extends QueueHandler
+{
+
+ function transport()
+ {
+ return 'twitter';
+ }
+
+ function start()
+ {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice)
+ {
+ return broadcast_twitter($notice);
+ }
+
+ function finish()
+ {
+ }
+
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new TwitterQueueHandler($id);
+
+$handler->runOnce();
diff --git a/scripts/update_facebook.php b/scripts/update_facebook.php
deleted file mode 100755
index 141bcfe0c..000000000
--- a/scripts/update_facebook.php
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env php
-<?php
-/*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-# Abort if called from a web server
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
- print "This script must be run from the command line\n";
- exit();
-}
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
-
-require_once INSTALLDIR . '/lib/common.php';
-require_once INSTALLDIR . '/lib/facebookutil.php';
-
-// For storing the last run date-time
-$last_updated_file = INSTALLDIR . '/scripts/facebook_last_updated';
-
-// Lock file name
-$tmp_file = INSTALLDIR . '/scripts/update_facebook.lock';
-
-// Make sure only one copy of the script is running at a time
-if (!($tmp_file = @fopen($tmp_file, "w")))
-{
- die("Can't open lock file. Script already running?");
-}
-
-$facebook = getFacebook();
-
-$current_time = time();
-
-$since = getLastUpdated();
-
-$notice = getFacebookNotices($since);
-
-$cnt = 0;
-
-while($notice->fetch()) {
-
- $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE);
- $user = $flink->getUser();
- $fbuid = $flink->foreign_id;
-
- if (!userCanUpdate($fbuid)) {
- continue;
- }
-
- $prefix = $facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
- $content = "$prefix $notice->content";
-
- if (($flink->noticesync & FOREIGN_NOTICE_SEND) == FOREIGN_NOTICE_SEND) {
-
- // If it's not a reply, or if the user WANTS to send replies...
- if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $content) ||
- (($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) == FOREIGN_NOTICE_SEND_REPLY)) {
-
- // Avoid a Loop
- if ($notice->source != 'Facebook') {
- updateStatus($fbuid, $content);
- updateProfileBox($facebook, $flink, $notice);
- $cnt++;
- }
- }
- }
-}
-
-if ($cnt > 0) {
- print date('r', $current_time) .
- ": Found $cnt new notices to send to Facebook since last run at " .
- date('Y-m-d H:i:s', $since) . "\n";
-
-}
-
-#Save the last updated time. It needs to do this even if there were no
-#changes made, otherwise it will never create it and thus never send
-#any updates at all.
-updateLastUpdated($current_time);
-
-exit(0);
-
-
-function userCanUpdate($fbuid) {
-
- global $facebook;
-
- $result = false;
-
- try {
- $result = $facebook->api_client->users_hasAppPermission('status_update', $fbuid);
- } catch(FacebookRestClientException $e){
- print_r($e);
- }
-
- return $result;
-}
-
-
-function updateStatus($fbuid, $content) {
- global $facebook;
-
- try {
- $result = $facebook->api_client->users_setStatus($content, $fbuid, false, true);
- } catch(FacebookRestClientException $e){
- print_r($e);
- }
-}
-
-function getLastUpdated(){
- global $last_updated_file, $current_time;
-
- $file = fopen($last_updated_file, 'r');
-
- if ($file) {
- $last = fgets($file);
- } else {
- print "Unable to read $last_updated_file. Using current time.\n";
- return $current_time;
- }
-
- fclose($file);
-
- return $last;
-}
-
-function updateLastUpdated($time){
- global $last_updated_file;
- $file = fopen($last_updated_file, 'w') or die("Can't open $last_updated_file for writing!");
- fwrite($file, $time);
- fclose($file);
-}
diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php
index 01fe8914f..ef3f8c63d 100755
--- a/scripts/xmppdaemon.php
+++ b/scripts/xmppdaemon.php
@@ -208,6 +208,8 @@ class XMPPDaemon extends Daemon
{
if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) {
return true;
+ } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) {
+ return true;
} else {
return false;
}
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index 3b72d00ce..c741ed4cb 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -22,13 +22,11 @@ line-height:1.65;
position:relative;
}
h1,h2,h3,h4,h5,h6 {
-text-transform:uppercase;
margin-bottom:7px;
overflow:hidden;
}
h1 {
font-size:1.4em;
-line-height:1;
margin-bottom:18px;
}
h2 { font-size:1.3em; }
@@ -43,7 +41,6 @@ font-weight:bold;
legend {
font-weight:bold;
font-size:1.3em;
-text-transform:uppercase;
}
input, textarea, select, option {
padding:4px;
@@ -300,7 +297,7 @@ padding:4px 11px;
border-width:1px;
border-style:solid;
border-bottom:0;
-text-shadow: 4px 4px 4px #ddd;
+text-shadow: 2px 2px 2px #ddd;
font-weight:bold;
}
#site_nav_local_views .nav {
@@ -367,7 +364,6 @@ margin-right:4px;
#wrap {
margin:0 auto;
-width:71.714em;
width:1003px;
overflow:hidden;
}
@@ -904,7 +900,7 @@ left:0;
left:29px;
}
.notice-options .notice_delete {
-left:76px;
+right:0;
}
.notice-options .notice_reply dt {
display:none;
diff --git a/theme/base/css/mobile.css b/theme/base/css/mobile.css
new file mode 100644
index 000000000..eee98317c
--- /dev/null
+++ b/theme/base/css/mobile.css
@@ -0,0 +1,150 @@
+/** theme: base
+ *
+ * @package Laconica
+ * @author Meitar Moscovitz <meitar@maymay.net>
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+body {
+font-size:2.5em;
+}
+
+#wrap {
+width:95%;
+}
+
+#header,
+#header address,
+#anon_notice,
+#site_nav_local_views .nav,
+#form_notice,
+#form_notice .form_data li,
+#core,
+#content_inner,
+#notices_primary,
+.notice,
+.notice .entry-title,
+.notice div.entry-content,
+.notice-options,
+.notice .notice-options a,
+.pagination,
+.pagination .nav,
+.aside .section {
+float:none;
+}
+
+.notice-options .notice_reply,
+.notice-options .notice_delete,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:static;
+}
+
+#form_notice,
+#anon_notice,
+#footer,
+#form_notice .form_actions input.submit {
+width:auto;
+}
+
+.form_settings label {
+width:25%;
+}
+.form_settings .form_data p.form_guide {
+margin-left:26%;
+}
+
+#site_nav_global_primary {
+width:75%;
+}
+
+.entity_profile {
+width:65%;
+}
+.entity_actions {
+margin-left:0;
+}
+
+#form_notice,
+#anon_notice {
+clear:both;
+}
+
+#content,
+#aside_primary {
+width:96%;
+padding-left:2%;
+padding-right:2%;
+}
+
+#site_notice {
+position:static;
+float:right;
+clear:right;
+width:75%;
+margin-right:0;
+margin-bottom:11px;
+}
+
+.notices {
+font-size:1.5em;
+}
+
+#form_notice textarea {
+width:80%;
+height:5em;
+}
+#form_notice .form_note {
+right:20%;
+top:6em;
+}
+
+
+.vcard .photo,
+.section .vcard .photo {
+margin-right:18px;
+}
+.notice,
+.profile {
+margin-bottom:18px;
+}
+
+.notices .entry-title,
+.notices div.entry-content {
+width:90%;
+}
+.notice div.entry-content {
+margin-left:0;
+}
+
+.notice .author .photo {
+height:4.5em;
+width:4.5em;
+}
+.notice-options {
+position:absolute;
+top:0;
+right:0;
+padding-left:7%;
+width:3%;
+}
+
+.notice-options .notice_delete a {
+float:left;
+}
+.pagination .nav {
+overflow:auto;
+}
+
+#export_data {
+display:none;
+}
+
+#site_nav_local_views li {
+margin-right:4px;
+}
+#site_nav_local_views a {
+padding:18px 11px;
+}
diff --git a/theme/base/css/modal.css b/theme/base/css/modal.css
new file mode 100644
index 000000000..985e4adfa
--- /dev/null
+++ b/theme/base/css/modal.css
@@ -0,0 +1,22 @@
+/*
+ * SimpleModal Basic Modal Dialog
+ * http://www.ericmmartin.com/projects/simplemodal/
+ * http://code.google.com/p/simplemodal/
+ *
+ * Copyright (c) 2008 Eric Martin - http://ericmmartin.com
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Revision: $Id: basic.css 162 2008-12-01 23:36:58Z emartin24 $
+ *
+ */
+
+
+/* Overlay */
+#simplemodal-overlay {background-color:#000; cursor:wait;}
+
+/* Container */
+#simplemodal-container {height:240px; width:320px; background-color:#fff; border:3px solid #ccc;}
+#simplemodal-container a.modalCloseImg {background:url(../images/x.png) no-repeat; width:25px; height:29px; display:inline; z-index:3200; position:absolute; top:-15px; right:-18px; cursor:pointer;}
+#simplemodal-container #basicModalContent {padding:8px;}
diff --git a/theme/base/css/modal_ie.css b/theme/base/css/modal_ie.css
new file mode 100644
index 000000000..eab4637c0
--- /dev/null
+++ b/theme/base/css/modal_ie.css
@@ -0,0 +1,16 @@
+/*
+ * SimpleModal Basic Modal Dialog
+ * http://www.ericmmartin.com/projects/simplemodal/
+ * http://code.google.com/p/simplemodal/
+ *
+ * Copyright (c) 2008 Eric Martin - http://ericmmartin.com
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ * Revision: $Id: basic_ie.css 162 2008-12-01 23:36:58Z emartin24 $
+ *
+ */
+
+/* IE 6 hacks*/
+#simplemodal-container a.modalCloseImg {background:none; right:-14px; width:22px; height:26px; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/x.png',sizingMethod='scale');}
diff --git a/theme/base/css/print.css b/theme/base/css/print.css
new file mode 100644
index 000000000..2da3e5e44
--- /dev/null
+++ b/theme/base/css/print.css
@@ -0,0 +1,36 @@
+/** theme: base
+ *
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+a:after { background-color:#fff; }
+a:not([href^="#"]):after { content:" ( "attr(href)" ) "; }
+
+img { border:none; }
+p { orphans: 2; widows: 1; }
+
+#site_nav_global_primary,
+#site_nav_local_views,
+#form_notice,
+.pagination,
+#site_nav_global_secondary,
+.entity_actions,
+.notice-options,
+#aside_primary,
+.form_subcription_edit .submit {
+display:none;
+}
+
+.timestamp dt, .timestamp dd,
+.device dt, .device dd {
+display:inline;
+}
+
+.profiles li,
+.notices li {
+margin-bottom:18px;
+}
diff --git a/theme/base/images/x.png b/theme/base/images/x.png
new file mode 100644
index 000000000..c11f7af69
--- /dev/null
+++ b/theme/base/images/x.png
Binary files differ
diff --git a/theme/readme.txt b/theme/readme.txt
index 335801385..4998b3c98 100644
--- a/theme/readme.txt
+++ b/theme/readme.txt
@@ -31,3 +31,6 @@ cp -r ./default ./mytheme
nano ./mytheme/css/display.css
3. Search and replace a colour or a path to the background image of your choice.
+
+4. Set /config.php to load 'mytheme':
+$config['site']['theme'] = 'mytheme';