summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--EVENTS.txt15
-rw-r--r--README345
-rw-r--r--actions/accesstoken.php2
-rw-r--r--actions/all.php10
-rw-r--r--actions/allrss.php10
-rw-r--r--actions/api.php69
-rw-r--r--actions/attachment.php172
-rw-r--r--actions/attachment_ajax.php83
-rw-r--r--actions/attachment_thumbnail.php84
-rw-r--r--actions/avatarbynickname.php2
-rw-r--r--actions/block.php19
-rw-r--r--actions/blockedfromgroup.php315
-rw-r--r--actions/conversation.php294
-rw-r--r--actions/deletenotice.php4
-rw-r--r--actions/disfavor.php4
-rw-r--r--actions/doc.php2
-rw-r--r--actions/editgroup.php65
-rw-r--r--actions/facebookhome.php62
-rw-r--r--actions/facebookinvite.php2
-rw-r--r--actions/facebooklogin.php2
-rw-r--r--actions/facebookremove.php2
-rw-r--r--actions/facebooksettings.php4
-rw-r--r--actions/favor.php2
-rw-r--r--actions/favorited.php2
-rw-r--r--actions/favoritesrss.php2
-rw-r--r--actions/featured.php5
-rw-r--r--actions/file.php72
-rw-r--r--actions/finishopenidlogin.php2
-rw-r--r--actions/finishremotesubscribe.php2
-rw-r--r--actions/foaf.php129
-rw-r--r--actions/groupblock.php215
-rw-r--r--actions/groupdesignsettings.php315
-rw-r--r--actions/grouplogo.php2
-rw-r--r--actions/groupmembers.php336
-rw-r--r--actions/grouprss.php1
-rw-r--r--actions/groups.php13
-rw-r--r--actions/groupsearch.php2
-rw-r--r--actions/groupunblock.php149
-rw-r--r--actions/invite.php8
-rw-r--r--actions/joingroup.php5
-rw-r--r--actions/logout.php18
-rw-r--r--actions/makeadmin.php166
-rw-r--r--actions/microsummary.php2
-rw-r--r--actions/newgroup.php51
-rw-r--r--actions/newnotice.php194
-rw-r--r--actions/noticesearch.php2
-rw-r--r--actions/noticesearchrss.php15
-rw-r--r--actions/nudge.php2
-rw-r--r--actions/openidlogin.php2
-rw-r--r--actions/opensearch.php2
-rw-r--r--actions/othersettings.php11
-rw-r--r--actions/peoplesearch.php46
-rw-r--r--actions/peopletag.php2
-rw-r--r--actions/postnotice.php2
-rw-r--r--actions/public.php14
-rw-r--r--actions/publicrss.php2
-rw-r--r--actions/publicxrds.php2
-rw-r--r--actions/recoverpassword.php2
-rw-r--r--actions/register.php13
-rw-r--r--actions/remotesubscribe.php2
-rw-r--r--actions/replies.php3
-rw-r--r--actions/repliesrss.php2
-rw-r--r--actions/requesttoken.php2
-rw-r--r--actions/showfavorites.php19
-rw-r--r--actions/showgroup.php64
-rw-r--r--actions/shownotice.php53
-rw-r--r--actions/showstream.php16
-rw-r--r--actions/subedit.php2
-rw-r--r--actions/subscribe.php2
-rw-r--r--actions/subscribers.php32
-rw-r--r--actions/subscriptions.php37
-rw-r--r--actions/sup.php6
-rw-r--r--actions/tag.php7
-rw-r--r--actions/tagother.php2
-rw-r--r--actions/tagrss.php2
-rw-r--r--actions/twitapiaccount.php21
-rw-r--r--actions/twitapiblocks.php12
-rw-r--r--actions/twitapidirect_messages.php78
-rw-r--r--actions/twitapifavorites.php147
-rw-r--r--actions/twitapifriendships.php121
-rw-r--r--actions/twitapihelp.php6
-rw-r--r--actions/twitapinotifications.php2
-rw-r--r--actions/twitapisearchatom.php28
-rw-r--r--actions/twitapisearchjson.php9
-rw-r--r--actions/twitapistatuses.php421
-rw-r--r--actions/twitapiusers.php12
-rw-r--r--actions/twittersettings.php28
-rw-r--r--actions/unblock.php2
-rw-r--r--actions/unsubscribe.php2
-rw-r--r--actions/updateprofile.php2
-rw-r--r--actions/userauthorization.php2
-rw-r--r--actions/userbyid.php2
-rw-r--r--actions/userdesignsettings.php285
-rw-r--r--actions/usergroups.php3
-rw-r--r--actions/userrss.php26
-rw-r--r--actions/xrds.php2
-rw-r--r--background/.gitignore0
-rw-r--r--classes/Avatar.php30
-rw-r--r--classes/Design.php155
-rw-r--r--classes/Fave.php44
-rw-r--r--classes/File.php197
-rw-r--r--classes/File_oembed.php90
-rw-r--r--classes/File_redirection.php204
-rw-r--r--classes/File_thumbnail.php60
-rw-r--r--classes/File_to_post.php70
-rw-r--r--classes/Foreign_link.php12
-rw-r--r--classes/Foreign_user.php22
-rw-r--r--classes/Group_alias.php41
-rw-r--r--classes/Group_block.php115
-rw-r--r--[-rwxr-xr-x]classes/Group_inbox.php6
-rw-r--r--[-rwxr-xr-x]classes/Group_member.php0
-rw-r--r--classes/Memcached_DataObject.php22
-rw-r--r--classes/Notice.php337
-rw-r--r--classes/Notice_inbox.php23
-rw-r--r--classes/Notice_tag.php2
-rw-r--r--classes/Profile.php233
-rw-r--r--classes/Profile_block.php2
-rw-r--r--classes/Queue_item.php11
-rw-r--r--[-rwxr-xr-x]classes/Related_group.php0
-rw-r--r--classes/Remote_profile.php2
-rw-r--r--classes/Session.php129
-rw-r--r--classes/Status_network.php197
-rw-r--r--classes/Subscription.php2
-rw-r--r--classes/User.php106
-rw-r--r--[-rwxr-xr-x]classes/User_group.php157
-rw-r--r--[-rwxr-xr-x]classes/laconica.ini106
-rw-r--r--classes/laconica.links.ini14
-rw-r--r--classes/statusnet.ini18
-rw-r--r--config.php.sample397
-rw-r--r--db/074to080.sql109
-rw-r--r--db/foreign_services.sql3
-rw-r--r--db/innodb.sql2
-rw-r--r--db/laconica.sql116
-rw-r--r--db/laconica_pg.sql78
-rw-r--r--db/notice_source.sql1
-rw-r--r--db/site.sql21
-rw-r--r--doc-src/tos300
-rw-r--r--extlib/Console/Getopt.php290
-rw-r--r--extlib/DB/DataObject.php17
-rw-r--r--extlib/HTTP/Request.php1521
-rw-r--r--extlib/HTTP/Request/Listener.php106
-rw-r--r--extlib/MIME/Type.php523
-rw-r--r--extlib/MIME/Type/Extension.php298
-rw-r--r--extlib/MIME/Type/Parameter.php163
-rw-r--r--extlib/Mail/mimeDecode.php849
-rw-r--r--extlib/Net/URL2.php813
-rw-r--r--extlib/Services/oEmbed.php357
-rw-r--r--extlib/Services/oEmbed/Exception.php65
-rw-r--r--extlib/Services/oEmbed/Exception/NoSupport.php63
-rw-r--r--extlib/Services/oEmbed/Object.php126
-rw-r--r--extlib/Services/oEmbed/Object/Common.php139
-rw-r--r--extlib/Services/oEmbed/Object/Exception.php65
-rw-r--r--extlib/Services/oEmbed/Object/Link.php73
-rw-r--r--extlib/Services/oEmbed/Object/Photo.php89
-rw-r--r--extlib/Services/oEmbed/Object/Rich.php82
-rw-r--r--extlib/Services/oEmbed/Object/Video.php82
-rw-r--r--extlib/Stomp.php594
-rw-r--r--extlib/Stomp/Exception.php57
-rw-r--r--extlib/Stomp/Frame.php80
-rw-r--r--extlib/Stomp/Message.php37
-rw-r--r--extlib/Stomp/Message/Bytes.php47
-rw-r--r--extlib/Stomp/Message/Map.php55
-rw-r--r--extlib/System/Command.php587
-rw-r--r--extlib/Validate.php2167
-rw-r--r--extlib/XMPPHP/BOSH.php2
-rw-r--r--extlib/XMPPHP/XMLStream.php8
-rw-r--r--extlib/XMPPHP/XMPP.php13
-rw-r--r--extlib/facebook/facebook.php147
-rw-r--r--extlib/facebook/facebook_desktop.php2
-rwxr-xr-x[-rw-r--r--]extlib/facebook/facebookapi_php5_restlib.php1026
-rw-r--r--index.php82
-rw-r--r--install.php227
-rw-r--r--js/farbtastic/LICENSE.txt341
-rw-r--r--js/farbtastic/farbtastic.js345
-rwxr-xr-xjs/farbtastic/marker.pngbin0 -> 652 bytes
-rw-r--r--js/farbtastic/mask.pngbin0 -> 2020 bytes
-rw-r--r--js/farbtastic/wheel.pngbin0 -> 11733 bytes
-rw-r--r--js/install.js18
-rw-r--r--js/jcrop/jquery.Jcrop.go.js77
-rw-r--r--js/jquery.form.js4
-rw-r--r--js/jquery.joverlay.js271
-rw-r--r--js/jquery.joverlay.min.js7
-rw-r--r--js/userdesign.go.js99
-rw-r--r--js/util.js116
-rw-r--r--lib/Shorturl_api.php3
-rw-r--r--lib/accountsettingsaction.php3
-rw-r--r--lib/action.php89
-rw-r--r--lib/arraywrapper.php2
-rw-r--r--lib/attachmentlist.php357
-rw-r--r--lib/attachmentnoticesection.php (renamed from lib/personal.php)41
-rw-r--r--lib/attachmenttagcloudsection.php83
-rw-r--r--lib/channel.php2
-rw-r--r--lib/clienterroraction.php2
-rw-r--r--lib/command.php17
-rw-r--r--lib/commandinterpreter.php2
-rw-r--r--lib/common.php203
-rw-r--r--lib/currentuserdesignaction.php92
-rw-r--r--lib/daemon.php23
-rw-r--r--lib/dberroraction.php2
-rw-r--r--lib/dbqueuemanager.php166
-rw-r--r--lib/designsettings.php477
-rw-r--r--lib/error.php2
-rw-r--r--lib/event.php28
-rw-r--r--lib/facebookaction.php278
-rw-r--r--lib/facebookutil.php83
-rw-r--r--lib/form.php16
-rw-r--r--lib/galleryaction.php5
-rw-r--r--lib/groupdesignaction.php89
-rw-r--r--lib/groupeditform.php39
-rw-r--r--lib/grouplist.php2
-rw-r--r--lib/groupnav.php12
-rw-r--r--lib/grouptagcloudsection.php21
-rw-r--r--lib/imagefile.php3
-rw-r--r--lib/jabber.php10
-rw-r--r--lib/jsonsearchresultslist.php1
-rw-r--r--lib/language.php2
-rw-r--r--lib/mail.php72
-rw-r--r--lib/mailbox.php23
-rw-r--r--lib/noticeform.php15
-rw-r--r--lib/noticelist.php24
-rw-r--r--lib/noticesection.php39
-rw-r--r--lib/oauthstore.php2
-rw-r--r--lib/omb.php2
-rw-r--r--lib/openid.php2
-rw-r--r--lib/ownerdesignaction.php92
-rw-r--r--lib/peoplesearchresults.php75
-rw-r--r--lib/ping.php2
-rw-r--r--lib/popularnoticesection.php4
-rw-r--r--lib/profileaction.php47
-rw-r--r--lib/profilelist.php179
-rw-r--r--lib/profileminilist.php24
-rw-r--r--lib/profilesection.php2
-rw-r--r--lib/queuehandler.php79
-rw-r--r--lib/queuemanager.php74
-rw-r--r--lib/router.php55
-rw-r--r--lib/rssaction.php44
-rw-r--r--lib/search_engines.php41
-rw-r--r--lib/searchaction.php6
-rw-r--r--lib/servererroraction.php2
-rw-r--r--lib/settingsaction.php2
-rw-r--r--lib/snapshot.php227
-rw-r--r--lib/stompqueuemanager.php169
-rw-r--r--lib/subgroupnav.php2
-rw-r--r--lib/subs.php15
-rw-r--r--lib/subscriptionlist.php131
-rw-r--r--lib/tagcloudsection.php6
-rw-r--r--lib/theme.php38
-rw-r--r--lib/twitter.php103
-rw-r--r--lib/twitterapi.php205
-rw-r--r--lib/unqueuemanager.php85
-rw-r--r--lib/util.php416
-rw-r--r--lib/webcolor.php192
-rw-r--r--lib/xmppqueuehandler.php50
-rw-r--r--plugins/FBConnect/FBCLoginGroupNav.php110
-rw-r--r--plugins/FBConnect/FBCSettingsNav.php113
-rw-r--r--plugins/FBConnect/FBC_XDReceiver.php73
-rw-r--r--plugins/FBConnect/FBConnectAuth.php454
-rw-r--r--plugins/FBConnect/FBConnectLogin.php67
-rw-r--r--plugins/FBConnect/FBConnectPlugin.css36
-rw-r--r--plugins/FBConnect/FBConnectPlugin.php395
-rw-r--r--plugins/FBConnect/FBConnectSettings.php203
-rw-r--r--plugins/FBConnect/fbfavicon.icobin0 -> 1150 bytes
-rwxr-xr-xscripts/allsites.php40
-rw-r--r--scripts/commandline.inc174
-rw-r--r--scripts/decache.php37
-rwxr-xr-xscripts/delete_status_network.sh21
-rwxr-xr-xscripts/enjitqueuehandler.php47
-rwxr-xr-xscripts/facebookqueuehandler.php47
-rwxr-xr-xscripts/fixup_conversations.php70
-rwxr-xr-xscripts/fixup_hashtags.php2
-rwxr-xr-xscripts/fixup_inboxes.php2
-rwxr-xr-xscripts/fixup_notices_rendered.php2
-rwxr-xr-xscripts/fixup_replies.php2
-rw-r--r--scripts/fixup_utf8.php27
-rwxr-xr-xscripts/getpiddir.php18
-rwxr-xr-xscripts/getvaliddaemons.php19
-rwxr-xr-xscripts/inbox_users.php54
-rwxr-xr-xscripts/jabberqueuehandler.php45
-rwxr-xr-xscripts/maildaemon.php173
-rwxr-xr-xscripts/ombqueuehandler.php47
-rw-r--r--scripts/pingqueuehandler.php41
-rwxr-xr-xscripts/publicqueuehandler.php47
-rw-r--r--scripts/reportsnapshot.php (renamed from lib/stream.php)26
-rwxr-xr-xscripts/setpassword.php31
-rw-r--r--scripts/setup.cfg.sample14
-rwxr-xr-xscripts/setup_status_network.sh32
-rw-r--r--scripts/showcache.php71
-rwxr-xr-xscripts/sitemap.php133
-rwxr-xr-xscripts/smsqueuehandler.php44
-rwxr-xr-xscripts/sphinx-cron.sh2
-rwxr-xr-xscripts/sphinx-indexer.sh2
-rwxr-xr-xscripts/startdaemons.sh25
-rwxr-xr-xscripts/stopdaemons.sh5
-rwxr-xr-xscripts/synctwitterfriends.php17
-rw-r--r--scripts/triminboxes.php43
-rwxr-xr-xscripts/twitterqueuehandler.php47
-rwxr-xr-xscripts/twitterstatusfetcher.php645
-rw-r--r--scripts/uncache_users.php31
-rwxr-xr-xscripts/update_translations.php145
-rwxr-xr-xscripts/xmppconfirmhandler.php47
-rwxr-xr-xscripts/xmppdaemon.php130
-rw-r--r--sphinx.conf.sample82
-rw-r--r--theme/base/css/display.css280
-rw-r--r--theme/base/css/facebookapp.css3
-rw-r--r--theme/base/css/farbtastic.css32
-rw-r--r--theme/base/css/ie.css32
-rw-r--r--theme/base/css/ie6.css9
-rw-r--r--theme/base/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/base/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/base/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/base/images/icons/twotone/green/admin.gifbin0 -> 100 bytes
-rw-r--r--theme/base/images/icons/twotone/green/arrow-left.gif (renamed from theme/default/images/icons/twotone/green/arrow-left.gif)bin73 -> 73 bytes
-rw-r--r--theme/base/images/icons/twotone/green/arrow-right.gif (renamed from theme/default/images/icons/twotone/green/arrow-right.gif)bin74 -> 74 bytes
-rw-r--r--theme/base/images/icons/twotone/green/clip-01.gifbin0 -> 78 bytes
-rw-r--r--theme/base/images/icons/twotone/green/clip-02.gifbin0 -> 70 bytes
-rw-r--r--theme/base/images/icons/twotone/green/disfavourite.gif (renamed from theme/default/images/icons/twotone/green/disfavourite.gif)bin88 -> 88 bytes
-rw-r--r--theme/base/images/icons/twotone/green/edit.gif (renamed from theme/default/images/icons/twotone/green/edit.gif)bin75 -> 75 bytes
-rw-r--r--theme/base/images/icons/twotone/green/favourite.gif (renamed from theme/default/images/icons/twotone/green/favourite.gif)bin73 -> 73 bytes
-rw-r--r--theme/base/images/icons/twotone/green/mail.gif (renamed from theme/default/images/icons/twotone/green/mail.gif)bin82 -> 82 bytes
-rw-r--r--theme/base/images/icons/twotone/green/news.gif (renamed from theme/default/images/icons/twotone/green/news.gif)bin76 -> 76 bytes
-rw-r--r--theme/base/images/icons/twotone/green/quote.gif (renamed from theme/default/images/icons/twotone/green/quote.gif)bin79 -> 79 bytes
-rw-r--r--theme/base/images/icons/twotone/green/reply.gif (renamed from theme/default/images/icons/twotone/green/reply.gif)bin79 -> 79 bytes
-rw-r--r--theme/base/images/icons/twotone/green/shield.gif (renamed from theme/default/images/icons/twotone/green/shield.gif)bin85 -> 85 bytes
-rw-r--r--theme/base/images/icons/twotone/green/trash.gif (renamed from theme/default/images/icons/twotone/green/trash.gif)bin77 -> 77 bytes
-rw-r--r--theme/base/logo.pngbin0 -> 4988 bytes
-rw-r--r--theme/biz/css/base.css1194
-rw-r--r--theme/biz/css/display.css259
-rw-r--r--theme/biz/css/ie.css9
-rw-r--r--theme/biz/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/biz/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/biz/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/biz/images/illustrations/illu_pattern-01.pngbin0 -> 935 bytes
-rw-r--r--theme/biz/images/illustrations/illu_pattern-02.pngbin0 -> 9498 bytes
-rw-r--r--theme/biz/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/cloudy/css/display.css1552
-rw-r--r--theme/cloudy/css/ie.css34
-rw-r--r--theme/cloudy/default-avatar-mini.pngbin0 -> 1349 bytes
-rw-r--r--theme/cloudy/default-avatar-profile.pngbin0 -> 9256 bytes
-rw-r--r--theme/cloudy/default-avatar-stream.pngbin0 -> 3829 bytes
-rw-r--r--theme/cloudy/images/icons/icon_atom.pngbin0 -> 820 bytes
-rw-r--r--theme/cloudy/images/icons/icon_disfavourite.gifbin0 -> 701 bytes
-rw-r--r--theme/cloudy/images/icons/icon_favourite.gifbin0 -> 397 bytes
-rw-r--r--theme/cloudy/images/icons/icon_foaf.gif (renamed from theme/default/images/icons/icon_foaf.gif)bin1144 -> 1144 bytes
-rw-r--r--theme/cloudy/images/icons/icon_processing.gifbin0 -> 673 bytes
-rw-r--r--theme/cloudy/images/icons/icon_reply.gifbin0 -> 336 bytes
-rw-r--r--theme/cloudy/images/icons/icon_rss.pngbin0 -> 777 bytes
-rw-r--r--theme/cloudy/images/icons/icon_trash.gifbin0 -> 148 bytes
-rw-r--r--theme/cloudy/images/icons/icon_vcard.gif (renamed from theme/default/images/icons/icon_vcard.gif)bin331 -> 331 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/arrow-left.gif (renamed from theme/identica/images/icons/twotone/green/arrow-left.gif)bin73 -> 73 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/arrow-right.gif (renamed from theme/identica/images/icons/twotone/green/arrow-right.gif)bin74 -> 74 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/edit.gif (renamed from theme/identica/images/icons/twotone/green/edit.gif)bin75 -> 75 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/mail.gif (renamed from theme/identica/images/icons/twotone/green/mail.gif)bin82 -> 82 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/news.gif (renamed from theme/identica/images/icons/twotone/green/news.gif)bin76 -> 76 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/quote.gif (renamed from theme/identica/images/icons/twotone/green/quote.gif)bin79 -> 79 bytes
-rw-r--r--theme/cloudy/images/icons/twotone/green/shield.gif (renamed from theme/identica/images/icons/twotone/green/shield.gif)bin85 -> 85 bytes
-rw-r--r--theme/cloudy/images/illustrations/illu_arrow-up-01.gifbin0 -> 68 bytes
-rw-r--r--theme/cloudy/images/illustrations/illu_clouds-01.gifbin0 -> 14251 bytes
-rw-r--r--theme/cloudy/images/illustrations/illu_jcrop.gifbin0 -> 329 bytes
-rw-r--r--theme/cloudy/images/illustrations/illu_progress_loading-01.gifbin0 -> 5886 bytes
-rw-r--r--theme/cloudy/images/illustrations/illu_unicorn-01.pngbin0 -> 5695 bytes
-rw-r--r--theme/cloudy/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/default/css/display.css154
-rw-r--r--theme/default/css/ie.css11
-rw-r--r--theme/default/images/icons/icon_atom.jpgbin1117 -> 0 bytes
-rw-r--r--theme/default/images/icons/icon_rss.jpgbin1166 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/against.gifbin85 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/arrow-down.gifbin75 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/arrow-downleft.gifbin75 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/arrow-downright.gifbin75 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/arrow-up.gifbin74 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/arrow-upleft.gifbin75 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/arrow-upright.gifbin73 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/back-forth.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/bookmark.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/bulb.gifbin83 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/calendar.gifbin83 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/calendar2.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/camera.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/cart.gifbin86 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/caution.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/chart.gifbin81 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/checkmark.gifbin76 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/clipboard.gifbin80 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/clock.gifbin81 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/closed-folder.gifbin78 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/database.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/diskette.gifbin85 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/document.gifbin81 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/double-arrow.gifbin78 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/eject.gifbin73 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/exclaim.gifbin74 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/fastforward.gifbin77 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/flag.gifbin80 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/graph.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/grow.gifbin90 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/headphones.gifbin78 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/home.gifbin81 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/hourglass.gifbin85 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/info.gifbin73 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/key.gifbin76 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/lock.gifbin83 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/move.gifbin80 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/music.gifbin80 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/note.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/open-folder.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/paper-clip.gifbin78 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/paper-clip2.gifbin80 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/pause.gifbin75 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/phone.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/play.gifbin75 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/plus.gifbin74 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/print.gifbin85 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/question-mark.gifbin74 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/refresh.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/rewind.gifbin77 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/search.gifbin76 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/skip-back.gifbin76 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/skip.gifbin78 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/skull.gifbin85 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/statusbar.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/stop.gifbin72 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/template.gifbin81 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/text-bigger.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/text-smaller.gifbin80 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/two-docs.gifbin83 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/twotone.gifbin2461 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/undo.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/user.gifbin77 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/vegetable.gifbin82 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/x.gifbin79 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/zoom-in.gifbin86 -> 0 bytes
-rw-r--r--theme/default/images/icons/twotone/green/zoom-out.gifbin85 -> 0 bytes
-rw-r--r--theme/default/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/h4ck3r/css/base.css1139
-rw-r--r--theme/h4ck3r/css/display.css236
-rw-r--r--theme/h4ck3r/css/ie.css9
-rw-r--r--theme/h4ck3r/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/h4ck3r/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/h4ck3r/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gifbin0 -> 432979 bytes
-rw-r--r--theme/h4ck3r/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/identica/css/display.css132
-rw-r--r--theme/identica/css/ie.css11
-rw-r--r--theme/identica/images/icons/icon_atom.jpgbin1117 -> 0 bytes
-rw-r--r--theme/identica/images/icons/icon_foaf.gifbin1144 -> 0 bytes
-rw-r--r--theme/identica/images/icons/icon_rss.jpgbin1166 -> 0 bytes
-rw-r--r--theme/identica/images/icons/icon_vcard.gifbin331 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/against.gifbin85 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/arrow-down.gifbin75 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/arrow-downleft.gifbin75 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/arrow-downright.gifbin75 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/arrow-up.gifbin74 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/arrow-upleft.gifbin75 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/arrow-upright.gifbin73 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/back-forth.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/bookmark.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/bulb.gifbin83 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/calendar.gifbin83 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/calendar2.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/camera.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/cart.gifbin86 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/caution.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/chart.gifbin81 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/checkmark.gifbin76 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/clipboard.gifbin80 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/clock.gifbin81 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/closed-folder.gifbin78 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/database.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/disfavourite.gifbin88 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/diskette.gifbin85 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/document.gifbin81 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/double-arrow.gifbin78 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/eject.gifbin73 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/exclaim.gifbin74 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/fastforward.gifbin77 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/favourite.gifbin73 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/flag.gifbin80 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/graph.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/grow.gifbin90 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/headphones.gifbin78 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/home.gifbin81 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/hourglass.gifbin85 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/info.gifbin73 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/key.gifbin76 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/lock.gifbin83 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/move.gifbin80 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/music.gifbin80 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/note.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/open-folder.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/paper-clip.gifbin78 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/paper-clip2.gifbin80 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/pause.gifbin75 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/phone.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/play.gifbin75 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/plus.gifbin74 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/print.gifbin85 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/question-mark.gifbin74 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/refresh.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/reply.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/rewind.gifbin77 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/search.gifbin76 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/skip-back.gifbin76 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/skip.gifbin78 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/skull.gifbin85 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/statusbar.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/stop.gifbin72 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/template.gifbin81 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/text-bigger.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/text-smaller.gifbin80 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/trash.gifbin77 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/two-docs.gifbin83 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/twotone.gifbin2461 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/undo.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/user.gifbin77 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/vegetable.gifbin82 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/x.gifbin79 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/zoom-in.gifbin86 -> 0 bytes
-rw-r--r--theme/identica/images/icons/twotone/green/zoom-out.gifbin85 -> 0 bytes
-rw-r--r--theme/otalk/css/base.css1211
-rw-r--r--theme/otalk/css/display.css292
-rw-r--r--theme/otalk/css/ie.css9
-rw-r--r--theme/otalk/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/otalk/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/otalk/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/otalk/images/illustrations/illu_arrow-left-01.gifbin0 -> 75 bytes
-rw-r--r--theme/otalk/images/illustrations/illu_pattern-01.pngbin0 -> 3218 bytes
-rw-r--r--theme/otalk/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/pigeonthoughts/css/base.css1269
-rw-r--r--theme/pigeonthoughts/css/display.css340
-rw-r--r--theme/pigeonthoughts/css/ie.css9
-rw-r--r--theme/pigeonthoughts/default-avatar-mini.pngbin0 -> 646 bytes
-rw-r--r--theme/pigeonthoughts/default-avatar-profile.pngbin0 -> 2853 bytes
-rw-r--r--theme/pigeonthoughts/default-avatar-stream.pngbin0 -> 1487 bytes
-rw-r--r--theme/pigeonthoughts/images/illustrations/illu_pigeons-01.pngbin0 -> 72649 bytes
-rw-r--r--theme/pigeonthoughts/images/illustrations/illu_pigeons-02.pngbin0 -> 3538 bytes
-rw-r--r--theme/pigeonthoughts/logo.pngbin0 -> 2228 bytes
-rw-r--r--theme/readme.txt10
538 files changed, 33320 insertions, 3966 deletions
diff --git a/.gitignore b/.gitignore
index da6947bfd..f4c2bba5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
avatar/*
+background/*
files/*
+file/*
_darcs/*
logs/*
config.php
@@ -16,3 +18,9 @@ dataobject.ini
.buildpath
.project
.settings
+TODO.rym
+config-*.php
+good-config.php
+lac08.log
+php.log
+config.php.*
diff --git a/EVENTS.txt b/EVENTS.txt
index e0ce11666..2c43469d4 100644
--- a/EVENTS.txt
+++ b/EVENTS.txt
@@ -1,5 +1,4 @@
-InitializePlugin: a chance to initialize a plugin in a complete
- environment
+InitializePlugin: a chance to initialize a plugin in a complete environment
CleanupPlugin: a chance to cleanup a plugin at the end of a program
@@ -109,5 +108,17 @@ EndSubGroupNav: At the end of the subscriptions group nav menu
RouterInitialized: After the router instance has been initialized
- $m: the Net_URL_Mapper that has just been set up
+StartLogout: Before logging out
+- $action: the logout action
+
+EndLogout: After logging out
+- $action: the logout action
+
ArgsInitialized: After the argument array has been initialized
- $args: associative array of arguments, can be modified
+
+StartAddressData: Allows the site owner to provide additional information about themselves for contact (e.g., tagline, email, location)
+- $action: the current action
+
+EndAddressData: At the end of <address>
+- $action: the current action
diff --git a/README b/README
index 679a096b3..40e526355 100644
--- a/README
+++ b/README
@@ -2,8 +2,8 @@
README
------
-Laconica 0.7.4 ("Can't Get There From Here")
-29 May 2009
+Laconica 0.8.0 ("Shiny Happy People")
+8 July 2009
This is the README file for Laconica, the Open Source microblogging
platform. It includes installation instructions, descriptions of
@@ -71,29 +71,52 @@ for additional terms.
New this version
================
-This is a minor bug-fix and feature release since version 0.7.3,
-released Apr 4 2009. Notable changes this version:
-
-- Improved handling of UTF-8 characters. The new code is *not* backwards
- compatible by default; see "Upgrading" below for instructions on
- converting existing databases to the correct character set.
-- Unroll joins for large queries. This greatly enhanced database
- performance -- up to 50x for some queries -- at the expense of making
- an extra DB hit for some queries.
-- Added an optional plugin to use WikiHashtags
- (http://hashtags.wikia.com/) for the sidebar on hashtag pages.
-- Optimized Twitter friend synchronization.
-- Better error handling for Ajax posting of notices, including
- HTTP errors and timeouts.
-- Experimental Comet plugin -- supports the cometd and the Bayeux
- protocol. Using this plugin, you can show "real time" updates on the
- public and tag pages. However, server configuration is complex.
-- If queues are enabled, update inboxes and memcached off-line. Speeds
- up posting considerably.
-- Correctly shorten links posted through XMPP.
-- <link> elements for pagination, supported by some browsers like Opera.
-- Corrected date format in search API.
-- Made the public XRDS file work correctly.
+This is a major feature release since version 0.7.4, released May 31
+2009. Notable changes this version:
+
+- Support for a hosted service (status network). Multiple sites can
+ share the same codebase but use different databases.
+- OEmbed. Links to pages that support OEmbed (http://www.oembed.com/)
+ become popup links, and the media are shown in a special lightbox.
+- File attachments. Users can attach files of the size and type approved
+ by an administrator, and a shortened link will be included in the
+ notice.
+- Related notices are organized into conversations, with each reply a
+ branch in a tree. Conversations have pages and are linked to from each
+ notice in the conversation.
+- User designs. Users can specify colours and backgrounds
+ for their profile pages and other "personal" pages.
+- Group designs. Group administrators can specify similar designs for
+ group profiles and related pages.
+- Site designs. Site authors can specify a design (background and
+ colors) for the site.
+- New themes. Five new themes are added to the base release; these show
+ off the flexibility of Laconica's theming system.
+- Statistics. Public sites will periodically send usage statistics,
+ configuration options, and dependency information to Laconica dev site.
+ This will help us understand how the software is used and plan future
+ versions of the software.
+- Additional hooks. The hooks and plugins system introduced in 0.7.x was
+ expanded with additional points of access.
+- Facebook Connect. A new plugin allows logging in with Facebook Connect
+ (http://developers.facebook.com/connect.php).
+- A session handler. A new optional session handler class to manage PHP
+ sessions reliably and quickly for large sites.
+- STOMP queuing. Queue management for offline daemons has been
+ abstracted with three concrete instances. A new interface that should
+ work with STOMP servers like ActiveMQ and RabbitMQ is available, which
+ should make things scale better.
+- Group block. Group admins can block users from joining or posting to
+ a group.
+- Group aliases. Groups can be referred to with aliases, additional
+ names. For example, "!yul" and "!montreal" can be the same group.
+- Bidirectional Twitter bridge. Users can read the tweets their Twitter
+ friends post on Twitter.
+- Adaptation of WordPress.com Terms of Service (http://en.wordpress.com/tos/)
+ as default TOS for Laconica sites.
+- Better command-line handling for scripts, including standard options
+ and ability to set hostname and path from the command line.
+- Many, many bug fixes.
Prerequisites
=============
@@ -176,6 +199,11 @@ and the URLs are listed here for your convenience.
version may render your Laconica site unable to send or receive XMPP
messages.
- Facebook library. Used for the Facebook application.
+- PEAR Services_oEmbed. Used for some multimedia integration.
+- PEAR HTTP_Request is an oEmbed dependency.
+- PEAR Validate is an oEmbed dependency.
+- PEAR Net_URL2 is an oEmbed dependency.
+- Console_GetOpt for parsing command-line options.
A design goal of Laconica is that the basic Web functionality should
work on even the most restrictive commercial hosting services.
@@ -193,9 +221,9 @@ especially if you've previously installed PHP/MySQL packages.
1. Unpack the tarball you downloaded on your Web server. Usually a
command like this will work:
- tar zxf laconica-0.7.4.tar.gz
+ tar zxf laconica-0.8.0.tar.gz
- ...which will make a laconica-0.7.4 subdirectory in your current
+ ...which will make a laconica-0.8.0 subdirectory in your current
directory. (If you don't have shell access on your Web server, you
may have to unpack the tarball on your local computer and FTP the
files to the server.)
@@ -203,7 +231,7 @@ especially if you've previously installed PHP/MySQL packages.
2. Move the tarball to a directory of your choosing in your Web root
directory. Usually something like this will work:
- mv laconica-0.7.4 /var/www/mublog
+ mv laconica-0.8.0 /var/www/mublog
This will make your Laconica instance available in the mublog path of
your server, like "http://example.net/mublog". "microblog" or
@@ -507,6 +535,11 @@ All the daemons write their process IDs (pids) to /var/run/ by
default. This can be useful for starting, stopping, and monitoring the
daemons.
+With version 0.8.0, it's now possible to use a STOMP server instead of
+our kind of hacky home-grown DB-based queue solution. See the "queues"
+config section below for how to configure to use STOMP. As of this
+writing, the software has been tested with ActiveMQ (
+
Twitter Friends Syncing
-----------------------
@@ -697,11 +730,11 @@ However, older installations will have the incorrect storage, and will
consequently show up "wrong" in browsers. See below for how to deal
with this situation.
-If you've been using Laconica 0.6, 0.5 or lower, or if you've been
-tracking the "git" version of the software, you will probably want
-to upgrade and keep your existing data. There is no automated upgrade
-procedure in Laconica 0.7.4. Try these step-by-step instructions; read
-to the end first before trying them.
+If you've been using Laconica 0.7, 0.6, 0.5 or lower, or if you've
+been tracking the "git" version of the software, you will probably
+want to upgrade and keep your existing data. There is no automated
+upgrade procedure in Laconica 0.8.0. Try these step-by-step
+instructions; read to the end first before trying them.
0. Download Laconica and set up all the prerequisites as if you were
doing a new install.
@@ -721,20 +754,31 @@ to the end first before trying them.
5. Once all writing processes to your site are turned off, make a
final backup of the Web directory and database.
6. Move your Laconica directory to a backup spot, like "mublog.bak".
-7. Unpack your Laconica 0.6 tarball and move it to "mublog" or
+7. Unpack your Laconica 0.8.0 tarball and move it to "mublog" or
wherever your code used to be.
8. Copy the config.php file and avatar directory from your old
directory to your new directory.
9. Copy htaccess.sample to .htaccess in the new directory. Change the
RewriteBase to use the correct path.
-10. Rebuild the database. For MySQL, go to your Laconica directory and
- run the rebuilddb.sh script like this:
+10. Rebuild the database. NOTE: this step is destructive and cannot be
+ reversed. YOU CAN EASILY DESTROY YOUR SITE WITH THIS STEP. Don't
+ do it without a known-good backup!
+
+ If your database is at version 0.7.4, you can run a special upgrade
+ script:
+
+ mysql -u<rootuser> -p<rootpassword> <database> db/074to080.sql
+
+ Otherwise, go to your Laconica directory and AFTER YOU MAKE A
+ BACKUP run the rebuilddb.sh script like this:
./scripts/rebuilddb.sh rootuser rootpassword database db/laconica.sql
Here, rootuser and rootpassword are the username and password for a
user who can drop and create databases as well as tables; typically
- that's _not_ the user Laconica runs as.
+ that's _not_ the user Laconica runs as. Note that rebuilddb.sh drops
+ your database and rebuilds it; if there is an error you have no
+ database. Make sure you have a backup.
For PostgreSQL databases there is an equivalent, rebuilddb_psql.sh,
which operates slightly differently. Read the documentation in that
script before running it.
@@ -786,6 +830,9 @@ problem.
3. When fixup_inboxes is finished, you can set the enabled flag to
'true'.
+NOTE: we will drop support for non-inboxed sites in the 0.9.x version
+of Laconica. It's time to switch now!
+
UTF-8 Database
--------------
@@ -812,7 +859,7 @@ what to do.
Configuration options
=====================
-The sole configuration file for Laconica (excepting configurations for
+The main configuration file for Laconica (excepting configurations for
dependency software) is config.php in your Laconica directory. If you
edit any other file in the directory, like lib/common.php (where most
of the defaults are defined), you will lose your configuration options
@@ -850,6 +897,8 @@ fancy: whether or not your site uses fancy URLs (see Fancy URLs
logfile: full path to a file for Laconica to save logging
information to. You may want to use this if you don't have
access to syslog.
+logdebug: whether to log additional debug info like backtraces on
+ hard errors. Default false.
locale_path: full path to the directory for locale data. Unless you
store all your locale data in one place, you probably
don't need to use this.
@@ -904,6 +953,15 @@ sslserver: use an alternate server name for SSL URLs, like
shorturllength: Length of URL at which URLs in a message exceeding 140
characters will be sent to the user's chosen
shortening service.
+<<<<<<< HEAD:README
+design: a default design (colors and background) for the site.
+ Sub-items are: backgroundcolor, contentcolor, sidebarcolor,
+ textcolor, linkcolor, backgroundimage, disposition.
+dupelimit: minimum time allowed for one person to say the same thing
+ twice. Default 60s. Anything lower is considered a user
+ or UI error.
+=======
+>>>>>>> 0.7.x:README
db
--
@@ -954,6 +1012,10 @@ appname: The name that Laconica uses to log messages. By default it's
"laconica", but if you have more than one installation on the
server, you may want to change the name for each instance so
you can track log messages more easily.
+priority: level to log at. Currently ignored.
+facility: what syslog facility to used. Defaults to LOG_USER, only
+ reset if you know what syslog is and have a good reason
+ to change it.
queue
-----
@@ -963,7 +1025,19 @@ sending out SMS email or XMPP messages, for off-line processing. See
'Queues and daemons' above for how to set this up.
enabled: Whether to uses queues. Defaults to false.
-
+subsystem: Which kind of queueserver to use. Values include "db" for
+ our hacked-together database queuing (no other server
+ required) and "stomp" for a stomp server.
+stomp_server: "broker URI" for stomp server. Something like
+ "tcp://hostname:61613". More complicated ones are
+ possible; see your stomp server's documentation for
+ details.
+queue_basename: a root name to use for queues (stomp only). Typically
+ something like '/queue/sitename/' makes sense.
+stomp_username: username for connecting to the stomp server; defaults
+ to null.
+stomp_password: password for connecting to the stomp server; defaults
+ to null.
license
-------
@@ -1007,6 +1081,12 @@ avatar
For configuring avatar access.
+dir: Directory to look for avatar files and to put them into.
+ Defaults to avatar subdirectory of install directory; if
+ you change it, make sure to change path, too.
+path: Path to avatars. Defaults to path for avatar subdirectory,
+ but you can change it if you wish. Note that this will
+ be included with the avatar server, too.
server: If set, defines another server where avatars are stored in the
root directory. Note that the 'avatar' subdir still has to be
writeable. You'd typically use this to split HTTP requests on
@@ -1027,14 +1107,23 @@ localonly: If set to true, only messages posted by users of this
blacklist: An array of IDs of users to hide from the public stream.
Useful if you have someone making excessive Twitterfeed posts
to the site, other kinds of automated posts, testing bots, etc.
+autosource: Sources of notices that are from automatic posters, and thus
+ should be kept off the public timeline. Default empty.
theme
-----
server: Like avatars, you can speed up page loading by pointing the
- theme file lookup to another server (virtual or real). The
- theme server's root path should map to the Laconica "theme"
- subdirectory. Defaults to NULL.
+ theme file lookup to another server (virtual or real).
+ Defaults to NULL, meaning to use the site server.
+dir: Directory where theme files are stored. Used to determine
+ whether to show parts of a theme file. Defaults to the theme
+ subdirectory of the install directory.
+path: Path part of theme URLs, before the theme name. Relative to the
+ theme server. It may make sense to change this path when upgrading,
+ (using version numbers as the path) to make sure that all files are
+ reloaded by caching clients or proxies. Defaults to null,
+ which means to use the site path + '/theme'.
xmpp
----
@@ -1066,6 +1155,13 @@ debug: if turned on, this will make the XMPP library blurt out all of
public: an array of JIDs to send _all_ notices to. This is useful for
participating in third-party search and archiving services.
+invite
+------
+
+For configuring invites.
+
+enabled: Whether to allow users to send invites. Default true.
+
tag
---
@@ -1075,6 +1171,15 @@ dropoff: Decay factor for tag listing, in seconds.
Defaults to exponential decay over ten days; you can twiddle
with it to try and get better results for your site.
+popular
+-------
+
+Settings for the "popular" section of the site.
+
+dropoff: Decay factor for popularity listing, in seconds.
+ Defaults to exponential decay over ten days; you can twiddle
+ with it to try and get better results for your site.
+
daemon
------
@@ -1099,6 +1204,13 @@ database data in memcached <http://www.danga.com/memcached/>.
enabled: Set to true to enable. Default false.
server: a string with the hostname of the memcached server. Can also
be an array of hostnames, if you've got more than one server.
+base: memcached uses key-value pairs to store data. We build long,
+ funny-looking keys to make sure we don't have any conflicts. The
+ base of the key is usually a simplified version of the site name
+ (like "Identi.ca" => "identica"), but you can overwrite this if
+ you need to. You can safely ignore it if you only have one
+ Laconica site using your memcached server.
+port: Port to connect to; defaults to 11211.
sphinx
------
@@ -1121,6 +1233,7 @@ source: The name to use for the source of posts to Twitter. Defaults
Twitter <http://twitter.com/help/request_source>, you can use
that here instead. Status updates on Twitter will then have
links to your site.
+taguri: base for tag:// URIs. Defaults to site-server + ',2009'.
inboxes
-------
@@ -1169,6 +1282,148 @@ welcome: nickname of a user account that sends welcome messages to new
If either of these special user accounts are specified, the users should
be created before the configuration is updated.
+snapshot
+--------
+
+The software will, by default, send statistical snapshots about the
+local installation to a stats server on the laconi.ca Web site. This
+data is used by the developers to prioritize development decisions. No
+identifying data about users or organizations is collected. The data
+is available to the public for review. Participating in this survey
+helps Laconica developers take your needs into account when updating
+the software.
+
+run: string indicating when to run the statistics. Values can be 'web'
+ (run occasionally at Web time), 'cron' (run from a cron script),
+ or 'never' (don't ever run). If you set it to 'cron', remember to
+ schedule the script to run on a regular basis.
+frequency: if run value is 'web', how often to report statistics.
+ Measured in Web hits; depends on how active your site is.
+ Default is 10000 -- that is, one report every 10000 Web hits,
+ on average.
+reporturl: URL to post statistics to. Defaults to Laconica developers'
+ report system, but if they go evil or disappear you may
+ need to update this to another value. Note: if you
+ don't want to report stats, it's much better to
+ set 'run' to 'never' than to set this value to something
+ nonsensical.
+
+attachments
+-----------
+
+The software lets users upload files with their notices. You can configure
+the types of accepted files by mime types and a trio of quota options:
+per file, per user (total), per user per month.
+
+We suggest the use of the pecl file_info extension to handle mime type
+detection.
+
+supported: an array of mime types you accept to store and distribute,
+ like 'image/gif', 'video/mpeg', 'audio/mpeg', etc. Make sure you
+ setup your server to properly recognize the types you want to
+ support.
+uploads: false to disable uploading files with notices (true by default).
+filecommand: The required MIME_Type library may need to use the 'file'
+ command. It tries the one in the Web server's path, but if
+ you're having problems with uploads, try setting this to the
+ correct value. Note: 'file' must accept '-b' and '-i' options.
+
+For quotas, be sure you've set the upload_max_filesize and post_max_size
+in php.ini to be large enough to handle your upload. In httpd.conf
+(if you're using apache), check that the LimitRequestBody directive isn't
+set too low (it's optional, so it may not be there at all).
+
+file_quota: maximum size for a single file upload in bytes. A user can send
+ any amount of notices with attachments as long as each attachment
+ is smaller than file_quota.
+user_quota: total size in bytes a user can store on this server. Each user
+ can store any number of files as long as their total size does
+ not exceed the user_quota.
+monthly_quota: total size permitted in the current month. This is the total
+ size in bytes that a user can upload each month.
+dir: directory accessible to the Web process where uploads should go.
+ Defaults to the 'file' subdirectory of the install directory, which
+ should be writeable by the Web user.
+server: server name to use when creating URLs for uploaded files.
+ Defaults to null, meaning to use the default Web server. Using
+ a virtual server here can speed up Web performance.
+path: URL path, relative to the server, to find files. Defaults to
+ main path + '/file/'.
+filecommand: command to use for determining the type of a file. May be
+ skipped if fileinfo extension is installed. Defaults to
+ '/usr/bin/file'.
+
+group
+-----
+
+Options for group functionality.
+
+maxaliases: maximum number of aliases a group can have. Default 3. Set
+ to 0 or less to prevent aliases in a group.
+
+oohembed
+--------
+
+oEmbed endpoint for multimedia attachments (links in posts).
+
+endpoint: oohembed endpoint using http://oohembed.com/ software.
+
+search
+------
+
+Some stuff for search.
+
+type: type of search. Ignored if PostgreSQL or Sphinx are enabled. Can either
+ be 'fulltext' (default) or 'like'. The former is faster and more efficient
+ but requires the lame old MyISAM engine for MySQL. The latter
+ will work with InnoDB but could be miserably slow on large
+ systems. We'll probably add another type sometime in the future,
+ with our own indexing system (maybe like MediaWiki's).
+
+sessions
+--------
+
+Session handling.
+
+handle: boolean. Whether we should register our own PHP session-handling
+ code (using the database and memcache if enabled). Defaults to false.
+ Setting this to true makes some sense on large or multi-server
+ sites, but it probably won't hurt for smaller ones, either.
+debug: whether to output debugging info for session storage. Can help
+ with weird session bugs, sometimes. Default false.
+
+background
+----------
+
+Users can upload backgrounds for their pages; this section defines
+their use.
+
+server: the server to use for background. Using a separate (even
+ virtual) server for this can speed up load times. Default is
+ null; same as site server.
+dir: directory to write backgrounds too. Default is '/background/'
+ subdir of install dir.
+path: path to backgrounds. Default is sub-path of install path; note
+ that you may need to change this if you change site-path too.
+
+twitterbridge
+-------------
+
+A bi-direction bridge to Twitter (http://twitter.com/).
+
+enabled: default false. If true, will show user's Twitter friends'
+ notices in their inbox and faves pages, only to the user. You
+ must also run the twitterstatusfetcher.php script.
+
+ping
+----
+
+Using the "XML-RPC Ping" method initiated by weblogs.com, the site can
+notify third-party servers of updates.
+
+notify: an array of URLs for ping endpoints. Default is the empty
+ array (no notification).
+
Troubleshooting
===============
@@ -1265,7 +1520,7 @@ if anyone's been overlooked in error.
* Ori Avtalion
* Meitar Moscovitz
* Ken Sheppardson (Trac server, man-about-town)
-* Tiago 'gouki' Faria (i18n managerx)
+* Tiago 'gouki' Faria (i18n manager)
* Sean Murphy
* Leslie Michael Orchard
* Eric Helgeson
@@ -1274,6 +1529,10 @@ if anyone's been overlooked in error.
* Tobias Diekershoff
* Dan Moore
* Fil
+* Jeff Mitchell
+* Brenda Wallace
+* Jeffery To
+* Federico Marani
Thanks also to the developers of our upstream library code and to the
thousands of people who have tried out Identi.ca, installed Laconi.ca,
diff --git a/actions/accesstoken.php b/actions/accesstoken.php
index 46b43c702..2a8cd1713 100644
--- a/actions/accesstoken.php
+++ b/actions/accesstoken.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/all.php b/actions/all.php
index 03179a246..f06ead2a8 100644
--- a/actions/all.php
+++ b/actions/all.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -98,7 +98,13 @@ class AllAction extends ProfileAction
function showContent()
{
- $notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+ $cur = common_current_user();
+
+ if (!empty($cur) && $cur->id == $this->user->id) {
+ $notice = $this->user->noticeInbox(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+ } else {
+ $notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+ }
$nl = new NoticeList($notice, $this);
diff --git a/actions/allrss.php b/actions/allrss.php
index 45f3946a6..885a67f61 100644
--- a/actions/allrss.php
+++ b/actions/allrss.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -81,6 +81,14 @@ class AllrssAction extends Rss10Action
*/
function getNotices($limit=0)
{
+ $cur = common_current_user();
+
+ if (!empty($cur) && $cur->id == $user->id) {
+ $notice = $this->user->noticeInbox(0, $limit);
+ } else {
+ $notice = $this->user->noticesWithFriends(0, $limit);
+ }
+
$user = $this->user;
$notice = $user->noticesWithFriends(0, $limit);
$notices = array();
diff --git a/actions/api.php b/actions/api.php
index b8da852b5..18c3b68d4 100644
--- a/actions/api.php
+++ b/actions/api.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -67,20 +67,22 @@ class ApiAction extends Action
$this->process_command();
} else {
# basic authentication failed
- common_log(LOG_WARNING, "Failed API auth attempt, nickname: $nickname.");
+ list($proxy, $ip) = common_client_ip();
+
+ common_log(LOG_WARNING, "Failed API auth attempt, nickname = $nickname, proxy = $proxy, ip = $ip.");
$this->show_basic_auth_error();
}
}
} else {
- # Caller might give us a username even if not required
- if (isset($_SERVER['PHP_AUTH_USER'])) {
- $user = User::staticGet('nickname', $_SERVER['PHP_AUTH_USER']);
- if ($user) {
- $this->user = $user;
- }
- # Twitter doesn't throw an error if the user isn't found
- }
+ // Caller might give us a username even if not required
+ if (isset($_SERVER['PHP_AUTH_USER'])) {
+ $user = User::staticGet('nickname', $_SERVER['PHP_AUTH_USER']);
+ if ($user) {
+ $this->user = $user;
+ }
+ # Twitter doesn't throw an error if the user isn't found
+ }
$this->process_command();
}
@@ -115,7 +117,7 @@ class ApiAction extends Action
}
}
- # Whitelist of API methods that don't need authentication
+ // Whitelist of API methods that don't need authentication
function requires_auth()
{
static $noauth = array( 'statuses/public_timeline',
@@ -133,28 +135,61 @@ class ApiAction extends Action
'statuses/replies',
'statuses/mentions',
'statuses/followers',
- 'favorites/favorites');
+ 'favorites/favorites',
+ 'friendships/show');
$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 $fullname != 'laconica/config' || false;
}
+ // bareauth: only needs auth if without an argument or query param specifying user
+
if (in_array($fullname, $bareauth)) {
- # bareauth: only needs auth if without an argument or query param specifying user
- if ($this->api_arg || $this->arg('id') || is_numeric($this->arg('user_id')) || $this->arg('screen_name')) {
+
+ // Special case: friendships/show only needs auth if source_id or
+ // source_screen_name is not specified as a param
+
+ if ($fullname == 'friendships/show') {
+
+ $source_id = $this->arg('source_id');
+ $source_screen_name = $this->arg('source_screen_name');
+
+ if (empty($source_id) && empty($source_screen_name)) {
+ return true;
+ }
+
return false;
- } else {
+ }
+
+ // if all of these are empty, auth is required
+
+ $id = $this->arg('id');
+ $user_id = $this->arg('user_id');
+ $screen_name = $this->arg('screen_name');
+
+ if (empty($this->api_arg) &&
+ empty($id) &&
+ empty($user_id) &&
+ empty($screen_name)) {
return true;
+ } else {
+ return false;
}
+
} else if (in_array($fullname, $noauth)) {
- # noauth: never needs auth
+
+ // noauth: never needs auth
+
return false;
} else {
- # everybody else needs auth
+
+ // everybody else needs auth
+
return true;
}
}
diff --git a/actions/attachment.php b/actions/attachment.php
new file mode 100644
index 000000000..ee4cd9640
--- /dev/null
+++ b/actions/attachment.php
@@ -0,0 +1,172 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * 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 Personal
+ * @package Laconica
+ * @author Evan Prodromou <evan@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/attachmentlist.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @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 AttachmentAction extends Action
+{
+ /**
+ * Attachment object to show
+ */
+
+ var $attachment = null;
+
+ /**
+ * Load attributes based on database arguments
+ *
+ * Loads all the DB stuff
+ *
+ * @param array $args $_REQUEST array
+ *
+ * @return success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if ($id = $this->trimmed('attachment')) {
+ $this->attachment = File::staticGet($id);
+ }
+
+ if (empty($this->attachment)) {
+ $this->clientError(_('No such attachment.'), 404);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Is this action read-only?
+ *
+ * @return boolean true
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string title of the page
+ */
+ function title()
+ {
+ $a = new Attachment($this->attachment);
+ return $a->title();
+ }
+
+ /**
+ * Handle input
+ *
+ * Only handles get, so just show the page.
+ *
+ * @param array $args $_REQUEST data (unused)
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if (empty($this->attachment->filename)) {
+
+ // if it's not a local file, gtfo
+
+ common_redirect($this->attachment->url, 303);
+
+ } else {
+ $this->showPage();
+ }
+ }
+
+ /**
+ * Don't show local navigation
+ *
+ * @return void
+ */
+
+ function showLocalNavBlock()
+ {
+ }
+
+ /**
+ * Fill the content area of the page
+ *
+ * Shows a single notice list item.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $ali = new Attachment($this->attachment, $this);
+ $cnt = $ali->show();
+ }
+
+ /**
+ * Don't show page notice
+ *
+ * @return void
+ */
+
+ function showPageNoticeBlock()
+ {
+ }
+
+ /**
+ * Show aside: this attachments appears in what notices
+ *
+ * @return void
+ */
+ function showSections() {
+ $ns = new AttachmentNoticeSection($this);
+ $ns->show();
+ $atcs = new AttachmentTagCloudSection($this);
+ $atcs->show();
+ }
+}
+
diff --git a/actions/attachment_ajax.php b/actions/attachment_ajax.php
new file mode 100644
index 000000000..4caa159f3
--- /dev/null
+++ b/actions/attachment_ajax.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * 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 Personal
+ * @package Laconica
+ * @author Evan Prodromou <evan@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.'/actions/attachment.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @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 Attachment_ajaxAction extends AttachmentAction
+{
+ /**
+ * Show page, a template method.
+ *
+ * @return nothing
+ */
+ function showPage()
+ {
+ if (Event::handle('StartShowBody', array($this))) {
+ $this->showCore();
+ Event::handle('EndShowBody', array($this));
+ }
+ }
+
+ function handle($args)
+ {
+ $this->showPage();
+ }
+
+ /**
+ * Show core.
+ *
+ * Shows local navigation, content block and aside.
+ *
+ * @return nothing
+ */
+ function showCore()
+ {
+ $this->elementStart('div', array('id' => 'core'));
+ if (Event::handle('StartShowContentBlock', array($this))) {
+ $this->showContentBlock();
+ Event::handle('EndShowContentBlock', array($this));
+ }
+ $this->elementEnd('div');
+ }
+}
+
diff --git a/actions/attachment_thumbnail.php b/actions/attachment_thumbnail.php
new file mode 100644
index 000000000..248d16e38
--- /dev/null
+++ b/actions/attachment_thumbnail.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * 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 Personal
+ * @package Laconica
+ * @author Evan Prodromou <evan@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.'/actions/attachment.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @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 Attachment_thumbnailAction extends AttachmentAction
+{
+
+ function handle($args)
+ {
+ $this->showPage();
+ }
+
+ /**
+ * Show page, a template method.
+ *
+ * @return nothing
+ */
+ function showPage()
+ {
+ if (Event::handle('StartShowBody', array($this))) {
+ $this->showCore();
+ Event::handle('EndShowBody', array($this));
+ }
+ }
+
+ /**
+ * Show core.
+ *
+ * Shows local navigation, content block and aside.
+ *
+ * @return nothing
+ */
+ function showCore()
+ {
+ $file_thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id);
+ if (empty($file_thumbnail->url)) {
+ return;
+ }
+ $this->element('img', array('src' => $file_thumbnail->url, 'alt' => 'Thumbnail'));
+ }
+
+}
+
diff --git a/actions/avatarbynickname.php b/actions/avatarbynickname.php
index e92a99372..3e615261f 100644
--- a/actions/avatarbynickname.php
+++ b/actions/avatarbynickname.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/block.php b/actions/block.php
index 0efee5932..06f92254e 100644
--- a/actions/block.php
+++ b/actions/block.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -125,16 +125,18 @@ class BlockAction extends Action
function areYouSureForm()
{
$id = $this->profile->id;
+ $this->elementStart('form', array('id' => 'block-' . $id,
+ 'method' => 'post',
+ 'class' => 'form_settings form_entity_block',
+ 'action' => common_local_url('block')));
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+ $this->element('legend', _('Block user'));
$this->element('p', null,
_('Are you sure you want to block this user? '.
'Afterwards, they will be unsubscribed from you, '.
'unable to subscribe to you in the future, and '.
'you will not be notified of any @-replies from them.'));
- $this->elementStart('form', array('id' => 'block-' . $id,
- 'method' => 'post',
- 'class' => 'block',
- 'action' => common_local_url('block')));
- $this->hidden('token', common_session_token());
$this->element('input', array('id' => 'blockto-' . $id,
'name' => 'blockto',
'type' => 'hidden',
@@ -144,8 +146,9 @@ class BlockAction extends Action
$this->hidden($k, $v);
}
}
- $this->submit('no', _('No'));
- $this->submit('yes', _('Yes'));
+ $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not block this user from this group"));
+ $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Block this user from this group'));
+ $this->elementEnd('fieldset');
$this->elementEnd('form');
}
diff --git a/actions/blockedfromgroup.php b/actions/blockedfromgroup.php
new file mode 100644
index 000000000..5c1eab354
--- /dev/null
+++ b/actions/blockedfromgroup.php
@@ -0,0 +1,315 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * List of group members
+ *
+ * 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 Group
+ * @package Laconica
+ * @author Evan Prodromou <evan@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);
+}
+
+/**
+ * List of profiles blocked from this group
+ *
+ * @category Group
+ * @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 BlockedfromgroupAction extends GroupDesignAction
+{
+ var $page = null;
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
+
+ $nickname_arg = $this->arg('nickname');
+ $nickname = common_canonical_nickname($nickname_arg);
+
+ // Permanent redirect on non-canonical nickname
+
+ if ($nickname_arg != $nickname) {
+ $args = array('nickname' => $nickname);
+ if ($this->page != 1) {
+ $args['page'] = $this->page;
+ }
+ common_redirect(common_local_url('blockedfromgroup', $args), 301);
+ return false;
+ }
+
+ if (!$nickname) {
+ $this->clientError(_('No nickname'), 404);
+ return false;
+ }
+
+ $this->group = User_group::staticGet('nickname', $nickname);
+
+ if (!$this->group) {
+ $this->clientError(_('No such group'), 404);
+ return false;
+ }
+
+ return true;
+ }
+
+ function title()
+ {
+ if ($this->page == 1) {
+ return sprintf(_('%s blocked profiles'),
+ $this->group->nickname);
+ } else {
+ return sprintf(_('%s blocked profiles, page %d'),
+ $this->group->nickname,
+ $this->page);
+ }
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ function showPageNotice()
+ {
+ $this->element('p', 'instructions',
+ _('A list of the users blocked from joining this group.'));
+ }
+
+ function showLocalNav()
+ {
+ $nav = new GroupNav($this, $this->group);
+ $nav->show();
+ }
+
+ function showContent()
+ {
+ $offset = ($this->page-1) * PROFILES_PER_PAGE;
+ $limit = PROFILES_PER_PAGE + 1;
+
+ $cnt = 0;
+
+ $blocked = $this->group->getBlocked($offset, $limit);
+
+ if ($blocked) {
+ $blocked_list = new GroupBlockList($blocked, $this->group, $this);
+ $cnt = $blocked_list->show();
+ }
+
+ $blocked->free();
+
+ $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
+ $this->page, 'blockedfromgroup',
+ array('nickname' => $this->group->nickname));
+ }
+}
+
+class GroupBlockList extends ProfileList
+{
+ var $group = null;
+
+ function __construct($profile, $group, $action)
+ {
+ parent::__construct($profile, $action);
+
+ $this->group = $group;
+ }
+
+ function newListItem($profile)
+ {
+ return new GroupBlockListItem($profile, $this->group, $this->action);
+ }
+}
+
+class GroupBlockListItem extends ProfileListItem
+{
+ var $group = null;
+
+ function __construct($profile, $group, $action)
+ {
+ parent::__construct($profile, $action);
+
+ $this->group = $group;
+ }
+
+ function showActions()
+ {
+ $this->startActions();
+ $this->showGroupUnblockForm();
+ $this->endActions();
+ }
+
+ function showGroupUnblockForm()
+ {
+ $user = common_current_user();
+
+ if (!empty($user) && $user->id != $this->profile->id && $user->isAdmin($this->group)) {
+ $this->out->elementStart('li', 'entity_block');
+ $bf = new GroupUnblockForm($this->out, $this->profile, $this->group,
+ array('action' => 'blockedfromgroup',
+ 'nickname' => $this->group->nickname));
+ $bf->show();
+ $this->out->elementEnd('li');
+ }
+ }
+}
+
+/**
+ * Form for unblocking a user from a group
+ *
+ * @category Form
+ * @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/
+ *
+ * @see UnblockForm
+ */
+
+class GroupUnblockForm extends Form
+{
+ /**
+ * Profile of user to block
+ */
+
+ var $profile = null;
+
+ /**
+ * Group to block the user from
+ */
+
+ var $group = null;
+
+ /**
+ * Return-to args
+ */
+
+ var $args = null;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out output channel
+ * @param Profile $profile profile of user to block
+ * @param User_group $group group to block user from
+ * @param array $args return-to args
+ */
+
+ function __construct($out=null, $profile=null, $group=null, $args=null)
+ {
+ parent::__construct($out);
+
+ $this->profile = $profile;
+ $this->group = $group;
+ $this->args = $args;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ // This should be unique for the page.
+ return 'unblock-' . $this->profile->id;
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'form_group_unblock';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('groupunblock');
+ }
+
+ /**
+ * Legend of the Form
+ *
+ * @return void
+ */
+ function formLegend()
+ {
+ $this->out->element('legend', null, _('Unblock user from group'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->hidden('unblockto-' . $this->profile->id,
+ $this->profile->id,
+ 'unblockto');
+ $this->out->hidden('unblockgroup-' . $this->group->id,
+ $this->group->id,
+ 'unblockgroup');
+ if ($this->args) {
+ foreach ($this->args as $k => $v) {
+ $this->out->hidden('returnto-' . $k, $v);
+ }
+ }
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('submit', _('Unblock'), 'submit', null, _('Unblock this user'));
+ }
+}
diff --git a/actions/conversation.php b/actions/conversation.php
new file mode 100644
index 000000000..0eb0d86d6
--- /dev/null
+++ b/actions/conversation.php
@@ -0,0 +1,294 @@
+<?php
+/**
+ * Display a conversation in the browser
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@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) 2009, Control Yourself, 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);
+}
+
+// XXX: not sure how to do paging yet,
+// so set a 60-notice limit
+
+require_once INSTALLDIR.'/lib/noticelist.php';
+
+/**
+ * Conversation tree in the browser
+ *
+ * @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 ConversationAction extends Action
+{
+ var $id = null;
+ var $page = null;
+
+ /**
+ * Initialization.
+ *
+ * @param array $args Web and URL arguments
+ *
+ * @return boolean false if id not passed in
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->id = $this->trimmed('id');
+ if (empty($this->id)) {
+ return false;
+ }
+ $this->id = $this->id+0;
+ $this->page = $this->trimmed('page');
+ if (empty($this->page)) {
+ $this->page = 1;
+ }
+ return true;
+ }
+
+ /**
+ * Handle the action
+ *
+ * @param array $args Web and URL arguments
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ /**
+ * Returns the page title
+ *
+ * @return string page title
+ */
+
+ function title()
+ {
+ return _("Conversation");
+ }
+
+ /**
+ * Show content.
+ *
+ * Display a hierarchical unordered list in the content area.
+ * Uses ConversationTree to do most of the heavy lifting.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $notices = Notice::conversationStream($this->id, null, null);
+
+ $ct = new ConversationTree($notices, $this);
+
+ $cnt = $ct->show();
+ }
+}
+
+/**
+ * Conversation tree
+ *
+ * The widget class for displaying a hierarchical list of notices.
+ *
+ * @category Widget
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ */
+
+class ConversationTree extends NoticeList
+{
+ var $tree = null;
+ var $table = null;
+
+ /**
+ * Show the tree of notices
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $cnt = $this->_buildTree();
+
+ $this->out->elementStart('div', array('id' =>'notices_primary'));
+ $this->out->element('h2', null, _('Notices'));
+ $this->out->elementStart('ol', array('class' => 'notices xoxo'));
+
+ if (array_key_exists('root', $this->tree)) {
+ $rootid = $this->tree['root'][0];
+ $this->showNoticePlus($rootid);
+ }
+
+ $this->out->elementEnd('ol');
+ $this->out->elementEnd('div');
+
+ return $cnt;
+ }
+
+ function _buildTree()
+ {
+ $this->tree = array();
+ $this->table = array();
+
+ while ($this->notice->fetch()) {
+
+ $cnt++;
+
+ $id = $this->notice->id;
+ $notice = clone($this->notice);
+
+ $this->table[$id] = $notice;
+
+ if (is_null($notice->reply_to)) {
+ $this->tree['root'] = array($notice->id);
+ } else if (array_key_exists($notice->reply_to, $this->tree)) {
+ $this->tree[$notice->reply_to][] = $notice->id;
+ } else {
+ $this->tree[$notice->reply_to] = array($notice->id);
+ }
+ }
+
+ return $cnt;
+ }
+
+ /**
+ * Shows a notice plus its list of children.
+ *
+ * @param integer $id ID of the notice to show
+ *
+ * @return void
+ */
+
+ function showNoticePlus($id)
+ {
+ $notice = $this->table[$id];
+
+ // We take responsibility for doing the li
+
+ $this->out->elementStart('li', array('class' => 'hentry notice',
+ 'id' => 'notice-' . $id));
+
+ $item = $this->newListItem($notice);
+ $item->show();
+
+ if (array_key_exists($id, $this->tree)) {
+ $children = $this->tree[$id];
+
+ $this->out->elementStart('ol', array('class' => 'notices'));
+
+ sort($children);
+
+ foreach ($children as $child) {
+ $this->showNoticePlus($child);
+ }
+
+ $this->out->elementEnd('ol');
+ }
+
+ $this->out->elementEnd('li');
+ }
+
+ /**
+ * Override parent class to return our preferred item.
+ *
+ * @param Notice $notice Notice to display
+ *
+ * @return NoticeListItem a list item to show
+ */
+
+ function newListItem($notice)
+ {
+ return new ConversationTreeItem($notice, $this->out);
+ }
+}
+
+/**
+ * Conversation tree list item
+ *
+ * Special class of NoticeListItem for use inside conversation trees.
+ *
+ * @category Widget
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ */
+
+class ConversationTreeItem extends NoticeListItem
+{
+ /**
+ * start a single notice.
+ *
+ * The default creates the <li>; we skip, since the ConversationTree
+ * takes care of that.
+ *
+ * @return void
+ */
+
+ function showStart()
+ {
+ return;
+ }
+
+ /**
+ * finish the notice
+ *
+ * The default closes the <li>; we skip, since the ConversationTree
+ * takes care of that.
+ *
+ * @return void
+ */
+
+ function showEnd()
+ {
+ return;
+ }
+
+ /**
+ * show link to notice conversation page
+ *
+ * Since we're only used on the conversation page, we skip this
+ *
+ * @return void
+ */
+
+ function showContext()
+ {
+ return;
+ }
+}
diff --git a/actions/deletenotice.php b/actions/deletenotice.php
index 6c350b33a..e733f9650 100644
--- a/actions/deletenotice.php
+++ b/actions/deletenotice.php
@@ -112,8 +112,8 @@ class DeletenoticeAction extends DeleteAction
$this->hidden('token', common_session_token());
$this->hidden('notice', $this->trimmed('notice'));
$this->element('p', null, _('Are you sure you want to delete this notice?'));
- $this->submit('form_action-yes', _('Yes'), 'submit form_action-primary', 'yes');
- $this->submit('form_action-no', _('No'), 'submit form_action-secondary', 'no');
+ $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not delete this notice"));
+ $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Delete this notice'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
diff --git a/actions/disfavor.php b/actions/disfavor.php
index bc13b09da..02e01d6e0 100644
--- a/actions/disfavor.php
+++ b/actions/disfavor.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -75,7 +75,7 @@ class DisfavorAction extends Action
return;
}
$fave = new Fave();
- $fave->user_id = $this->id;
+ $fave->user_id = $user->id;
$fave->notice_id = $notice->id;
if (!$fave->find(true)) {
$this->clientError(_('This notice is not a favorite!'));
diff --git a/actions/doc.php b/actions/doc.php
index e6508030b..54ae13803 100644
--- a/actions/doc.php
+++ b/actions/doc.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/editgroup.php b/actions/editgroup.php
index 39dad0465..6aa6f8b11 100644
--- a/actions/editgroup.php
+++ b/actions/editgroup.php
@@ -23,6 +23,7 @@
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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/
@@ -40,14 +41,15 @@ if (!defined('LACONICA')) {
* @category Group
* @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/
*/
-class EditgroupAction extends Action
+class EditgroupAction extends GroupDesignAction
{
+
var $msg;
- var $group = null;
function title()
{
@@ -171,6 +173,7 @@ class EditgroupAction extends Action
$homepage = $this->trimmed('homepage');
$description = $this->trimmed('description');
$location = $this->trimmed('location');
+ $aliasstring = $this->trimmed('aliases');
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
@@ -201,6 +204,39 @@ class EditgroupAction extends Action
return;
}
+ if (!empty($aliasstring)) {
+ $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring)));
+ } else {
+ $aliases = array();
+ }
+
+ if (count($aliases) > common_config('group', 'maxaliases')) {
+ $this->showForm(sprintf(_('Too many aliases! Maximum %d.'),
+ common_config('group', 'maxaliases')));
+ return;
+ }
+
+ foreach ($aliases as $alias) {
+ if (!Validate::string($alias, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => NICKNAME_FMT))) {
+ $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
+ return;
+ }
+ if ($this->nicknameExists($alias)) {
+ $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
+ $alias));
+ return;
+ }
+ // XXX assumes alphanum nicknames
+ if (strcmp($alias, $nickname) == 0) {
+ $this->showForm(_('Alias can\'t be the same as nickname.'));
+ return;
+ }
+ }
+
+ $this->group->query('BEGIN');
+
$orig = clone($this->group);
$this->group->nickname = $nickname;
@@ -217,6 +253,14 @@ class EditgroupAction extends Action
$this->serverError(_('Could not update group.'));
}
+ $result = $this->group->setAliases($aliases);
+
+ if (!$result) {
+ $this->serverError(_('Could not create aliases.'));
+ }
+
+ $this->group->query('COMMIT');
+
if ($this->group->nickname != $orig->nickname) {
common_redirect(common_local_url('editgroup',
array('nickname' => $nickname)),
@@ -229,9 +273,20 @@ class EditgroupAction extends Action
function nicknameExists($nickname)
{
$group = User_group::staticGet('nickname', $nickname);
- return (!is_null($group) &&
- $group != false &&
- $group->id != $this->group->id);
+
+ if (!empty($group) &&
+ $group->id != $this->group->id) {
+ return true;
+ }
+
+ $alias = Group_alias::staticGet('alias', $nickname);
+
+ if (!empty($alias) &&
+ $alias->group_id != $this->group->id) {
+ return true;
+ }
+
+ return false;
}
}
diff --git a/actions/facebookhome.php b/actions/facebookhome.php
index 4c2b26355..34989c978 100644
--- a/actions/facebookhome.php
+++ b/actions/facebookhome.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -21,29 +21,28 @@ if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/lib/facebookaction.php';
-
class FacebookhomeAction extends FacebookAction
{
var $page = null;
-
+
function prepare($argarray)
- {
+ {
parent::prepare($argarray);
-
+
$this->page = $this->trimmed('page');
-
+
if (!$this->page) {
$this->page = 1;
}
-
+
return true;
}
function handle($args)
{
- parent::handle($args);
-
+ parent::handle($args);
+
// If the user has opted not to initially allow the app to have
// Facebook status update permission, store that preference. Only
// promt the user the first time she uses the app
@@ -73,7 +72,7 @@ class FacebookhomeAction extends FacebookAction
$this->updateProfileBox($notice);
}
- if ($this->arg('status_submit') == 'Send') {
+ if ($this->arg('status_submit') == 'Send') {
$this->saveNewNotice();
}
@@ -81,7 +80,7 @@ class FacebookhomeAction extends FacebookAction
// Facebook status update permission? Then show the main page
// of the app
$this->showPage();
-
+
} else {
// User hasn't authenticated yet, prompt for creds
@@ -89,12 +88,12 @@ class FacebookhomeAction extends FacebookAction
}
}
-
+
function login()
{
-
+
$this->showStylesheets();
-
+
$nickname = common_canonical_nickname($this->trimmed('nickname'));
$password = $this->arg('password');
@@ -115,7 +114,7 @@ class FacebookhomeAction extends FacebookAction
$flink->foreign_id = $this->fbuid;
$flink->service = FACEBOOK_SERVICE;
$flink->created = common_sql_now();
- $flink->set_flags(true, false, false);
+ $flink->set_flags(true, false, false, false);
$flink_id = $flink->insert();
@@ -141,13 +140,12 @@ class FacebookhomeAction extends FacebookAction
$this->facebook->api_client->data_setUserPreference(
FACEBOOK_PROMPTED_UPDATE_PREF, 'false');
}
-
function showNoticeForm()
{
$post_action = "$this->app_uri/index.php";
-
- $notice_form = new FacebookNoticeForm($this, $post_action, null,
+
+ $notice_form = new FacebookNoticeForm($this, $post_action, null,
$post_action, $this->user);
$notice_form->show();
}
@@ -163,9 +161,8 @@ class FacebookhomeAction extends FacebookAction
function showContent()
{
- $notice = $this->user->noticesWithFriends(($this->page-1) *
- NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
-
+ $notice = $this->user->noticeInbox(($this->page-1) * NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+
$nl = new NoticeList($notice, $this);
$cnt = $nl->show();
@@ -176,7 +173,7 @@ class FacebookhomeAction extends FacebookAction
function showNoticeList($notice)
{
-
+
$nl = new NoticeList($notice, $this);
return $nl->show();
}
@@ -201,16 +198,16 @@ class FacebookhomeAction extends FacebookAction
$this->elementStart('ul', array('id' => 'fb-permissions-list'));
$this->elementStart('li', array('id' => 'fb-permissions-item'));
-
+
$next = urlencode("$this->app_uri/index.php");
$api_key = common_config('facebook', 'apikey');
-
+
$auth_url = 'http://www.facebook.com/authorize.php?api_key=' .
- $api_key . '&v=1.0&ext_perm=status_update&next=' . $next .
+ $api_key . '&v=1.0&ext_perm=status_update&next=' . $next .
'&next_cancel=' . $next . '&submit=skip';
-
+
$this->elementStart('span', array('class' => 'facebook-button'));
- $this->element('a', array('href' => $auth_url),
+ $this->element('a', array('href' => $auth_url),
sprintf(_('Okay, do it!'), $this->app_name));
$this->elementEnd('span');
@@ -225,7 +222,7 @@ class FacebookhomeAction extends FacebookAction
$this->elementEnd('div');
}
-
+
/**
* Generate pagination links
*
@@ -239,11 +236,11 @@ class FacebookhomeAction extends FacebookAction
*/
function pagination($have_before, $have_after, $page, $action, $args=null)
{
-
+
// Does a little before-after block for next/prev page
-
+
// XXX: Fix so this uses common_local_url() if possible.
-
+
if ($have_before || $have_after) {
$this->elementStart('div', array('class' => 'pagination'));
$this->elementStart('dl', null);
@@ -254,7 +251,7 @@ class FacebookhomeAction extends FacebookAction
if ($have_before) {
$pargs = array('page' => $page-1);
$newargs = $args ? array_merge($args, $pargs) : $pargs;
- $this->elementStart('li', array('class' => 'nav_prev'));
+ $this->elementStart('li', array('class' => 'nav_prev'));
$this->element('a', array('href' => "$action?page=$newargs[page]", 'rel' => 'prev'),
_('After'));
$this->elementEnd('li');
@@ -274,6 +271,5 @@ class FacebookhomeAction extends FacebookAction
$this->elementEnd('div');
}
}
-
}
diff --git a/actions/facebookinvite.php b/actions/facebookinvite.php
index 2207580f7..f43d04e27 100644
--- a/actions/facebookinvite.php
+++ b/actions/facebookinvite.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/facebooklogin.php b/actions/facebooklogin.php
index 94d494a82..22007da4f 100644
--- a/actions/facebooklogin.php
+++ b/actions/facebooklogin.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/facebookremove.php b/actions/facebookremove.php
index 376e12a2e..9ca7a77a8 100644
--- a/actions/facebookremove.php
+++ b/actions/facebookremove.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/facebooksettings.php b/actions/facebooksettings.php
index 236460c1c..ee2c279ab 100644
--- a/actions/facebooksettings.php
+++ b/actions/facebooksettings.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -55,7 +55,7 @@ class FacebooksettingsAction extends FacebookAction
$prefix = $this->trimmed('prefix');
$original = clone($this->flink);
- $this->flink->set_flags($noticesync, $replysync, false);
+ $this->flink->set_flags($noticesync, $replysync, false, false);
$result = $this->flink->update($original);
$this->facebook->api_client->data_setUserPreference(FACEBOOK_NOTICE_PREFIX,
diff --git a/actions/favor.php b/actions/favor.php
index a7aff87f2..fe51e34a2 100644
--- a/actions/favor.php
+++ b/actions/favor.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/favorited.php b/actions/favorited.php
index c902d80f5..156c7a700 100644
--- a/actions/favorited.php
+++ b/actions/favorited.php
@@ -194,7 +194,7 @@ class FavoritedAction extends Action
$qry = 'SELECT notice.*, '.
$weightexpr . ' as weight ' .
'FROM notice JOIN fave ON notice.id = fave.notice_id ' .
- 'GROUP BY id,profile_id,uri,content,rendered,url,created,notice.modified,reply_to,is_local,source ' .
+ 'GROUP BY id,profile_id,uri,content,rendered,url,created,notice.modified,reply_to,is_local,source,notice.conversation ' .
'ORDER BY weight DESC';
$offset = ($this->page - 1) * NOTICES_PER_PAGE;
diff --git a/actions/favoritesrss.php b/actions/favoritesrss.php
index 6b46b8dec..c439a9a62 100644
--- a/actions/favoritesrss.php
+++ b/actions/favoritesrss.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/featured.php b/actions/featured.php
index 79eba2aa6..04365687d 100644
--- a/actions/featured.php
+++ b/actions/featured.php
@@ -32,7 +32,7 @@ if (!defined('LACONICA')) {
exit(1);
}
-require_once(INSTALLDIR.'/lib/profilelist.php');
+require_once INSTALLDIR.'/lib/profilelist.php';
require_once INSTALLDIR.'/lib/publicgroupnav.php';
/**
@@ -107,7 +107,6 @@ class FeaturedAction extends Action
$featured_nicks = common_config('nickname', 'featured');
-
if (count($featured_nicks) > 0) {
$quoted = array();
@@ -136,7 +135,7 @@ class FeaturedAction extends Action
$cnt = $profile->find();
if ($cnt > 0) {
- $featured = new ProfileList($profile, null, $this);
+ $featured = new ProfileList($profile, $this);
$featured->show();
}
diff --git a/actions/file.php b/actions/file.php
new file mode 100644
index 000000000..8310e48df
--- /dev/null
+++ b/actions/file.php
@@ -0,0 +1,72 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/actions/shownotice.php');
+
+class FileAction extends Action
+{
+ var $id = null;
+ var $filerec = null;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->id = $this->trimmed('notice');
+ if (empty($this->id)) {
+ $this->clientError(_('No notice id'));
+ }
+ $notice = Notice::staticGet('id', $this->id);
+ if (empty($notice)) {
+ $this->clientError(_('No notice'));
+ }
+ $atts = $notice->attachments();
+ if (empty($atts)) {
+ $this->clientError(_('No attachments'));
+ }
+ foreach ($atts as $att) {
+ if (!empty($att->filename)) {
+ $this->filerec = $att;
+ break;
+ }
+ }
+ if (empty($this->filerec)) {
+ $this->clientError(_('No uploaded attachments'));
+ }
+ return true;
+ }
+
+ function handle() {
+ common_redirect($this->filerec->url);
+ }
+
+ /**
+ * Is this action read-only?
+ *
+ * @return boolean true
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+}
+
diff --git a/actions/finishopenidlogin.php b/actions/finishopenidlogin.php
index b08b96df6..e9f7c746b 100644
--- a/actions/finishopenidlogin.php
+++ b/actions/finishopenidlogin.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/finishremotesubscribe.php b/actions/finishremotesubscribe.php
index 3e3a81715..5c764aeb0 100644
--- a/actions/finishremotesubscribe.php
+++ b/actions/finishremotesubscribe.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/foaf.php b/actions/foaf.php
index 2d5b78d12..b481b2437 100644
--- a/actions/foaf.php
+++ b/actions/foaf.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -82,14 +82,18 @@ class FoafAction extends Action
'http://www.w3.org/2000/01/rdf-schema#',
'xmlns:geo' =>
'http://www.w3.org/2003/01/geo/wgs84_pos#',
+ 'xmlns:bio' =>
+ 'http://purl.org/vocab/bio/0.1/',
+ 'xmlns:sioc' =>
+ 'http://rdfs.org/sioc/ns#',
'xmlns' => 'http://xmlns.com/foaf/0.1/'));
// This is the document about the user
$this->showPpd('', $this->user->uri);
- // XXX: might not be a person
- $this->elementStart('Person', array('rdf:about' =>
+ // Would be nice to tell if they were a Person or not (e.g. a #person usertag?)
+ $this->elementStart('Agent', array('rdf:about' =>
$this->user->uri));
$this->element('mbox_sha1sum', null, sha1('mailto:' . $this->user->email));
if ($this->profile->fullname) {
@@ -98,8 +102,11 @@ class FoafAction extends Action
if ($this->profile->homepage) {
$this->element('homepage', array('rdf:resource' => $this->profile->homepage));
}
+ if ($this->profile->profileurl) {
+ $this->element('weblog', array('rdf:resource' => $this->profile->profileurl));
+ }
if ($this->profile->bio) {
- $this->element('rdfs:comment', null, $this->profile->bio);
+ $this->element('bio:olb', null, $this->profile->bio);
}
// XXX: more structured location data
if ($this->profile->location) {
@@ -110,10 +117,7 @@ class FoafAction extends Action
$this->elementEnd('based_near');
}
- $this->showMicrobloggingAccount($this->profile, common_root_url());
-
$avatar = $this->profile->getOriginalAvatar();
-
if ($avatar) {
$this->elementStart('img');
$this->elementStart('Image', array('rdf:about' => $avatar->url));
@@ -129,39 +133,8 @@ class FoafAction extends Action
$this->elementEnd('img');
}
- // Get people user is subscribed to
-
- $person = array();
-
- $sub = new Subscription();
- $sub->subscriber = $this->profile->id;
- $sub->whereAdd('subscriber != subscribed');
-
- if ($sub->find()) {
- while ($sub->fetch()) {
- if (!empty($sub->token)) {
- $other = Remote_profile::staticGet('id', $sub->subscribed);
- } else {
- $other = User::staticGet('id', $sub->subscribed);
- }
- if (empty($other)) {
- common_debug('Got a bad subscription: '.print_r($sub,true));
- continue;
- }
- $this->element('knows', array('rdf:resource' => $other->uri));
- $person[$other->uri] = array(LISTENEE,
- $other->id,
- $other->nickname,
- (empty($sub->token)) ? 'User' : 'Remote_profile');
- $other->free();
- $other = null;
- unset($other);
- }
- }
-
- $sub->free();
- $sub = null;
- unset($sub);
+ $person = $this->showMicrobloggingAccount($this->profile,
+ common_root_url(), $this->user->uri, false);
// Get people who subscribe to user
@@ -198,7 +171,15 @@ class FoafAction extends Action
$sub = null;
unset($sub);
- $this->elementEnd('Person');
+ foreach ($person as $uri => $p) {
+ list($type, $id, $nickname, $cls) = $p;
+ if ($type == BOTH) {
+ $this->element('knows', array('rdf:resource' => $uri));
+ }
+ }
+
+ $this->elementEnd('Agent');
+
foreach ($person as $uri => $p) {
$foaf_url = null;
@@ -207,16 +188,18 @@ class FoafAction extends Action
$foaf_url = common_local_url('foaf', array('nickname' => $nickname));
}
$profile = Profile::staticGet($id);
- $this->elementStart('Person', array('rdf:about' => $uri));
- if ($type == LISTENER || $type == BOTH) {
+ $this->elementStart('Agent', array('rdf:about' => $uri));
+ if ($type == BOTH) {
$this->element('knows', array('rdf:resource' => $this->user->uri));
}
- $this->showMicrobloggingAccount($profile, ($cls == 'User') ?
- common_root_url() : null);
+ $this->showMicrobloggingAccount($profile,
+ ($cls == 'User') ? common_root_url() : null,
+ $uri,
+ true);
if ($foaf_url) {
$this->element('rdfs:seeAlso', array('rdf:resource' => $foaf_url));
}
- $this->elementEnd('Person');
+ $this->elementEnd('Agent');
if ($foaf_url) {
$this->showPpd($foaf_url, $uri);
}
@@ -237,18 +220,66 @@ class FoafAction extends Action
$this->elementEnd('PersonalProfileDocument');
}
- function showMicrobloggingAccount($profile, $service=null)
+ function showMicrobloggingAccount($profile, $service=null, $useruri=null, $isSubscriber=false)
{
+ $attr = array();
+ if ($useruri) {
+ $attr['rdf:about'] = $useruri . '#acct';
+ }
+
// Their account
$this->elementStart('holdsAccount');
- $this->elementStart('OnlineAccount');
+ $this->elementStart('OnlineAccount', $attr);
if ($service) {
$this->element('accountServiceHomepage', array('rdf:resource' =>
$service));
}
$this->element('accountName', null, $profile->nickname);
- $this->element('homepage', array('rdf:resource' => $profile->profileurl));
+ $this->element('accountProfilePage', array('rdf:resource' => $profile->profileurl));
+ if ($useruri) {
+ $this->element('sioc:account_of', array('rdf:resource'=>$useruri));
+ }
+
+ $person = array();
+
+ if ($isSubscriber) {
+ $this->element('sioc:follows', array('rdf:resource'=>$this->user->uri . '#acct'));
+ } else {
+ // Get people user is subscribed to
+ $sub = new Subscription();
+ $sub->subscriber = $profile->id;
+ $sub->whereAdd('subscriber != subscribed');
+
+ if ($sub->find()) {
+ while ($sub->fetch()) {
+ if (!empty($sub->token)) {
+ $other = Remote_profile::staticGet('id', $sub->subscribed);
+ } else {
+ $other = User::staticGet('id', $sub->subscribed);
+ }
+ if (empty($other)) {
+ common_debug('Got a bad subscription: '.print_r($sub,true));
+ continue;
+ }
+ $this->element('sioc:follows', array('rdf:resource' => $other->uri.'#acct'));
+ $person[$other->uri] = array(LISTENEE,
+ $other->id,
+ $other->nickname,
+ (empty($sub->token)) ? 'User' : 'Remote_profile');
+ $other->free();
+ $other = null;
+ unset($other);
+ }
+ }
+
+ $sub->free();
+ $sub = null;
+ unset($sub);
+ }
+
$this->elementEnd('OnlineAccount');
$this->elementEnd('holdsAccount');
+
+ return $person;
}
}
diff --git a/actions/groupblock.php b/actions/groupblock.php
new file mode 100644
index 000000000..ce2c6c144
--- /dev/null
+++ b/actions/groupblock.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * Block a user from a group action class.
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@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, 2009, Control Yourself, 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);
+}
+
+/**
+ * Block a user from a group
+ *
+ * @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 GroupblockAction extends Action
+{
+ var $profile = null;
+ var $group = null;
+
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ if (!common_logged_in()) {
+ $this->clientError(_('Not logged in.'));
+ return false;
+ }
+ $token = $this->trimmed('token');
+ if (empty($token) || $token != common_session_token()) {
+ $this->clientError(_('There was a problem with your session token. Try again, please.'));
+ return;
+ }
+ $id = $this->trimmed('blockto');
+ if (empty($id)) {
+ $this->clientError(_('No profile specified.'));
+ return false;
+ }
+ $this->profile = Profile::staticGet('id', $id);
+ if (empty($this->profile)) {
+ $this->clientError(_('No profile with that ID.'));
+ return false;
+ }
+ $group_id = $this->trimmed('blockgroup');
+ if (empty($group_id)) {
+ $this->clientError(_('No group specified.'));
+ return false;
+ }
+ $this->group = User_group::staticGet('id', $group_id);
+ if (empty($this->group)) {
+ $this->clientError(_('No such group.'));
+ return false;
+ }
+ $user = common_current_user();
+ if (!$user->isAdmin($this->group)) {
+ $this->clientError(_('Only an admin can block group members.'), 401);
+ return false;
+ }
+ if (Group_block::isBlocked($this->group, $this->profile)) {
+ $this->clientError(_('User is already blocked from group.'));
+ return false;
+ }
+ // XXX: could have proactive blocks, but we don't have UI for it.
+ if (!$this->profile->isMember($this->group)) {
+ $this->clientError(_('User is not a member of group.'));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle request
+ *
+ * Shows a page with list of favorite notices
+ *
+ * @param array $args $_REQUEST args; handled in prepare()
+ *
+ * @return void
+ */
+ function handle($args)
+ {
+ parent::handle($args);
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if ($this->arg('no')) {
+ common_redirect(common_local_url('groupmembers',
+ array('nickname' => $this->group->nickname)),
+ 303);
+ } elseif ($this->arg('yes')) {
+ $this->blockProfile();
+ } elseif ($this->arg('blockto')) {
+ $this->showPage();
+ }
+ }
+ }
+
+ function showContent() {
+ $this->areYouSureForm();
+ }
+
+ function title() {
+ return _('Block user from group');
+ }
+
+ function showNoticeForm() {
+ // nop
+ }
+
+ /**
+ * Confirm with user.
+ *
+ * Shows a confirmation form.
+ *
+ * @return void
+ */
+
+ function areYouSureForm()
+ {
+ $id = $this->profile->id;
+ $this->element('p', null,
+ sprintf(_('Are you sure you want to block user "%s" from the group "%s"? '.
+ 'They will be removed from the group, unable to post, and '.
+ 'unable to subscribe to the group in the future.'),
+ $this->profile->getBestName(),
+ $this->group->getBestName()));
+ $this->elementStart('form', array('id' => 'block-' . $id,
+ 'method' => 'post',
+ 'class' => 'block',
+ 'action' => common_local_url('groupblock')));
+ $this->hidden('token', common_session_token());
+ $this->hidden('blockto-' . $this->profile->id,
+ $this->profile->id,
+ 'blockto');
+ $this->hidden('blockgroup-' . $this->group->id,
+ $this->group->id,
+ 'blockgroup');
+ foreach ($this->args as $k => $v) {
+ if (substr($k, 0, 9) == 'returnto-') {
+ $this->hidden($k, $v);
+ }
+ }
+ $this->submit('no', _('No'));
+ $this->submit('yes', _('Yes'));
+ $this->elementEnd('form');
+ }
+
+ /**
+ * Actually block a user.
+ *
+ * @return void
+ */
+
+ function blockProfile()
+ {
+ $block = Group_block::blockProfile($this->group, $this->profile,
+ common_current_user());
+
+ if (empty($block)) {
+ $this->serverError(_("Database error blocking user from group."));
+ return false;
+ }
+
+ // Now, gotta figure where we go back to
+ foreach ($this->args as $k => $v) {
+ if ($k == 'returnto-action') {
+ $action = $v;
+ } elseif (substr($k, 0, 9) == 'returnto-') {
+ $args[substr($k, 9)] = $v;
+ }
+ }
+
+ if ($action) {
+ common_redirect(common_local_url($action, $args), 303);
+ } else {
+ common_redirect(common_local_url('groupmembers',
+ array('nickname' => $this->group->nickname)),
+ 303);
+ }
+ }
+}
+
diff --git a/actions/groupdesignsettings.php b/actions/groupdesignsettings.php
new file mode 100644
index 000000000..bb01243c6
--- /dev/null
+++ b/actions/groupdesignsettings.php
@@ -0,0 +1,315 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Change user password
+ *
+ * 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 Settings
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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/designsettings.php';
+
+/**
+ * Set a group's design
+ *
+ * Saves a design for a given group
+ *
+ * @category Settings
+ * @package Laconica
+ * @author Zach Copley <zach@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 GroupDesignSettingsAction extends DesignSettingsAction
+{
+ var $group = null;
+
+ /**
+ * Sets the right action for the form, and passes request args into
+ * the base action
+ *
+ * @param array $args misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (!common_config('inboxes', 'enabled')) {
+ $this->serverError(_('Inboxes must be enabled for groups to work'));
+ return false;
+ }
+
+ if (!common_logged_in()) {
+ $this->clientError(_('You must be logged in to edit a group.'));
+ return false;
+ }
+
+ $nickname_arg = $this->trimmed('nickname');
+ $nickname = common_canonical_nickname($nickname_arg);
+
+ // Permanent redirect on non-canonical nickname
+
+ if ($nickname_arg != $nickname) {
+ $args = array('nickname' => $nickname);
+ common_redirect(common_local_url('groupdesignsettings', $args), 301);
+ return false;
+ }
+
+ if (!$nickname) {
+ $this->clientError(_('No nickname'), 404);
+ return false;
+ }
+
+ $groupid = $this->trimmed('groupid');
+
+ if ($groupid) {
+ $this->group = User_group::staticGet('id', $groupid);
+ } else {
+ $this->group = User_group::staticGet('nickname', $nickname);
+ }
+
+ if (!$this->group) {
+ $this->clientError(_('No such group'), 404);
+ return false;
+ }
+
+ $cur = common_current_user();
+
+ if (!$cur->isAdmin($this->group)) {
+ $this->clientError(_('You must be an admin to edit the group'), 403);
+ return false;
+ }
+
+ $this->submitaction = common_local_url('groupdesignsettings',
+ array('nickname' => $this->group->nickname));
+
+ return true;
+ }
+
+ /**
+ * A design for this action
+ *
+ * if the group attribute has been set, returns that group's
+ * design.
+ *
+ * @return Design a design object to use
+ */
+
+ function getDesign()
+ {
+
+ if (empty($this->group)) {
+ return null;
+ }
+
+ return $this->group->getDesign();
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('Group design');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('Customize the way your group looks ' .
+ 'with a background image and a colour palette of your choice.');
+ }
+
+ /**
+ * Override to show group nav stuff
+ *
+ * @return nothing
+ */
+
+ function showLocalNav()
+ {
+ $nav = new GroupNav($this, $this->group);
+ $nav->show();
+ }
+
+ /**
+ * Get the design we want to edit
+ *
+ * @return Design
+ */
+
+ function getWorkingDesign()
+ {
+
+ $design = null;
+
+ if (isset($this->group)) {
+ $design = $this->group->getDesign();
+ }
+
+ if (empty($design)) {
+ $design = $this->defaultDesign();
+ }
+
+ return $design;
+ }
+
+ /**
+ * Content area of the page
+ *
+ * Shows a form for changing the design
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $this->showDesignForm($this->getWorkingDesign());
+ }
+
+ /**
+ * Save or update the group's design settings
+ *
+ * @return void
+ */
+
+ function saveDesign()
+ {
+ try {
+
+ $bgcolor = new WebColor($this->trimmed('design_background'));
+ $ccolor = new WebColor($this->trimmed('design_content'));
+ $sbcolor = new WebColor($this->trimmed('design_sidebar'));
+ $tcolor = new WebColor($this->trimmed('design_text'));
+ $lcolor = new WebColor($this->trimmed('design_links'));
+
+ } catch (WebColorException $e) {
+ $this->showForm($e->getMessage());
+ return;
+ }
+
+ $onoff = $this->arg('design_background-image_onoff');
+
+ $on = false;
+ $off = false;
+ $tile = false;
+
+ if ($onoff == 'on') {
+ $on = true;
+ } else {
+ $off = true;
+ }
+
+ $repeat = $this->boolean('design_background-image_repeat');
+
+ if ($repeat) {
+ $tile = true;
+ }
+
+ $design = $this->group->getDesign();
+
+ if (!empty($design)) {
+
+ // update design
+
+ $original = clone($design);
+
+ $design->backgroundcolor = $bgcolor->intValue();
+ $design->contentcolor = $ccolor->intValue();
+ $design->sidebarcolor = $sbcolor->intValue();
+ $design->textcolor = $tcolor->intValue();
+ $design->linkcolor = $lcolor->intValue();
+
+ $design->setDisposition($on, $off, $tile);
+
+ $result = $design->update($original);
+
+ if ($result === false) {
+ common_log_db_error($design, 'UPDATE', __FILE__);
+ $this->showForm(_('Couldn\'t update your design.'));
+ return;
+ }
+
+ } else {
+
+ $this->group->query('BEGIN');
+
+ // save new design
+
+ $design = new Design();
+
+ $design->backgroundcolor = $bgcolor->intValue();
+ $design->contentcolor = $ccolor->intValue();
+ $design->sidebarcolor = $sbcolor->intValue();
+ $design->textcolor = $tcolor->intValue();
+ $design->linkcolor = $lcolor->intValue();
+
+ $design->setDisposition($on, $off, $tile);
+
+ $id = $design->insert();
+
+ if (empty($id)) {
+ common_log_db_error($id, 'INSERT', __FILE__);
+ $this->showForm(_('Unable to save your design settings!'));
+ return;
+ }
+
+ $original = clone($this->group);
+ $this->group->design_id = $id;
+ $result = $this->group->update($original);
+
+ if (empty($result)) {
+ common_log_db_error($original, 'UPDATE', __FILE__);
+ $this->showForm(_('Unable to save your design settings!'));
+ $this->group->query('ROLLBACK');
+ return;
+ }
+
+ $this->group->query('COMMIT');
+
+ }
+
+ $this->saveBackgroundImage($design);
+
+ $this->showForm(_('Design preferences saved.'), true);
+ }
+
+}
diff --git a/actions/grouplogo.php b/actions/grouplogo.php
index fe6127da2..8f6158dac 100644
--- a/actions/grouplogo.php
+++ b/actions/grouplogo.php
@@ -50,7 +50,7 @@ define('MAX_ORIGINAL', 480);
* @link http://laconi.ca/
*/
-class GrouplogoAction extends Action
+class GrouplogoAction extends GroupDesignAction
{
var $mode = null;
var $imagefile = null;
diff --git a/actions/groupmembers.php b/actions/groupmembers.php
index 21e5ebbaa..14256526a 100644
--- a/actions/groupmembers.php
+++ b/actions/groupmembers.php
@@ -44,7 +44,7 @@ require_once INSTALLDIR.'/lib/publicgroupnav.php';
* @link http://laconi.ca/
*/
-class GroupmembersAction extends Action
+class GroupmembersAction extends GroupDesignAction
{
var $page = null;
@@ -127,7 +127,7 @@ class GroupmembersAction extends Action
$members = $this->group->getMembers($offset, $limit);
if ($members) {
- $member_list = new ProfileList($members, null, $this);
+ $member_list = new GroupMemberList($members, $this->group, $this);
$cnt = $member_list->show();
}
@@ -138,3 +138,335 @@ class GroupmembersAction extends Action
array('nickname' => $this->group->nickname));
}
}
+
+class GroupMemberList extends ProfileList
+{
+ var $group = null;
+
+ function __construct($profile, $group, $action)
+ {
+ parent::__construct($profile, $action);
+
+ $this->group = $group;
+ }
+
+ function newListItem($profile)
+ {
+ return new GroupMemberListItem($profile, $this->group, $this->action);
+ }
+}
+
+class GroupMemberListItem extends ProfileListItem
+{
+ var $group = null;
+
+ function __construct($profile, $group, $action)
+ {
+ parent::__construct($profile, $action);
+
+ $this->group = $group;
+ }
+
+ function showFullName()
+ {
+ parent::showFullName();
+ if ($this->profile->isAdmin($this->group)) {
+ $this->out->text(' ');
+ $this->out->element('span', 'role', _('Admin'));
+ }
+ }
+
+ function showActions()
+ {
+ $this->startActions();
+ $this->showSubscribeButton();
+ $this->showMakeAdminForm();
+ $this->showGroupBlockForm();
+ $this->endActions();
+ }
+
+ function showMakeAdminForm()
+ {
+ $user = common_current_user();
+
+ if (!empty($user) && $user->id != $this->profile->id && $user->isAdmin($this->group) &&
+ !$this->profile->isAdmin($this->group)) {
+ $this->out->elementStart('li', 'entity_make_admin');
+ $maf = new MakeAdminForm($this->out, $this->profile, $this->group,
+ array('action' => 'groupmembers',
+ 'nickname' => $this->group->nickname));
+ $maf->show();
+ $this->out->elementEnd('li');
+ }
+
+ }
+ function showGroupBlockForm()
+ {
+ $user = common_current_user();
+
+ if (!empty($user) && $user->id != $this->profile->id && $user->isAdmin($this->group)) {
+ $this->out->elementStart('li', 'entity_block');
+ $bf = new GroupBlockForm($this->out, $this->profile, $this->group,
+ array('action' => 'groupmembers',
+ 'nickname' => $this->group->nickname));
+ $bf->show();
+ $this->out->elementEnd('li');
+ }
+
+ }
+}
+
+/**
+ * Form for blocking a user from a group
+ *
+ * @category Form
+ * @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/
+ *
+ * @see BlockForm
+ */
+
+class GroupBlockForm extends Form
+{
+ /**
+ * Profile of user to block
+ */
+
+ var $profile = null;
+
+ /**
+ * Group to block the user from
+ */
+
+ var $group = null;
+
+ /**
+ * Return-to args
+ */
+
+ var $args = null;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out output channel
+ * @param Profile $profile profile of user to block
+ * @param User_group $group group to block user from
+ * @param array $args return-to args
+ */
+
+ function __construct($out=null, $profile=null, $group=null, $args=null)
+ {
+ parent::__construct($out);
+
+ $this->profile = $profile;
+ $this->group = $group;
+ $this->args = $args;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ // This should be unique for the page.
+ return 'block-' . $this->profile->id;
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'form_group_block';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('groupblock');
+ }
+
+ /**
+ * Legend of the Form
+ *
+ * @return void
+ */
+ function formLegend()
+ {
+ $this->out->element('legend', null, _('Block user from group'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->hidden('blockto-' . $this->profile->id,
+ $this->profile->id,
+ 'blockto');
+ $this->out->hidden('blockgroup-' . $this->group->id,
+ $this->group->id,
+ 'blockgroup');
+ if ($this->args) {
+ foreach ($this->args as $k => $v) {
+ $this->out->hidden('returnto-' . $k, $v);
+ }
+ }
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('submit', _('Block'), 'submit', null, _('Block this user'));
+ }
+}
+
+/**
+ * Form for making a user an admin for a group
+ *
+ * @category Form
+ * @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 MakeAdminForm extends Form
+{
+ /**
+ * Profile of user to block
+ */
+
+ var $profile = null;
+
+ /**
+ * Group to block the user from
+ */
+
+ var $group = null;
+
+ /**
+ * Return-to args
+ */
+
+ var $args = null;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out output channel
+ * @param Profile $profile profile of user to block
+ * @param User_group $group group to block user from
+ * @param array $args return-to args
+ */
+
+ function __construct($out=null, $profile=null, $group=null, $args=null)
+ {
+ parent::__construct($out);
+
+ $this->profile = $profile;
+ $this->group = $group;
+ $this->args = $args;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ // This should be unique for the page.
+ return 'makeadmin-' . $this->profile->id;
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'form_make_admin';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('makeadmin', array('nickname' => $this->group->nickname));
+ }
+
+ /**
+ * Legend of the Form
+ *
+ * @return void
+ */
+
+ function formLegend()
+ {
+ $this->out->element('legend', null, _('Make user an admin of the group'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->hidden('profileid-' . $this->profile->id,
+ $this->profile->id,
+ 'profileid');
+ $this->out->hidden('groupid-' . $this->group->id,
+ $this->group->id,
+ 'groupid');
+ if ($this->args) {
+ foreach ($this->args as $k => $v) {
+ $this->out->hidden('returnto-' . $k, $v);
+ }
+ }
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('submit', _('Make Admin'), 'submit', null, _('Make this user an admin'));
+ }
+}
diff --git a/actions/grouprss.php b/actions/grouprss.php
index 0b7280a11..2bdcaafb2 100644
--- a/actions/grouprss.php
+++ b/actions/grouprss.php
@@ -116,6 +116,7 @@ class groupRssAction extends Rss10Action
return null;
}
+ $notices = array();
$notice = $group->getNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit);
while ($notice->fetch()) {
diff --git a/actions/groups.php b/actions/groups.php
index 26b52a5fc..3d62843ed 100644
--- a/actions/groups.php
+++ b/actions/groups.php
@@ -100,11 +100,13 @@ class GroupsAction extends Action
function showContent()
{
- $this->elementStart('p', array('id' => 'new_group'));
- $this->element('a', array('href' => common_local_url('newgroup'),
- 'class' => 'more'),
- _('Create a new group'));
- $this->elementEnd('p');
+ if (common_logged_in()) {
+ $this->elementStart('p', array('id' => 'new_group'));
+ $this->element('a', array('href' => common_local_url('newgroup'),
+ 'class' => 'more'),
+ _('Create a new group'));
+ $this->elementEnd('p');
+ }
$offset = ($this->page-1) * GROUPS_PER_PAGE;
$limit = GROUPS_PER_PAGE + 1;
@@ -113,6 +115,7 @@ class GroupsAction extends Action
$groups->orderBy('created DESC');
$groups->limit($offset, $limit);
+ $cnt = 0;
if ($groups->find()) {
$gl = new GroupList($groups, null, $this);
$cnt = $gl->show();
diff --git a/actions/groupsearch.php b/actions/groupsearch.php
index 06b4a7755..c50466ce6 100644
--- a/actions/groupsearch.php
+++ b/actions/groupsearch.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/groupunblock.php b/actions/groupunblock.php
new file mode 100644
index 000000000..6beb46352
--- /dev/null
+++ b/actions/groupunblock.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Block a user from a group action class.
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@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, 2009, Control Yourself, 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);
+}
+
+/**
+ * Unlock a user from a group
+ *
+ * @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 GroupunblockAction extends Action
+{
+ var $profile = null;
+ var $group = null;
+
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ if (!common_logged_in()) {
+ $this->clientError(_('Not logged in.'));
+ return false;
+ }
+ $token = $this->trimmed('token');
+ if (empty($token) || $token != common_session_token()) {
+ $this->clientError(_('There was a problem with your session token. Try again, please.'));
+ return;
+ }
+ $id = $this->trimmed('unblockto');
+ if (empty($id)) {
+ $this->clientError(_('No profile specified.'));
+ return false;
+ }
+ $this->profile = Profile::staticGet('id', $id);
+ if (empty($this->profile)) {
+ $this->clientError(_('No profile with that ID.'));
+ return false;
+ }
+ $group_id = $this->trimmed('unblockgroup');
+ if (empty($group_id)) {
+ $this->clientError(_('No group specified.'));
+ return false;
+ }
+ $this->group = User_group::staticGet('id', $group_id);
+ if (empty($this->group)) {
+ $this->clientError(_('No such group.'));
+ return false;
+ }
+ $user = common_current_user();
+ if (!$user->isAdmin($this->group)) {
+ $this->clientError(_('Only an admin can unblock group members.'), 401);
+ return false;
+ }
+ if (!Group_block::isBlocked($this->group, $this->profile)) {
+ $this->clientError(_('User is not blocked from group.'));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle request
+ *
+ * @param array $args $_REQUEST args; handled in prepare()
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->unblockProfile();
+ }
+ }
+
+ /**
+ * Unblock a user.
+ *
+ * @return void
+ */
+
+ function unblockProfile()
+ {
+ $result = Group_block::unblockProfile($this->group, $this->profile);
+
+ if (!$result) {
+ $this->serverError(_('Error removing the block.'));
+ return;
+ }
+
+ foreach ($this->args as $k => $v) {
+ if ($k == 'returnto-action') {
+ $action = $v;
+ } else if (substr($k, 0, 9) == 'returnto-') {
+ $args[substr($k, 9)] = $v;
+ }
+ }
+
+ if ($action) {
+ common_redirect(common_local_url($action, $args), 303);
+ } else {
+ common_redirect(common_local_url('blockedfromgroup',
+ array('nickname' => $this->group->nickname)),
+ 303);
+ }
+ }
+}
+
diff --git a/actions/invite.php b/actions/invite.php
index 7e52cdbcc..bdea4807d 100644
--- a/actions/invite.php
+++ b/actions/invite.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -19,7 +19,7 @@
if (!defined('LACONICA')) { exit(1); }
-class InviteAction extends Action
+class InviteAction extends CurrentUserDesignAction
{
var $mode = null;
var $error = null;
@@ -35,7 +35,9 @@ class InviteAction extends Action
function handle($args)
{
parent::handle($args);
- if (!common_logged_in()) {
+ if (!common_config('invite', 'enabled')) {
+ $this->clientError(_('Invites have been disabled.'));
+ } else if (!common_logged_in()) {
$this->clientError(sprintf(_('You must be logged in to invite other users to use %s'),
common_config('site', 'name')));
return;
diff --git a/actions/joingroup.php b/actions/joingroup.php
index a5d82ddc7..0e4f96eaf 100644
--- a/actions/joingroup.php
+++ b/actions/joingroup.php
@@ -96,6 +96,11 @@ class JoingroupAction extends Action
return false;
}
+ if (Group_block::isBlocked($this->group, $cur->getProfile())) {
+ $this->clientError(_('You have been blocked from that group by the admin.'), 403);
+ return false;
+ }
+
return true;
}
diff --git a/actions/logout.php b/actions/logout.php
index 9f3bfe247..3fcfb4f4e 100644
--- a/actions/logout.php
+++ b/actions/logout.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -70,10 +70,20 @@ class LogoutAction extends Action
if (!common_logged_in()) {
$this->clientError(_('Not logged in.'));
} else {
- common_set_user(null);
- common_real_login(false); // not logged in
- common_forgetme(); // don't log back in!
+ if (Event::handle('StartLogout', array($this))) {
+ $this->logout();
+ }
+ Event::handle('EndLogout', array($this));
+
common_redirect(common_local_url('public'), 303);
}
}
+
+ function logout()
+ {
+ common_set_user(null);
+ common_real_login(false); // not logged in
+ common_forgetme(); // don't log back in!
+ }
+
}
diff --git a/actions/makeadmin.php b/actions/makeadmin.php
new file mode 100644
index 000000000..6fc2cf9ab
--- /dev/null
+++ b/actions/makeadmin.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * Make another user an admin of a group
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@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, 2009, Control Yourself, 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);
+}
+
+/**
+ * Make another user an admin of a group
+ *
+ * @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 MakeadminAction extends Action
+{
+ var $profile = null;
+ var $group = null;
+
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ if (!common_logged_in()) {
+ $this->clientError(_('Not logged in.'));
+ return false;
+ }
+ $token = $this->trimmed('token');
+ if (empty($token) || $token != common_session_token()) {
+ $this->clientError(_('There was a problem with your session token. Try again, please.'));
+ return;
+ }
+ $id = $this->trimmed('profileid');
+ if (empty($id)) {
+ $this->clientError(_('No profile specified.'));
+ return false;
+ }
+ $this->profile = Profile::staticGet('id', $id);
+ if (empty($this->profile)) {
+ $this->clientError(_('No profile with that ID.'));
+ return false;
+ }
+ $group_id = $this->trimmed('groupid');
+ if (empty($group_id)) {
+ $this->clientError(_('No group specified.'));
+ return false;
+ }
+ $this->group = User_group::staticGet('id', $group_id);
+ if (empty($this->group)) {
+ $this->clientError(_('No such group.'));
+ return false;
+ }
+ $user = common_current_user();
+ if (!$user->isAdmin($this->group)) {
+ $this->clientError(_('Only an admin can make another user an admin.'), 401);
+ return false;
+ }
+ if ($this->profile->isAdmin($this->group)) {
+ $this->clientError(sprintf(_('%s is already an admin for group "%s".'),
+ $this->profile->getBestName(),
+ $this->group->getBestName()),
+ 401);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle request
+ *
+ * @param array $args $_REQUEST args; handled in prepare()
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->makeAdmin();
+ }
+ }
+
+ /**
+ * Make user an admin
+ *
+ * @return void
+ */
+
+ function makeAdmin()
+ {
+ $member = Group_member::pkeyGet(array('group_id' => $this->group->id,
+ 'profile_id' => $this->profile->id));
+
+ if (empty($member)) {
+ $this->serverError(_('Can\'t get membership record for %s in group %s'),
+ $this->profile->getBestName(),
+ $this->group->getBestName());
+ }
+
+ $orig = clone($member);
+
+ $member->is_admin = 1;
+
+ $result = $member->update($orig);
+
+ if (!$result) {
+ common_log_db_error($member, 'UPDATE', __FILE__);
+ $this->serverError(_('Can\'t make %s an admin for group %s'),
+ $this->profile->getBestName(),
+ $this->group->getBestName());
+ }
+
+ foreach ($this->args as $k => $v) {
+ if ($k == 'returnto-action') {
+ $action = $v;
+ } else if (substr($k, 0, 9) == 'returnto-') {
+ $args[substr($k, 9)] = $v;
+ }
+ }
+
+ if ($action) {
+ common_redirect(common_local_url($action, $args), 303);
+ } else {
+ common_redirect(common_local_url('groupmembers',
+ array('nickname' => $this->group->nickname)),
+ 303);
+ }
+ }
+}
diff --git a/actions/microsummary.php b/actions/microsummary.php
index 0b408ec95..6884a919a 100644
--- a/actions/microsummary.php
+++ b/actions/microsummary.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/newgroup.php b/actions/newgroup.php
index 67cd6b2f1..0289e77c2 100644
--- a/actions/newgroup.php
+++ b/actions/newgroup.php
@@ -123,6 +123,7 @@ class NewgroupAction extends Action
$homepage = $this->trimmed('homepage');
$description = $this->trimmed('description');
$location = $this->trimmed('location');
+ $aliasstring = $this->trimmed('aliases');
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
@@ -153,6 +154,37 @@ class NewgroupAction extends Action
return;
}
+ if (!empty($aliasstring)) {
+ $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring)));
+ } else {
+ $aliases = array();
+ }
+
+ if (count($aliases) > common_config('group', 'maxaliases')) {
+ $this->showForm(sprintf(_('Too many aliases! Maximum %d.'),
+ common_config('group', 'maxaliases')));
+ return;
+ }
+
+ foreach ($aliases as $alias) {
+ if (!Validate::string($alias, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => NICKNAME_FMT))) {
+ $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
+ return;
+ }
+ if ($this->nicknameExists($alias)) {
+ $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
+ $alias));
+ return;
+ }
+ // XXX assumes alphanum nicknames
+ if (strcmp($alias, $nickname) == 0) {
+ $this->showForm(_('Alias can\'t be the same as nickname.'));
+ return;
+ }
+ }
+
$cur = common_current_user();
// Checked in prepare() above
@@ -177,6 +209,12 @@ class NewgroupAction extends Action
$this->serverError(_('Could not create group.'));
}
+ $result = $group->setAliases($aliases);
+
+ if (!$result) {
+ $this->serverError(_('Could not create aliases.'));
+ }
+
$member = new Group_member();
$member->group_id = $group->id;
@@ -199,7 +237,18 @@ class NewgroupAction extends Action
function nicknameExists($nickname)
{
$group = User_group::staticGet('nickname', $nickname);
- return (!is_null($group) && $group != false);
+
+ if (!empty($group)) {
+ return true;
+ }
+
+ $alias = Group_alias::staticGet('alias', $nickname);
+
+ if (!empty($alias)) {
+ return true;
+ }
+
+ return false;
}
}
diff --git a/actions/newnotice.php b/actions/newnotice.php
index cbd04c58b..e254eac49 100644
--- a/actions/newnotice.php
+++ b/actions/newnotice.php
@@ -84,20 +84,24 @@ class NewnoticeAction extends Action
function handle($args)
{
- parent::handle($args);
-
if (!common_logged_in()) {
$this->clientError(_('Not logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ // check for this before token since all POST and FILES data
+ // is losts when size is exceeded
+ if (empty($_POST) && $_SERVER['CONTENT_LENGTH']) {
+ $this->clientError(sprintf(_('The server was unable to handle ' .
+ 'that much POST data (%s bytes) due to its current configuration.'),
+ $_SERVER['CONTENT_LENGTH']));
+ }
+ parent::handle($args);
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
$this->clientError(_('There was a problem with your session token. '.
'Try again, please.'));
- return;
}
-
try {
$this->saveNewNotice();
} catch (Exception $e) {
@@ -109,6 +113,33 @@ class NewnoticeAction extends Action
}
}
+ function getUploadedFileType() {
+ require_once 'MIME/Type.php';
+
+ $cmd = &PEAR::getStaticProperty('MIME_Type', 'fileCmd');
+ $cmd = common_config('attachments', 'filecommand');
+
+ $filetype = MIME_Type::autoDetect($_FILES['attach']['tmp_name']);
+ if (in_array($filetype, common_config('attachments', 'supported'))) {
+ return $filetype;
+ }
+ $media = MIME_Type::getMedia($filetype);
+ if ('application' !== $media) {
+ $hint = sprintf(_(' Try using another %s format.'), $media);
+ } else {
+ $hint = '';
+ }
+ $this->clientError(sprintf(
+ _('%s is not a supported filetype on this server.'), $filetype) . $hint);
+ }
+
+ function isRespectsQuota($user) {
+ $file = new File;
+ $ret = $file->isRespectsQuota($user,$_FILES['attach']['size']);
+ if (true === $ret) return true;
+ $this->clientError($ret);
+ }
+
/**
* Save a new notice, based on arguments
*
@@ -131,7 +162,6 @@ class NewnoticeAction extends Action
$this->clientError(_('No content!'));
} else {
$content_shortened = common_shorten_links($content);
-
if (mb_strlen($content_shortened) > 140) {
$this->clientError(_('That\'s too long. '.
'Max notice size is 140 chars.'));
@@ -158,12 +188,80 @@ class NewnoticeAction extends Action
$replyto = 'false';
}
- $notice = Notice::saveNew($user->id, $content, 'web', 1,
+ if (isset($_FILES['attach']['error'])) {
+ switch ($_FILES['attach']['error']) {
+ case UPLOAD_ERR_NO_FILE:
+ // no file uploaded, nothing to do
+ break;
+
+ case UPLOAD_ERR_OK:
+ $mimetype = $this->getUploadedFileType();
+ if (!$this->isRespectsQuota($user)) {
+ die('clientError() should trigger an exception before reaching here.');
+ }
+ break;
+
+ case UPLOAD_ERR_INI_SIZE:
+ $this->clientError(_('The uploaded file exceeds the upload_max_filesize directive in php.ini.'));
+
+ case UPLOAD_ERR_FORM_SIZE:
+ $this->clientError(_('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'));
+
+ case UPLOAD_ERR_PARTIAL:
+ $this->clientError(_('The uploaded file was only partially uploaded.'));
+
+ case UPLOAD_ERR_NO_TMP_DIR:
+ $this->clientError(_('Missing a temporary folder.'));
+
+ case UPLOAD_ERR_CANT_WRITE:
+ $this->clientError(_('Failed to write file to disk.'));
+
+ case UPLOAD_ERR_EXTENSION:
+ $this->clientError(_('File upload stopped by extension.'));
+
+ default:
+ die('Should never reach here.');
+ }
+ }
+
+ if (isset($mimetype)) {
+ $filename = $this->saveFile($mimetype);
+ if (empty($filename)) {
+ $this->clientError(_('Couldn\'t save file.'));
+ }
+
+ $fileRecord = $this->storeFile($filename, $mimetype);
+
+ $fileurl = common_local_url('attachment',
+ array('attachment' => $fileRecord->id));
+
+ // not sure this is necessary -- Zach
+ $this->maybeAddRedir($fileRecord->id, $fileurl);
+
+ $short_fileurl = common_shorten_url($fileurl);
+ $content_shortened .= ' ' . $short_fileurl;
+
+ if (mb_strlen($content_shortened) > 140) {
+ $this->deleteFile($filename);
+ $this->clientError(_('Max notice size is 140 chars, including attachment URL.'));
+ }
+
+ // Also, not sure this is necessary -- Zach
+ $this->maybeAddRedir($fileRecord->id, $short_fileurl);
+ }
+
+ $notice = Notice::saveNew($user->id, $content_shortened, 'web', 1,
($replyto == 'false') ? null : $replyto);
if (is_string($notice)) {
+ if (isset($filename)) {
+ $this->deleteFile($filename);
+ }
$this->clientError($notice);
- return;
+ }
+
+ if (isset($mimetype)) {
+ $this->attachFile($notice, $fileRecord);
}
common_broadcast_notice($notice);
@@ -191,6 +289,87 @@ class NewnoticeAction extends Action
}
}
+ function saveFile($mimetype) {
+
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ $this->serverError(_('Somehow lost the login in saveFile'));
+ }
+
+ $basename = basename($_FILES['attach']['name']);
+
+ $filename = File::filename($cur->getProfile(), $basename, $mimetype);
+
+ $filepath = File::path($filename);
+
+ if (move_uploaded_file($_FILES['attach']['tmp_name'], $filepath)) {
+ return $filename;
+ } else {
+ $this->clientError(_('File could not be moved to destination directory.'));
+ }
+ }
+
+ function deleteFile($filename)
+ {
+ $filepath = File::path($filename);
+ @unlink($filepath);
+ }
+
+ function storeFile($filename, $mimetype) {
+
+ $file = new File;
+ $file->filename = $filename;
+
+ $file->url = File::url($filename);
+
+ $filepath = File::path($filename);
+
+ $file->size = filesize($filepath);
+ $file->date = time();
+ $file->mimetype = $mimetype;
+
+ $file_id = $file->insert();
+
+ if (!$file_id) {
+ common_log_db_error($file, "INSERT", __FILE__);
+ $this->clientError(_('There was a database error while saving your file. Please try again.'));
+ }
+
+ return $file;
+ }
+
+ function rememberFile($file, $short)
+ {
+ $this->maybeAddRedir($file->id, $short);
+ }
+
+ function maybeAddRedir($file_id, $url)
+ {
+ $file_redir = File_redirection::staticGet('url', $url);
+
+ if (empty($file_redir)) {
+ $file_redir = new File_redirection;
+ $file_redir->url = $url;
+ $file_redir->file_id = $file_id;
+
+ $result = $file_redir->insert();
+
+ if (!$result) {
+ common_log_db_error($file_redir, "INSERT", __FILE__);
+ $this->clientError(_('There was a database error while saving your file. Please try again.'));
+ }
+ }
+ }
+
+ function attachFile($notice, $filerec)
+ {
+ File_to_post::processNew($filerec->id, $notice->id);
+
+ $this->maybeAddRedir($filerec->id,
+ common_local_url('file', array('notice' => $notice->id)));
+ }
+
/**
* Show an Ajax-y error message
*
@@ -295,3 +474,4 @@ class NewnoticeAction extends Action
$nli->show();
}
}
+
diff --git a/actions/noticesearch.php b/actions/noticesearch.php
index d996998fc..49b473d9e 100644
--- a/actions/noticesearch.php
+++ b/actions/noticesearch.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/noticesearchrss.php b/actions/noticesearchrss.php
index f6da969ee..2a4b2060d 100644
--- a/actions/noticesearchrss.php
+++ b/actions/noticesearchrss.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -67,11 +67,16 @@ class NoticesearchrssAction extends Rss10Action
if (!$limit) $limit = 20;
$search_engine->limit(0, $limit, true);
- $search_engine->query($q);
- $notice->find();
+ if (false === $search_engine->query($q)) {
+ $cnt = 0;
+ } else {
+ $cnt = $notice->find();
+ }
- while ($notice->fetch()) {
- $notices[] = clone($notice);
+ if ($cnt > 0) {
+ while ($notice->fetch()) {
+ $notices[] = clone($notice);
+ }
}
return $notices;
diff --git a/actions/nudge.php b/actions/nudge.php
index c23d3e643..78c0ee566 100644
--- a/actions/nudge.php
+++ b/actions/nudge.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/openidlogin.php b/actions/openidlogin.php
index 1a4372d73..a8d052096 100644
--- a/actions/openidlogin.php
+++ b/actions/openidlogin.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/opensearch.php b/actions/opensearch.php
index d1f4895ce..4fe95c93b 100644
--- a/actions/opensearch.php
+++ b/actions/opensearch.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/othersettings.php b/actions/othersettings.php
index b542233ca..1277f8052 100644
--- a/actions/othersettings.php
+++ b/actions/othersettings.php
@@ -83,14 +83,12 @@ class OthersettingsAction extends AccountSettingsAction
{
$user = common_current_user();
-
$this->elementStart('form', array('method' => 'post',
'id' => 'form_settings_other',
'class' => 'form_settings',
'action' =>
common_local_url('othersettings')));
$this->elementStart('fieldset');
- $this->element('legend', null, _('URL Auto-shortening'));
$this->hidden('token', common_session_token());
// I18N
@@ -109,10 +107,14 @@ class OthersettingsAction extends AccountSettingsAction
$this->elementStart('ul', 'form_data');
$this->elementStart('li');
- $this->dropdown('urlshorteningservice', _('Service'),
+ $this->dropdown('urlshorteningservice', _('Shorten URLs with'),
$services, _('Automatic shortening service to use.'),
false, $user->urlshorteningservice);
$this->elementEnd('li');
+ $this->elementStart('li');
+ $this->checkbox('viewdesigns', _('View profile designs'),
+ $user->viewdesigns, _('Show or hide profile designs.'));
+ $this->elementEnd('li');
$this->elementEnd('ul');
$this->submit('save', _('Save'));
$this->elementEnd('fieldset');
@@ -145,6 +147,8 @@ class OthersettingsAction extends AccountSettingsAction
return;
}
+ $viewdesigns = $this->boolean('viewdesigns');
+
$user = common_current_user();
assert(!is_null($user)); // should already be checked
@@ -154,6 +158,7 @@ class OthersettingsAction extends AccountSettingsAction
$original = clone($user);
$user->urlshorteningservice = $urlshorteningservice;
+ $user->viewdesigns = $viewdesigns;
$result = $user->update($original);
diff --git a/actions/peoplesearch.php b/actions/peoplesearch.php
index 65d970dd1..60ddb6a82 100644
--- a/actions/peoplesearch.php
+++ b/actions/peoplesearch.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -87,3 +87,47 @@ class PeoplesearchAction extends SearchAction
}
}
+/**
+ * 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, $action);
+
+ $this->terms = array_map('preg_quote',
+ array_map('htmlspecialchars', $terms));
+
+ $this->pattern = '/('.implode('|',$terms).')/i';
+ }
+
+ function newProfileItem($profile)
+ {
+ return new PeopleSearchResultItem($profile, $this->action);
+ }
+}
+
+class PeopleSearchResultItem extends ProfileListItem
+{
+ function highlight($text)
+ {
+ return preg_replace($this->pattern, '<strong>\\1</strong>', htmlspecialchars($text));
+ }
+}
+
diff --git a/actions/peopletag.php b/actions/peopletag.php
index 5add75485..dd3c1c089 100644
--- a/actions/peopletag.php
+++ b/actions/peopletag.php
@@ -124,7 +124,7 @@ class PeopletagAction extends Action
$profile->query(sprintf($qry, $this->tag, $lim));
- $pl = new ProfileList($profile, null, $this);
+ $pl = new ProfileList($profile, $this);
$cnt = $pl->show();
$this->pagination($this->page > 1,
diff --git a/actions/postnotice.php b/actions/postnotice.php
index 3e98b3cd5..eb2d63b61 100644
--- a/actions/postnotice.php
+++ b/actions/postnotice.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/public.php b/actions/public.php
index 27153f131..ef9ef0d1a 100644
--- a/actions/public.php
+++ b/actions/public.php
@@ -35,6 +35,10 @@ require_once INSTALLDIR.'/lib/publicgroupnav.php';
require_once INSTALLDIR.'/lib/noticelist.php';
require_once INSTALLDIR.'/lib/feedlist.php';
+// Farther than any human will go
+
+define('MAX_PUBLIC_PAGE', 100);
+
/**
* Action for displaying the public stream
*
@@ -74,6 +78,10 @@ class PublicAction extends Action
parent::prepare($args);
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
+ if ($this->page > MAX_PUBLIC_PAGE) {
+ $this->clientError(sprintf(_("Beyond the page limit (%s)"), MAX_PUBLIC_PAGE));
+ }
+
common_set_returnto($this->selfUrl());
return true;
@@ -174,8 +182,10 @@ class PublicAction extends Action
$message .= _('Be the first to post!');
}
else {
- $message .= _('Why not [register an account](%%action.register%%) and be the first to post!');
- }
+ if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
+ $message .= _('Why not [register an account](%%action.register%%) and be the first to post!');
+ }
+ }
$this->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message));
diff --git a/actions/publicrss.php b/actions/publicrss.php
index bc52f2952..7e8df9625 100644
--- a/actions/publicrss.php
+++ b/actions/publicrss.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/publicxrds.php b/actions/publicxrds.php
index 283a932ca..0a1421550 100644
--- a/actions/publicxrds.php
+++ b/actions/publicxrds.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php
index 82263fcd5..2afd052a7 100644
--- a/actions/recoverpassword.php
+++ b/actions/recoverpassword.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/register.php b/actions/register.php
index 033cf557f..dcbbbdb6a 100644
--- a/actions/register.php
+++ b/actions/register.php
@@ -382,6 +382,19 @@ class RegisterAction extends Action
function showFormContent()
{
+ $code = $this->trimmed('code');
+
+ $invite = null;
+
+ if ($code) {
+ $invite = Invitation::staticGet($code);
+ }
+
+ if (common_config('site', 'inviteonly') && !($code && $invite)) {
+ $this->clientError(_('Sorry, only invited people can register.'));
+ return;
+ }
+
$this->elementStart('form', array('method' => 'post',
'id' => 'form_register',
'class' => 'form_settings',
diff --git a/actions/remotesubscribe.php b/actions/remotesubscribe.php
index 0b1174896..e658f8d37 100644
--- a/actions/remotesubscribe.php
+++ b/actions/remotesubscribe.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/replies.php b/actions/replies.php
index eac4d0a3a..d7ed440e9 100644
--- a/actions/replies.php
+++ b/actions/replies.php
@@ -45,9 +45,8 @@ require_once INSTALLDIR.'/lib/feedlist.php';
* @link http://laconi.ca/
*/
-class RepliesAction extends Action
+class RepliesAction extends OwnerDesignAction
{
- var $user = null;
var $page = null;
/**
diff --git a/actions/repliesrss.php b/actions/repliesrss.php
index 2017c4309..a87e2870d 100644
--- a/actions/repliesrss.php
+++ b/actions/repliesrss.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/requesttoken.php b/actions/requesttoken.php
index 9507e3d6c..8d1e3f004 100644
--- a/actions/requesttoken.php
+++ b/actions/requesttoken.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/showfavorites.php b/actions/showfavorites.php
index 865045337..8efe9d30a 100644
--- a/actions/showfavorites.php
+++ b/actions/showfavorites.php
@@ -45,7 +45,7 @@ require_once INSTALLDIR.'/lib/feedlist.php';
* @link http://laconi.ca/
*/
-class ShowfavoritesAction extends Action
+class ShowfavoritesAction extends OwnerDesignAction
{
/** User we're getting the faves of */
var $user = null;
@@ -191,10 +191,21 @@ class ShowfavoritesAction extends Action
function showContent()
{
- $notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
- NOTICES_PER_PAGE + 1);
+ $cur = common_current_user();
- if (!$notice) {
+ if (!empty($cur) && $cur->id == $this->user->id) {
+
+ // Show imported/gateway notices as well as local if
+ // the user is looking at his own favorites
+
+ $notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
+ NOTICES_PER_PAGE + 1, true);
+ } else {
+ $notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
+ NOTICES_PER_PAGE + 1, false);
+ }
+
+ if (empty($notice)) {
$this->serverError(_('Could not retrieve favorite notices.'));
return;
}
diff --git a/actions/showgroup.php b/actions/showgroup.php
index 29b6fa1e6..ce11d574e 100644
--- a/actions/showgroup.php
+++ b/actions/showgroup.php
@@ -47,10 +47,9 @@ define('MEMBERS_PER_SECTION', 27);
* @link http://laconi.ca/
*/
-class ShowgroupAction extends Action
+class ShowgroupAction extends GroupDesignAction
{
- /** group we're viewing. */
- var $group = null;
+
/** page we're viewing. */
var $page = null;
@@ -272,6 +271,17 @@ class ShowgroupAction extends Action
$this->elementEnd('dl');
}
+ if (common_config('group', 'maxaliases') > 0) {
+ $aliases = $this->group->getAliases();
+
+ if (!empty($aliases)) {
+ $this->elementStart('dl', 'entity_aliases');
+ $this->element('dt', null, _('Aliases'));
+ $this->element('dd', 'aliases', implode(' ', $aliases));
+ $this->elementEnd('dl');
+ }
+ }
+
$this->elementEnd('div');
$this->elementStart('div', 'entity_actions');
@@ -283,7 +293,7 @@ class ShowgroupAction extends Action
if ($cur->isMember($this->group)) {
$lf = new LeaveForm($this, $this->group);
$lf->show();
- } else {
+ } else if (!Group_block::isBlocked($this->group, $cur->getProfile())) {
$jf = new JoinForm($this, $this->group);
$jf->show();
}
@@ -321,6 +331,7 @@ class ShowgroupAction extends Action
{
$this->showMembers();
$this->showStatistics();
+ $this->showAdmins();
$cloud = new GroupTagCloudSection($this, $this->group);
$cloud->show();
}
@@ -344,7 +355,7 @@ class ShowgroupAction extends Action
$this->element('h2', null, _('Members'));
- $pml = new ProfileMiniList($member, null, $this);
+ $pml = new ProfileMiniList($member, $this);
$cnt = $pml->show();
if ($cnt == 0) {
$this->element('p', null, _('(None)'));
@@ -360,6 +371,18 @@ class ShowgroupAction extends Action
}
/**
+ * Show list of admins
+ *
+ * @return void
+ */
+
+ function showAdmins()
+ {
+ $adminSection = new GroupAdminSection($this, $this->group);
+ $adminSection->show();
+ }
+
+ /**
* Show some statistics
*
* @return void
@@ -413,3 +436,34 @@ class ShowgroupAction extends Action
$this->elementEnd('div');
}
}
+
+class GroupAdminSection extends ProfileSection
+{
+ var $group;
+
+ function __construct($out, $group)
+ {
+ parent::__construct($out);
+ $this->group = $group;
+ }
+
+ function getProfiles()
+ {
+ return $this->group->getAdmins();
+ }
+
+ function title()
+ {
+ return _('Admins');
+ }
+
+ function divId()
+ {
+ return 'group_admins';
+ }
+
+ function moreUrl()
+ {
+ return null;
+ }
+} \ No newline at end of file
diff --git a/actions/shownotice.php b/actions/shownotice.php
index 1be1e2414..1ec38a76b 100644
--- a/actions/shownotice.php
+++ b/actions/shownotice.php
@@ -45,7 +45,7 @@ require_once INSTALLDIR.'/lib/feedlist.php';
* @link http://laconi.ca/
*/
-class ShownoticeAction extends Action
+class ShownoticeAction extends OwnerDesignAction
{
/**
* Notice object to show
@@ -83,18 +83,25 @@ class ShownoticeAction extends Action
$this->notice = Notice::staticGet($id);
- if (!$this->notice) {
+ if (empty($this->notice)) {
$this->clientError(_('No such notice.'), 404);
return false;
}
$this->profile = $this->notice->getProfile();
- if (!$this->profile) {
+ if (empty($this->profile)) {
$this->serverError(_('Notice has no profile'), 500);
return false;
}
+ $this->user = User::staticGet('id', $this->profile->id);
+
+ if (empty($this->user)) {
+ $this->serverError(_('Not a local notice'), 500);
+ return false;
+ }
+
$this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
return true;
@@ -158,8 +165,14 @@ class ShownoticeAction extends Action
function title()
{
+ if (!empty($this->profile->fullname)) {
+ $base = $this->profile->fullname . ' (' . $this->user->nickname . ') ';
+ } else {
+ $base = $this->user->nickname;
+ }
+
return sprintf(_('%1$s\'s status on %2$s'),
- $this->profile->nickname,
+ $base,
common_exact_date($this->notice->created));
}
@@ -208,10 +221,10 @@ class ShownoticeAction extends Action
function showContent()
{
- $this->elementStart('ul', array('class' => 'notices'));
- $nli = new NoticeListItem($this->notice, $this);
+ $this->elementStart('ol', array('class' => 'notices xoxo'));
+ $nli = new SingleNoticeItem($this->notice, $this);
$nli->show();
- $this->elementEnd('ul');
+ $this->elementEnd('ol');
}
/**
@@ -264,3 +277,29 @@ class ShownoticeAction extends Action
}
}
}
+
+class SingleNoticeItem extends NoticeListItem
+{
+ /**
+ * recipe function for displaying a single notice.
+ *
+ * We overload to show attachments.
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $this->showStart();
+ $this->showNotice();
+ $this->showNoticeAttachments();
+ $this->showNoticeInfo();
+ $this->showNoticeOptions();
+ $this->showEnd();
+ }
+
+ function showNoticeAttachments() {
+ $al = new AttachmentList($this->notice, $this->out);
+ $al->show();
+ }
+}
diff --git a/actions/showstream.php b/actions/showstream.php
index 641228bc7..cd5d4bb70 100644
--- a/actions/showstream.php
+++ b/actions/showstream.php
@@ -68,6 +68,9 @@ class ShowstreamAction extends ProfileAction
} else {
$base = $this->user->nickname;
}
+ if (!empty($this->tag)) {
+ $base .= sprintf(_(' tagged %s'), $this->tag);
+ }
if ($this->page == 1) {
return $base;
@@ -110,6 +113,15 @@ class ShowstreamAction extends ProfileAction
function getFeeds()
{
+ if (!empty($this->tag)) {
+ return array(new Feed(Feed::RSS1,
+ common_local_url('userrss',
+ array('nickname' => $this->user->nickname,
+ 'tag' => $this->tag)),
+ sprintf(_('Notice feed for %s tagged %s (RSS 1.0)'),
+ $this->user->nickname, $this->tag)));
+ }
+
return array(new Feed(Feed::RSS1,
common_local_url('userrss',
array('nickname' => $this->user->nickname)),
@@ -356,7 +368,9 @@ class ShowstreamAction extends ProfileAction
function showNotices()
{
- $notice = $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+ $notice = empty($this->tag)
+ ? $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1)
+ : $this->user->getTaggedNotices($this->tag, ($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1, 0, 0, null);
$pnl = new ProfileNoticeList($notice, $this);
$cnt = $pnl->show();
diff --git a/actions/subedit.php b/actions/subedit.php
index 8ca2d7914..2e1bf5538 100644
--- a/actions/subedit.php
+++ b/actions/subedit.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/subscribe.php b/actions/subscribe.php
index 0bc522867..15b89a312 100644
--- a/actions/subscribe.php
+++ b/actions/subscribe.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/subscribers.php b/actions/subscribers.php
index 4482de9a7..66ac00fb1 100644
--- a/actions/subscribers.php
+++ b/actions/subscribers.php
@@ -130,18 +130,34 @@ class SubscribersAction extends GalleryAction
}
}
-class SubscribersList extends ProfileList
+class SubscribersList extends SubscriptionList
{
- function showBlockForm()
+ function newListItem($profile)
{
- $bf = new BlockForm($this->out, $this->profile,
- array('action' => 'subscribers',
- 'nickname' => $this->owner->nickname));
- $bf->show();
+ return new SubscribersListItem($profile, $this->owner, $this->action);
}
+}
- function isReadOnly($args)
+class SubscribersListItem extends SubscriptionListItem
+{
+ function showActions()
{
- return true;
+ $this->startActions();
+ $this->showSubscribeButton();
+ // Relevant code!
+ $this->showBlockForm();
+ $this->endActions();
+ }
+
+ function showBlockForm()
+ {
+ $user = common_current_user();
+
+ if (!empty($user) && $this->owner->id == $user->id) {
+ $bf = new BlockForm($this->out, $this->profile,
+ array('action' => 'subscribers',
+ 'nickname' => $this->owner->nickname));
+ $bf->show();
+ }
}
}
diff --git a/actions/subscriptions.php b/actions/subscriptions.php
index 095b18ad8..42bdae10f 100644
--- a/actions/subscriptions.php
+++ b/actions/subscriptions.php
@@ -137,22 +137,49 @@ class SubscriptionsAction extends GalleryAction
}
}
-class SubscriptionsList extends ProfileList
+// XXX SubscriptionsList and SubscriptionList are dangerously close
+
+class SubscriptionsList extends SubscriptionList
+{
+ function newListItem($profile)
+ {
+ return new SubscriptionsListItem($profile, $this->owner, $this->action);
+ }
+}
+
+class SubscriptionsListItem extends SubscriptionListItem
{
- function showOwnerControls($profile)
+ function showProfile()
+ {
+ $this->startProfile();
+ $this->showAvatar();
+ $this->showFullName();
+ $this->showLocation();
+ $this->showHomepage();
+ $this->showBio();
+ $this->showTags();
+ // Relevant portion!
+ $cur = common_current_user();
+ if (!empty($cur) && $cur->id == $this->owner->id) {
+ $this->showOwnerControls();
+ }
+ $this->endProfile();
+ }
+
+ function showOwnerControls()
{
$sub = Subscription::pkeyGet(array('subscriber' => $this->owner->id,
- 'subscribed' => $profile->id));
+ 'subscribed' => $this->profile->id));
if (!$sub) {
return;
}
- $this->out->elementStart('form', array('id' => 'subedit-' . $profile->id,
+ $this->out->elementStart('form', array('id' => 'subedit-' . $this->profile->id,
'method' => 'post',
'class' => 'form_subscription_edit',
'action' => common_local_url('subedit')));
$this->out->hidden('token', common_session_token());
- $this->out->hidden('profile', $profile->id);
+ $this->out->hidden('profile', $this->profile->id);
$this->out->checkbox('jabber', _('Jabber'), $sub->jabber);
$this->out->checkbox('sms', _('SMS'), $sub->sms);
$this->out->submit('save', _('Save'));
diff --git a/actions/sup.php b/actions/sup.php
index 691153d6a..a5b665562 100644
--- a/actions/sup.php
+++ b/actions/sup.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -63,11 +63,13 @@ class SupAction extends Action
# XXX: cache this. Depends on how big this protocol becomes;
# Re-doing this query every 15 seconds isn't the end of the world.
+ $divider = common_sql_date(time() - $seconds);
+
$notice->query('SELECT profile_id, max(id) AS max_id ' .
'FROM notice ' .
((common_config('db','type') == 'pgsql') ?
'WHERE extract(epoch from created) > (extract(epoch from now()) - ' . $seconds . ') ' :
- 'WHERE created > (now() - ' . $seconds . ') ' ) .
+ 'WHERE created > "'.$divider.'" ' ) .
'GROUP BY profile_id');
$updates = array();
diff --git a/actions/tag.php b/actions/tag.php
index f5ca06f05..888aba062 100644
--- a/actions/tag.php
+++ b/actions/tag.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -76,11 +76,6 @@ class TagAction extends Action
sprintf(_('Feed for tag %s'), $this->tag)));
}
- function showPageNotice()
- {
- return sprintf(_('Messages tagged "%s", most recent first'), $this->tag);
- }
-
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 0c5bb7cf3..96246f799 100644
--- a/actions/tagother.php
+++ b/actions/tagother.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/tagrss.php b/actions/tagrss.php
index 83cf3afe2..f69374fca 100644
--- a/actions/tagrss.php
+++ b/actions/tagrss.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/twitapiaccount.php b/actions/twitapiaccount.php
index 8b956f897..f2a7534a2 100644
--- a/actions/twitapiaccount.php
+++ b/actions/twitapiaccount.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -51,7 +53,8 @@ class TwitapiaccountAction extends TwitterapiAction
parent::handle($args);
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
- $this->clientError(_('This method requires a POST.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST.'),
+ 400, $apidata['content-type']);
return;
}
@@ -60,24 +63,20 @@ class TwitapiaccountAction extends TwitterapiAction
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']);
+ $this->clientError(_('That\'s too long. Max notice size is 255 chars.'),
+ 406, $apidate['content-type']);
return;
}
- $user = $apidata['user'];
+ $user = $apidata['user']; // Always the auth user
$profile = $user->getProfile();
- if (!$profile) {
- $this->serverError(_('User has no profile.'));
- return;
- }
-
$orig_profile = clone($profile);
$profile->location = $location;
$result = $profile->update($orig_profile);
- if (!$result) {
+ if (empty($result)) {
common_log_db_error($profile, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t save profile.'));
return;
diff --git a/actions/twitapiblocks.php b/actions/twitapiblocks.php
index 8135adef3..d8e72efb1 100644
--- a/actions/twitapiblocks.php
+++ b/actions/twitapiblocks.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -31,12 +33,12 @@ class TwitapiblocksAction extends TwitterapiAction
$blockee = $this->get_user($apidata['api_arg'], $apidata);
- if (!$blockee) {
+ if (empty($blockee)) {
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
- $user = $apidata['user'];
+ $user = $apidata['user']; // Always the auth user
if ($user->hasBlocked($blockee) || $user->block($blockee)) {
$type = $apidata['content-type'];
@@ -53,7 +55,7 @@ class TwitapiblocksAction extends TwitterapiAction
parent::handle($args);
$blockee = $this->get_user($apidata['api_arg'], $apidata);
- if (!$blockee) {
+ if (empty($blockee)) {
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
diff --git a/actions/twitapidirect_messages.php b/actions/twitapidirect_messages.php
index d2dbdb619..bd27e9d20 100644
--- a/actions/twitapidirect_messages.php
+++ b/actions/twitapidirect_messages.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -38,42 +40,34 @@ class Twitapidirect_messagesAction extends TwitterapiAction
function show_messages($args, $apidata, $type)
{
- $user = $apidata['user'];
-
- $count = $this->arg('count');
- $since = $this->arg('since');
- $since_id = $this->arg('since_id');
- $max_id = $this->arg('max_id');
-
- $page = $this->arg('page');
-
- if (!$page) {
- $page = 1;
- }
+ $user = $apidata['user']; // Always the auth user
- if (!$count) {
- $count = 20;
- }
-
- $message = new Message();
-
- $title = null;
+ $message = new Message();
+ $title = null;
$subtitle = null;
- $link = null;
- $server = common_root_url();
+ $link = null;
+ $server = common_root_url();
if ($type == 'received') {
$message->to_profile = $user->id;
$title = sprintf(_("Direct messages to %s"), $user->nickname);
- $subtitle = sprintf(_("All the direct messages sent to %s"), $user->nickname);
+ $subtitle = sprintf(_("All the direct messages sent to %s"),
+ $user->nickname);
$link = $server . $user->nickname . '/inbox';
} else {
$message->from_profile = $user->id;
$title = _('Direct Messages You\'ve Sent');
- $subtitle = sprintf(_("All the direct messages sent from %s"), $user->nickname);
+ $subtitle = sprintf(_("All the direct messages sent from %s"),
+ $user->nickname);
$link = $server . $user->nickname . '/outbox';
}
+ $page = (int)$this->arg('page', 1);
+ $count = (int)$this->arg('count', 20);
+ $max_id = (int)$this->arg('max_id', 0);
+ $since_id = (int)$this->arg('since_id', 0);
+ $since = $this->arg('since');
+
if ($max_id) {
$message->whereAdd("id <= $max_id");
}
@@ -82,25 +76,23 @@ class Twitapidirect_messagesAction extends TwitterapiAction
$message->whereAdd("id > $since_id");
}
- $since = strtotime($this->arg('since'));
-
if ($since) {
$d = date('Y-m-d H:i:s', $since);
$message->whereAdd("created > '$d'");
}
$message->orderBy('created DESC, id DESC');
- $message->limit((($page-1)*20), $count);
+ $message->limit((($page-1)*$count), $count);
$message->find();
switch($apidata['content-type']) {
- case 'xml':
+ case 'xml':
$this->show_xml_dmsgs($message);
break;
- case 'rss':
+ case 'rss':
$this->show_rss_dmsgs($message, $title, $link, $subtitle);
break;
- case 'atom':
+ case 'atom':
$selfuri = common_root_url() . 'api/direct_messages';
$selfuri .= ($type == 'received') ? '.atom' : '/sent.atom';
$taguribase = common_config('integration', 'taguri');
@@ -111,12 +103,13 @@ class Twitapidirect_messagesAction extends TwitterapiAction
$id = "tag:$taguribase:DirectMessages:" . $user->id;
}
- $this->show_atom_dmsgs($message, $title, $link, $subtitle, $selfuri, $id);
+ $this->show_atom_dmsgs($message, $title, $link, $subtitle,
+ $selfuri, $id);
break;
- case 'json':
+ case 'json':
$this->show_json_dmsgs($message);
break;
- default:
+ default:
$this->clientError(_('API method not found!'), $code = 404);
}
@@ -128,22 +121,24 @@ class Twitapidirect_messagesAction extends TwitterapiAction
parent::handle($args);
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
- $this->clientError(_('This method requires a POST.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST.'),
+ 400, $apidata['content-type']);
return;
}
$user = $apidata['user'];
- $source = $this->trimmed('source'); // Not supported by Twitter.
+ $source = $this->trimmed('source'); // Not supported by Twitter.
$reserved_sources = array('web', 'omb', 'mail', 'xmpp', 'api');
- if (!$source || in_array($source, $reserved_sources)) {
+ if (empty($source) || in_array($source, $reserved_sources)) {
$source = 'api';
}
$content = $this->trimmed('text');
- if (!$content) {
- $this->clientError(_('No message text!'), $code = 406, $apidata['content-type']);
+ if (empty($content)) {
+ $this->clientError(_('No message text!'),
+ $code = 406, $apidata['content-type']);
} else {
$content_shortened = common_shorten_links($content);
if (mb_strlen($content_shortened) > 140) {
@@ -155,8 +150,9 @@ class Twitapidirect_messagesAction extends TwitterapiAction
$other = $this->get_user($this->trimmed('user'));
- if (!$other) {
- $this->clientError(_('Recipient user not found.'), $code = 403, $apidata['content-type']);
+ if (empty($other)) {
+ $this->clientError(_('Recipient user not found.'),
+ $code = 403, $apidata['content-type']);
return;
} else if (!$user->mutuallySubscribed($other)) {
$this->clientError(_('Can\'t send direct messages to users who aren\'t your friend.'),
diff --git a/actions/twitapifavorites.php b/actions/twitapifavorites.php
index 31dce341b..8256668f3 100644
--- a/actions/twitapifavorites.php
+++ b/actions/twitapifavorites.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -31,50 +33,48 @@ class TwitapifavoritesAction extends TwitterapiAction
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
- if (!$user) {
+ if (empty($user)) {
+ if ($apidata['content-type'] == 'xml') {
+ $this->show_single_xml_status($notice);
+ } elseif ($apidata['content-type'] == 'json') {
+ $this->show_single_json_status($notice);
+ }
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
$profile = $user->getProfile();
- if (!$profile) {
- $this->serverError(_('User has no profile.'));
- return;
- }
-
- $page = $this->arg('page');
-
- if (!$page) {
- $page = 1;
- }
-
- if (!$count) {
- $count = 20;
- }
-
- $notice = $user->favoriteNotices((($page-1)*20), $count);
-
- if (!$notice) {
- $this->serverError(_('Could not retrieve favorite notices.'));
- return;
- }
-
- $sitename = common_config('site', 'name');
- $title = sprintf(_('%s / Favorites from %s'), $sitename, $user->nickname);
+ $sitename = common_config('site', 'name');
+ $title = sprintf(_('%s / Favorites from %s'), $sitename,
+ $user->nickname);
$taguribase = common_config('integration', 'taguri');
- $id = "tag:$taguribase:Favorites:".$user->id;
- $link = common_local_url('favorites', array('nickname' => $user->nickname));
- $subtitle = sprintf(_('%s updates favorited by %s / %s.'), $sitename, $profile->getBestName(), $user->nickname);
+ $id = "tag:$taguribase:Favorites:".$user->id;
+ $link = common_local_url('favorites',
+ array('nickname' => $user->nickname));
+ $subtitle = sprintf(_('%s updates favorited by %s / %s.'), $sitename,
+ $profile->getBestName(), $user->nickname);
+
+ $page = (int)$this->arg('page', 1);
+ $count = (int)$this->arg('count', 20);
+ $max_id = (int)$this->arg('max_id', 0);
+ $since_id = (int)$this->arg('since_id', 0);
+ $since = $this->arg('since');
+
+ if (!empty($this->auth_user) && $this->auth_user->id == $user->id) {
+ $notice = $user->favoriteNotices(($page-1)*$count, $count, true);
+ } else {
+ $notice = $user->favoriteNotices(($page-1)*$count, $count, false);
+ }
switch($apidata['content-type']) {
- case 'xml':
+ case 'xml':
$this->show_xml_timeline($notice);
break;
- case 'rss':
+ case 'rss':
$this->show_rss_timeline($notice, $title, $link, $subtitle);
break;
- case 'atom':
+ case 'atom':
if (isset($apidata['api_arg'])) {
$selfuri = $selfuri = common_root_url() .
'api/favorites/' . $apidata['api_arg'] . '.atom';
@@ -82,12 +82,13 @@ class TwitapifavoritesAction extends TwitterapiAction
$selfuri = $selfuri = common_root_url() .
'api/favorites.atom';
}
- $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri);
+ $this->show_atom_timeline($notice, $title, $id, $link,
+ $subtitle, null, $selfuri);
break;
- case 'json':
+ case 'json':
$this->show_json_timeline($notice);
break;
- default:
+ default:
$this->clientError(_('API method not found!'), $code = 404);
}
@@ -99,8 +100,8 @@ class TwitapifavoritesAction extends TwitterapiAction
// Check for RESTfulness
if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'DELETE'))) {
- // XXX: Twitter just prints the err msg, no XML / JSON.
- $this->clientError(_('This method requires a POST or DELETE.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST or DELETE.'),
+ 400, $apidata['content-type']);
return;
}
@@ -109,26 +110,27 @@ class TwitapifavoritesAction extends TwitterapiAction
return;
}
- $this->auth_user = $apidata['user'];
- $user = $this->auth_user;
+ $user = $apidata['user']; // Always the auth user
$notice_id = $apidata['api_arg'];
- $notice = Notice::staticGet($notice_id);
+ $notice = Notice::staticGet($notice_id);
- if (!$notice) {
- $this->clientError(_('No status found with that ID.'), 404, $apidata['content-type']);
+ if (empty($notice)) {
+ $this->clientError(_('No status found with that ID.'),
+ 404, $apidata['content-type']);
return;
}
// XXX: Twitter lets you fave things repeatedly via api.
if ($user->hasFave($notice)) {
- $this->clientError(_('This notice is already a favorite!'), 403, $apidata['content-type']);
+ $this->clientError(_('This status is already a favorite!'),
+ 403, $apidata['content-type']);
return;
}
$fave = Fave::addNew($user, $notice);
- if (!$fave) {
- $this->serverError(_('Could not create favorite.'));
+ if (empty($fave)) {
+ $this->clientError(_('Could not create favorite.'));
return;
}
@@ -146,10 +148,59 @@ class TwitapifavoritesAction extends TwitterapiAction
function destroy($args, $apidata)
{
parent::handle($args);
- $this->serverError(_('API method under construction.'), $code=501);
+
+ // Check for RESTfulness
+ if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'DELETE'))) {
+ $this->clientError(_('This method requires a POST or DELETE.'),
+ 400, $apidata['content-type']);
+ return;
+ }
+
+ if (!in_array($apidata['content-type'], array('xml', 'json'))) {
+ $this->clientError(_('API method not found!'), $code = 404);
+ return;
+ }
+
+ $user = $apidata['user']; // Always the auth user
+ $notice_id = $apidata['api_arg'];
+ $notice = Notice::staticGet($notice_id);
+
+ if (empty($notice)) {
+ $this->clientError(_('No status found with that ID.'),
+ 404, $apidata['content-type']);
+ return;
+ }
+
+ $fave = new Fave();
+ $fave->user_id = $this->id;
+ $fave->notice_id = $notice->id;
+
+ if (!$fave->find(true)) {
+ $this->clientError(_('That status is not a favorite!'),
+ 403, $apidata['content-type']);
+ return;
+ }
+
+ $result = $fave->delete();
+
+ if (!$result) {
+ common_log_db_error($fave, 'DELETE', __FILE__);
+ $this->clientError(_('Could not delete favorite.'), 404);
+ return;
+ }
+
+ $user->blowFavesCache();
+
+ if ($apidata['content-type'] == 'xml') {
+ $this->show_single_xml_status($notice);
+ } elseif ($apidata['content-type'] == 'json') {
+ $this->show_single_json_status($notice);
+ }
+
}
- // XXX: these two funcs swiped from faves. Maybe put in util.php, or some common base class?
+ // XXX: these two funcs swiped from faves.
+ // Maybe put in util.php, or some common base class?
function notify($fave, $notice, $user)
{
diff --git a/actions/twitapifriendships.php b/actions/twitapifriendships.php
index 2f8250e0d..5fb55e9ff 100644
--- a/actions/twitapifriendships.php
+++ b/actions/twitapifriendships.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -29,23 +31,25 @@ class TwitapifriendshipsAction extends TwitterapiAction
parent::handle($args);
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
- $this->clientError(_('This method requires a POST.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST.'),
+ 400, $apidata['content-type']);
return;
}
- $id = $apidata['api_arg'];
-
+ $id = $apidata['api_arg'];
$other = $this->get_user($id);
- if (!$other) {
- $this->clientError(_('Could not follow user: User not found.'), 403, $apidata['content-type']);
+ if (empty($other)) {
+ $this->clientError(_('Could not follow user: User not found.'),
+ 403, $apidata['content-type']);
return;
}
$user = $apidata['user'];
if ($user->isSubscribed($other)) {
- $errmsg = sprintf(_('Could not follow user: %s is already on your list.'), $other->nickname);
+ $errmsg = sprintf(_('Could not follow user: %s is already on your list.'),
+ $other->nickname);
$this->clientError($errmsg, 403, $apidata['content-type']);
return;
}
@@ -60,8 +64,9 @@ class TwitapifriendshipsAction extends TwitterapiAction
$result = $sub->insert();
- if (!$result) {
- $errmsg = sprintf(_('Could not follow user: %s is already on your list.'), $other->nickname);
+ if (empty($result)) {
+ $errmsg = sprintf(_('Could not follow user: %s is already on your list.'),
+ $other->nickname);
$this->clientError($errmsg, 400, $apidata['content-type']);
return;
}
@@ -82,7 +87,8 @@ class TwitapifriendshipsAction extends TwitterapiAction
parent::handle($args);
if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'DELETE'))) {
- $this->clientError(_('This method requires a POST or DELETE.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST or DELETE.'),
+ 400, $apidata['content-type']);
return;
}
@@ -91,7 +97,7 @@ class TwitapifriendshipsAction extends TwitterapiAction
# We can't subscribe to a remote person, but we can unsub
$other = $this->get_profile($id);
- $user = $apidata['user'];
+ $user = $apidata['user']; // Alwyas the auth user
$sub = new Subscription();
$sub->subscriber = $user->id;
@@ -102,7 +108,8 @@ class TwitapifriendshipsAction extends TwitterapiAction
$sub->delete();
$sub->query('COMMIT');
} else {
- $this->clientError(_('You are not friends with the specified user.'), 403, $apidata['content-type']);
+ $this->clientError(_('You are not friends with the specified user.'),
+ 403, $apidata['content-type']);
return;
}
@@ -128,8 +135,9 @@ class TwitapifriendshipsAction extends TwitterapiAction
$user_a = $this->get_user($user_a_id);
$user_b = $this->get_user($user_b_id);
- if (!$user_a || !$user_b) {
- $this->clientError(_('Two user ids or screen_names must be supplied.'), 400, $apidata['content-type']);
+ if (empty($user_a) || empty($user_b)) {
+ $this->clientError(_('Two user ids or screen_names must be supplied.'),
+ 400, $apidata['content-type']);
return;
}
@@ -152,4 +160,85 @@ class TwitapifriendshipsAction extends TwitterapiAction
}
-} \ No newline at end of file
+ function show($args, $apidata)
+ {
+ parent::handle($args);
+
+ if (!in_array($apidata['content-type'], array('xml', 'json'))) {
+ $this->clientError(_('API method not found!'), $code = 404);
+ return;
+ }
+
+ $source_id = (int)$this->trimmed('source_id');
+ $source_screen_name = $this->trimmed('source_screen_name');
+
+ // If the source is not specified for an unauthenticated request,
+ // the method will return an HTTP 403.
+
+ if (empty($source_id) && empty($source_screen_name)) {
+ if (empty($apidata['user'])) {
+ $this->clientError(_('Could not determine source user.'),
+ $code = 403);
+ return;
+ }
+ }
+
+ $source = null;
+
+ if (!empty($source_id)) {
+ $source = User::staticGet($source_id);
+ } elseif (!empty($source_screen_name)) {
+ $source = User::staticGet('nickname', $source_screen_name);
+ } else {
+ $source = $apidata['user'];
+ }
+
+ // If a source or target is specified but does not exist,
+ // the method will return an HTTP 404.
+
+ if (empty($source)) {
+ $this->clientError(_('Could not determine source user.'),
+ $code = 404);
+ return;
+ }
+
+ $target_id = (int)$this->trimmed('target_id');
+ $target_screen_name = $this->trimmed('target_screen_name');
+
+ $target = null;
+
+ if (!empty($target_id)) {
+ $target = User::staticGet($target_id);
+ } elseif (!empty($target_screen_name)) {
+ $target = User::staticGet('nickname', $target_screen_name);
+ } else {
+ $this->clientError(_('Target user not specified.'),
+ $code = 403);
+ return;
+ }
+
+ if (empty($target)) {
+ $this->clientError(_('Could not find target user.'),
+ $code = 404);
+ return;
+ }
+
+ $result = $this->twitter_relationship_array($source, $target);
+
+ switch ($apidata['content-type']) {
+ case 'xml':
+ $this->init_document('xml');
+ $this->show_twitter_xml_relationship($result[relationship]);
+ $this->end_document('xml');
+ break;
+ case 'json':
+ $this->init_document('json');
+ print json_encode($result);
+ $this->end_document('json');
+ break;
+ default:
+ break;
+ }
+ }
+
+}
diff --git a/actions/twitapihelp.php b/actions/twitapihelp.php
index db5892baf..dab2b34f9 100644
--- a/actions/twitapihelp.php
+++ b/actions/twitapihelp.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
diff --git a/actions/twitapinotifications.php b/actions/twitapinotifications.php
index 411971af1..09b11766b 100644
--- a/actions/twitapinotifications.php
+++ b/actions/twitapinotifications.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/twitapisearchatom.php b/actions/twitapisearchatom.php
index eb9ab5d8e..3678213c3 100644
--- a/actions/twitapisearchatom.php
+++ b/actions/twitapisearchatom.php
@@ -165,24 +165,30 @@ class TwitapisearchatomAction extends TwitterapiAction
$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();
+ if (false === $search_engine->query($q)) {
+ $this->cnt = 0;
+ } else {
+ $this->cnt = $notice->find();
+ }
$cnt = 0;
+ $this->max_id = 0;
- while ($notice->fetch()) {
+ if ($this->cnt > 0) {
+ while ($notice->fetch()) {
- ++$cnt;
+ ++$cnt;
- if (!$this->max_id) {
- $this->max_id = $notice->id;
- }
+ if (!$this->max_id) {
+ $this->max_id = $notice->id;
+ }
- if ($cnt > $this->rpp) {
- break;
- }
+ if ($cnt > $this->rpp) {
+ break;
+ }
- $notices[] = clone($notice);
+ $notices[] = clone($notice);
+ }
}
return $notices;
diff --git a/actions/twitapisearchjson.php b/actions/twitapisearchjson.php
index b0e3be687..27a717bfc 100644
--- a/actions/twitapisearchjson.php
+++ b/actions/twitapisearchjson.php
@@ -124,8 +124,11 @@ class TwitapisearchjsonAction extends TwitterapiAction
$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();
+ if (false === $search_engine->query($q)) {
+ $cnt = 0;
+ } else {
+ $cnt = $notice->find();
+ }
// TODO: since_id, lang, geocode
@@ -146,4 +149,4 @@ class TwitapisearchjsonAction extends TwitterapiAction
{
return true;
}
-} \ No newline at end of file
+}
diff --git a/actions/twitapistatuses.php b/actions/twitapistatuses.php
index 1fbde6639..c9943698d 100644
--- a/actions/twitapistatuses.php
+++ b/actions/twitapistatuses.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -26,64 +28,45 @@ class TwitapistatusesAction extends TwitterapiAction
function public_timeline($args, $apidata)
{
- parent::handle($args);
-
- $sitename = common_config('site', 'name');
- $title = sprintf(_("%s public timeline"), $sitename);
-
- $taguribase = common_config('integration', 'taguri');
- $id = "tag:$taguribase:PublicTimeline";
- $link = common_root_url();
-
- $subtitle = sprintf(_("%s updates from everyone!"), $sitename);
-
- // Number of public statuses to return by default -- Twitter sends 20
- $MAX_PUBSTATUSES = 20;
-
- // FIXME: To really live up to the spec we need to build a list
+ // XXX: To really live up to the spec we need to build a list
// of notices by users who have custom avatars, so fix this SQL -- Zach
- $page = $this->arg('page');
- $since_id = $this->arg('since_id');
- $max_id = $this->arg('max_id');
-
- if (!$page) {
- $page = 1;
- }
- if (!$since_id) {
- $since_id = 0;
- }
- if (!$max_id) {
- $max_id = 0;
- }
-
- $since = strtotime($this->arg('since'));
+ parent::handle($args);
- $notice = Notice::publicStream((($page-1)*$MAX_PUBSTATUSES), $MAX_PUBSTATUSES, $since_id, $max_id, $since);
+ $sitename = common_config('site', 'name');
+ $title = sprintf(_("%s public timeline"), $sitename);
+ $taguribase = common_config('integration', 'taguri');
+ $id = "tag:$taguribase:PublicTimeline";
+ $link = common_root_url();
+ $subtitle = sprintf(_("%s updates from everyone!"), $sitename);
- if ($notice) {
+ $page = (int)$this->arg('page', 1);
+ $count = (int)$this->arg('count', 20);
+ $max_id = (int)$this->arg('max_id', 0);
+ $since_id = (int)$this->arg('since_id', 0);
+ $since = $this->arg('since');
- switch($apidata['content-type']) {
- case 'xml':
- $this->show_xml_timeline($notice);
- break;
- case 'rss':
- $this->show_rss_timeline($notice, $title, $link, $subtitle);
- break;
- case 'atom':
- $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
- $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri);
- break;
- case 'json':
- $this->show_json_timeline($notice);
- break;
- default:
- $this->clientError(_('API method not found!'), $code = 404);
- break;
- }
+ $notice = Notice::publicStream(($page-1)*$count, $count, $since_id,
+ $max_id, $since);
- } else {
- $this->serverError(_('Couldn\'t find any statuses.'), $code = 503);
+ switch($apidata['content-type']) {
+ case 'xml':
+ $this->show_xml_timeline($notice);
+ break;
+ case 'rss':
+ $this->show_rss_timeline($notice, $title, $link, $subtitle);
+ break;
+ case 'atom':
+ $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
+ $this->show_atom_timeline($notice, $title, $id, $link,
+ $subtitle, null, $selfuri);
+ break;
+ case 'json':
+ $this->show_json_timeline($notice);
+ break;
+ default:
+ $this->clientError(_('API method not found!'), $code = 404);
+ break;
}
}
@@ -92,68 +75,62 @@ class TwitapistatusesAction extends TwitterapiAction
{
parent::handle($args);
- $since = $this->arg('since');
- $since_id = $this->arg('since_id');
- $count = $this->arg('count');
- $page = $this->arg('page');
- $max_id = $this->arg('max_id');
-
- if (!$page) {
- $page = 1;
- }
-
- if (!$count) {
- $count = 20;
- }
-
- if (!$since_id) {
- $since_id = 0;
- }
-
- if (!$max_id) {
- $max_id = 0;
- }
-
- $since = strtotime($this->arg('since'));
+ $this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
- $this->auth_user = $user;
if (empty($user)) {
- $this->clientError(_('No such user!'), 404, $apidata['content-type']);
+ $this->clientError(_('No such user!'), 404,
+ $apidata['content-type']);
return;
}
- $profile = $user->getProfile();
- $sitename = common_config('site', 'name');
- $title = sprintf(_("%s and friends"), $user->nickname);
+ $profile = $user->getProfile();
+ $sitename = common_config('site', 'name');
+ $title = sprintf(_("%s and friends"), $user->nickname);
$taguribase = common_config('integration', 'taguri');
- $id = "tag:$taguribase:FriendsTimeline:" . $user->id;
- $link = common_local_url('all', array('nickname' => $user->nickname));
- $subtitle = sprintf(_('Updates from %1$s and friends on %2$s!'), $user->nickname, $sitename);
-
- $notice = $user->noticesWithFriends(($page-1)*20, $count, $since_id, $max_id, $since);
+ $id = "tag:$taguribase:FriendsTimeline:" . $user->id;
+ $link = common_local_url('all',
+ array('nickname' => $user->nickname));
+ $subtitle = sprintf(_('Updates from %1$s and friends on %2$s!'),
+ $user->nickname, $sitename);
+
+ $page = (int)$this->arg('page', 1);
+ $count = (int)$this->arg('count', 20);
+ $max_id = (int)$this->arg('max_id', 0);
+ $since_id = (int)$this->arg('since_id', 0);
+ $since = $this->arg('since');
+
+ if (!empty($this->auth_user) && $this->auth_user->id == $user->id) {
+ $notice = $user->noticeInbox(($page-1)*$count,
+ $count, $since_id, $max_id, $since);
+ } else {
+ $notice = $user->noticesWithFriends(($page-1)*$count,
+ $count, $since_id, $max_id, $since);
+ }
switch($apidata['content-type']) {
- case 'xml':
+ case 'xml':
$this->show_xml_timeline($notice);
break;
- case 'rss':
+ case 'rss':
$this->show_rss_timeline($notice, $title, $link, $subtitle);
break;
- case 'atom':
+ case 'atom':
if (isset($apidata['api_arg'])) {
$selfuri = common_root_url() .
- 'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom';
+ 'api/statuses/friends_timeline/' .
+ $apidata['api_arg'] . '.atom';
} else {
$selfuri = common_root_url() .
'api/statuses/friends_timeline.atom';
}
- $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri);
+ $this->show_atom_timeline($notice, $title, $id, $link,
+ $subtitle, null, $selfuri);
break;
- case 'json':
+ case 'json':
$this->show_json_timeline($notice);
break;
- default:
+ default:
$this->clientError(_('API method not found!'), $code = 404);
}
@@ -166,48 +143,21 @@ class TwitapistatusesAction extends TwitterapiAction
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
- if (!$user) {
+ if (empty($user)) {
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
$profile = $user->getProfile();
- if (!$profile) {
- $this->serverError(_('User has no profile.'));
- return;
- }
-
- $count = $this->arg('count');
- $since = $this->arg('since');
- $since_id = $this->arg('since_id');
- $page = $this->arg('page');
- $max_id = $this->arg('max_id');
-
- if (!$page) {
- $page = 1;
- }
-
- if (!$count) {
- $count = 20;
- }
-
- if (!$since_id) {
- $since_id = 0;
- }
-
- if (!$max_id) {
- $max_id = 0;
- }
-
- $since = strtotime($this->arg('since'));
-
- $sitename = common_config('site', 'name');
- $title = sprintf(_("%s timeline"), $user->nickname);
+ $sitename = common_config('site', 'name');
+ $title = sprintf(_("%s timeline"), $user->nickname);
$taguribase = common_config('integration', 'taguri');
- $id = "tag:$taguribase:UserTimeline:".$user->id;
- $link = common_local_url('showstream', array('nickname' => $user->nickname));
- $subtitle = sprintf(_('Updates from %1$s on %2$s!'), $user->nickname, $sitename);
+ $id = "tag:$taguribase:UserTimeline:".$user->id;
+ $link = common_local_url('showstream',
+ array('nickname' => $user->nickname));
+ $subtitle = sprintf(_('Updates from %1$s on %2$s!'),
+ $user->nickname, $sitename);
# FriendFeed's SUP protocol
# Also added RSS and Atom feeds
@@ -215,26 +165,34 @@ class TwitapistatusesAction extends TwitterapiAction
$suplink = common_local_url('sup', null, null, $user->id);
header('X-SUP-ID: '.$suplink);
- # XXX: since
+ $page = (int)$this->arg('page', 1);
+ $count = (int)$this->arg('count', 20);
+ $max_id = (int)$this->arg('max_id', 0);
+ $since_id = (int)$this->arg('since_id', 0);
+ $since = $this->arg('since');
- $notice = $user->getNotices((($page-1)*20), $count, $since_id, $max_id, $since);
+ $notice = $user->getNotices(($page-1)*$count,
+ $count, $since_id, $max_id, $since);
switch($apidata['content-type']) {
case 'xml':
$this->show_xml_timeline($notice);
break;
case 'rss':
- $this->show_rss_timeline($notice, $title, $link, $subtitle, $suplink);
+ $this->show_rss_timeline($notice, $title, $link,
+ $subtitle, $suplink);
break;
case 'atom':
if (isset($apidata['api_arg'])) {
$selfuri = common_root_url() .
- 'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom';
+ 'api/statuses/user_timeline/' .
+ $apidata['api_arg'] . '.atom';
} else {
$selfuri = common_root_url() .
'api/statuses/user_timeline.atom';
}
- $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri);
+ $this->show_atom_timeline($notice, $title, $id, $link,
+ $subtitle, $suplink, $selfuri);
break;
case 'json':
$this->show_json_timeline($notice);
@@ -247,7 +205,6 @@ class TwitapistatusesAction extends TwitterapiAction
function update($args, $apidata)
{
-
parent::handle($args);
if (!in_array($apidata['content-type'], array('xml', 'json'))) {
@@ -256,21 +213,24 @@ class TwitapistatusesAction extends TwitterapiAction
}
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
- $this->clientError(_('This method requires a POST.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST.'),
+ 400, $apidata['content-type']);
return;
}
- $this->auth_user = $apidata['user'];
- $user = $this->auth_user;
+ $user = $apidata['user']; // Always the auth user
+
$status = $this->trimmed('status');
$source = $this->trimmed('source');
- $in_reply_to_status_id = intval($this->trimmed('in_reply_to_status_id'));
+ $in_reply_to_status_id =
+ intval($this->trimmed('in_reply_to_status_id'));
$reserved_sources = array('web', 'omb', 'mail', 'xmpp', 'api');
- if (!$source || in_array($source, $reserved_sources)) {
+
+ if (empty($source) || in_array($source, $reserved_sources)) {
$source = 'api';
}
- if (!$status) {
+ if (empty($status)) {
// XXX: Note: In this case, Twitter simply returns '200 OK'
// No error is given, but the status is not posted to the
@@ -288,9 +248,9 @@ class TwitapistatusesAction extends TwitterapiAction
// as "truncated." Sending this error may screw up some clients
// that assume Twitter will truncate for them. Should we just
// truncate too? -- Zach
- $this->clientError(_('That\'s too long. Max notice size is 140 chars.'), $code = 406, $apidata['content-type']);
+ $this->clientError(_('That\'s too long. Max notice size is 140 chars.'),
+ $code = 406, $apidata['content-type']);
return;
-
}
}
@@ -321,13 +281,15 @@ class TwitapistatusesAction extends TwitterapiAction
if ($reply) {
$reply_to = $in_reply_to_status_id;
} else {
- $this->clientError(_('Not found'), $code = 404, $apidata['content-type']);
+ $this->clientError(_('Not found'), $code = 404,
+ $apidata['content-type']);
return;
}
}
- $notice = Notice::saveNew($user->id, html_entity_decode($status, ENT_NOQUOTES, 'UTF-8'),
- $source, 1, $reply_to);
+ $notice = Notice::saveNew($user->id,
+ html_entity_decode($status, ENT_NOQUOTES, 'UTF-8'),
+ $source, 1, $reply_to);
if (is_string($notice)) {
$this->serverError($notice);
@@ -343,71 +305,55 @@ class TwitapistatusesAction extends TwitterapiAction
function mentions($args, $apidata)
{
-
parent::handle($args);
- $since = $this->arg('since');
- $count = $this->arg('count');
- $page = $this->arg('page');
- $since_id = $this->arg('since_id');
- $max_id = $this->arg('max_id');
-
$user = $this->get_user($apidata['api_arg'], $apidata);
$this->auth_user = $apidata['user'];
+
+ if (empty($user)) {
+ $this->clientError(_('No such user!'), 404,
+ $apidata['content-type']);
+ return;
+ }
+
$profile = $user->getProfile();
- $sitename = common_config('site', 'name');
- $title = sprintf(_('%1$s / Updates mentioning %2$s'),
+ $sitename = common_config('site', 'name');
+ $title = sprintf(_('%1$s / Updates mentioning %2$s'),
$sitename, $user->nickname);
$taguribase = common_config('integration', 'taguri');
- $id = "tag:$taguribase:Mentions:".$user->id;
- $link = common_local_url('replies', array('nickname' => $user->nickname));
- $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'),
+ $id = "tag:$taguribase:Mentions:".$user->id;
+ $link = common_local_url('replies',
+ array('nickname' => $user->nickname));
+ $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'),
$sitename, $user->nickname, $profile->getBestName());
- if (!$page) {
- $page = 1;
- }
-
- if (!$count) {
- $count = 20;
- }
-
- if (!$since_id) {
- $since_id = 0;
- }
+ $page = (int)$this->arg('page', 1);
+ $count = (int)$this->arg('count', 20);
+ $max_id = (int)$this->arg('max_id', 0);
+ $since_id = (int)$this->arg('since_id', 0);
+ $since = $this->arg('since');
- if (!$max_id) {
- $max_id = 0;
- }
-
- $since = strtotime($this->arg('since'));
-
- $notice = $user->getReplies((($page-1)*20),
+ $notice = $user->getReplies(($page-1)*$count,
$count, $since_id, $max_id, $since);
- $notices = array();
-
- while ($notice->fetch()) {
- $notices[] = clone($notice);
- }
switch($apidata['content-type']) {
- case 'xml':
- $this->show_xml_timeline($notices);
+ case 'xml':
+ $this->show_xml_timeline($notice);
break;
- case 'rss':
- $this->show_rss_timeline($notices, $title, $link, $subtitle);
+ case 'rss':
+ $this->show_rss_timeline($notice, $title, $link, $subtitle);
break;
- case 'atom':
+ case 'atom':
$selfuri = common_root_url() .
ltrim($_SERVER['QUERY_STRING'], 'p=');
- $this->show_atom_timeline($notices, $title, $id, $link, $subtitle,
+ $this->show_atom_timeline($notice, $title, $id, $link, $subtitle,
null, $selfuri);
break;
- case 'json':
- $this->show_json_timeline($notices);
+ case 'json':
+ $this->show_json_timeline($notice);
break;
- default:
+ default:
$this->clientError(_('API method not found!'), $code = 404);
}
@@ -427,9 +373,19 @@ class TwitapistatusesAction extends TwitterapiAction
return;
}
+ // 'id' is an undocumented parameter in Twitter's API. Several
+ // clients make use of it, so we support it too.
+
+ // show.json?id=12345 takes precedence over /show/12345.json
+
$this->auth_user = $apidata['user'];
- $notice_id = $apidata['api_arg'];
- $notice = Notice::staticGet($notice_id);
+ $notice_id = $this->trimmed('id');
+
+ if (empty($notice_id)) {
+ $notice_id = $apidata['api_arg'];
+ }
+
+ $notice = Notice::staticGet((int)$notice_id);
if ($notice) {
if ($apidata['content-type'] == 'xml') {
@@ -438,15 +394,15 @@ class TwitapistatusesAction extends TwitterapiAction
$this->show_single_json_status($notice);
}
} else {
- // XXX: Twitter just sets a 404 header and doens't bother to return an err msg
- $this->clientError(_('No status with that ID found.'), 404, $apidata['content-type']);
+ // XXX: Twitter just sets a 404 header and doens't bother
+ // to return an err msg
+ $this->clientError(_('No status with that ID found.'),
+ 404, $apidata['content-type']);
}
-
}
function destroy($args, $apidata)
{
-
parent::handle($args);
if (!in_array($apidata['content-type'], array('xml', 'json'))) {
@@ -457,17 +413,18 @@ class TwitapistatusesAction extends TwitterapiAction
// Check for RESTfulness
if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'DELETE'))) {
// XXX: Twitter just prints the err msg, no XML / JSON.
- $this->clientError(_('This method requires a POST or DELETE.'), 400, $apidata['content-type']);
+ $this->clientError(_('This method requires a POST or DELETE.'),
+ 400, $apidata['content-type']);
return;
}
- $this->auth_user = $apidata['user'];
- $user = $this->auth_user;
+ $user = $apidata['user']; // Always the auth user
$notice_id = $apidata['api_arg'];
- $notice = Notice::staticGet($notice_id);
+ $notice = Notice::staticGet($notice_id);
- if (!$notice) {
- $this->clientError(_('No status found with that ID.'), 404, $apidata['content-type']);
+ if (empty($notice)) {
+ $this->clientError(_('No status found with that ID.'),
+ 404, $apidata['content-type']);
return;
}
@@ -483,7 +440,8 @@ class TwitapistatusesAction extends TwitterapiAction
$this->show_single_json_status($notice);
}
} else {
- $this->clientError(_('You may not delete another user\'s status.'), 403, $apidata['content-type']);
+ $this->clientError(_('You may not delete another user\'s status.'),
+ 403, $apidata['content-type']);
}
}
@@ -514,42 +472,41 @@ class TwitapistatusesAction extends TwitterapiAction
function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false)
{
-
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
- if (!$user) {
+ if (empty($user)) {
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
- $page = $this->trimmed('page');
-
- if (!$page || !is_numeric($page)) {
- $page = 1;
- }
-
$profile = $user->getProfile();
- if (!$profile) {
- $this->serverError(_('User has no profile.'));
- return;
- }
-
$sub = new Subscription();
$sub->$user_attr = $profile->id;
- $since = strtotime($this->trimmed('since'));
-
- if ($since) {
- $d = date('Y-m-d H:i:s', $since);
- $sub->whereAdd("created > '$d'");
- }
-
$sub->orderBy('created DESC');
+ // Normally, page 100 friends at a time
+
if (!$onlyIDs) {
- $sub->limit(($page-1)*100, 100);
+ $page = $this->arg('page', 1);
+ $count = $this->arg('count', 100);
+ $sub->limit(($page-1)*$count, $count);
+ } else {
+
+ // If we're just looking at IDs, return
+ // ALL of them, unless the user specifies a page,
+ // in which case, return 500 per page.
+
+ $page = $this->arg('page');
+ if (!empty($page)) {
+ if ($page < 1) {
+ $page = 1;
+ }
+ $count = 500;
+ $sub->limit(($page-1)*$count, $count);
+ }
}
$others = array();
@@ -578,21 +535,21 @@ class TwitapistatusesAction extends TwitterapiAction
function show_profiles($profiles, $type)
{
switch ($type) {
- case 'xml':
+ case 'xml':
$this->elementStart('users', array('type' => 'array'));
foreach ($profiles as $profile) {
$this->show_profile($profile);
}
$this->elementEnd('users');
break;
- case 'json':
+ case 'json':
$arrays = array();
foreach ($profiles as $profile) {
$arrays[] = $this->twitter_user_array($profile, true);
}
print json_encode($arrays);
break;
- default:
+ default:
$this->clientError(_('unsupported file type'));
}
}
@@ -600,21 +557,21 @@ class TwitapistatusesAction extends TwitterapiAction
function showIDs($profiles, $type)
{
switch ($type) {
- case 'xml':
+ case 'xml':
$this->elementStart('ids');
foreach ($profiles as $profile) {
$this->element('id', null, $profile->id);
}
$this->elementEnd('ids');
break;
- case 'json':
+ case 'json':
$ids = array();
foreach ($profiles as $profile) {
$ids[] = (int)$profile->id;
}
print json_encode($ids);
break;
- default:
+ default:
$this->clientError(_('unsupported file type'));
}
}
@@ -627,8 +584,8 @@ class TwitapistatusesAction extends TwitterapiAction
function supported($cmd)
{
-
- $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', 'FavCommand', 'OnCommand', 'OffCommand');
+ $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand',
+ 'FavCommand', 'OnCommand', 'OffCommand');
if (in_array(get_class($cmd), $cmdlist)) {
return true;
diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php
index a47fdfbc3..fea41b397 100644
--- a/actions/twitapiusers.php
+++ b/actions/twitapiusers.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
require_once(INSTALLDIR.'/lib/twitterapi.php');
@@ -41,10 +43,10 @@ class TwitapiusersAction extends TwitterapiAction
if ($email) {
$user = User::staticGet('email', $email);
} else {
- $user = $this->get_user($apidata['api_arg']);
+ $user = $this->get_user($apidata['api_arg'], $apidata);
}
- if (!$user) {
+ if (empty($user)) {
$this->clientError(_('Not found.'), 404, $apidata['content-type']);
return;
}
@@ -56,7 +58,7 @@ class TwitapiusersAction extends TwitterapiAction
return;
}
- $twitter_user = $this->twitter_user_array($profile, true);
+ $twitter_user = $this->twitter_user_array($user->getProfile(), true);
if ($apidata['content-type'] == 'xml') {
$this->init_document('xml');
diff --git a/actions/twittersettings.php b/actions/twittersettings.php
index 0b98eef59..2b742788e 100644
--- a/actions/twittersettings.php
+++ b/actions/twittersettings.php
@@ -138,7 +138,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$this->elementStart('ul', 'form_data');
$this->elementStart('li');
- $this->checkbox('noticesync',
+ $this->checkbox('noticesend',
_('Automatically send my notices to Twitter.'),
($flink) ?
($flink->noticesync & FOREIGN_NOTICE_SEND) :
@@ -158,6 +158,22 @@ class TwittersettingsAction extends ConnectSettingsAction
($flink->friendsync & FOREIGN_FRIEND_RECV) :
false);
$this->elementEnd('li');
+
+ if (common_config('twitterbridge','enabled')) {
+ $this->elementStart('li');
+ $this->checkbox('noticerecv',
+ _('Import my Friends Timeline.'),
+ ($flink) ?
+ ($flink->noticesync & FOREIGN_NOTICE_RECV) :
+ false);
+ $this->elementEnd('li');
+ } else {
+ // preserve setting even if bidrection bridge toggled off
+ if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
+ $this->hidden('noticerecv', true, 'noticerecv');
+ }
+ }
+
$this->elementEnd('ul');
if ($flink) {
@@ -320,7 +336,8 @@ class TwittersettingsAction extends ConnectSettingsAction
{
$screen_name = $this->trimmed('twitter_username');
$password = $this->trimmed('twitter_password');
- $noticesync = $this->boolean('noticesync');
+ $noticesend = $this->boolean('noticesend');
+ $noticerecv = $this->boolean('noticerecv');
$replysync = $this->boolean('replysync');
$friendsync = $this->boolean('friendsync');
@@ -363,7 +380,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$flink->credentials = $password;
$flink->created = common_sql_now();
- $flink->set_flags($noticesync, $replysync, $friendsync);
+ $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$flink_id = $flink->insert();
@@ -421,7 +438,8 @@ class TwittersettingsAction extends ConnectSettingsAction
function savePreferences()
{
- $noticesync = $this->boolean('noticesync');
+ $noticesend = $this->boolean('noticesend');
+ $noticerecv = $this->boolean('noticerecv');
$friendsync = $this->boolean('friendsync');
$replysync = $this->boolean('replysync');
@@ -450,7 +468,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$original = clone($flink);
- $flink->set_flags($noticesync, $replysync, $friendsync);
+ $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$result = $flink->update($original);
diff --git a/actions/unblock.php b/actions/unblock.php
index 6e671c9dd..05d57c60d 100644
--- a/actions/unblock.php
+++ b/actions/unblock.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/unsubscribe.php b/actions/unsubscribe.php
index 7dcab04c0..19275041a 100644
--- a/actions/unsubscribe.php
+++ b/actions/unsubscribe.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/updateprofile.php b/actions/updateprofile.php
index 08cb31ae0..d8b62fb09 100644
--- a/actions/updateprofile.php
+++ b/actions/updateprofile.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/userauthorization.php b/actions/userauthorization.php
index 168019149..8dc2c808d 100644
--- a/actions/userauthorization.php
+++ b/actions/userauthorization.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/userbyid.php b/actions/userbyid.php
index 4a985fcd7..8b686ae10 100644
--- a/actions/userbyid.php
+++ b/actions/userbyid.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/actions/userdesignsettings.php b/actions/userdesignsettings.php
new file mode 100644
index 000000000..d7949951a
--- /dev/null
+++ b/actions/userdesignsettings.php
@@ -0,0 +1,285 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Change user password
+ *
+ * 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 Settings
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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/designsettings.php';
+
+/**
+ * Set a user's design
+ *
+ * Saves a design for a given user
+ *
+ * @category Settings
+ * @package Laconica
+ * @author Zach Copley <zach@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 UserDesignSettingsAction extends DesignSettingsAction
+{
+ /**
+ * Sets the right action for the form, and passes request args into
+ * the base action
+ *
+ * @param array $args misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->submitaction = common_local_url('userdesignsettings');
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('Profile design');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('Customize the way your profile looks ' .
+ 'with a background image and a colour palette of your choice.');
+ }
+
+ /**
+ * Get the design we want to edit
+ *
+ * @return Design
+ */
+
+ function getWorkingDesign()
+ {
+
+ $user = common_current_user();
+ $design = $user->getDesign();
+
+ if (empty($design)) {
+ $design = $this->defaultDesign();
+ }
+
+ return $design;
+ }
+
+ /**
+ * Content area of the page
+ *
+ * Shows a form for changing the design
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $this->showDesignForm($this->getWorkingDesign());
+ }
+
+ /**
+ * Save or update the user's design settings
+ *
+ * @return void
+ */
+
+ function saveDesign()
+ {
+ foreach ($this->args as $key => $val) {
+ if (preg_match('/(#ho|ho)Td.*g/i', $val)) {
+ $this->sethd();
+ return;
+ }
+ }
+
+ try {
+ $bgcolor = new WebColor($this->trimmed('design_background'));
+ $ccolor = new WebColor($this->trimmed('design_content'));
+ $sbcolor = new WebColor($this->trimmed('design_sidebar'));
+ $tcolor = new WebColor($this->trimmed('design_text'));
+ $lcolor = new WebColor($this->trimmed('design_links'));
+ } catch (WebColorException $e) {
+ $this->showForm($e->getMessage());
+ return;
+ }
+
+ $onoff = $this->arg('design_background-image_onoff');
+
+ $on = false;
+ $off = false;
+ $tile = false;
+
+ if ($onoff == 'on') {
+ $on = true;
+ } else {
+ $off = true;
+ }
+
+ $repeat = $this->boolean('design_background-image_repeat');
+
+ if ($repeat) {
+ $tile = true;
+ }
+
+ $user = common_current_user();
+ $design = $user->getDesign();
+
+ if (!empty($design)) {
+
+ $original = clone($design);
+
+ $design->backgroundcolor = $bgcolor->intValue();
+ $design->contentcolor = $ccolor->intValue();
+ $design->sidebarcolor = $sbcolor->intValue();
+ $design->textcolor = $tcolor->intValue();
+ $design->linkcolor = $lcolor->intValue();
+
+ $design->setDisposition($on, $off, $tile);
+
+ $result = $design->update($original);
+
+ if ($result === false) {
+ common_log_db_error($design, 'UPDATE', __FILE__);
+ $this->showForm(_('Couldn\'t update your design.'));
+ return;
+ }
+
+ // update design
+ } else {
+
+ $user->query('BEGIN');
+
+ // save new design
+ $design = new Design();
+
+ $design->backgroundcolor = $bgcolor->intValue();
+ $design->contentcolor = $ccolor->intValue();
+ $design->sidebarcolor = $sbcolor->intValue();
+ $design->textcolor = $tcolor->intValue();
+ $design->linkcolor = $lcolor->intValue();
+
+ $design->setDisposition($on, $off, $tile);
+
+ $id = $design->insert();
+
+ if (empty($id)) {
+ common_log_db_error($id, 'INSERT', __FILE__);
+ $this->showForm(_('Unable to save your design settings!'));
+ return;
+ }
+
+ $original = clone($user);
+ $user->design_id = $id;
+ $result = $user->update($original);
+
+ if (empty($result)) {
+ common_log_db_error($original, 'UPDATE', __FILE__);
+ $this->showForm(_('Unable to save your design settings!'));
+ $user->query('ROLLBACK');
+ return;
+ }
+
+ $user->query('COMMIT');
+
+ }
+
+ $this->saveBackgroundImage($design);
+
+ $this->showForm(_('Design preferences saved.'), true);
+ }
+
+ /**
+ * Alternate default colors
+ *
+ * @return nothing
+ */
+
+ function sethd()
+ {
+
+ $user = common_current_user();
+ $design = $user->getDesign();
+
+ $user->query('BEGIN');
+
+ // alternate colors
+ $design = new Design();
+
+ $design->backgroundcolor = 16184329;
+ $design->contentcolor = 16059904;
+ $design->sidebarcolor = 16059904;
+ $design->textcolor = 0;
+ $design->linkcolor = 16777215;
+
+ $design->setDisposition(false, true, false);
+
+ $id = $design->insert();
+
+ if (empty($id)) {
+ common_log_db_error($id, 'INSERT', __FILE__);
+ $this->showForm(_('Unable to save your design settings!'));
+ return;
+ }
+
+ $original = clone($user);
+ $user->design_id = $id;
+ $result = $user->update($original);
+
+ if (empty($result)) {
+ common_log_db_error($original, 'UPDATE', __FILE__);
+ $this->showForm(_('Unable to save your design settings!'));
+ $user->query('ROLLBACK');
+ return;
+ }
+
+ $user->query('COMMIT');
+
+ $this->saveBackgroundImage($design);
+
+ $this->showForm(_('Enjoy your hotdog!'), true);
+ }
+
+}
diff --git a/actions/usergroups.php b/actions/usergroups.php
index e3088dcbd..7ead6e6e4 100644
--- a/actions/usergroups.php
+++ b/actions/usergroups.php
@@ -46,9 +46,8 @@ require_once INSTALLDIR.'/lib/grouplist.php';
* @link http://laconi.ca/
*/
-class UsergroupsAction extends Action
+class UsergroupsAction extends OwnerDesignAction
{
- var $user = null;
var $page = null;
var $profile = null;
diff --git a/actions/userrss.php b/actions/userrss.php
index 5861d9ee3..8a940865f 100644
--- a/actions/userrss.php
+++ b/actions/userrss.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -25,14 +25,15 @@ require_once(INSTALLDIR.'/lib/rssaction.php');
class UserrssAction extends Rss10Action
{
-
var $user = null;
+ var $tag = null;
function prepare($args)
{
parent::prepare($args);
- $nickname = $this->trimmed('nickname');
+ $nickname = $this->trimmed('nickname');
$this->user = User::staticGet('nickname', $nickname);
+ $this->tag = $this->trimmed('tag');
if (!$this->user) {
$this->clientError(_('No such user.'));
@@ -42,6 +43,25 @@ class UserrssAction extends Rss10Action
}
}
+ function getTaggedNotices($tag = null, $limit=0)
+ {
+ $user = $this->user;
+
+ if (is_null($user)) {
+ return null;
+ }
+
+ $notice = $user->getTaggedNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit, 0, 0, null, $tag);
+
+ $notices = array();
+ while ($notice->fetch()) {
+ $notices[] = clone($notice);
+ }
+
+ return $notices;
+ }
+
+
function getNotices($limit=0)
{
diff --git a/actions/xrds.php b/actions/xrds.php
index 1335b6b80..9327a3c83 100644
--- a/actions/xrds.php
+++ b/actions/xrds.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/background/.gitignore b/background/.gitignore
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/background/.gitignore
diff --git a/classes/Avatar.php b/classes/Avatar.php
index db9d78e47..5e8b315fe 100644
--- a/classes/Avatar.php
+++ b/classes/Avatar.php
@@ -55,19 +55,43 @@ class Avatar extends Memcached_DataObject
static function path($filename)
{
- return INSTALLDIR . '/avatar/' . $filename;
+ $dir = common_config('avatar', 'dir');
+
+ if ($dir[strlen($dir)-1] != '/') {
+ $dir .= '/';
+ }
+
+ return $dir . $filename;
}
static function url($filename)
{
- return common_path('avatar/'.$filename);
+ $path = common_config('avatar', 'path');
+
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
+
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
+
+ $server = common_config('avatar', 'server');
+
+ if (empty($server)) {
+ $server = common_config('site', 'server');
+ }
+
+ // XXX: protocol
+
+ return 'http://'.$server.$path.$filename;
}
function displayUrl()
{
$server = common_config('avatar', 'server');
if ($server) {
- return 'http://'.$server.'/'.$this->filename;
+ return Avatar::url($this->filename);
} else {
return $this->url;
}
diff --git a/classes/Design.php b/classes/Design.php
new file mode 100644
index 000000000..0927fcda7
--- /dev/null
+++ b/classes/Design.php
@@ -0,0 +1,155 @@
+<?php
+/*
+ * Laconica - the distributed open-source microblogging tool
+ * Copyright (C) 2009, Control Yourself, 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);
+}
+
+define('BACKGROUND_ON', 1);
+define('BACKGROUND_OFF', 2);
+define('BACKGROUND_TILE', 4);
+
+/**
+ * Table Definition for design
+ */
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+require_once INSTALLDIR . '/lib/webcolor.php';
+
+class Design extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'design'; // table name
+ public $id; // int(4) primary_key not_null
+ public $backgroundcolor; // int(4)
+ public $contentcolor; // int(4)
+ public $sidebarcolor; // int(4)
+ public $textcolor; // int(4)
+ public $linkcolor; // int(4)
+ public $backgroundimage; // varchar(255)
+ public $disposition; // tinyint(1) default_1
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Design',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function showCSS($out)
+ {
+ try {
+
+ $bgcolor = new WebColor($this->backgroundcolor);
+ $ccolor = new WebColor($this->contentcolor);
+ $sbcolor = new WebColor($this->sidebarcolor);
+ $tcolor = new WebColor($this->textcolor);
+ $lcolor = new WebColor($this->linkcolor);
+
+ } catch (WebColorException $e) {
+ // This shouldn't happen
+ common_log(LOG_ERR, "Unable to create color for design $id.",
+ __FILE__);
+ }
+
+ $css = 'body { background-color: #' . $bgcolor->hexValue() . ' }' . "\n";
+ $css .= '#content, #site_nav_local_views .current a { background-color: #';
+ $css .= $ccolor->hexValue() . '} '."\n";
+ $css .= '#aside_primary { background-color: #'. $sbcolor->hexValue() . ' }' . "\n";
+ $css .= 'html body { color: #'. $tcolor->hexValue() . ' }'. "\n";
+ $css .= 'a { color: #' . $lcolor->hexValue() . ' }' . "\n";
+
+ if (!empty($this->backgroundimage) &&
+ $this->disposition & BACKGROUND_ON) {
+
+ $repeat = ($this->disposition & BACKGROUND_TILE) ?
+ 'background-repeat:repeat;' :
+ 'background-repeat:no-repeat;';
+
+ $css .= 'body { background-image:url(' .
+ Design::url($this->backgroundimage) .
+ '); ' . $repeat . ' background-attachment:fixed; }' . "\n";
+ }
+
+ $out->element('style', array('type' => 'text/css'), $css);
+
+ }
+
+ static function filename($id, $extension, $extra=null)
+ {
+ return $id . (($extra) ? ('-' . $extra) : '') . $extension;
+ }
+
+ static function path($filename)
+ {
+ $dir = common_config('background', 'dir');
+
+ if ($dir[strlen($dir)-1] != '/') {
+ $dir .= '/';
+ }
+
+ return $dir . $filename;
+ }
+
+ static function url($filename)
+ {
+ $path = common_config('background', 'path');
+
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
+
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
+
+ $server = common_config('background', 'server');
+
+ if (empty($server)) {
+ $server = common_config('site', 'server');
+ }
+
+ // XXX: protocol
+
+ return 'http://'.$server.$path.$filename;
+ }
+
+ function setDisposition($on, $off, $tile)
+ {
+ if ($on) {
+ $this->disposition |= BACKGROUND_ON;
+ } else {
+ $this->disposition &= ~BACKGROUND_ON;
+ }
+
+ if ($off) {
+ $this->disposition |= BACKGROUND_OFF;
+ } else {
+ $this->disposition &= ~BACKGROUND_OFF;
+ }
+
+ if ($tile) {
+ $this->disposition |= BACKGROUND_TILE;
+ } else {
+ $this->disposition &= ~BACKGROUND_TILE;
+ }
+ }
+
+}
diff --git a/classes/Fave.php b/classes/Fave.php
index 572334ce4..c3ec62dcf 100644
--- a/classes/Fave.php
+++ b/classes/Fave.php
@@ -37,52 +37,62 @@ class Fave extends Memcached_DataObject
return Memcached_DataObject::pkeyGet('Fave', $kv);
}
- function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE)
+ function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false)
{
$ids = Notice::stream(array('Fave', '_streamDirect'),
- array($user_id),
+ array($user_id, $own),
+ ($own) ? 'fave:ids_by_user_own:'.$user_id :
'fave:ids_by_user:'.$user_id,
$offset, $limit);
return $ids;
}
- function _streamDirect($user_id, $offset, $limit, $since_id, $max_id, $since)
+ function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since)
{
$fav = new Fave();
-
- $fav->user_id = $user_id;
-
- $fav->selectAdd();
- $fav->selectAdd('notice_id');
+ $qry = null;
+
+ if ($own) {
+ $qry = 'SELECT fave.* FROM fave ';
+ $qry .= 'WHERE fave.user_id = ' . $user_id . ' ';
+ } else {
+ $qry = 'SELECT fave.* FROM fave ';
+ $qry .= 'INNER JOIN notice ON fave.notice_id = notice.id ';
+ $qry .= 'WHERE fave.user_id = ' . $user_id . ' ';
+ $qry .= 'AND notice.is_local != ' . NOTICE_GATEWAY . ' ';
+ }
if ($since_id != 0) {
- $fav->whereAdd('notice_id > ' . $since_id);
+ $qry .= 'AND notice_id > ' . $since_id . ' ';
}
if ($max_id != 0) {
- $fav->whereAdd('notice_id <= ' . $max_id);
+ $qry .= 'AND notice_id <= ' . $max_id . ' ';
}
if (!is_null($since)) {
- $fav->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\'');
+ $qry .= 'AND modified > \'' . date('Y-m-d H:i:s', $since) . '\' ';
}
// NOTE: we sort by fave time, not by notice time!
- $fav->orderBy('modified DESC');
+ $qry .= 'ORDER BY modified DESC ';
if (!is_null($offset)) {
- $fav->limit($offset, $limit);
+ $qry .= "LIMIT $offset, $limit";
}
+ $fav->query($qry);
+
$ids = array();
- if ($fav->find()) {
- while ($fav->fetch()) {
- $ids[] = $fav->notice_id;
- }
+ while ($fav->fetch()) {
+ $ids[] = $fav->notice_id;
}
+ $fav->free();
+ unset($fav);
+
return $ids;
}
}
diff --git a/classes/File.php b/classes/File.php
new file mode 100644
index 000000000..533cc6e71
--- /dev/null
+++ b/classes/File.php
@@ -0,0 +1,197 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+require_once INSTALLDIR.'/classes/File_redirection.php';
+require_once INSTALLDIR.'/classes/File_oembed.php';
+require_once INSTALLDIR.'/classes/File_thumbnail.php';
+require_once INSTALLDIR.'/classes/File_to_post.php';
+//require_once INSTALLDIR.'/classes/File_redirection.php';
+
+/**
+ * Table Definition for file
+ */
+
+class File extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'file'; // table name
+ public $id; // int(4) primary_key not_null
+ public $url; // varchar(255) unique_key
+ public $mimetype; // varchar(50)
+ public $size; // int(4)
+ public $title; // varchar(255)
+ public $date; // int(4)
+ public $protected; // int(4)
+ public $filename; // varchar(255)
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('File',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function isProtected($url) {
+ return 'http://www.facebook.com/login.php' === $url;
+ }
+
+ function getAttachments($post_id) {
+ $query = "select file.* from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $this->escape($post_id);
+ $this->query($query);
+ $att = array();
+ while ($this->fetch()) {
+ $att[] = clone($this);
+ }
+ $this->free();
+ return $att;
+ }
+
+ function saveNew($redir_data, $given_url) {
+ $x = new File;
+ $x->url = $given_url;
+ if (!empty($redir_data['protected'])) $x->protected = $redir_data['protected'];
+ if (!empty($redir_data['title'])) $x->title = $redir_data['title'];
+ if (!empty($redir_data['type'])) $x->mimetype = $redir_data['type'];
+ if (!empty($redir_data['size'])) $x->size = intval($redir_data['size']);
+ if (isset($redir_data['time']) && $redir_data['time'] > 0) $x->date = intval($redir_data['time']);
+ $file_id = $x->insert();
+
+ if (isset($redir_data['type'])
+ && ('text/html' === substr($redir_data['type'], 0, 9))
+ && ($oembed_data = File_oembed::_getOembed($given_url))
+ && isset($oembed_data['json'])) {
+ File_oembed::saveNew($oembed_data['json'], $file_id);
+ }
+ return $x;
+ }
+
+ function processNew($given_url, $notice_id) {
+ if (empty($given_url)) return -1; // error, no url to process
+ $given_url = File_redirection::_canonUrl($given_url);
+ if (empty($given_url)) return -1; // error, no url to process
+ $file = File::staticGet('url', $given_url);
+ if (empty($file)) {
+ $file_redir = File_redirection::staticGet('url', $given_url);
+ if (empty($file_redir)) {
+ common_debug("processNew() '$given_url' not a known redirect.\n");
+ $redir_data = File_redirection::where($given_url);
+ $redir_url = $redir_data['url'];
+ if ($redir_url === $given_url) {
+ $x = File::saveNew($redir_data, $given_url);
+ $file_id = $x->id;
+ } else {
+ $x = File::processNew($redir_url, $notice_id);
+ $file_id = $x->id;
+ File_redirection::saveNew($redir_data, $file_id, $given_url);
+ }
+ } else {
+ $file_id = $file_redir->file_id;
+ }
+ } else {
+ $file_id = $file->id;
+ $x = $file;
+ }
+
+ if (empty($x)) {
+ $x = File::staticGet($file_id);
+ if (empty($x)) die('Impossible!');
+ }
+
+ File_to_post::processNew($file_id, $notice_id);
+ return $x;
+ }
+
+ function isRespectsQuota($user,$fileSize) {
+ if ($fileSize > common_config('attachments', 'file_quota')) {
+ return sprintf(_('No file may be larger than %d bytes ' .
+ 'and the file you sent was %d bytes. Try to upload a smaller version.'),
+ common_config('attachments', 'file_quota'), $fileSize);
+ }
+
+ $query = "select sum(size) as total from file join file_to_post on file_to_post.file_id = file.id join notice on file_to_post.post_id = notice.id where profile_id = {$user->id} and file.url like '%/notice/%/file'";
+ $this->query($query);
+ $this->fetch();
+ $total = $this->total + $fileSize;
+ if ($total > common_config('attachments', 'user_quota')) {
+ return sprintf(_('A file this large would exceed your user quota of %d bytes.'), common_config('attachments', 'user_quota'));
+ }
+
+ $query .= ' month(modified) = month(now()) and year(modified) = year(now())';
+ $this->query($query);
+ $this->fetch();
+ $total = $this->total + $fileSize;
+ if ($total > common_config('attachments', 'monthly_quota')) {
+ return sprintf(_('A file this large would exceed your monthly quota of %d bytes.'), common_config('attachments', 'monthly_quota'));
+ }
+ return true;
+ }
+
+ // where should the file go?
+
+ static function filename($profile, $basename, $mimetype)
+ {
+ require_once 'MIME/Type/Extension.php';
+ $mte = new MIME_Type_Extension();
+ $ext = $mte->getExtension($mimetype);
+ $nickname = $profile->nickname;
+ $datestamp = strftime('%Y%m%dT%H%M%S', time());
+ $random = strtolower(common_confirmation_code(32));
+ return "$nickname-$datestamp-$random.$ext";
+ }
+
+ static function path($filename)
+ {
+ $dir = common_config('attachments', 'dir');
+
+ if ($dir[strlen($dir)-1] != '/') {
+ $dir .= '/';
+ }
+
+ return $dir . $filename;
+ }
+
+ static function url($filename)
+ {
+ $path = common_config('attachments', 'path');
+
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
+
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
+
+ $server = common_config('attachments', 'server');
+
+ if (empty($server)) {
+ $server = common_config('site', 'server');
+ }
+
+ // XXX: protocol
+
+ return 'http://'.$server.$path.$filename;
+ }
+}
+
diff --git a/classes/File_oembed.php b/classes/File_oembed.php
new file mode 100644
index 000000000..69230e4a4
--- /dev/null
+++ b/classes/File_oembed.php
@@ -0,0 +1,90 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+
+/**
+ * Table Definition for file_oembed
+ */
+
+class File_oembed extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'file_oembed'; // table name
+ public $file_id; // int(4) primary_key not_null
+ public $version; // varchar(20)
+ public $type; // varchar(20)
+ public $provider; // varchar(50)
+ public $provider_url; // varchar(255)
+ public $width; // int(4)
+ public $height; // int(4)
+ public $html; // text()
+ public $title; // varchar(255)
+ public $author_name; // varchar(50)
+ public $author_url; // varchar(255)
+ public $url; // varchar(255)
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('File_oembed',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function sequenceKey()
+ {
+ return array(false, false, false);
+ }
+
+ function _getOembed($url, $maxwidth = 500, $maxheight = 400, $format = 'json') {
+ $cmd = common_config('oohembed', 'endpoint') . '?url=' . urlencode($url);
+ if (is_int($maxwidth)) $cmd .= "&maxwidth=$maxwidth";
+ if (is_int($maxheight)) $cmd .= "&maxheight=$maxheight";
+ if (is_string($format)) $cmd .= "&format=$format";
+ $oe = @file_get_contents($cmd);
+ if (false === $oe) return false;
+ return array($format => (('json' === $format) ? json_decode($oe, true) : $oe));
+ }
+
+ function saveNew($data, $file_id) {
+ $file_oembed = new File_oembed;
+ $file_oembed->file_id = $file_id;
+ $file_oembed->version = $data['version'];
+ $file_oembed->type = $data['type'];
+ if (!empty($data['provider_name'])) $file_oembed->provider = $data['provider_name'];
+ if (!isset($file_oembed->provider) && !empty($data['provide'])) $file_oembed->provider = $data['provider'];
+ if (!empty($data['provide_url'])) $file_oembed->provider_url = $data['provider_url'];
+ if (!empty($data['width'])) $file_oembed->width = intval($data['width']);
+ if (!empty($data['height'])) $file_oembed->height = intval($data['height']);
+ if (!empty($data['html'])) $file_oembed->html = $data['html'];
+ if (!empty($data['title'])) $file_oembed->title = $data['title'];
+ if (!empty($data['author_name'])) $file_oembed->author_name = $data['author_name'];
+ if (!empty($data['author_url'])) $file_oembed->author_url = $data['author_url'];
+ if (!empty($data['url'])) $file_oembed->url = $data['url'];
+ $file_oembed->insert();
+ if (!empty($data['thumbnail_url'])) {
+ File_thumbnail::saveNew($data, $file_id);
+ }
+ }
+}
+
diff --git a/classes/File_redirection.php b/classes/File_redirection.php
new file mode 100644
index 000000000..d6fa0bcb6
--- /dev/null
+++ b/classes/File_redirection.php
@@ -0,0 +1,204 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+require_once INSTALLDIR.'/classes/File.php';
+require_once INSTALLDIR.'/classes/File_oembed.php';
+
+define('USER_AGENT', 'Laconica user agent / file probe');
+
+/**
+ * Table Definition for file_redirection
+ */
+
+class File_redirection extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'file_redirection'; // table name
+ public $url; // varchar(255) primary_key not_null
+ public $file_id; // int(4)
+ public $redirections; // int(4)
+ public $httpcode; // int(4)
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('File_redirection',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function _commonCurl($url, $redirs) {
+ $curlh = curl_init();
+ curl_setopt($curlh, CURLOPT_URL, $url);
+ curl_setopt($curlh, CURLOPT_AUTOREFERER, true); // # setup referer header when folowing redirects
+ curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 10); // # seconds to wait
+ curl_setopt($curlh, CURLOPT_MAXREDIRS, $redirs); // # max number of http redirections to follow
+ curl_setopt($curlh, CURLOPT_USERAGENT, USER_AGENT);
+ curl_setopt($curlh, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
+ curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curlh, CURLOPT_FILETIME, true);
+ curl_setopt($curlh, CURLOPT_HEADER, true); // Include header in output
+ return $curlh;
+ }
+
+ function _redirectWhere_imp($short_url, $redirs = 10, $protected = false) {
+ if ($redirs < 0) return false;
+
+ // let's see if we know this...
+ $a = File::staticGet('url', $short_url);
+
+ if (!empty($a)) {
+ // this is a direct link to $a->url
+ return $a->url;
+ } else {
+ $b = File_redirection::staticGet('url', $short_url);
+ if (!empty($b)) {
+ // this is a redirect to $b->file_id
+ $a = File::staticGet('id', $b->file_id);
+ return $a->url;
+ }
+ }
+
+ $curlh = File_redirection::_commonCurl($short_url, $redirs);
+ // Don't include body in output
+ curl_setopt($curlh, CURLOPT_NOBODY, true);
+ curl_exec($curlh);
+ $info = curl_getinfo($curlh);
+ curl_close($curlh);
+
+ if (405 == $info['http_code']) {
+ $curlh = File_redirection::_commonCurl($short_url, $redirs);
+ curl_exec($curlh);
+ $info = curl_getinfo($curlh);
+ curl_close($curlh);
+ }
+
+ if (!empty($info['redirect_count']) && File::isProtected($info['url'])) {
+ return File_redirection::_redirectWhere_imp($short_url, $info['redirect_count'] - 1, true);
+ }
+
+ $ret = array('code' => $info['http_code']
+ , 'redirects' => $info['redirect_count']
+ , 'url' => $info['url']);
+
+ if (!empty($info['content_type'])) $ret['type'] = $info['content_type'];
+ if ($protected) $ret['protected'] = true;
+ if (!empty($info['download_content_length'])) $ret['size'] = $info['download_content_length'];
+ if (isset($info['filetime']) && ($info['filetime'] > 0)) $ret['time'] = $info['filetime'];
+ return $ret;
+ }
+
+ function where($in_url) {
+ $ret = File_redirection::_redirectWhere_imp($in_url);
+ return $ret;
+ }
+
+ function makeShort($long_url) {
+
+ $canon = File_redirection::_canonUrl($long_url);
+
+ $short_url = File_redirection::_userMakeShort($canon);
+
+ // Did we get one? Is it shorter?
+ if (!empty($short_url) && mb_strlen($short_url) < mb_strlen($long_url)) {
+ return $short_url;
+ } else {
+ return $long_url;
+ }
+ }
+
+ function _userMakeShort($long_url) {
+ $short_url = common_shorten_url($long_url);
+ if (!empty($short_url) && $short_url != $long_url) {
+ $short_url = (string)$short_url;
+ // store it
+ $file = File::staticGet('url', $long_url);
+ if (empty($file)) {
+ $redir_data = File_redirection::where($long_url);
+ $file = File::saveNew($redir_data, $long_url);
+ $file_id = $file->id;
+ if (!empty($redir_data['oembed']['json'])) {
+ File_oembed::saveNew($redir_data['oembed']['json'], $file_id);
+ }
+ } else {
+ $file_id = $file->id;
+ }
+ $file_redir = File_redirection::staticGet('url', $short_url);
+ if (empty($file_redir)) {
+ $file_redir = new File_redirection;
+ $file_redir->url = $short_url;
+ $file_redir->file_id = $file_id;
+ $file_redir->insert();
+ }
+ return $short_url;
+ }
+ return null;
+ }
+
+ function _canonUrl($in_url, $default_scheme = 'http://') {
+ if (empty($in_url)) return false;
+ $out_url = $in_url;
+ $p = parse_url($out_url);
+ if (empty($p['host']) || empty($p['scheme'])) {
+ list($scheme) = explode(':', $in_url, 2);
+ switch ($scheme) {
+ case 'fax':
+ case 'tel':
+ $out_url = str_replace('.-()', '', $out_url);
+ break;
+
+ case 'mailto':
+ case 'aim':
+ case 'jabber':
+ case 'xmpp':
+ // don't touch anything
+ break;
+
+ default:
+ $out_url = $default_scheme . ltrim($out_url, '/');
+ $p = parse_url($out_url);
+ if (empty($p['scheme'])) return false;
+ break;
+ }
+ }
+
+ if (('ftp' == $p['scheme']) || ('http' == $p['scheme']) || ('https' == $p['scheme'])) {
+ if (empty($p['host'])) return false;
+ if (empty($p['path'])) {
+ $out_url .= '/';
+ }
+ }
+
+ return $out_url;
+ }
+
+ function saveNew($data, $file_id, $url) {
+ $file_redir = new File_redirection;
+ $file_redir->url = $url;
+ $file_redir->file_id = $file_id;
+ $file_redir->redirections = intval($data['redirects']);
+ $file_redir->httpcode = intval($data['code']);
+ $file_redir->insert();
+ }
+}
+
diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php
new file mode 100644
index 000000000..44b92a2fa
--- /dev/null
+++ b/classes/File_thumbnail.php
@@ -0,0 +1,60 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+
+/**
+ * Table Definition for file_thumbnail
+ */
+
+class File_thumbnail extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'file_thumbnail'; // table name
+ public $file_id; // int(4) primary_key not_null
+ public $url; // varchar(255) unique_key
+ public $width; // int(4)
+ public $height; // int(4)
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('File_thumbnail',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function sequenceKey()
+ {
+ return array(false, false, false);
+ }
+
+ function saveNew($data, $file_id) {
+ $tn = new File_thumbnail;
+ $tn->file_id = $file_id;
+ $tn->url = $data['thumbnail_url'];
+ $tn->width = intval($data['thumbnail_width']);
+ $tn->height = intval($data['thumbnail_height']);
+ $tn->insert();
+ }
+}
+
diff --git a/classes/File_to_post.php b/classes/File_to_post.php
new file mode 100644
index 000000000..d35febb77
--- /dev/null
+++ b/classes/File_to_post.php
@@ -0,0 +1,70 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+
+/**
+ * Table Definition for file_to_post
+ */
+
+class File_to_post extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'file_to_post'; // table name
+ public $file_id; // int(4) primary_key not_null
+ public $post_id; // int(4) primary_key not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('File_to_post',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function processNew($file_id, $notice_id) {
+ static $seen = array();
+ if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) {
+
+ $f2p = File_to_post::pkeyGet(array('post_id' => $notice_id,
+ 'file_id' => $file_id));
+ if (empty($f2p)) {
+ $f2p = new File_to_post;
+ $f2p->file_id = $file_id;
+ $f2p->post_id = $notice_id;
+ $f2p->insert();
+ }
+
+ if (empty($seen[$notice_id])) {
+ $seen[$notice_id] = array($file_id);
+ } else {
+ $seen[$notice_id][] = $file_id;
+ }
+ }
+ }
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('File_to_post', $kv);
+ }
+}
+
diff --git a/classes/Foreign_link.php b/classes/Foreign_link.php
index af2b3f189..c0b356ece 100644
--- a/classes/Foreign_link.php
+++ b/classes/Foreign_link.php
@@ -11,7 +11,7 @@ class Foreign_link extends Memcached_DataObject
public $__table = 'foreign_link'; // table name
public $user_id; // int(4) primary_key not_null
- public $foreign_id; // int(4) primary_key not_null
+ public $foreign_id; // bigint(8) primary_key not_null unsigned
public $service; // int(4) primary_key not_null
public $credentials; // varchar(255)
public $noticesync; // tinyint(1) not_null default_1
@@ -59,13 +59,19 @@ class Foreign_link extends Memcached_DataObject
return null;
}
- function set_flags($noticesync, $replysync, $friendsync)
+ function set_flags($noticesend, $noticerecv, $replysync, $friendsync)
{
- if ($noticesync) {
+ if ($noticesend) {
$this->noticesync |= FOREIGN_NOTICE_SEND;
} else {
$this->noticesync &= ~FOREIGN_NOTICE_SEND;
}
+
+ if ($noticerecv) {
+ $this->noticesync |= FOREIGN_NOTICE_RECV;
+ } else {
+ $this->noticesync &= ~FOREIGN_NOTICE_RECV;
+ }
if ($replysync) {
$this->noticesync |= FOREIGN_NOTICE_SEND_REPLY;
diff --git a/classes/Foreign_user.php b/classes/Foreign_user.php
index 61727abe5..8b3e03dfb 100644
--- a/classes/Foreign_user.php
+++ b/classes/Foreign_user.php
@@ -4,42 +4,41 @@
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Foreign_user extends Memcached_DataObject
+class Foreign_user extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'foreign_user'; // table name
- public $id; // int(4) primary_key not_null
+ public $id; // bigint(8) primary_key not_null
public $service; // int(4) primary_key not_null
public $uri; // varchar(255) unique_key not_null
- public $nickname; // varchar(255)
+ public $nickname; // varchar(255)
public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
/* Static get */
- function staticGet($k,$v=null)
- { return Memcached_DataObject::staticGet('Foreign_user',$k,$v); }
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Foreign_user',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
-
+
// XXX: This only returns a 1->1 single obj mapping. Change? Or make
// a getForeignUsers() that returns more than one? --Zach
- static function getForeignUser($id, $service) {
+ static function getForeignUser($id, $service) {
$fuser = new Foreign_user();
$fuser->whereAdd("service = $service");
$fuser->whereAdd("id = $id");
$fuser->limit(1);
-
+
if ($fuser->find()) {
$fuser->fetch();
return $fuser;
}
-
- return null;
+
+ return null;
}
-
+
function updateKeys(&$orig)
{
$parts = array();
@@ -68,5 +67,4 @@ class Foreign_user extends Memcached_DataObject
return $result;
}
-
}
diff --git a/classes/Group_alias.php b/classes/Group_alias.php
new file mode 100644
index 000000000..e801e50e1
--- /dev/null
+++ b/classes/Group_alias.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Table Definition for group_alias
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+
+class Group_alias extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'group_alias'; // table name
+ public $alias; // varchar(64) primary_key not_null
+ public $group_id; // int(4) not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Group_alias',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+}
diff --git a/classes/Group_block.php b/classes/Group_block.php
new file mode 100644
index 000000000..7922c19a9
--- /dev/null
+++ b/classes/Group_block.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Table Definition for group_block
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+
+class Group_block extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'group_block'; // table name
+ public $group_id; // int(4) primary_key not_null
+ public $blocked; // int(4) primary_key not_null
+ public $blocker; // int(4) not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Group_block',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('Group_block', $kv);
+ }
+
+ static function isBlocked($group, $profile)
+ {
+ $block = Group_block::pkeyGet(array('group_id' => $group->id,
+ 'blocked' => $profile->id));
+ return !empty($block);
+ }
+
+ static function blockProfile($group, $profile, $blocker)
+ {
+ // Insert the block
+
+ $block = new Group_block();
+
+ $block->query('BEGIN');
+
+ $block->group_id = $group->id;
+ $block->blocked = $profile->id;
+ $block->blocker = $blocker->id;
+
+ $result = $block->insert();
+
+ if (!$result) {
+ common_log_db_error($block, 'INSERT', __FILE__);
+ return null;
+ }
+
+ // Delete membership if any
+
+ $member = new Group_member();
+
+ $member->group_id = $group->id;
+ $member->profile_id = $profile->id;
+
+ if ($member->find(true)) {
+ $result = $member->delete();
+ if (!$result) {
+ common_log_db_error($member, 'DELETE', __FILE__);
+ return null;
+ }
+ }
+
+ // Commit, since both have been done
+
+ $block->query('COMMIT');
+
+ return $block;
+ }
+
+ static function unblockProfile($group, $profile)
+ {
+ $block = Group_block::pkeyGet(array('group_id' => $group->id,
+ 'blocked' => $profile->id));
+
+ if (empty($block)) {
+ return null;
+ }
+
+ $result = $block->delete();
+
+ if (!$result) {
+ common_log_db_error($block, 'DELETE', __FILE__);
+ return null;
+ }
+
+ return true;
+ }
+
+}
diff --git a/classes/Group_inbox.php b/classes/Group_inbox.php
index b80ba4272..1af7439f7 100755..100644
--- a/classes/Group_inbox.php
+++ b/classes/Group_inbox.php
@@ -14,8 +14,14 @@ class Group_inbox extends Memcached_DataObject
public $created; // datetime() not_null
/* Static get */
+
function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Group_inbox',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('Group_inbox', $kv);
+ }
}
diff --git a/classes/Group_member.php b/classes/Group_member.php
index 3c23a991f..3c23a991f 100755..100644
--- a/classes/Group_member.php
+++ b/classes/Group_member.php
diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php
index 33ac70dd0..f7cbb9d5b 100644
--- a/classes/Memcached_DataObject.php
+++ b/classes/Memcached_DataObject.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -193,7 +193,14 @@ class Memcached_DataObject extends DB_DataObject
// unable to connect to sphinx' search daemon
if (!$connected) {
if ('mysql' === common_config('db', 'type')) {
- $search_engine = new MySQLSearch($this, $table);
+ $type = common_config('search', 'type');
+ if ($type == 'like') {
+ $search_engine = new MySQLLikeSearch($this, $table);
+ } else if ($type == 'fulltext') {
+ $search_engine = new MySQLSearch($this, $table);
+ } else {
+ throw new ServerException('Unknown search type: ' . $type);
+ }
} else {
$search_engine = new PGSearch($this, $table);
}
@@ -242,13 +249,16 @@ class Memcached_DataObject extends DB_DataObject
if (common_config('db', 'type') == 'mysql' &&
common_config('db', 'utf8')) {
$conn = $DB->connection;
- if ($DB instanceof DB_mysqli) {
- mysqli_set_charset($conn, 'utf8');
- } else if ($DB instanceof DB_mysql) {
- mysql_set_charset('utf8', $conn);
+ if (!empty($conn)) {
+ if ($DB instanceof DB_mysqli) {
+ mysqli_set_charset($conn, 'utf8');
+ } else if ($DB instanceof DB_mysql) {
+ mysql_set_charset('utf8', $conn);
+ }
}
}
}
return $result;
}
+
}
diff --git a/classes/Notice.php b/classes/Notice.php
index bca4b22c4..75044cf63 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -29,6 +29,13 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
define('NOTICE_CACHE_WINDOW', 61);
+define('NOTICE_LOCAL_PUBLIC', 1);
+define('NOTICE_REMOTE_OMB', 0);
+define('NOTICE_LOCAL_NONPUBLIC', -1);
+define('NOTICE_GATEWAY', -2);
+
+define('MAX_BOXCARS', 128);
+
class Notice extends Memcached_DataObject
{
###START_AUTOCODE
@@ -46,6 +53,7 @@ class Notice extends Memcached_DataObject
public $reply_to; // int(4)
public $is_local; // tinyint(1)
public $source; // varchar(32)
+ public $conversation; // int(4)
/* Static get */
function staticGet($k,$v=NULL) {
@@ -119,7 +127,8 @@ class Notice extends Memcached_DataObject
}
}
- static function saveNew($profile_id, $content, $source=null, $is_local=1, $reply_to=null, $uri=null) {
+ static function saveNew($profile_id, $content, $source=null,
+ $is_local=1, $reply_to=null, $uri=null, $created=null) {
$profile = Profile::staticGet($profile_id);
@@ -170,12 +179,24 @@ class Notice extends Memcached_DataObject
$notice->query('BEGIN');
$notice->reply_to = $reply_to;
- $notice->created = common_sql_now();
+ if (!empty($created)) {
+ $notice->created = $created;
+ } else {
+ $notice->created = common_sql_now();
+ }
$notice->content = $final;
$notice->rendered = common_render_content($final, $notice);
$notice->source = $source;
$notice->uri = $uri;
+ if (!empty($reply_to)) {
+ $reply_notice = Notice::staticGet('id', $reply_to);
+ if (!empty($reply_notice)) {
+ $notice->reply_to = $reply_to;
+ $notice->conversation = $reply_notice->conversation;
+ }
+ }
+
if (Event::handle('StartNoticeSave', array(&$notice))) {
$id = $notice->insert();
@@ -202,7 +223,14 @@ class Notice extends Memcached_DataObject
$notice->saveTags();
$notice->addToInboxes();
- $notice->saveGroups();
+
+ $notice->saveUrls();
+ $orig2 = clone($notice);
+ $notice->rendered = common_render_content($final, $notice);
+ if (!$notice->update($orig2)) {
+ common_log_db_error($notice, 'UPDATE', __FILE__);
+ return _('Problem saving notice.');
+ }
$notice->query('COMMIT');
@@ -217,6 +245,22 @@ class Notice extends Memcached_DataObject
return $notice;
}
+ /** save all urls in the notice to the db
+ *
+ * follow redirects and save all available file information
+ * (mimetype, date, size, oembed, etc.)
+ *
+ * @return void
+ */
+ function saveUrls() {
+ common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id);
+ }
+
+ function saveUrl($data) {
+ list($url, $notice_id) = $data;
+ File::processNew($url, $notice_id);
+ }
+
static function checkDupes($profile_id, $content) {
$profile = Profile::staticGet($profile_id);
if (!$profile) {
@@ -265,6 +309,44 @@ class Notice extends Memcached_DataObject
return true;
}
+ function getUploadedAttachment() {
+ $post = clone $this;
+ $query = 'select file.url as up, file.id as i from file join file_to_post on file.id = file_id where post_id=' . $post->escape($post->id) . ' and url like "%/notice/%/file"';
+ $post->query($query);
+ $post->fetch();
+ if (empty($post->up) || empty($post->i)) {
+ $ret = false;
+ } else {
+ $ret = array($post->up, $post->i);
+ }
+ $post->free();
+ return $ret;
+ }
+
+ function hasAttachments() {
+ $post = clone $this;
+ $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id);
+ $post->query($query);
+ $post->fetch();
+ $n_attachments = intval($post->n_attachments);
+ $post->free();
+ return $n_attachments;
+ }
+
+ function attachments() {
+ // XXX: cache this
+ $att = array();
+ $f2p = new File_to_post;
+ $f2p->post_id = $this->id;
+ if ($f2p->find()) {
+ while ($f2p->fetch()) {
+ $f = File::staticGet($f2p->file_id);
+ $att[] = clone($f);
+ }
+ }
+ return $att;
+ }
+
function blowCaches($blowLast=false)
{
$this->blowSubsCache($blowLast);
@@ -273,6 +355,21 @@ class Notice extends Memcached_DataObject
$this->blowPublicCache($blowLast);
$this->blowTagCache($blowLast);
$this->blowGroupCache($blowLast);
+ $this->blowConversationCache($blowLast);
+ $profile = Profile::staticGet($this->profile_id);
+ $profile->blowNoticeCount();
+ }
+
+ function blowConversationCache($blowLast=false)
+ {
+ $cache = common_memcache();
+ if ($cache) {
+ $ck = common_cache_key('notice:conversation_ids:'.$this->conversation);
+ $cache->delete($ck);
+ if ($blowLast) {
+ $cache->delete($ck.';last');
+ }
+ }
}
function blowGroupCache($blowLast=false)
@@ -313,6 +410,12 @@ class Notice extends Memcached_DataObject
if ($tag->find()) {
while ($tag->fetch()) {
$tag->blowCache($blowLast);
+ $ck = 'profile:notice_ids_tagged:' . $this->profile_id . ':' . $tag->tag;
+
+ $cache->delete($ck);
+ if ($blowLast) {
+ $cache->delete($ck . ';last');
+ }
}
}
$tag->free();
@@ -334,8 +437,10 @@ class Notice extends Memcached_DataObject
while ($user->fetch()) {
$cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
+ $cache->delete(common_cache_key('notice_inbox:by_user_own:'.$user->id));
if ($blowLast) {
$cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
+ $cache->delete(common_cache_key('notice_inbox:by_user_own:'.$user->id.';last'));
}
}
$user->free();
@@ -397,8 +502,10 @@ class Notice extends Memcached_DataObject
if ($fave->find()) {
while ($fave->fetch()) {
$cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id));
+ $cache->delete(common_cache_key('fave:by_user_own:'.$fave->user_id));
if ($blowLast) {
$cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last'));
+ $cache->delete(common_cache_key('fave:by_user_own:'.$fave->user_id.';last'));
}
}
}
@@ -601,7 +708,10 @@ class Notice extends Memcached_DataObject
if (!empty($cache)) {
$notices = array();
foreach ($ids as $id) {
- $notices[] = Notice::staticGet('id', $id);
+ $n = Notice::staticGet('id', $id);
+ if (!empty($n)) {
+ $notices[] = $n;
+ }
}
return new ArrayWrapper($notices);
} else {
@@ -670,34 +780,148 @@ class Notice extends Memcached_DataObject
return $ids;
}
+ function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+ {
+ $ids = Notice::stream(array('Notice', '_conversationStreamDirect'),
+ array($id),
+ 'notice:conversation_ids:'.$id,
+ $offset, $limit, $since_id, $max_id, $since);
+
+ return Notice::getStreamByIds($ids);
+ }
+
+ function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+ {
+ $notice = new Notice();
+
+ $notice->selectAdd(); // clears it
+ $notice->selectAdd('id');
+
+ $notice->conversation = $id;
+
+ $notice->orderBy('id DESC');
+
+ if (!is_null($offset)) {
+ $notice->limit($offset, $limit);
+ }
+
+ if ($since_id != 0) {
+ $notice->whereAdd('id > ' . $since_id);
+ }
+
+ if ($max_id != 0) {
+ $notice->whereAdd('id <= ' . $max_id);
+ }
+
+ if (!is_null($since)) {
+ $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+ }
+
+ $ids = array();
+
+ if ($notice->find()) {
+ while ($notice->fetch()) {
+ $ids[] = $notice->id;
+ }
+ }
+
+ $notice->free();
+ $notice = NULL;
+
+ return $ids;
+ }
+
function addToInboxes()
{
$enabled = common_config('inboxes', 'enabled');
if ($enabled === true || $enabled === 'transitional') {
+
+ // XXX: loads constants
+
$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 . "' " .
- "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
- 'WHERE subscription.subscribed = ' . $this->profile_id . ' ' .
- 'AND NOT EXISTS (SELECT user_id, notice_id ' .
- 'FROM notice_inbox ' .
- "WHERE user_id = $UT.id " .
- 'AND notice_id = ' . $this->id . ' )';
- if ($enabled === 'transitional') {
- $qry .= " AND $UT.inboxed = 1";
- }
- $inbox->query($qry);
+
+ $users = $this->getSubscribedUsers();
+
+ // FIXME: kind of ignoring 'transitional'...
+ // we'll probably stop supporting inboxless mode
+ // in 0.9.x
+
+ $ni = array();
+
+ foreach ($users as $id) {
+ $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
+ }
+
+ $groups = $this->saveGroups();
+
+ foreach ($groups as $group) {
+ $users = $group->getUserMembers();
+ foreach ($users as $id) {
+ if (!array_key_exists($id, $ni)) {
+ $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
+ }
+ }
+ }
+
+ $cnt = 0;
+
+ $qryhdr = 'INSERT INTO notice_inbox (user_id, notice_id, source, created) VALUES ';
+ $qry = $qryhdr;
+
+ foreach ($ni as $id => $source) {
+ if ($cnt > 0) {
+ $qry .= ', ';
+ }
+ $qry .= '('.$id.', '.$this->id.', '.$source.', "'.$this->created.'") ';
+ $cnt++;
+ if ($cnt >= MAX_BOXCARS) {
+ $inbox = new Notice_inbox();
+ $inbox->query($qry);
+ $qry = $qryhdr;
+ $cnt = 0;
+ }
+ }
+
+ if ($cnt > 0) {
+ $inbox = new Notice_inbox();
+ $inbox->query($qry);
+ }
}
+
return;
}
+ function getSubscribedUsers()
+ {
+ $user = new User();
+
+ $qry =
+ 'SELECT id ' .
+ 'FROM user JOIN subscription '.
+ 'ON user.id = subscription.subscriber ' .
+ 'WHERE subscription.subscribed = %d ';
+
+ $user->query(sprintf($qry, $this->profile_id));
+
+ $ids = array();
+
+ while ($user->fetch()) {
+ $ids[] = $user->id;
+ }
+
+ $user->free();
+
+ return $ids;
+ }
+
function saveGroups()
{
+ $groups = array();
+
$enabled = common_config('inboxes', 'enabled');
if ($enabled !== true && $enabled !== 'transitional') {
- return;
+ return $groups;
}
/* extract all !group */
@@ -705,7 +929,7 @@ class Notice extends Memcached_DataObject
strtolower($this->content),
$match);
if (!$count) {
- return true;
+ return $groups;
}
$profile = $this->getProfile();
@@ -714,16 +938,16 @@ class Notice extends Memcached_DataObject
foreach (array_unique($match[1]) as $nickname) {
/* XXX: remote groups. */
- $group = User_group::staticGet('nickname', $nickname);
+ $group = User_group::getForNickname($nickname);
- if (!$group) {
+ if (empty($group)) {
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));
+ 'notice_id' => $this->id));
if (is_null($tag)) {
$this->saveTag($nickname);
@@ -731,41 +955,36 @@ class Notice extends Memcached_DataObject
if ($profile->isMember($group)) {
- $gi = new Group_inbox();
-
- $gi->group_id = $group->id;
- $gi->notice_id = $this->id;
- $gi->created = common_sql_now();
-
- $result = $gi->insert();
+ $result = $this->addToGroupInbox($group);
if (!$result) {
common_log_db_error($gi, 'INSERT', __FILE__);
}
- // FIXME: do this in an offline daemon
-
- $this->addToGroupInboxes($group);
+ $groups[] = clone($group);
}
}
+
+ return $groups;
}
- function addToGroupInboxes($group)
+ function addToGroupInbox($group)
{
- $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 " .
- "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 ' .
- 'FROM notice_inbox ' .
- "WHERE user_id = $UT.id " .
- 'AND notice_id = ' . $this->id . ' )';
- if ($enabled === 'transitional') {
- $qry .= " AND $UT.inboxed = 1";
- }
- $result = $inbox->query($qry);
+ $gi = Group_inbox::pkeyGet(array('group_id' => $group->id,
+ 'notice_id' => $this->id));
+
+ if (empty($gi)) {
+
+ $gi = new Group_inbox();
+
+ $gi->group_id = $group->id;
+ $gi->notice_id = $this->id;
+ $gi->created = $this->created;
+
+ return $gi->insert();
+ }
+
+ return true;
}
function saveReplies()
@@ -804,6 +1023,7 @@ class Notice extends Memcached_DataObject
if ($recipient_notice) {
$orig = clone($this);
$this->reply_to = $recipient_notice->id;
+ $this->conversation = $recipient_notice->conversation;
$this->update($orig);
}
}
@@ -853,6 +1073,14 @@ class Notice extends Memcached_DataObject
}
}
+ // If it's not a reply, make it the root of a new conversation
+
+ if (empty($this->conversation)) {
+ $orig = clone($this);
+ $this->conversation = $this->id;
+ $this->update($orig);
+ }
+
foreach (array_keys($replied) as $recipient) {
$user = User::staticGet('id', $recipient);
if ($user) {
@@ -938,6 +1166,18 @@ class Notice extends Memcached_DataObject
}
$tag->free();
+ # Enclosures
+ $attachments = $this->attachments();
+ if($attachments){
+ foreach($attachments as $attachment){
+ $attributes = array('rel'=>'enclosure','href'=>$attachment->url,'type'=>$attachment->mimetype,'length'=>$attachment->size);
+ if($attachment->title){
+ $attributes['title']=$attachment->title;
+ }
+ $xs->element('link', $attributes, null);
+ }
+ }
+
$xs->elementEnd('entry');
return $xs->getString();
@@ -961,6 +1201,7 @@ class Notice extends Memcached_DataObject
if (empty($cache) ||
$since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) ||
+ is_null($limit) ||
($offset + $limit) > NOTICE_CACHE_WINDOW) {
return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
$max_id, $since)));
diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php
index 8a27e1747..940381f84 100644
--- a/classes/Notice_inbox.php
+++ b/classes/Notice_inbox.php
@@ -25,6 +25,11 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
define('INBOX_CACHE_WINDOW', 101);
+define('NOTICE_INBOX_SOURCE_SUB', 1);
+define('NOTICE_INBOX_SOURCE_GROUP', 2);
+define('NOTICE_INBOX_SOURCE_REPLY', 3);
+define('NOTICE_INBOX_SOURCE_GATEWAY', -1);
+
class Notice_inbox extends Memcached_DataObject
{
###START_AUTOCODE
@@ -43,20 +48,25 @@ class Notice_inbox extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
- function stream($user_id, $offset, $limit, $since_id, $max_id, $since)
+ function stream($user_id, $offset, $limit, $since_id, $max_id, $since, $own=false)
{
return Notice::stream(array('Notice_inbox', '_streamDirect'),
- array($user_id),
- 'notice_inbox:by_user:'.$user_id,
+ array($user_id, $own),
+ ($own) ? 'notice_inbox:by_user:'.$user_id :
+ 'notice_inbox:by_user_own:'.$user_id,
$offset, $limit, $since_id, $max_id, $since);
}
- function _streamDirect($user_id, $offset, $limit, $since_id, $max_id, $since)
+ function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id, $since)
{
$inbox = new Notice_inbox();
$inbox->user_id = $user_id;
+ if (!$own) {
+ $inbox->whereAdd('source != ' . NOTICE_INBOX_SOURCE_GATEWAY);
+ }
+
if ($since_id != 0) {
$inbox->whereAdd('notice_id > ' . $since_id);
}
@@ -85,4 +95,9 @@ class Notice_inbox extends Memcached_DataObject
return $ids;
}
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('Notice_inbox', $kv);
+ }
}
diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php
index 758a66594..4e52ef269 100644
--- a/classes/Notice_tag.php
+++ b/classes/Notice_tag.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/classes/Profile.php b/classes/Profile.php
index 3d13cd46a..224b61bd2 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -153,18 +153,67 @@ class Profile extends Memcached_DataObject
return null;
}
- function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
+ function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null)
+ {
+ $ids = Notice::stream(array($this, '_streamTaggedDirect'),
+ array($tag),
+ 'profile:notice_ids_tagged:' . $this->id . ':' . $tag,
+ $offset, $limit, $since_id, $max_id, $since);
+ return Notice::getStreamByIds($ids);
+ }
+
+ function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, $since=null)
{
// XXX: I'm not sure this is going to be any faster. It probably isn't.
$ids = Notice::stream(array($this, '_streamDirect'),
array(),
'profile:notice_ids:' . $this->id,
- $offset, $limit, $since_id, $max_id);
+ $offset, $limit, $since_id, $max_id, $since);
return Notice::getStreamByIds($ids);
}
- function _streamDirect($offset, $limit, $since_id, $max_id, $since)
+ function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id, $since)
+ {
+ // XXX It would be nice to do this without a join
+
+ $notice = new Notice();
+
+ $query =
+ "select id from notice join notice_tag on id=notice_id where tag='".
+ $notice->escape($tag) .
+ "' and profile_id=" . $notice->escape($this->id);
+
+ if ($since_id != 0) {
+ $query .= " and id > $since_id";
+ }
+
+ if ($max_id != 0) {
+ $query .= " and id < $max_id";
+ }
+
+ if (!is_null($since)) {
+ $query .= " and created > '" . date('Y-m-d H:i:s', $since) . "'";
+ }
+
+ $query .= ' order by id DESC';
+
+ if (!is_null($offset)) {
+ $query .= " limit $offset, $limit";
+ }
+
+ $notice->query($query);
+
+ $ids = array();
+
+ while ($notice->fetch()) {
+ $ids[] = $notice->id;
+ }
+
+ return $ids;
+ }
+
+ function _streamDirect($offset, $limit, $since_id, $max_id, $since = null)
{
$notice = new Notice();
@@ -240,4 +289,180 @@ class Profile extends Memcached_DataObject
return Avatar::defaultImage($size);
}
}
+
+ function getSubscriptions($offset=0, $limit=null)
+ {
+ $qry =
+ 'SELECT profile.* ' .
+ 'FROM profile JOIN subscription ' .
+ 'ON profile.id = subscription.subscribed ' .
+ 'WHERE subscription.subscriber = %d ' .
+ 'AND subscription.subscribed != subscription.subscriber ' .
+ 'ORDER BY subscription.created DESC ';
+
+ if (common_config('db','type') == 'pgsql') {
+ $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+ } else {
+ $qry .= ' LIMIT ' . $offset . ', ' . $limit;
+ }
+
+ $profile = new Profile();
+
+ $profile->query(sprintf($qry, $this->id));
+
+ return $profile;
+ }
+
+ function getSubscribers($offset=0, $limit=null)
+ {
+ $qry =
+ 'SELECT profile.* ' .
+ 'FROM profile JOIN subscription ' .
+ 'ON profile.id = subscription.subscriber ' .
+ 'WHERE subscription.subscribed = %d ' .
+ 'AND subscription.subscribed != subscription.subscriber ' .
+ 'ORDER BY subscription.created DESC ';
+
+ if ($offset) {
+ if (common_config('db','type') == 'pgsql') {
+ $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+ } else {
+ $qry .= ' LIMIT ' . $offset . ', ' . $limit;
+ }
+ }
+
+ $profile = new Profile();
+
+ $cnt = $profile->query(sprintf($qry, $this->id));
+
+ return $profile;
+ }
+
+ function subscriptionCount()
+ {
+ $c = common_memcache();
+
+ if (!empty($c)) {
+ $cnt = $c->get(common_cache_key('profile:subscription_count:'.$this->id));
+ if (is_integer($cnt)) {
+ return (int) $cnt;
+ }
+ }
+
+ $sub = new Subscription();
+ $sub->subscriber = $this->id;
+
+ $cnt = (int) $sub->count('distinct subscribed');
+
+ $cnt = ($cnt > 0) ? $cnt - 1 : $cnt;
+
+ if (!empty($c)) {
+ $c->set(common_cache_key('profile:subscription_count:'.$this->id), $cnt);
+ }
+
+ common_debug("subscriptionCount == $cnt");
+ return $cnt;
+ }
+
+ function subscriberCount()
+ {
+ $c = common_memcache();
+ if (!empty($c)) {
+ $cnt = $c->get(common_cache_key('profile:subscriber_count:'.$this->id));
+ if (is_integer($cnt)) {
+ return (int) $cnt;
+ }
+ }
+
+ $sub = new Subscription();
+ $sub->subscribed = $this->id;
+
+ $cnt = (int) $sub->count('distinct subscriber');
+
+ $cnt = ($cnt > 0) ? $cnt - 1 : $cnt;
+
+ if (!empty($c)) {
+ $c->set(common_cache_key('profile:subscriber_count:'.$this->id), $cnt);
+ }
+
+ common_debug("subscriberCount == $cnt");
+ return $cnt;
+ }
+
+ function faveCount()
+ {
+ $c = common_memcache();
+ if (!empty($c)) {
+ $cnt = $c->get(common_cache_key('profile:fave_count:'.$this->id));
+ if (is_integer($cnt)) {
+ return (int) $cnt;
+ }
+ }
+
+ $faves = new Fave();
+ $faves->user_id = $this->id;
+ $cnt = (int) $faves->count('distinct notice_id');
+
+ if (!empty($c)) {
+ $c->set(common_cache_key('profile:fave_count:'.$this->id), $cnt);
+ }
+
+ common_debug("faveCount == $cnt");
+ return $cnt;
+ }
+
+ function noticeCount()
+ {
+ $c = common_memcache();
+
+ if (!empty($c)) {
+ $cnt = $c->get(common_cache_key('profile:notice_count:'.$this->id));
+ if (is_integer($cnt)) {
+ return (int) $cnt;
+ }
+ }
+
+ $notices = new Notice();
+ $notices->profile_id = $this->id;
+ $cnt = (int) $notices->count('distinct id');
+
+ if (!empty($c)) {
+ $c->set(common_cache_key('profile:notice_count:'.$this->id), $cnt);
+ }
+
+ common_debug("noticeCount == $cnt");
+ return $cnt;
+ }
+
+ function blowSubscriberCount()
+ {
+ $c = common_memcache();
+ if (!empty($c)) {
+ $c->delete(common_cache_key('profile:subscriber_count:'.$this->id));
+ }
+ }
+
+ function blowSubscriptionCount()
+ {
+ $c = common_memcache();
+ if (!empty($c)) {
+ $c->delete(common_cache_key('profile:subscription_count:'.$this->id));
+ }
+ }
+
+ function blowFaveCount()
+ {
+ $c = common_memcache();
+ if (!empty($c)) {
+ $c->delete(common_cache_key('profile:fave_count:'.$this->id));
+ }
+ }
+
+ function blowNoticeCount()
+ {
+ $c = common_memcache();
+ if (!empty($c)) {
+ $c->delete(common_cache_key('profile:notice_count:'.$this->id));
+ }
+ }
}
diff --git a/classes/Profile_block.php b/classes/Profile_block.php
index 551e690e2..feadea42d 100644
--- a/classes/Profile_block.php
+++ b/classes/Profile_block.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/classes/Queue_item.php b/classes/Queue_item.php
index 9b909ec22..295c321b5 100644
--- a/classes/Queue_item.php
+++ b/classes/Queue_item.php
@@ -4,7 +4,7 @@
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Queue_item extends Memcached_DataObject
+class Queue_item extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
@@ -13,7 +13,7 @@ class Queue_item extends Memcached_DataObject
public $notice_id; // int(4) primary_key not_null
public $transport; // varchar(8) primary_key not_null
public $created; // datetime() not_null
- public $claimed; // datetime()
+ public $claimed; // datetime()
/* Static get */
function staticGet($k,$v=null)
@@ -24,7 +24,7 @@ class Queue_item extends Memcached_DataObject
function sequenceKey()
{ return array(false, false); }
-
+
static function top($transport) {
$qi = new Queue_item();
@@ -54,4 +54,9 @@ class Queue_item extends Memcached_DataObject
$qi = null;
return null;
}
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('Queue_item', $kv);
+ }
}
diff --git a/classes/Related_group.php b/classes/Related_group.php
index c00ad9c44..c00ad9c44 100755..100644
--- a/classes/Related_group.php
+++ b/classes/Related_group.php
diff --git a/classes/Remote_profile.php b/classes/Remote_profile.php
index 5aa6d913e..975852dd9 100644
--- a/classes/Remote_profile.php
+++ b/classes/Remote_profile.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/classes/Session.php b/classes/Session.php
new file mode 100644
index 000000000..93fd99baa
--- /dev/null
+++ b/classes/Session.php
@@ -0,0 +1,129 @@
+<?php
+/**
+ * Table Definition for session
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2009, Control Yourself, 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.'/classes/Memcached_DataObject.php';
+
+class Session extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'session'; // table name
+ public $id; // varchar(32) primary_key not_null
+ public $session_data; // text()
+ public $created; // datetime() not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Session',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ static function logdeb($msg)
+ {
+ if (common_config('sessions', 'debug')) {
+ common_debug("Session: " . $msg);
+ }
+ }
+
+ static function open($save_path, $session_name)
+ {
+ return true;
+ }
+
+ static function close()
+ {
+ return true;
+ }
+
+ static function read($id)
+ {
+ self::logdeb("Fetching session '$id'");
+
+ $session = Session::staticGet('id', $id);
+
+ if (empty($session)) {
+ return '';
+ } else {
+ return (string)$session->session_data;
+ }
+ }
+
+ static function write($id, $session_data)
+ {
+ self::logdeb("Writing session '$id'");
+
+ $session = Session::staticGet('id', $id);
+
+ if (empty($session)) {
+ $session = new Session();
+
+ $session->id = $id;
+ $session->session_data = $session_data;
+ $session->created = common_sql_now();
+
+ return $session->insert();
+ } else {
+ $session->session_data = $session_data;
+
+ return $session->update();
+ }
+ }
+
+ static function destroy($id)
+ {
+ self::logdeb("Deleting session $id");
+
+ $session = Session::staticGet('id', $id);
+
+ if (!empty($session)) {
+ return $session->delete();
+ }
+ }
+
+ static function gc($maxlifetime)
+ {
+ self::logdeb("garbage collection (maxlifetime = $maxlifetime)");
+
+ $epoch = time() - $maxlifetime;
+
+ $qry = 'DELETE FROM session ' .
+ 'WHERE modified < "'.$epoch.'"';
+
+ $session = new Session();
+
+ $result = $session->query($qry);
+
+ self::logdeb("garbage collection result = $result");
+ }
+
+ static function setSaveHandler()
+ {
+ self::logdeb("setting save handlers");
+ $result = session_set_save_handler('Session::open', 'Session::close', 'Session::read',
+ 'Session::write', 'Session::destroy', 'Session::gc');
+ self::logdeb("save handlers result = $result");
+ return $result;
+ }
+}
diff --git a/classes/Status_network.php b/classes/Status_network.php
new file mode 100644
index 000000000..dbd722e88
--- /dev/null
+++ b/classes/Status_network.php
@@ -0,0 +1,197 @@
+<?php
+/**
+ * Table Definition for status_network
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2009, Control Yourself, 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); }
+
+class Status_network extends DB_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'status_network'; // table name
+ public $nickname; // varchar(64) primary_key not_null
+ public $hostname; // varchar(255) unique_key
+ public $pathname; // varchar(255) unique_key
+ public $dbhost; // varchar(255)
+ public $dbuser; // varchar(255)
+ public $dbpass; // varchar(255)
+ public $dbname; // varchar(255)
+ public $sitename; // varchar(255)
+ public $theme; // varchar(255)
+ public $logo; // varchar(255)
+ public $created; // datetime() not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Status_network',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ static $cache = null;
+ static $base = null;
+
+ static function setupDB($dbhost, $dbuser, $dbpass, $dbname, $servers)
+ {
+ global $config;
+
+ $config['db']['database_'.$dbname] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname";
+ $config['db']['ini_'.$dbname] = INSTALLDIR.'/classes/statusnet.ini';
+ $config['db']['table_status_network'] = $dbname;
+
+ self::$cache = new Memcache();
+
+ if (is_array($servers)) {
+ foreach($servers as $server) {
+ self::$cache->addServer($server);
+ }
+ } else {
+ self::$cache->addServer($servers);
+ }
+
+ self::$base = $dbname;
+ }
+
+ static function cacheKey($k, $v) {
+ return 'laconica:' . self::$base . ':status_network:'.$k.':'.$v;
+ }
+
+ static function memGet($k, $v)
+ {
+ $ck = self::cacheKey($k, $v);
+
+ $sn = self::$cache->get($ck);
+
+ if (empty($sn)) {
+ $sn = self::staticGet($k, $v);
+ if (!empty($sn)) {
+ self::$cache->set($ck, $sn);
+ }
+ }
+
+ return $sn;
+ }
+
+ function decache()
+ {
+ $keys = array('nickname', 'hostname', 'pathname');
+ foreach ($keys as $k) {
+ $ck = self::cacheKey($k, $this->$k);
+ self::$cache->delete($ck);
+ }
+ }
+
+ function update($orig=null)
+ {
+ if (is_object($orig)) {
+ $orig->decache(); # might be different keys
+ }
+ return parent::update($orig);
+ }
+
+ function delete()
+ {
+ $this->decache(); # while we still have the values!
+ return parent::delete();
+ }
+
+ static function setupSite($servername, $pathname, $wildcard)
+ {
+ global $config;
+
+ $sn = null;
+
+ // XXX I18N, probably not crucial for hostnames
+ // XXX This probably needs a tune up
+
+ if (0 == strncasecmp(strrev($wildcard), strrev($servername), strlen($wildcard))) {
+ // special case for exact match
+ if (0 == strcasecmp($servername, $wildcard)) {
+ $sn = self::memGet('nickname', '');
+ } else {
+ $parts = explode('.', $servername);
+ $sn = self::memGet('nickname', strtolower($parts[0]));
+ }
+ } else {
+ $sn = self::memGet('hostname', strtolower($servername));
+
+ if (empty($sn)) {
+ // Try for a no-www address
+ if (0 == strncasecmp($servername, 'www.', 4)) {
+ $sn = self::memGet('hostname', strtolower(substr($servername, 4)));
+ }
+ }
+ }
+
+ if (!empty($sn)) {
+ if (!empty($sn->hostname) && 0 != strcasecmp($sn->hostname, $servername)) {
+ $sn->redirectToHostname();
+ }
+ $dbhost = (empty($sn->dbhost)) ? 'localhost' : $sn->dbhost;
+ $dbuser = (empty($sn->dbuser)) ? $sn->nickname : $sn->dbuser;
+ $dbpass = $sn->dbpass;
+ $dbname = (empty($sn->dbname)) ? $sn->nickname : $sn->dbname;
+
+ $config['db']['database'] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname";
+
+ $config['site']['name'] = $sn->sitename;
+
+ if (!empty($sn->theme)) {
+ $config['site']['theme'] = $sn->theme;
+ }
+ if (!empty($sn->logo)) {
+ $config['site']['logo'] = $sn->logo;
+ }
+
+ return $sn;
+ } else {
+ return null;
+ }
+ }
+
+ // Code partially mooked from http://www.richler.de/en/php-redirect/
+ // (C) 2006 by Heiko Richler http://www.richler.de/
+ // LGPL
+
+ function redirectToHostname()
+ {
+ $destination = 'http://'.$this->hostname;
+ $destination .= $_SERVER['REQUEST_URI'];
+
+ $old = 'http'.
+ (($_SERVER['HTTPS'] == 'on') ? 'S' : '').
+ '://'.
+ $_SERVER['HTTP_HOST'].
+ $_SERVER['REQUEST_URI'].
+ $_SERVER['QUERY_STRING'];
+ if ($old == $destination) { // this would be a loop!
+ // error_log(...) ?
+ return false;
+ }
+
+ header('HTTP/1.1 301 Moved Permanently');
+ header("Location: $destination");
+
+ print "<a href='$destination'>$destination</a>\n";
+
+ exit;
+ }
+}
diff --git a/classes/Subscription.php b/classes/Subscription.php
index 3fe0d167f..d4580fcba 100644
--- a/classes/Subscription.php
+++ b/classes/Subscription.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/classes/User.php b/classes/User.php
index 59451258e..6c1f149e4 100644
--- a/classes/User.php
+++ b/classes/User.php
@@ -62,14 +62,13 @@ class User extends Memcached_DataObject
public $autosubscribe; // tinyint(1)
public $urlshorteningservice; // varchar(50) default_ur1.ca
public $inboxed; // tinyint(1)
+ public $design_id; // int(4)
+ public $viewdesigns; // tinyint(1) default_1
public $created; // datetime() not_null
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
@@ -406,19 +405,28 @@ class User extends Memcached_DataObject
return Notice::getStreamByIds($ids);
}
+ function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) {
+ $profile = $this->getProfile();
+ if (!$profile) {
+ return null;
+ } else {
+ return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id, $since);
+ }
+ }
+
function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
{
$profile = $this->getProfile();
if (!$profile) {
return null;
} else {
- return $profile->getNotices($offset, $limit, $since_id, $before_id);
+ return $profile->getNotices($offset, $limit, $since_id, $before_id, $since);
}
}
- function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE)
+ function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE, $own=false)
{
- $ids = Fave::stream($this->id, $offset, $limit);
+ $ids = Fave::stream($this->id, $offset, $limit, $own);
return Notice::getStreamByIds($ids);
}
@@ -434,6 +442,33 @@ class User extends Memcached_DataObject
$qry =
'SELECT notice.* ' .
'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' .
+ 'WHERE subscription.subscriber = %d ' .
+ 'AND notice.is_local != ' . NOTICE_GATEWAY;
+ return Notice::getStream(sprintf($qry, $this->id),
+ 'user:notices_with_friends:' . $this->id,
+ $offset, $limit, $since_id, $before_id,
+ $order, $since);
+ } else if ($enabled === true ||
+ ($enabled == 'transitional' && $this->inboxed == 1)) {
+
+ $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, false);
+
+ return Notice::getStreamByIds($ids);
+ }
+ }
+
+ function noticeInbox($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+ {
+ $enabled = common_config('inboxes', 'enabled');
+
+ // Complicated code, depending on whether we support inboxes yet
+ // XXX: make this go away when inboxes become mandatory
+
+ if ($enabled === false ||
+ ($enabled == 'transitional' && $this->inboxed == 0)) {
+ $qry =
+ 'SELECT notice.* ' .
+ 'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' .
'WHERE subscription.subscriber = %d ';
return Notice::getStream(sprintf($qry, $this->id),
'user:notices_with_friends:' . $this->id,
@@ -442,7 +477,7 @@ class User extends Memcached_DataObject
} else if ($enabled === true ||
($enabled == 'transitional' && $this->inboxed == 1)) {
- $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
+ $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since, true);
return Notice::getStreamByIds($ids);
}
@@ -456,7 +491,11 @@ class User extends Memcached_DataObject
// ;last cache, too
$cache->delete(common_cache_key('fave:ids_by_user:'.$this->id));
$cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last'));
+ $cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id));
+ $cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id.';last'));
}
+ $profile = $this->getProfile();
+ $profile->blowFaveCount();
}
function getSelfTags()
@@ -565,50 +604,16 @@ class User extends Memcached_DataObject
function getSubscriptions($offset=0, $limit=null)
{
- $qry =
- 'SELECT profile.* ' .
- 'FROM profile JOIN subscription ' .
- 'ON profile.id = subscription.subscribed ' .
- 'WHERE subscription.subscriber = %d ' .
- 'AND subscription.subscribed != subscription.subscriber ' .
- 'ORDER BY subscription.created DESC ';
-
- if (common_config('db','type') == 'pgsql') {
- $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
- } else {
- $qry .= ' LIMIT ' . $offset . ', ' . $limit;
- }
-
- $profile = new Profile();
-
- $profile->query(sprintf($qry, $this->id));
-
- return $profile;
+ $profile = $this->getProfile();
+ assert(!empty($profile));
+ return $profile->getSubscriptions($offset, $limit);
}
function getSubscribers($offset=0, $limit=null)
{
- $qry =
- 'SELECT profile.* ' .
- 'FROM profile JOIN subscription ' .
- 'ON profile.id = subscription.subscriber ' .
- 'WHERE subscription.subscribed = %d ' .
- 'AND subscription.subscribed != subscription.subscriber ' .
- 'ORDER BY subscription.created DESC ';
-
- if ($offset) {
- if (common_config('db','type') == 'pgsql') {
- $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
- } else {
- $qry .= ' LIMIT ' . $offset . ', ' . $limit;
- }
- }
-
- $profile = new Profile();
-
- $cnt = $profile->query(sprintf($qry, $this->id));
-
- return $profile;
+ $profile = $this->getProfile();
+ assert(!empty($profile));
+ return $profile->getSubscribers($offset, $limit);
}
function getTaggedSubscribers($tag, $offset=0, $limit=null)
@@ -675,4 +680,9 @@ class User extends Memcached_DataObject
return ($cnt > 0);
}
+
+ function getDesign()
+ {
+ return Design::staticGet('id', $this->design_id);
+ }
}
diff --git a/classes/User_group.php b/classes/User_group.php
index a135015ba..27b444705 100755..100644
--- a/classes/User_group.php
+++ b/classes/User_group.php
@@ -19,6 +19,7 @@ class User_group extends Memcached_DataObject
public $homepage_logo; // varchar(255)
public $stream_logo; // varchar(255)
public $mini_logo; // varchar(255)
+ public $design_id; // int(4)
public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
@@ -125,6 +126,53 @@ class User_group extends Memcached_DataObject
return $members;
}
+ function getAdmins($offset=0, $limit=null)
+ {
+ $qry =
+ 'SELECT profile.* ' .
+ 'FROM profile JOIN group_member '.
+ 'ON profile.id = group_member.profile_id ' .
+ 'WHERE group_member.group_id = %d ' .
+ 'AND group_member.is_admin = 1 ' .
+ 'ORDER BY group_member.modified ASC ';
+
+ if ($limit != null) {
+ if (common_config('db','type') == 'pgsql') {
+ $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+ } else {
+ $qry .= ' LIMIT ' . $offset . ', ' . $limit;
+ }
+ }
+
+ $admins = new Profile();
+
+ $admins->query(sprintf($qry, $this->id));
+ return $admins;
+ }
+
+ function getBlocked($offset=0, $limit=null)
+ {
+ $qry =
+ 'SELECT profile.* ' .
+ 'FROM profile JOIN group_block '.
+ 'ON profile.id = group_block.blocked ' .
+ 'WHERE group_block.group_id = %d ' .
+ 'ORDER BY group_block.modified DESC ';
+
+ if ($limit != null) {
+ if (common_config('db','type') == 'pgsql') {
+ $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+ } else {
+ $qry .= ' LIMIT ' . $offset . ', ' . $limit;
+ }
+ }
+
+ $blocked = new Profile();
+
+ $blocked->query(sprintf($qry, $this->id));
+ return $blocked;
+ }
+
function setOriginal($filename)
{
$imagefile = new ImageFile($this->id, Avatar::path($filename));
@@ -137,4 +185,113 @@ class User_group extends Memcached_DataObject
common_debug(common_log_objstring($this));
return $this->update($orig);
}
+
+ function getBestName()
+ {
+ return ($this->fullname) ? $this->fullname : $this->nickname;
+ }
+
+ function getAliases()
+ {
+ $aliases = array();
+
+ // XXX: cache this
+
+ $alias = new Group_alias();
+
+ $alias->group_id = $this->id;
+
+ if ($alias->find()) {
+ while ($alias->fetch()) {
+ $aliases[] = $alias->alias;
+ }
+ }
+
+ $alias->free();
+
+ return $aliases;
+ }
+
+ function setAliases($newaliases) {
+
+ $newaliases = array_unique($newaliases);
+
+ $oldaliases = $this->getAliases();
+
+ # Delete stuff that's old that not in new
+
+ $to_delete = array_diff($oldaliases, $newaliases);
+
+ # Insert stuff that's in new and not in old
+
+ $to_insert = array_diff($newaliases, $oldaliases);
+
+ $alias = new Group_alias();
+
+ $alias->group_id = $this->id;
+
+ foreach ($to_delete as $delalias) {
+ $alias->alias = $delalias;
+ $result = $alias->delete();
+ if (!$result) {
+ common_log_db_error($alias, 'DELETE', __FILE__);
+ return false;
+ }
+ }
+
+ foreach ($to_insert as $insalias) {
+ $alias->alias = $insalias;
+ $result = $alias->insert();
+ if (!$result) {
+ common_log_db_error($alias, 'INSERT', __FILE__);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ static function getForNickname($nickname)
+ {
+ $nickname = common_canonical_nickname($nickname);
+ $group = User_group::staticGet('nickname', $nickname);
+ if (!empty($group)) {
+ return $group;
+ }
+ $alias = Group_alias::staticGet('alias', $nickname);
+ if (!empty($alias)) {
+ return User_group::staticGet('id', $alias->group_id);
+ }
+ return null;
+ }
+
+ function getDesign()
+ {
+ return Design::staticGet('id', $this->design_id);
+ }
+
+ function getUserMembers()
+ {
+ // XXX: cache this
+
+ $user = new User();
+
+ $qry =
+ 'SELECT id ' .
+ 'FROM user JOIN group_member '.
+ 'ON user.id = group_member.profile_id ' .
+ 'WHERE group_member.group_id = %d ';
+
+ $user->query(sprintf($qry, $this->id));
+
+ $ids = array();
+
+ while ($user->fetch()) {
+ $ids[] = $user->id;
+ }
+
+ $user->free();
+
+ return $ids;
+ }
}
diff --git a/classes/laconica.ini b/classes/laconica.ini
index c05419588..766bed75d 100755..100644
--- a/classes/laconica.ini
+++ b/classes/laconica.ini
@@ -38,6 +38,19 @@ modified = 384
[consumer__keys]
consumer_key = K
+[design]
+id = 129
+backgroundcolor = 1
+contentcolor = 1
+sidebarcolor = 1
+textcolor = 1
+linkcolor = 1
+backgroundimage = 2
+disposition = 17
+
+[design__keys]
+id = N
+
[fave]
notice_id = 129
user_id = 129
@@ -47,6 +60,68 @@ modified = 384
notice_id = K
user_id = K
+[file]
+id = 129
+url = 2
+mimetype = 2
+size = 1
+title = 2
+date = 1
+protected = 1
+filename = 2
+modified = 384
+
+[file__keys]
+id = N
+
+[file_oembed]
+file_id = 129
+version = 2
+type = 2
+provider = 2
+provider_url = 2
+width = 1
+height = 1
+html = 34
+title = 2
+author_name = 2
+author_url = 2
+url = 2
+modified = 384
+
+[file_oembed__keys]
+file_id = K
+
+[file_redirection]
+url = 130
+file_id = 1
+redirections = 1
+httpcode = 1
+modified = 384
+
+[file_redirection__keys]
+url = K
+
+[file_thumbnail]
+file_id = 129
+url = 2
+width = 1
+height = 1
+modified = 384
+
+[file_thumbnail__keys]
+file_id = K
+url = U
+
+[file_to_post]
+file_id = 129
+post_id = 129
+modified = 384
+
+[file_to_post__keys]
+file_id = K
+post_id = K
+
[foreign_link]
user_id = 129
foreign_id = 129
@@ -100,6 +175,24 @@ id = K
service = K
uri = U
+[group_alias]
+alias = 130
+group_id = 129
+modified = 384
+
+[group_alias__keys]
+alias = K
+
+[group_block]
+group_id = 129
+blocked = 129
+blocker = 129
+modified = 384
+
+[group_block__keys]
+group_id = K
+blocked = K
+
[group_inbox]
group_id = 129
notice_id = 129
@@ -170,6 +263,7 @@ modified = 384
reply_to = 1
is_local = 17
source = 2
+conversation = 1
[notice__keys]
id = N
@@ -286,6 +380,15 @@ replied_id = 1
notice_id = K
profile_id = K
+[session]
+id = 130
+session_data = 34
+created = 142
+modified = 384
+
+[session__keys]
+id = K
+
[sms_carrier]
id = 129
name = 2
@@ -353,6 +456,8 @@ uri = 2
autosubscribe = 17
urlshorteningservice = 2
inboxed = 17
+design_id = 1
+viewdesigns = 17
created = 142
modified = 384
@@ -376,6 +481,7 @@ original_logo = 2
homepage_logo = 2
stream_logo = 2
mini_logo = 2
+design_id = 1
created = 142
modified = 384
diff --git a/classes/laconica.links.ini b/classes/laconica.links.ini
index 173b18726..95c63f3c0 100644
--- a/classes/laconica.links.ini
+++ b/classes/laconica.links.ini
@@ -41,3 +41,17 @@ subscribed = profile:id
[fave]
notice_id = notice:id
user_id = user:id
+
+[file_oembed]
+file_id = file:id
+
+[file_redirection]
+file_id = file:id
+
+[file_thumbnail]
+file_id = file:id
+
+[file_to_post]
+file_id = file:id
+post_id = notice:id
+
diff --git a/classes/statusnet.ini b/classes/statusnet.ini
new file mode 100644
index 000000000..8123265e4
--- /dev/null
+++ b/classes/statusnet.ini
@@ -0,0 +1,18 @@
+[status_network]
+nickname = 130
+hostname = 2
+pathname = 2
+dbhost = 2
+dbuser = 2
+dbpass = 2
+dbname = 2
+sitename = 2
+theme = 2
+logo = 2
+created = 142
+modified = 384
+
+[status_network__keys]
+nickname = K
+hostname = U
+pathname = U
diff --git a/config.php.sample b/config.php.sample
index e70d9ab46..57aa6a6c8 100644
--- a/config.php.sample
+++ b/config.php.sample
@@ -3,190 +3,237 @@
if (!defined('LACONICA')) { exit(1); }
-#If you have downloaded libraries in random little places, you
-#can add the paths here
+// If you have downloaded libraries in random little places, you
+// can add the paths here
-#$extra_path = array("/opt/php-openid-2.0.1", "/usr/local/share/php");
-#set_include_path(implode(PATH_SEPARATOR, $extra_path) . PATH_SEPARATOR . get_include_path());
+// $extra_path = array("/opt/php-openid-2.0.1", "/usr/local/share/php");
+// set_include_path(implode(PATH_SEPARATOR, $extra_path) . PATH_SEPARATOR . get_include_path());
-# We get called by common.php, $config is a tree with lots of config
-# options
-# These are for configuring your URLs
+// We get called by common.php, $config is a tree with lots of config
+// options
+// These are for configuring your URLs
$config['site']['name'] = 'Just another Laconica microblog';
$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...
-#$config['site']['broughtby'] = 'Individual or Company';
-#$config['site']['broughtbyurl'] = 'http://example.net/';
-#If you don't want to let users register (say, for a one-person install)
-#Crude but effective -- register everybody, then lock down
-#$config['site']['closed'] = true;
-#Only allow registration for people invited by another user
-#$config['site']['inviteonly'] = true;
-#Make the site invisible to non-logged-in users
-#$config['site']['private'] = true;
-
-# 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;
-
-#To set your own logo, overriding the one in the theme
-#$config['site']['logo'] = '/mylogo.png';
-
-# 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
+// $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...
+// $config['site']['broughtby'] = 'Individual or Company';
+// $config['site']['broughtbyurl'] = 'http://example.net/';
+// If you don't want to let users register (say, for a one-person install)
+// Crude but effective -- register everybody, then lock down
+// $config['site']['closed'] = true;
+// Only allow registration for people invited by another user
+// $config['site']['inviteonly'] = true;
+// Make the site invisible to non-logged-in users
+// $config['site']['private'] = true;
+
+// If you want logging sent to a file instead of syslog
+// $config['site']['logfile'] = '/tmp/laconica.log';
+
+// Change the syslog facility that Laconica logs to (default is LOG_USER)
+// $config['syslog']['facility'] = LOG_LOCAL7;
+
+// Enables extra log information, for example full details of PEAR DB errors
+// $config['site']['logdebug'] = true;
+
+// To set your own logo, overriding the one in the theme
+// $config['site']['logo'] = '/mylogo.png';
+
+// 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
$config['db']['database'] = 'mysql://laconica:microblog@localhost/laconica';
-#$config['db']['ini_your_db_name'] = $config['db']['schema_location'].'/laconica.ini';
-# *** WARNING *** WARNING *** WARNING *** WARNING ***
-# Setting debug to a non-zero value will expose your DATABASE PASSWORD to Web users.
-# !!!!!! DO NOT SET THIS ON PRODUCTION SERVERS !!!!!! DB_DataObject's bug, btw, not
-# ours.
-# *** WARNING *** WARNING *** WARNING *** WARNING ***
-#$config['db']['debug'] = 0;
-#$config['db']['db_driver'] = 'MDB2';
-
-#Database type. For mysql, these defaults are fine. For postgresql, set
-#'quote_identifiers' to true and 'type' to 'pgsql':
-#$config['db']['quote_identifiers'] = false;
-#$config['db']['type'] = 'mysql';
-
-#session_set_cookie_params(0, '/'. $config['site']['path'] .'/');
-
-#Standard fancy-url clashes prevented by not allowing nicknames on a blacklist
-#Add your own here. Note: empty array by default
-#$config['nickname']['blacklist'][] = 'scobleizer';
-
-# sphinx search
+// $config['db']['ini_your_db_name'] = $config['db']['schema_location'].'/laconica.ini';
+// *** WARNING *** WARNING *** WARNING *** WARNING ***
+// Setting debug to a non-zero value will expose your DATABASE PASSWORD to Web users.
+// !!!!!! DO NOT SET THIS ON PRODUCTION SERVERS !!!!!! DB_DataObject's bug, btw, not
+// ours.
+// *** WARNING *** WARNING *** WARNING *** WARNING ***
+// $config['db']['debug'] = 0;
+// $config['db']['db_driver'] = 'MDB2';
+
+// Database type. For mysql, these defaults are fine. For postgresql, set
+// 'quote_identifiers' to true and 'type' to 'pgsql':
+// $config['db']['quote_identifiers'] = false;
+// $config['db']['type'] = 'mysql';
+
+// session_set_cookie_params(0, '/'. $config['site']['path'] .'/');
+
+// Standard fancy-url clashes prevented by not allowing nicknames on a blacklist
+// Add your own here. Note: empty array by default
+// $config['nickname']['blacklist'][] = 'scobleizer';
+
+// sphinx search
$config['sphinx']['enabled'] = false;
$config['sphinx']['server'] = 'localhost';
$config['sphinx']['port'] = 3312;
-# Users to populate the 'Featured' tab
-#$config['nickname']['featured'][] = 'scobleizer';
-
-# xmpp
-#$config['xmpp']['enabled'] = false;
-#$config['xmpp']['server'] = 'server.example.net';
-#$config['xmpp']['host'] = NULL; # Only set if different from server
-#$config['xmpp']['port'] = 5222;
-#$config['xmpp']['user'] = 'update';
-#$config['xmpp']['encryption'] = false;
-#$config['xmpp']['resource'] = 'uniquename';
-#$config['xmpp']['password'] = 'blahblahblah';
-#$config['xmpp']['public'][] = 'someindexer@example.net';
-#$config['xmpp']['debug'] = false;
-
-#Default locale info
-#$config['site']['timezone'] = 'Pacific/Auckland';
-#$config['site']['language'] = 'en_NZ';
-
-#Email info, used for all outbound email
-#$config['mail']['notifyfrom'] = 'microblog@example.net';
-#$config['mail']['domain'] = 'microblog.example.net';
-# See http://pear.php.net/manual/en/package.mail.mail.factory.php for options
-#$config['mail']['backend'] = 'smtp';
-#$config['mail']['params'] = array(
-# 'host' => 'localhost',
-# 'port' => 25,
-# );
-#For incoming email, if enabled. Defaults to site server name.
-#$config['mail']['domain'] = 'incoming.example.net';
-
-#exponential decay factor for tags, default 10 days
-#raise this if traffic is slow, lower it if it's fast
-#$config['tag']['dropoff'] = 86400.0 * 10;
-
-#exponential decay factor for popular (most favorited notices)
-#default 10 days -- similar to tag dropoff
-#$config['popular']['dropoff'] = 86400.0 * 10;
-
-#optionally show non-local messages in public timeline
-#$config['public']['localonly'] = false;
-
-#hide certain users from public pages, by ID
-#$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.
-#$config['queue']['enabled'] = true;
-
-#The following customise the behaviour of the various daemons:
-#$config['daemon']['piddir'] = '/var/run';
-#$config['daemon']['user'] = false;
-#$config['daemon']['group'] = false;
-
-#For installations with high traffic, laconica can use MemCached to cache
-#frequently requested information. Only enable the following if you have
-#MemCached up and running:
-#$config['memcached']['enabled'] = false;
-#$config['memcached']['server'] = 'localhost';
-#$config['memcached']['port'] = 11211;
-
-#Twitter integration source attribute. Note: default is Laconica
-#$config['integration']['source'] = 'Laconica';
-
-# Edit throttling. Off by default. If turned on, you can only post 20 notices
-# every 10 minutes. Admins may want to play with the settings to minimize inconvenience for
-# real users without getting uncontrollable floods from spammers or runaway bots.
-
-#$config['throttle']['enabled'] = true;
-#$config['throttle']['count'] = 100;
-#$config['throttle']['timespan'] = 3600;
-
-# List of users banned from posting (nicknames and/or IDs)
-#$config['profile']['banned'][] = 'hacker';
-#$config['profile']['banned'][] = 12345;
-
-# 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');
-
-# Use Templating (template: /tpl/index.php)
-# require_once('plugins/TemplatePlugin.php');
-# $tpl = new TemplatePlugin();
-
-#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;
-
-#Base string for minting Tag URIs in Atom feeds. Defaults to
-#"yourserver,2009". This needs to be configured properly for your Atom
-#feeds to validate. See: http://www.faqs.org/rfcs/rfc4151.html and
-#http://taguri.org/ Examples:
-#$config['integration']['taguri'] = 'example.net,2008';
-#$config['integration']['taguri'] = 'admin@example.net,2009-03-09'
-
-#Don't use SSL
-#$config['site']['ssl'] = 'never';
-#Use SSL only for sensitive pages (like login, password change)
-#$config['site']['ssl'] = 'sometimes';
-#Use SSL for all pages
-#$config['site']['ssl'] = 'always';
-
-#Use a different hostname for SSL-encrypted pages
-#$config['site']['sslserver'] = 'secure.example.org';
+// Users to populate the 'Featured' tab
+// $config['nickname']['featured'][] = 'scobleizer';
+
+// xmpp
+// $config['xmpp']['enabled'] = false;
+// $config['xmpp']['server'] = 'server.example.net';
+// $config['xmpp']['host'] = NULL; // Only set if different from server
+// $config['xmpp']['port'] = 5222;
+// $config['xmpp']['user'] = 'update';
+// $config['xmpp']['encryption'] = false;
+// $config['xmpp']['resource'] = 'uniquename';
+// $config['xmpp']['password'] = 'blahblahblah';
+// $config['xmpp']['public'][] = 'someindexer@example.net';
+// $config['xmpp']['debug'] = false;
+
+// Turn off invites
+// $config['invite']['enabled'] = false;
+
+// Default locale info
+// $config['site']['timezone'] = 'Pacific/Auckland';
+// $config['site']['language'] = 'en_NZ';
+
+// Email info, used for all outbound email
+// $config['mail']['notifyfrom'] = 'microblog@example.net';
+// $config['mail']['domain'] = 'microblog.example.net';
+// See http://pear.php.net/manual/en/package.mail.mail.factory.php for options
+// $config['mail']['backend'] = 'smtp';
+// $config['mail']['params'] = array(
+// 'host' => 'localhost',
+// 'port' => 25,
+// );
+// For incoming email, if enabled. Defaults to site server name.
+// $config['mail']['domain'] = 'incoming.example.net';
+
+// exponential decay factor for tags, default 10 days
+// raise this if traffic is slow, lower it if it's fast
+// $config['tag']['dropoff'] = 86400.0 * 10;
+
+// exponential decay factor for popular (most favorited notices)
+// default 10 days -- similar to tag dropoff
+// $config['popular']['dropoff'] = 86400.0 * 10;
+
+// optionally show non-local messages in public timeline
+// $config['public']['localonly'] = false;
+
+// hide certain users from public pages, by ID
+// $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.
+// $config['queue']['enabled'] = true;
+
+// Queue subsystem
+// subsystems: internal (default) or stomp
+// using stomp requires an external message queue server
+// $config['queue']['subsystem'] = 'stomp';
+// $config['queue']['stomp_server'] = 'tcp://localhost:61613';
+// use different queue_basename for each laconica instance managed by the server
+// $config['queue']['queue_basename'] = 'laconica';
+
+// The following customise the behaviour of the various daemons:
+// $config['daemon']['piddir'] = '/var/run';
+// $config['daemon']['user'] = false;
+// $config['daemon']['group'] = false;
+
+// For installations with high traffic, laconica can use MemCached to cache
+// frequently requested information. Only enable the following if you have
+// MemCached up and running:
+// $config['memcached']['enabled'] = false;
+// $config['memcached']['server'] = 'localhost';
+// $config['memcached']['port'] = 11211;
+
+// Twitter integration source attribute. Note: default is Laconica
+// $config['integration']['source'] = 'Laconica';
+
+// Enable bidirectional Twitter bridge
+//
+// NOTE: if you enable this you must also set $config['avatar']['path']
+//
+// $config['twitterbridge']['enabled'] = true;
+
+// Edit throttling. Off by default. If turned on, you can only post 20 notices
+// every 10 minutes. Admins may want to play with the settings to minimize inconvenience for
+// real users without getting uncontrollable floods from spammers or runaway bots.
+
+// $config['throttle']['enabled'] = true;
+// $config['throttle']['count'] = 100;
+// $config['throttle']['timespan'] = 3600;
+
+// List of users banned from posting (nicknames and/or IDs)
+// $config['profile']['banned'][] = 'hacker';
+// $config['profile']['banned'][] = 12345;
+
+// 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');
+
+// Use Templating (template: /tpl/index.php)
+// require_once('plugins/TemplatePlugin.php');
+// $tpl = new TemplatePlugin();
+
+// 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;
+
+// Base string for minting Tag URIs in Atom feeds. Defaults to
+// "yourserver,2009". This needs to be configured properly for your Atom
+// feeds to validate. See: http://www.faqs.org/rfcs/rfc4151.html and
+// http://taguri.org/ Examples:
+// $config['integration']['taguri'] = 'example.net,2008';
+// $config['integration']['taguri'] = 'admin@example.net,2009-03-09'
+
+// Don't use SSL
+// $config['site']['ssl'] = 'never';
+// Use SSL only for sensitive pages (like login, password change)
+// $config['site']['ssl'] = 'sometimes';
+// Use SSL for all pages
+// $config['site']['ssl'] = 'always';
+
+// Use a different hostname for SSL-encrypted pages
+// $config['site']['sslserver'] = 'secure.example.org';
+
+// If you have a lot of status networks on the same server, you can
+// store the site data in a database and switch as follows
+// Status_network::setupDB('localhost', 'statusnet', 'statuspass', 'statusnet');
+// if (!Status_network::setupSite($_server, $_path)) {
+// print "Error\n";
+// exit(1);
+// }
+
+// How often to send snapshots; in # of web hits. Ideally,
+// try to do this once per month (that is, make this equal to number
+// of hits per month)
+// $config['snapshot']['frequency'] = 10000;
+// If you don't want to report statistics to the central server, uncomment.
+// $config['snapshot']['run'] = 'never';
+// If you want to report statistics in a cron job instead.
+// $config['snapshot']['run'] = 'cron';
+
+// Support for file uploads (attachments),
+// select supported mimetypes and quotas (in bytes)
+// $config['attachments']['supported'] = array('image/png', 'application/ogg');
+// $config['attachments']['file_quota'] = 5000000;
+// $config['attachments']['user_quota'] = 50000000;
+// $config['attachments']['monthly_quota'] = 15000000;
+// $config['attachments']['uploads'] = true;
+
+// $config['oohembed']['endpoint'] = 'http://oohembed.com/oohembed/';
diff --git a/db/074to080.sql b/db/074to080.sql
new file mode 100644
index 000000000..ff0819159
--- /dev/null
+++ b/db/074to080.sql
@@ -0,0 +1,109 @@
+alter table user
+ add column design_id integer comment 'id of a design' references design(id),
+ add column viewdesigns tinyint default 1 comment 'whether to view user-provided designs';
+
+alter table notice add column
+ conversation integer comment 'id of root notice in this conversation' references notice (id),
+ add index notice_conversation_idx (conversation);
+
+alter table foreign_user
+ modify column id bigint not null comment 'unique numeric key on foreign service';
+
+alter table foreign_link
+ modify column foreign_id bigint unsigned comment 'link to user on foreign service, if exists';
+
+alter table user_group
+ add column design_id integer comment 'id of a design' references design(id);
+
+create table file (
+ id integer primary key auto_increment,
+ url varchar(255) comment 'destination URL after following redirections',
+ mimetype varchar(50) comment 'mime type of resource',
+ size integer comment 'size of resource when available',
+ title varchar(255) comment 'title of resource when available',
+ date integer(11) comment 'date of resource according to http query',
+ protected integer(1) comment 'true when URL is private (needs login)',
+ filename varchar(255) comment 'if a local file, name of the file',
+ modified timestamp comment 'date this record was modified',
+
+ unique(url)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
+
+create table file_oembed (
+ file_id integer primary key comment 'oEmbed for that URL/file' references file (id),
+ version varchar(20) comment 'oEmbed spec. version',
+ type varchar(20) comment 'oEmbed type: photo, video, link, rich',
+ provider varchar(50) comment 'name of this oEmbed provider',
+ provider_url varchar(255) comment 'URL of this oEmbed provider',
+ width integer comment 'width of oEmbed resource when available',
+ height integer comment 'height of oEmbed resource when available',
+ html text comment 'html representation of this oEmbed resource when applicable',
+ title varchar(255) comment 'title of oEmbed resource when available',
+ author_name varchar(50) comment 'author name for this oEmbed resource',
+ author_url varchar(255) comment 'author URL for this oEmbed resource',
+ url varchar(255) comment 'URL for this oEmbed resource when applicable (photo, link)',
+ modified timestamp comment 'date this record was modified'
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
+
+create table file_redirection (
+
+ url varchar(255) primary key comment 'short URL (or any other kind of redirect) for file (id)',
+ file_id integer comment 'short URL for what URL/file' references file (id),
+ redirections integer comment 'redirect count',
+ httpcode integer comment 'HTTP status code (20x, 30x, etc.)',
+ modified timestamp comment 'date this record was modified'
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table file_thumbnail (
+
+ file_id integer primary key comment 'thumbnail for what URL/file' references file (id),
+ url varchar(255) comment 'URL of thumbnail',
+ width integer comment 'width of thumbnail',
+ height integer comment 'height of thumbnail',
+ modified timestamp comment 'date this record was modified',
+
+ unique(url)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table file_to_post (
+
+ file_id integer comment 'id of URL/file' references file (id),
+ post_id integer comment 'id of the notice it belongs to' references notice (id),
+ modified timestamp comment 'date this record was modified',
+
+ constraint primary key (file_id, post_id)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table design (
+ id integer primary key auto_increment comment 'design ID',
+ backgroundcolor integer comment 'main background color',
+ contentcolor integer comment 'content area background color',
+ sidebarcolor integer comment 'sidebar background color',
+ textcolor integer comment 'text color',
+ linkcolor integer comment 'link color',
+ backgroundimage varchar(255) comment 'background image, if any',
+ disposition tinyint default 1 comment 'bit 1 = hide background image, bit 2 = display background image, bit 4 = tile background image'
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table group_block (
+ group_id integer not null comment 'group profile is blocked from' references user_group (id),
+ blocked integer not null comment 'profile that is blocked' references profile (id),
+ blocker integer not null comment 'user making the block' references user (id),
+ modified timestamp comment 'date of blocking',
+
+ constraint primary key (group_id, blocked)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table group_alias (
+
+ alias varchar(64) primary key comment 'additional nickname for the group',
+ group_id integer not null comment 'group profile is blocked from' references user_group (id),
+ modified timestamp comment 'date alias was created',
+
+ index group_alias_group_id_idx (group_id)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
diff --git a/db/foreign_services.sql b/db/foreign_services.sql
index 557ede024..79c04cee5 100644
--- a/db/foreign_services.sql
+++ b/db/foreign_services.sql
@@ -2,4 +2,5 @@ insert into foreign_service
(id, name, description, created)
values
('1','Twitter', 'Twitter Micro-blogging service', now()),
- ('2','Facebook', 'Facebook', now());
+ ('2','Facebook', 'Facebook', now()),
+ ('3','FacebookConnect', 'Facebook Connect', now());
diff --git a/db/innodb.sql b/db/innodb.sql
new file mode 100644
index 000000000..f3ab6cd69
--- /dev/null
+++ b/db/innodb.sql
@@ -0,0 +1,2 @@
+alter table profile drop index nickname, engine=InnoDB;
+alter table notice drop index content, engine=InnoDB;
diff --git a/db/laconica.sql b/db/laconica.sql
index c9730098e..2c04f680a 100644
--- a/db/laconica.sql
+++ b/db/laconica.sql
@@ -41,6 +41,7 @@ create table sms_carrier (
/* local users */
create table user (
+
id integer primary key comment 'foreign key to profile table' references profile (id),
nickname varchar(64) unique key comment 'nickname or username, duped in profile',
password varchar(255) comment 'salted password, can be null for OpenID users',
@@ -69,6 +70,9 @@ create table user (
autosubscribe tinyint 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 tinyint default 0 comment 'has an inbox been created for this user?',
+ design_id integer comment 'id of a design' references design(id),
+ viewdesigns tinyint default 1 comment 'whether to view user-provided designs',
+
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
@@ -114,8 +118,10 @@ create table notice (
reply_to integer comment 'notice replied to (usually a guess)' references notice (id),
is_local tinyint default 0 comment 'notice was generated by a user',
source varchar(32) comment 'source of comment, like "web", "im", or "clientname"',
+ conversation integer comment 'id of root notice in this conversation' references notice (id),
index notice_profile_id_idx (profile_id),
+ index notice_conversation_idx (conversation),
index notice_created_idx (created),
index notice_replyto_idx (reply_to),
FULLTEXT(content)
@@ -271,7 +277,7 @@ create table foreign_service (
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table foreign_user (
- id int not null comment 'unique numeric key on foreign service',
+ id bigint not null comment 'unique numeric key on foreign service',
service int not null comment 'foreign key to service' references foreign_service(id),
uri varchar(255) not null unique key comment 'identifying URI',
nickname varchar(255) comment 'nickname on foreign service',
@@ -283,7 +289,7 @@ create table foreign_user (
create table foreign_link (
user_id int comment 'link to user on this system, if exists' references user (id),
- foreign_id int comment 'link ' references foreign_user(id),
+ foreign_id bigint unsigned comment 'link to user on foreign service, if exists' references foreign_user(id),
service int not null comment 'foreign key to service' references foreign_service(id),
credentials varchar(255) comment 'authc credentials, typically a password',
noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies',
@@ -381,6 +387,7 @@ create table user_group (
homepage_logo varchar(255) comment 'homepage (profile) size logo',
stream_logo varchar(255) comment 'stream-sized logo',
mini_logo varchar(255) comment 'mini logo',
+ design_id integer comment 'id of a design' references design(id),
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
@@ -423,3 +430,108 @@ create table group_inbox (
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+create table file (
+
+ id integer primary key auto_increment,
+ url varchar(255) comment 'destination URL after following redirections',
+ mimetype varchar(50) comment 'mime type of resource',
+ size integer comment 'size of resource when available',
+ title varchar(255) comment 'title of resource when available',
+ date integer(11) comment 'date of resource according to http query',
+ protected integer(1) comment 'true when URL is private (needs login)',
+ filename varchar(255) comment 'if a local file, name of the file',
+
+ modified timestamp comment 'date this record was modified',
+
+ unique(url)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
+
+create table file_oembed (
+ file_id integer primary key comment 'oEmbed for that URL/file' references file (id),
+ version varchar(20) comment 'oEmbed spec. version',
+ type varchar(20) comment 'oEmbed type: photo, video, link, rich',
+ provider varchar(50) comment 'name of this oEmbed provider',
+ provider_url varchar(255) comment 'URL of this oEmbed provider',
+ width integer comment 'width of oEmbed resource when available',
+ height integer comment 'height of oEmbed resource when available',
+ html text comment 'html representation of this oEmbed resource when applicable',
+ title varchar(255) comment 'title of oEmbed resource when available',
+ author_name varchar(50) comment 'author name for this oEmbed resource',
+ author_url varchar(255) comment 'author URL for this oEmbed resource',
+ url varchar(255) comment 'URL for this oEmbed resource when applicable (photo, link)',
+ modified timestamp comment 'date this record was modified'
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
+
+create table file_redirection (
+
+ url varchar(255) primary key comment 'short URL (or any other kind of redirect) for file (id)',
+ file_id integer comment 'short URL for what URL/file' references file (id),
+ redirections integer comment 'redirect count',
+ httpcode integer comment 'HTTP status code (20x, 30x, etc.)',
+ modified timestamp comment 'date this record was modified'
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table file_thumbnail (
+
+ file_id integer primary key comment 'thumbnail for what URL/file' references file (id),
+ url varchar(255) comment 'URL of thumbnail',
+ width integer comment 'width of thumbnail',
+ height integer comment 'height of thumbnail',
+ modified timestamp comment 'date this record was modified',
+
+ unique(url)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table file_to_post (
+
+ file_id integer comment 'id of URL/file' references file (id),
+ post_id integer comment 'id of the notice it belongs to' references notice (id),
+ modified timestamp comment 'date this record was modified',
+
+ constraint primary key (file_id, post_id)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table design (
+ id integer primary key auto_increment comment 'design ID',
+ backgroundcolor integer comment 'main background color',
+ contentcolor integer comment 'content area background color',
+ sidebarcolor integer comment 'sidebar background color',
+ textcolor integer comment 'text color',
+ linkcolor integer comment 'link color',
+ backgroundimage varchar(255) comment 'background image, if any',
+ disposition tinyint default 1 comment 'bit 1 = hide background image, bit 2 = display background image, bit 4 = tile background image'
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table group_block (
+ group_id integer not null comment 'group profile is blocked from' references user_group (id),
+ blocked integer not null comment 'profile that is blocked' references profile (id),
+ blocker integer not null comment 'user making the block' references user (id),
+ modified timestamp comment 'date of blocking',
+
+ constraint primary key (group_id, blocked)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table group_alias (
+
+ alias varchar(64) primary key comment 'additional nickname for the group',
+ group_id integer not null comment 'group profile is blocked from' references user_group (id),
+ modified timestamp comment 'date alias was created',
+
+ index group_alias_group_id_idx (group_id)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table session (
+
+ id varchar(32) primary key comment 'session ID',
+ session_data text comment 'session data',
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified',
+
+ index session_modified_idx (modified)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; \ No newline at end of file
diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql
index a27a616f2..dae8b8faf 100644
--- a/db/laconica_pg.sql
+++ b/db/laconica_pg.sql
@@ -116,7 +116,9 @@ create table notice (
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' */,
- source varchar(32) /* comment 'source of comment, like "web", "im", or "clientname"' */
+ source varchar(32) /* comment 'source of comment, like "web", "im", or "clientname"' */,
+ conversation integer /*id of root notice in this conversation' */ references notice (id)
+
/* FULLTEXT(content) */
);
@@ -172,7 +174,7 @@ create table token (
tok char(32) not null /* comment 'identifying value' */,
secret char(32) not null /* comment 'secret value' */,
type integer not null default 0 /* comment 'request or access' */,
- state integer default 0 /* comment 'for requests; 0 = initial, 1 = authorized, 2 = used' */,
+ state integer default 0 /* comment 'for requests 0 = initial, 1 = authorized, 2 = used' */,
created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */,
@@ -346,7 +348,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 default CURRENT_TIMESTAMP /* comment 'date the notice was created' */,
- source integer default 1 /* comment 'reason it is in the inbox; 1=subscription' */,
+ source integer default 1 /* comment 'reason it is in the inbox: 1=subscription' */,
primary key (user_id, notice_id)
);
@@ -427,6 +429,76 @@ create table group_inbox (
);
create index group_inbox_created_idx on group_inbox using btree(created);
+
+/*attachments and URLs stuff */
+create sequence file_seq;
+create table file (
+ id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */,
+ url varchar(255) unique,
+ mimetype varchar(50),
+ size integer,
+ title varchar(255),
+ date integer,
+ protected integer
+);
+
+create sequence file_oembed_seq;
+create table file_oembed (
+ id bigint default nextval('file_oembed_seq') primary key /* comment 'unique identifier' */,
+ file_id bigint unique,
+ version varchar(20),
+ type varchar(20),
+ provider varchar(50),
+ provider_url varchar(255),
+ width integer,
+ height integer,
+ html text,
+ title varchar(255),
+ author_name varchar(50),
+ author_url varchar(255),
+ url varchar(255)
+);
+
+create sequence file_redirection_seq;
+create table file_redirection (
+ id bigint default nextval('file_redirection_seq') primary key /* comment 'unique identifier' */,
+ url varchar(255) unique,
+ file_id bigint,
+ redirections integer,
+ httpcode integer
+);
+
+create sequence file_thumbnail_seq;
+create table file_thumbnail (
+ id bigint default nextval('file_thumbnail_seq') primary key /* comment 'unique identifier' */,
+ file_id bigint unique,
+ url varchar(255) unique,
+ width integer,
+ height integer
+);
+
+create sequence file_to_post_seq;
+create table file_to_post (
+ id bigint default nextval('file_to_post_seq') primary key /* comment 'unique identifier' */,
+ file_id bigint,
+ post_id bigint,
+
+ unique(file_id, post_id)
+);
+
+create sequence design_seq;
+create table design (
+ id bigint default nextval('design_seq') /* comment 'design ID'*/,
+ backgroundcolor integer /* comment 'main background color'*/ ,
+ contentcolor integer /*comment 'content area background color'*/ ,
+ sidebarcolor integer /*comment 'sidebar background color'*/ ,
+ textcolor integer /*comment 'text color'*/ ,
+ linkcolor integer /*comment 'link color'*/,
+ backgroundimage varchar(255) /*comment 'background image, if any'*/,
+ disposition int default 1 /*comment 'bit 1 = hide background image, bit 2 = display background image, bit 4 = tile background image'*/,
+ primary key (id)
+);
+
/* Textsearch stuff */
create index textsearch_idx on profile using gist(textsearch);
diff --git a/db/notice_source.sql b/db/notice_source.sql
index 4c3dc2113..983ea9150 100644
--- a/db/notice_source.sql
+++ b/db/notice_source.sql
@@ -51,6 +51,7 @@ VALUES
('twidge','Twidge','http://software.complete.org/twidge', now()),
('twidroid','twidroid','http://www.twidroid.com/', now()),
('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()),
+ ('twitter','Twitter','http://twitter.com/', 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()),
diff --git a/db/site.sql b/db/site.sql
new file mode 100644
index 000000000..a9f64e5a5
--- /dev/null
+++ b/db/site.sql
@@ -0,0 +1,21 @@
+/* For managing multiple sites */
+
+create table status_network (
+
+ nickname varchar(64) primary key comment 'nickname',
+ hostname varchar(255) unique key comment 'alternate hostname if any',
+ pathname varchar(255) unique key comment 'alternate pathname if any',
+
+ dbhost varchar(255) comment 'database host',
+ dbuser varchar(255) comment 'database username',
+ dbpass varchar(255) comment 'database password',
+ dbname varchar(255) comment 'database name',
+
+ sitename varchar(255) comment 'display name',
+ theme varchar(255) comment 'theme name',
+ logo varchar(255) comment 'site logo',
+
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified'
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
diff --git a/doc-src/tos b/doc-src/tos
new file mode 100644
index 000000000..bcfc31981
--- /dev/null
+++ b/doc-src/tos
@@ -0,0 +1,300 @@
+The gist
+--------
+
+We (the folks at [%%site.broughtby%%](%%site.broughtbyurl%%)) run a
+service called %%site.name%% and would love for you to use it. Our
+service is designed to give you as much control and ownership over
+what goes in your notice stream as possible and encourage you to
+express yourself freely. However, be responsible in what you post. In
+particular, make sure that none of the prohibited items listed below
+appear in your notice stream or get linked to from your notice stream (things
+like spam, viruses, or hate content).
+
+You can review our [Public Stream](%%action.public%%) to get a sense
+of the types of notices that are welcome on our service (or not!). If
+you find a %%site.name%% account that you believe violates our terms
+of service, please check our [Contact](%%doc.contact%%) documentation.
+
+(Note: Automattic, Inc., original creators of the below Terms of
+Service, decided to make them available under a Creative Commons
+Sharealike license, which means you’re more than welcome to steal it
+and repurpose it for your own use. Just make sure to replace
+references to us with ones to you. They’d appreciate a link to
+[WordPress.com](http://www.wordpress.com/) somewhere on your site.
+They spent a lot of money and time on the below, and other people
+shouldn’t need to do the same. (We didn't!))
+
+Terms of Service
+----------------
+
+The following terms and conditions govern all use of the %%site.name%%
+website and all content, services and products available at or through
+the website (taken together, the Website). The Website is owned and
+operated by %%site.broughtby%% (“Operatorâ€). The Website is offered
+subject to your acceptance without modification of all of the terms
+and conditions contained herein and all other operating rules,
+policies (including, without limitation, Operator’s [Privacy
+Policy](%%doc.privacy%%))
+and procedures that may be published from time to time on this Site by
+Operator (collectively, the “Agreementâ€).
+
+Please read this Agreement carefully before accessing or using the
+Website. By accessing or using any part of the web site, you agree to
+become bound by the terms and conditions of this agreement. If you do
+not agree to all the terms and conditions of this agreement, then you
+may not access the Website or use any services. If these terms and
+conditions are considered an offer by Operator, acceptance is
+expressly limited to these terms. The Website is available only to
+individuals who are at least 13 years old.
+
+<ol>
+
+<li><strong>Your %%site.name%% Account and Site.</strong> If you
+create a notice stream on the Website, you are responsible for
+maintaining the security of your account and notice stream, and you
+are fully responsible for all activities that occur under the account
+and any other actions taken in connection with the notice stream. You
+must not describe or assign keywords to your notice stream in a
+misleading or unlawful manner, including in a manner intended to trade
+on the name or reputation of others, and Operator may change or remove
+any description or keyword that it considers inappropriate or
+unlawful, or otherwise likely to cause Operator liability. You must
+immediately notify Operator of any unauthorized uses of your notice
+stream, your account or any other breaches of security. Operator will
+not be liable for any acts or omissions by You, including any damages
+of any kind incurred as a result of such acts or omissions.</li>
+
+<li><strong>Responsibility of Contributors.</strong> If you operate a
+notice stream, comment on a notice stream, post material to the
+Website, post links on the Website, or otherwise make (or allow any
+third party to make) material available by means of the Website (any
+such material, “Contentâ€), You are entirely responsible for the
+content of, and any harm resulting from, that Content. That is the
+case regardless of whether the Content in question constitutes text,
+graphics, an audio file, or computer software. By making Content
+available, you represent and warrant that:
+
+<ul>
+
+<li>the downloading, copying and use of the Content will not infringe
+the proprietary rights, including but not limited to the copyright,
+patent, trademark or trade secret rights, of any third party;</li>
+
+<li>if your employer has rights to intellectual property you create,
+you have either (i) received permission from your employer to post or
+make available the Content, including but not limited to any software,
+or (ii) secured from your employer a waiver as to all rights in or to
+the Content;</li>
+
+<li>you have fully complied with any third-party licenses
+relating to the Content, and have done all things necessary to
+successfully pass through to end users any required terms;</li>
+
+<li>the Content does not contain or install any viruses, worms, malware,
+Trojan horses or other harmful or destructive content;</li>
+
+<li>the Content is not spam, and does not contain unethical or
+unwanted commercial content designed to drive traffic to third party
+sites or boost the search engine rankings of third party sites, or to
+further unlawful acts (such as phishing) or mislead recipients as to
+the source of the material (such as spoofing);</li>
+
+<li>if the Content is machine- or randomly-generated, it is for
+purposes of direct entertainment, information and/or utility for you
+or other users, and not for spam,</li>
+
+<li>the Content is not libelous or defamatory (more info on
+what that means), does not contain threats or incite violence towards
+individuals or entities, and does not violate the privacy or publicity
+rights of any third party;</li>
+
+<li>your notice stream is not getting advertised via unwanted electronic
+messages such as spam links on newsgroups, email lists, other notice streams
+and web sites, and similar unsolicited promotional methods;</li>
+
+<li>your notice stream is not named in a manner that misleads your
+readers into thinking that you are another person or company. For
+example, your notice stream’s URL or name is not the name of a person other
+than yourself or company other than your own; and</li>
+
+<li>you have, in the case of Content that includes computer code,
+accurately categorized and/or described the type, nature, uses and
+effects of the materials, whether requested to do so by Operator or
+otherwise.</li>
+
+</ul>
+
+<p>By submitting Content to Operator for inclusion on your Website, you
+grant Operator a world-wide, royalty-free, and non-exclusive license
+to reproduce, modify, adapt and publish the Content solely for the
+purpose of displaying, distributing and promoting your notice
+stream.</p>
+
+<p>By submitting Content to Operator for inclusion on your Website,
+you grant all readers the right to use, re-use, modify and/or
+re-distribute the Content under the terms of the <a
+href="%%license.url%%">%%license.title%%</a>.</p>
+
+<p>If you delete Content, Operator will use reasonable efforts to remove it from
+the Website, but you acknowledge that caching or references to the
+Content may not be made immediately unavailable.</p>
+
+<p>Without limiting any of those representations or warranties, Operator
+has the right (though not the obligation) to, in Operator’s sole
+discretion (i) refuse or remove any content that, in Operator’s
+reasonable opinion, violates any Operator policy or is in any way
+harmful or objectionable, or (ii) terminate or deny access to and use
+of the Website to any individual or entity for any reason, in
+Operator’s sole discretion.</p>
+</li>
+
+<li><strong>Responsibility of Website Visitors.</strong> Operator has not reviewed,
+and cannot review, all of the material, including computer software,
+posted to the Website, and cannot therefore be responsible for that
+material’s content, use or effects. By operating the Website,
+Operator does not represent or imply that it endorses the material
+there posted, or that it believes such material to be accurate, useful
+or non-harmful. You are responsible for taking precautions as
+necessary to protect yourself and your computer systems from viruses,
+worms, Trojan horses, and other harmful or destructive content. The
+Website may contain content that is offensive, indecent, or otherwise
+objectionable, as well as content containing technical inaccuracies,
+typographical mistakes, and other errors. The Website may also contain
+material that violates the privacy or publicity rights, or infringes
+the intellectual property and other proprietary rights, of third
+parties, or the downloading, copying or use of which is subject to
+additional terms and conditions, stated or unstated. Operator
+disclaims any responsibility for any harm resulting from the use by
+visitors of the Website, or from any downloading by those visitors of
+content there posted.</li>
+
+<li><strong>Content Posted on Other Websites.</strong> We have not reviewed, and
+cannot review, all of the material, including computer software, made
+available through the websites and webpages to which %%site.name%%
+links, and that link to %%site.name%%. Operator does not have any
+control over those external websites and webpages, and is not
+responsible for their contents or their use. By linking to a
+external website or webpage, Operator does not represent or
+imply that it endorses such website or webpage. You are responsible
+for taking precautions as necessary to protect yourself and your
+computer systems from viruses, worms, Trojan horses, and other harmful
+or destructive content. Operator disclaims any responsibility for
+any harm resulting from your use of external websites and
+webpages.</li>
+
+<li><strong>Copyright Infringement and DMCA Policy.</strong> As Operator asks
+others to respect its intellectual property rights, it respects the
+intellectual property rights of others. If you believe that material
+located on or linked to by %%site.name%% violates your copyright, you
+are encouraged to notify Operator in accordance with Operator’s
+Digital Millennium Copyright Act (â€DMCAâ€) Policy. Operator will
+respond to all such notices, including as required or appropriate by
+removing the infringing material or disabling all links to the
+infringing material. In the case of a visitor who may infringe or
+repeatedly infringes the copyrights or other intellectual property
+rights of Operator or others, Operator may, in its discretion,
+terminate or deny access to and use of the Website. In the case of
+such termination, Operator will have no obligation to provide a
+refund of any amounts previously paid to Operator.</li>
+
+<li><strong>Intellectual Property.</strong> This Agreement does not
+transfer from Operator to you any Operator or third party intellectual
+property, and all right, title and interest in and to such property
+will remain (as between the parties) solely with Operator.
+%%site.name%%, the %%site.name%% logo, and all other trademarks,
+service marks, graphics and logos used in connection with
+%%site.name%%, or the Website are trademarks or registered trademarks
+of Operator or Operator’s licensors. Other trademarks, service marks,
+graphics and logos used in connection with the Website may be the
+trademarks of other third parties. Your use of the Website grants you
+no right or license to reproduce or otherwise use any Operator or
+third-party trademarks.</li>
+
+<li><strong>Changes.</strong> Operator reserves the right, at its sole
+discretion, to modify or replace any part of this Agreement. It is
+your responsibility to check this Agreement periodically for changes.
+Your continued use of or access to the Website following the posting
+of any changes to this Agreement constitutes acceptance of those
+changes. Operator may also, in the future, offer new services and/or
+features through the Website (including, the release of new tools and
+resources). Such new features and/or services shall be subject to the
+terms and conditions of this Agreement.</li>
+
+<li><strong>Termination.</strong> Operator may terminate your access
+to all or any part of the Website at any time, with or without cause,
+with or without notice, effective immediately. If you wish to
+terminate this Agreement or your %%site.name%% account (if you have
+one), you may simply discontinue using the Website. All provisions of
+this Agreement which by their nature should survive termination shall
+survive termination, including, without limitation, ownership
+provisions, warranty disclaimers, indemnity and limitations of
+liability.</li>
+
+<li><strong>Disclaimer of Warranties.</strong> The Website is provided
+“as isâ€. Operator and its suppliers and licensors hereby disclaim all
+warranties of any kind, express or implied, including, without
+limitation, the warranties of merchantability, fitness for a
+particular purpose and non-infringement. Neither Operator nor its
+suppliers and licensors, makes any warranty that the Website will be
+error free or that access thereto will be continuous or uninterrupted.
+If you’re actually reading this, here’s a treat. You understand that
+you download from, or otherwise obtain content or services through,
+the Website at your own discretion and risk.</li>
+
+<li><strong>Limitation of Liability.</strong> In no event will
+Operator, or its suppliers or licensors, be liable with respect to any
+subject matter of this agreement under any contract, negligence,
+strict liability or other legal or equitable theory for: (i) any
+special, incidental or consequential damages; (ii) the cost of
+procurement or substitute products or services; (iii) for interruption
+of use or loss or corruption of data; or (iv) for any amounts that
+exceed the fees paid by you to Operator under this agreement during
+the twelve (12) month period prior to the cause of action. Operator
+shall have no liability for any failure or delay due to matters beyond
+their reasonable control. The foregoing shall not apply to the extent
+prohibited by applicable law.</li>
+
+<li><strong>General Representation and Warranty.</strong> You
+represent and warrant that (i) your use of the Website will be in
+strict accordance with the Operator Privacy Policy, with this
+Agreement and with all applicable laws and regulations (including
+without limitation any local laws or regulations in your country,
+state, city, or other governmental area, regarding online conduct and
+acceptable content, and including all applicable laws regarding the
+transmission of technical data exported from the United States or the
+country in which you reside) and (ii) your use of the Website will not
+infringe or misappropriate the intellectual property rights of any
+third party.</li>
+
+<li><strong>Indemnification.</strong> You agree to indemnify and hold
+harmless Operator, its contractors, and its licensors, and their
+respective directors, officers, employees and agents from and against
+any and all claims and expenses, including attorneys’ fees, arising
+out of your use of the Website, including but not limited to out of
+your violation this Agreement.</li>
+
+<li><strong>Miscellaneous.</strong> This Agreement constitutes the
+entire agreement between Operator and you concerning the subject
+matter hereof, and they may only be modified by a written amendment
+signed by an authorized executive of Operator, or by the posting by
+Operator of a revised version. If any part of this Agreement is held
+invalid or unenforceable, that part will be construed to reflect the
+parties’ original intent, and the remaining portions will remain in
+full force and effect. A waiver by either party of any term or
+condition of this Agreement or any breach thereof, in any one
+instance, will not waive such term or condition or any subsequent
+breach thereof. You may assign your rights under this Agreement to any
+party that consents to, and agrees to be bound by, its terms and
+conditions; Operator may assign its rights under this Agreement
+without condition. This Agreement will be binding upon and will inure
+to the benefit of the parties, their successors and permitted
+assigns.</li> </ol>
+
+*Originally published by Automattic, Inc. as the [WordPress.com Terms
+of Service](http://en.wordpress.com/tos/) and made available by them
+under the [Creative Commons Attribution-ShareAlike 3.0
+License](http://creativecommons.org/licenses/by-sa/3.0/).
+Modifications to remove reference to "VIP services", rename "blog" to
+"notice stream", remove the choice-of-venue clause, and add variables
+specific to instances of this software made by Control Yourself, Inc.
+and made available under the terms of the same license.* \ No newline at end of file
diff --git a/extlib/Console/Getopt.php b/extlib/Console/Getopt.php
new file mode 100644
index 000000000..bb9d69ca2
--- /dev/null
+++ b/extlib/Console/Getopt.php
@@ -0,0 +1,290 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | PHP Version 5 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2004 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 3.0 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available through the world-wide-web at the following url: |
+// | http://www.php.net/license/3_0.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Andrei Zmievski <andrei@php.net> |
+// +----------------------------------------------------------------------+
+//
+// $Id: Getopt.php,v 1.4 2007/06/12 14:58:56 cellog Exp $
+
+require_once 'PEAR.php';
+
+/**
+ * Command-line options parsing class.
+ *
+ * @author Andrei Zmievski <andrei@php.net>
+ *
+ */
+class Console_Getopt {
+ /**
+ * Parses the command-line options.
+ *
+ * The first parameter to this function should be the list of command-line
+ * arguments without the leading reference to the running program.
+ *
+ * The second parameter is a string of allowed short options. Each of the
+ * option letters can be followed by a colon ':' to specify that the option
+ * requires an argument, or a double colon '::' to specify that the option
+ * takes an optional argument.
+ *
+ * The third argument is an optional array of allowed long options. The
+ * leading '--' should not be included in the option name. Options that
+ * require an argument should be followed by '=', and options that take an
+ * option argument should be followed by '=='.
+ *
+ * The return value is an array of two elements: the list of parsed
+ * options and the list of non-option command-line arguments. Each entry in
+ * the list of parsed options is a pair of elements - the first one
+ * specifies the option, and the second one specifies the option argument,
+ * if there was one.
+ *
+ * Long and short options can be mixed.
+ *
+ * Most of the semantics of this function are based on GNU getopt_long().
+ *
+ * @param array $args an array of command-line arguments
+ * @param string $short_options specifies the list of allowed short options
+ * @param array $long_options specifies the list of allowed long options
+ *
+ * @return array two-element array containing the list of parsed options and
+ * the non-option arguments
+ *
+ * @access public
+ *
+ */
+ function getopt2($args, $short_options, $long_options = null)
+ {
+ return Console_Getopt::doGetopt(2, $args, $short_options, $long_options);
+ }
+
+ /**
+ * This function expects $args to start with the script name (POSIX-style).
+ * Preserved for backwards compatibility.
+ * @see getopt2()
+ */
+ function getopt($args, $short_options, $long_options = null)
+ {
+ return Console_Getopt::doGetopt(1, $args, $short_options, $long_options);
+ }
+
+ /**
+ * The actual implementation of the argument parsing code.
+ */
+ function doGetopt($version, $args, $short_options, $long_options = null)
+ {
+ // in case you pass directly readPHPArgv() as the first arg
+ if (PEAR::isError($args)) {
+ return $args;
+ }
+ if (empty($args)) {
+ return array(array(), array());
+ }
+ $opts = array();
+ $non_opts = array();
+
+ settype($args, 'array');
+
+ if ($long_options) {
+ sort($long_options);
+ }
+
+ /*
+ * Preserve backwards compatibility with callers that relied on
+ * erroneous POSIX fix.
+ */
+ if ($version < 2) {
+ if (isset($args[0]{0}) && $args[0]{0} != '-') {
+ array_shift($args);
+ }
+ }
+
+ reset($args);
+ while (list($i, $arg) = each($args)) {
+
+ /* The special element '--' means explicit end of
+ options. Treat the rest of the arguments as non-options
+ and end the loop. */
+ if ($arg == '--') {
+ $non_opts = array_merge($non_opts, array_slice($args, $i + 1));
+ break;
+ }
+
+ if ($arg{0} != '-' || (strlen($arg) > 1 && $arg{1} == '-' && !$long_options)) {
+ $non_opts = array_merge($non_opts, array_slice($args, $i));
+ break;
+ } elseif (strlen($arg) > 1 && $arg{1} == '-') {
+ $error = Console_Getopt::_parseLongOption(substr($arg, 2), $long_options, $opts, $args);
+ if (PEAR::isError($error))
+ return $error;
+ } elseif ($arg == '-') {
+ // - is stdin
+ $non_opts = array_merge($non_opts, array_slice($args, $i));
+ break;
+ } else {
+ $error = Console_Getopt::_parseShortOption(substr($arg, 1), $short_options, $opts, $args);
+ if (PEAR::isError($error))
+ return $error;
+ }
+ }
+
+ return array($opts, $non_opts);
+ }
+
+ /**
+ * @access private
+ *
+ */
+ function _parseShortOption($arg, $short_options, &$opts, &$args)
+ {
+ for ($i = 0; $i < strlen($arg); $i++) {
+ $opt = $arg{$i};
+ $opt_arg = null;
+
+ /* Try to find the short option in the specifier string. */
+ if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':')
+ {
+ return PEAR::raiseError("Console_Getopt: unrecognized option -- $opt");
+ }
+
+ if (strlen($spec) > 1 && $spec{1} == ':') {
+ if (strlen($spec) > 2 && $spec{2} == ':') {
+ if ($i + 1 < strlen($arg)) {
+ /* Option takes an optional argument. Use the remainder of
+ the arg string if there is anything left. */
+ $opts[] = array($opt, substr($arg, $i + 1));
+ break;
+ }
+ } else {
+ /* Option requires an argument. Use the remainder of the arg
+ string if there is anything left. */
+ if ($i + 1 < strlen($arg)) {
+ $opts[] = array($opt, substr($arg, $i + 1));
+ break;
+ } else if (list(, $opt_arg) = each($args)) {
+ /* Else use the next argument. */;
+ if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) {
+ return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt");
+ }
+ } else {
+ return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt");
+ }
+ }
+ }
+
+ $opts[] = array($opt, $opt_arg);
+ }
+ }
+
+ /**
+ * @access private
+ *
+ */
+ function _isShortOpt($arg)
+ {
+ return strlen($arg) == 2 && $arg[0] == '-' && preg_match('/[a-zA-Z]/', $arg[1]);
+ }
+
+ /**
+ * @access private
+ *
+ */
+ function _isLongOpt($arg)
+ {
+ return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' &&
+ preg_match('/[a-zA-Z]+$/', substr($arg, 2));
+ }
+
+ /**
+ * @access private
+ *
+ */
+ function _parseLongOption($arg, $long_options, &$opts, &$args)
+ {
+ @list($opt, $opt_arg) = explode('=', $arg, 2);
+ $opt_len = strlen($opt);
+
+ for ($i = 0; $i < count($long_options); $i++) {
+ $long_opt = $long_options[$i];
+ $opt_start = substr($long_opt, 0, $opt_len);
+ $long_opt_name = str_replace('=', '', $long_opt);
+
+ /* Option doesn't match. Go on to the next one. */
+ if ($long_opt_name != $opt) {
+ continue;
+ }
+
+ $opt_rest = substr($long_opt, $opt_len);
+
+ /* Check that the options uniquely matches one of the allowed
+ options. */
+ if ($i + 1 < count($long_options)) {
+ $next_option_rest = substr($long_options[$i + 1], $opt_len);
+ } else {
+ $next_option_rest = '';
+ }
+ if ($opt_rest != '' && $opt{0} != '=' &&
+ $i + 1 < count($long_options) &&
+ $opt == substr($long_options[$i+1], 0, $opt_len) &&
+ $next_option_rest != '' &&
+ $next_option_rest{0} != '=') {
+ return PEAR::raiseError("Console_Getopt: option --$opt is ambiguous");
+ }
+
+ if (substr($long_opt, -1) == '=') {
+ if (substr($long_opt, -2) != '==') {
+ /* Long option requires an argument.
+ Take the next argument if one wasn't specified. */;
+ if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) {
+ return PEAR::raiseError("Console_Getopt: option --$opt requires an argument");
+ }
+ if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) {
+ return PEAR::raiseError("Console_Getopt: option requires an argument --$opt");
+ }
+ }
+ } else if ($opt_arg) {
+ return PEAR::raiseError("Console_Getopt: option --$opt doesn't allow an argument");
+ }
+
+ $opts[] = array('--' . $opt, $opt_arg);
+ return;
+ }
+
+ return PEAR::raiseError("Console_Getopt: unrecognized option --$opt");
+ }
+
+ /**
+ * Safely read the $argv PHP array across different PHP configurations.
+ * Will take care on register_globals and register_argc_argv ini directives
+ *
+ * @access public
+ * @return mixed the $argv PHP array or PEAR error if not registered
+ */
+ function readPHPArgv()
+ {
+ global $argv;
+ if (!is_array($argv)) {
+ if (!@is_array($_SERVER['argv'])) {
+ if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
+ return PEAR::raiseError("Console_Getopt: Could not read cmd args (register_argc_argv=Off?)");
+ }
+ return $GLOBALS['HTTP_SERVER_VARS']['argv'];
+ }
+ return $_SERVER['argv'];
+ }
+ return $argv;
+ }
+
+}
+
+?>
diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php
index b1a1a4e21..0c6a13dc2 100644
--- a/extlib/DB/DataObject.php
+++ b/extlib/DB/DataObject.php
@@ -2357,6 +2357,8 @@ class DB_DataObject extends DB_DataObject_Overload
$t= explode(' ',microtime());
$_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1];
+
+ do {
if ($_DB_driver == 'DB') {
$result = $DB->query($string);
@@ -2374,8 +2376,19 @@ class DB_DataObject extends DB_DataObject_Overload
break;
}
}
-
-
+
+ // try to reconnect, at most 3 times
+ $again = false;
+ if (is_a($result, 'PEAR_Error')
+ AND $result->getCode() == DB_ERROR_NODBSELECTED
+ AND $cpt++<3) {
+ $DB->disconnect();
+ sleep(1);
+ $DB->connect($DB->dsn);
+ $again = true;
+ }
+
+ } while ($again);
if (is_a($result,'PEAR_Error')) {
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
diff --git a/extlib/HTTP/Request.php b/extlib/HTTP/Request.php
new file mode 100644
index 000000000..42eac3b14
--- /dev/null
+++ b/extlib/HTTP/Request.php
@@ -0,0 +1,1521 @@
+<?php
+/**
+ * Class for performing HTTP requests
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2002-2007, 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.
+ *
+ * @category HTTP
+ * @package HTTP_Request
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author Alexey Borzov <avb@php.net>
+ * @copyright 2002-2007 Richard Heyes
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Request.php,v 1.63 2008/10/11 11:07:10 avb Exp $
+ * @link http://pear.php.net/package/HTTP_Request/
+ */
+
+/**
+ * PEAR and PEAR_Error classes (for error handling)
+ */
+require_once 'PEAR.php';
+/**
+ * Socket class
+ */
+require_once 'Net/Socket.php';
+/**
+ * URL handling class
+ */
+require_once 'Net/URL.php';
+
+/**#@+
+ * Constants for HTTP request methods
+ */
+define('HTTP_REQUEST_METHOD_GET', 'GET', true);
+define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true);
+define('HTTP_REQUEST_METHOD_POST', 'POST', true);
+define('HTTP_REQUEST_METHOD_PUT', 'PUT', true);
+define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true);
+define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);
+define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true);
+/**#@-*/
+
+/**#@+
+ * Constants for HTTP request error codes
+ */
+define('HTTP_REQUEST_ERROR_FILE', 1);
+define('HTTP_REQUEST_ERROR_URL', 2);
+define('HTTP_REQUEST_ERROR_PROXY', 4);
+define('HTTP_REQUEST_ERROR_REDIRECTS', 8);
+define('HTTP_REQUEST_ERROR_RESPONSE', 16);
+define('HTTP_REQUEST_ERROR_GZIP_METHOD', 32);
+define('HTTP_REQUEST_ERROR_GZIP_READ', 64);
+define('HTTP_REQUEST_ERROR_GZIP_DATA', 128);
+define('HTTP_REQUEST_ERROR_GZIP_CRC', 256);
+/**#@-*/
+
+/**#@+
+ * Constants for HTTP protocol versions
+ */
+define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);
+define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);
+/**#@-*/
+
+if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
+ /**
+ * Whether string functions are overloaded by their mbstring equivalents
+ */
+ define('HTTP_REQUEST_MBSTRING', true);
+} else {
+ /**
+ * @ignore
+ */
+ define('HTTP_REQUEST_MBSTRING', false);
+}
+
+/**
+ * Class for performing HTTP requests
+ *
+ * Simple example (fetches yahoo.com and displays it):
+ * <code>
+ * $a = &new HTTP_Request('http://www.yahoo.com/');
+ * $a->sendRequest();
+ * echo $a->getResponseBody();
+ * </code>
+ *
+ * @category HTTP
+ * @package HTTP_Request
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 1.4.4
+ */
+class HTTP_Request
+{
+ /**#@+
+ * @access private
+ */
+ /**
+ * Instance of Net_URL
+ * @var Net_URL
+ */
+ var $_url;
+
+ /**
+ * Type of request
+ * @var string
+ */
+ var $_method;
+
+ /**
+ * HTTP Version
+ * @var string
+ */
+ var $_http;
+
+ /**
+ * Request headers
+ * @var array
+ */
+ var $_requestHeaders;
+
+ /**
+ * Basic Auth Username
+ * @var string
+ */
+ var $_user;
+
+ /**
+ * Basic Auth Password
+ * @var string
+ */
+ var $_pass;
+
+ /**
+ * Socket object
+ * @var Net_Socket
+ */
+ var $_sock;
+
+ /**
+ * Proxy server
+ * @var string
+ */
+ var $_proxy_host;
+
+ /**
+ * Proxy port
+ * @var integer
+ */
+ var $_proxy_port;
+
+ /**
+ * Proxy username
+ * @var string
+ */
+ var $_proxy_user;
+
+ /**
+ * Proxy password
+ * @var string
+ */
+ var $_proxy_pass;
+
+ /**
+ * Post data
+ * @var array
+ */
+ var $_postData;
+
+ /**
+ * Request body
+ * @var string
+ */
+ var $_body;
+
+ /**
+ * A list of methods that MUST NOT have a request body, per RFC 2616
+ * @var array
+ */
+ var $_bodyDisallowed = array('TRACE');
+
+ /**
+ * Methods having defined semantics for request body
+ *
+ * Content-Length header (indicating that the body follows, section 4.3 of
+ * RFC 2616) will be sent for these methods even if no body was added
+ *
+ * @var array
+ */
+ var $_bodyRequired = array('POST', 'PUT');
+
+ /**
+ * Files to post
+ * @var array
+ */
+ var $_postFiles = array();
+
+ /**
+ * Connection timeout.
+ * @var float
+ */
+ var $_timeout;
+
+ /**
+ * HTTP_Response object
+ * @var HTTP_Response
+ */
+ var $_response;
+
+ /**
+ * Whether to allow redirects
+ * @var boolean
+ */
+ var $_allowRedirects;
+
+ /**
+ * Maximum redirects allowed
+ * @var integer
+ */
+ var $_maxRedirects;
+
+ /**
+ * Current number of redirects
+ * @var integer
+ */
+ var $_redirects;
+
+ /**
+ * Whether to append brackets [] to array variables
+ * @var bool
+ */
+ var $_useBrackets = true;
+
+ /**
+ * Attached listeners
+ * @var array
+ */
+ var $_listeners = array();
+
+ /**
+ * Whether to save response body in response object property
+ * @var bool
+ */
+ var $_saveBody = true;
+
+ /**
+ * Timeout for reading from socket (array(seconds, microseconds))
+ * @var array
+ */
+ var $_readTimeout = null;
+
+ /**
+ * Options to pass to Net_Socket::connect. See stream_context_create
+ * @var array
+ */
+ var $_socketOptions = null;
+ /**#@-*/
+
+ /**
+ * Constructor
+ *
+ * Sets up the object
+ * @param string The url to fetch/access
+ * @param array Associative array of parameters which can have the following keys:
+ * <ul>
+ * <li>method - Method to use, GET, POST etc (string)</li>
+ * <li>http - HTTP Version to use, 1.0 or 1.1 (string)</li>
+ * <li>user - Basic Auth username (string)</li>
+ * <li>pass - Basic Auth password (string)</li>
+ * <li>proxy_host - Proxy server host (string)</li>
+ * <li>proxy_port - Proxy server port (integer)</li>
+ * <li>proxy_user - Proxy auth username (string)</li>
+ * <li>proxy_pass - Proxy auth password (string)</li>
+ * <li>timeout - Connection timeout in seconds (float)</li>
+ * <li>allowRedirects - Whether to follow redirects or not (bool)</li>
+ * <li>maxRedirects - Max number of redirects to follow (integer)</li>
+ * <li>useBrackets - Whether to append [] to array variable names (bool)</li>
+ * <li>saveBody - Whether to save response body in response object property (bool)</li>
+ * <li>readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li>
+ * <li>socketOptions - Options to pass to Net_Socket object (array)</li>
+ * </ul>
+ * @access public
+ */
+ function HTTP_Request($url = '', $params = array())
+ {
+ $this->_method = HTTP_REQUEST_METHOD_GET;
+ $this->_http = HTTP_REQUEST_HTTP_VER_1_1;
+ $this->_requestHeaders = array();
+ $this->_postData = array();
+ $this->_body = null;
+
+ $this->_user = null;
+ $this->_pass = null;
+
+ $this->_proxy_host = null;
+ $this->_proxy_port = null;
+ $this->_proxy_user = null;
+ $this->_proxy_pass = null;
+
+ $this->_allowRedirects = false;
+ $this->_maxRedirects = 3;
+ $this->_redirects = 0;
+
+ $this->_timeout = null;
+ $this->_response = null;
+
+ foreach ($params as $key => $value) {
+ $this->{'_' . $key} = $value;
+ }
+
+ if (!empty($url)) {
+ $this->setURL($url);
+ }
+
+ // Default useragent
+ $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');
+
+ // We don't do keep-alives by default
+ $this->addHeader('Connection', 'close');
+
+ // Basic authentication
+ if (!empty($this->_user)) {
+ $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass));
+ }
+
+ // Proxy authentication (see bug #5913)
+ if (!empty($this->_proxy_user)) {
+ $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass));
+ }
+
+ // Use gzip encoding if possible
+ if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) {
+ $this->addHeader('Accept-Encoding', 'gzip');
+ }
+ }
+
+ /**
+ * Generates a Host header for HTTP/1.1 requests
+ *
+ * @access private
+ * @return string
+ */
+ function _generateHostHeader()
+ {
+ if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) {
+ $host = $this->_url->host . ':' . $this->_url->port;
+
+ } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) {
+ $host = $this->_url->host . ':' . $this->_url->port;
+
+ } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) {
+ $host = $this->_url->host . ':' . $this->_url->port;
+
+ } else {
+ $host = $this->_url->host;
+ }
+
+ return $host;
+ }
+
+ /**
+ * Resets the object to its initial state (DEPRECATED).
+ * Takes the same parameters as the constructor.
+ *
+ * @param string $url The url to be requested
+ * @param array $params Associative array of parameters
+ * (see constructor for details)
+ * @access public
+ * @deprecated deprecated since 1.2, call the constructor if this is necessary
+ */
+ function reset($url, $params = array())
+ {
+ $this->HTTP_Request($url, $params);
+ }
+
+ /**
+ * Sets the URL to be requested
+ *
+ * @param string The url to be requested
+ * @access public
+ */
+ function setURL($url)
+ {
+ $this->_url = &new Net_URL($url, $this->_useBrackets);
+
+ if (!empty($this->_url->user) || !empty($this->_url->pass)) {
+ $this->setBasicAuth($this->_url->user, $this->_url->pass);
+ }
+
+ if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {
+ $this->addHeader('Host', $this->_generateHostHeader());
+ }
+
+ // set '/' instead of empty path rather than check later (see bug #8662)
+ if (empty($this->_url->path)) {
+ $this->_url->path = '/';
+ }
+ }
+
+ /**
+ * Returns the current request URL
+ *
+ * @return string Current request URL
+ * @access public
+ */
+ function getUrl()
+ {
+ return empty($this->_url)? '': $this->_url->getUrl();
+ }
+
+ /**
+ * Sets a proxy to be used
+ *
+ * @param string Proxy host
+ * @param int Proxy port
+ * @param string Proxy username
+ * @param string Proxy password
+ * @access public
+ */
+ function setProxy($host, $port = 8080, $user = null, $pass = null)
+ {
+ $this->_proxy_host = $host;
+ $this->_proxy_port = $port;
+ $this->_proxy_user = $user;
+ $this->_proxy_pass = $pass;
+
+ if (!empty($user)) {
+ $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
+ }
+ }
+
+ /**
+ * Sets basic authentication parameters
+ *
+ * @param string Username
+ * @param string Password
+ */
+ function setBasicAuth($user, $pass)
+ {
+ $this->_user = $user;
+ $this->_pass = $pass;
+
+ $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
+ }
+
+ /**
+ * Sets the method to be used, GET, POST etc.
+ *
+ * @param string Method to use. Use the defined constants for this
+ * @access public
+ */
+ function setMethod($method)
+ {
+ $this->_method = $method;
+ }
+
+ /**
+ * Sets the HTTP version to use, 1.0 or 1.1
+ *
+ * @param string Version to use. Use the defined constants for this
+ * @access public
+ */
+ function setHttpVer($http)
+ {
+ $this->_http = $http;
+ }
+
+ /**
+ * Adds a request header
+ *
+ * @param string Header name
+ * @param string Header value
+ * @access public
+ */
+ function addHeader($name, $value)
+ {
+ $this->_requestHeaders[strtolower($name)] = $value;
+ }
+
+ /**
+ * Removes a request header
+ *
+ * @param string Header name to remove
+ * @access public
+ */
+ function removeHeader($name)
+ {
+ if (isset($this->_requestHeaders[strtolower($name)])) {
+ unset($this->_requestHeaders[strtolower($name)]);
+ }
+ }
+
+ /**
+ * Adds a querystring parameter
+ *
+ * @param string Querystring parameter name
+ * @param string Querystring parameter value
+ * @param bool Whether the value is already urlencoded or not, default = not
+ * @access public
+ */
+ function addQueryString($name, $value, $preencoded = false)
+ {
+ $this->_url->addQueryString($name, $value, $preencoded);
+ }
+
+ /**
+ * Sets the querystring to literally what you supply
+ *
+ * @param string The querystring data. Should be of the format foo=bar&x=y etc
+ * @param bool Whether data is already urlencoded or not, default = already encoded
+ * @access public
+ */
+ function addRawQueryString($querystring, $preencoded = true)
+ {
+ $this->_url->addRawQueryString($querystring, $preencoded);
+ }
+
+ /**
+ * Adds postdata items
+ *
+ * @param string Post data name
+ * @param string Post data value
+ * @param bool Whether data is already urlencoded or not, default = not
+ * @access public
+ */
+ function addPostData($name, $value, $preencoded = false)
+ {
+ if ($preencoded) {
+ $this->_postData[$name] = $value;
+ } else {
+ $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value);
+ }
+ }
+
+ /**
+ * Recursively applies the callback function to the value
+ *
+ * @param mixed Callback function
+ * @param mixed Value to process
+ * @access private
+ * @return mixed Processed value
+ */
+ function _arrayMapRecursive($callback, $value)
+ {
+ if (!is_array($value)) {
+ return call_user_func($callback, $value);
+ } else {
+ $map = array();
+ foreach ($value as $k => $v) {
+ $map[$k] = $this->_arrayMapRecursive($callback, $v);
+ }
+ return $map;
+ }
+ }
+
+ /**
+ * Adds a file to form-based file upload
+ *
+ * Used to emulate file upload via a HTML form. The method also sets
+ * Content-Type of HTTP request to 'multipart/form-data'.
+ *
+ * If you just want to send the contents of a file as the body of HTTP
+ * request you should use setBody() method.
+ *
+ * @access public
+ * @param string name of file-upload field
+ * @param mixed file name(s)
+ * @param mixed content-type(s) of file(s) being uploaded
+ * @return bool true on success
+ * @throws PEAR_Error
+ */
+ function addFile($inputName, $fileName, $contentType = 'application/octet-stream')
+ {
+ if (!is_array($fileName) && !is_readable($fileName)) {
+ return PEAR::raiseError("File '{$fileName}' is not readable", HTTP_REQUEST_ERROR_FILE);
+ } elseif (is_array($fileName)) {
+ foreach ($fileName as $name) {
+ if (!is_readable($name)) {
+ return PEAR::raiseError("File '{$name}' is not readable", HTTP_REQUEST_ERROR_FILE);
+ }
+ }
+ }
+ $this->addHeader('Content-Type', 'multipart/form-data');
+ $this->_postFiles[$inputName] = array(
+ 'name' => $fileName,
+ 'type' => $contentType
+ );
+ return true;
+ }
+
+ /**
+ * Adds raw postdata (DEPRECATED)
+ *
+ * @param string The data
+ * @param bool Whether data is preencoded or not, default = already encoded
+ * @access public
+ * @deprecated deprecated since 1.3.0, method setBody() should be used instead
+ */
+ function addRawPostData($postdata, $preencoded = true)
+ {
+ $this->_body = $preencoded ? $postdata : urlencode($postdata);
+ }
+
+ /**
+ * Sets the request body (for POST, PUT and similar requests)
+ *
+ * @param string Request body
+ * @access public
+ */
+ function setBody($body)
+ {
+ $this->_body = $body;
+ }
+
+ /**
+ * Clears any postdata that has been added (DEPRECATED).
+ *
+ * Useful for multiple request scenarios.
+ *
+ * @access public
+ * @deprecated deprecated since 1.2
+ */
+ function clearPostData()
+ {
+ $this->_postData = null;
+ }
+
+ /**
+ * Appends a cookie to "Cookie:" header
+ *
+ * @param string $name cookie name
+ * @param string $value cookie value
+ * @access public
+ */
+ function addCookie($name, $value)
+ {
+ $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : '';
+ $this->addHeader('Cookie', $cookies . $name . '=' . $value);
+ }
+
+ /**
+ * Clears any cookies that have been added (DEPRECATED).
+ *
+ * Useful for multiple request scenarios
+ *
+ * @access public
+ * @deprecated deprecated since 1.2
+ */
+ function clearCookies()
+ {
+ $this->removeHeader('Cookie');
+ }
+
+ /**
+ * Sends the request
+ *
+ * @access public
+ * @param bool Whether to store response body in Response object property,
+ * set this to false if downloading a LARGE file and using a Listener
+ * @return mixed PEAR error on error, true otherwise
+ */
+ function sendRequest($saveBody = true)
+ {
+ if (!is_a($this->_url, 'Net_URL')) {
+ return PEAR::raiseError('No URL given', HTTP_REQUEST_ERROR_URL);
+ }
+
+ $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;
+ $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;
+
+ if (strcasecmp($this->_url->protocol, 'https') == 0) {
+ // Bug #14127, don't try connecting to HTTPS sites without OpenSSL
+ if (version_compare(PHP_VERSION, '4.3.0', '<') || !extension_loaded('openssl')) {
+ return PEAR::raiseError('Need PHP 4.3.0 or later with OpenSSL support for https:// requests',
+ HTTP_REQUEST_ERROR_URL);
+ } elseif (isset($this->_proxy_host)) {
+ return PEAR::raiseError('HTTPS proxies are not supported', HTTP_REQUEST_ERROR_PROXY);
+ }
+ $host = 'ssl://' . $host;
+ }
+
+ // magic quotes may fuck up file uploads and chunked response processing
+ $magicQuotes = ini_get('magic_quotes_runtime');
+ ini_set('magic_quotes_runtime', false);
+
+ // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
+ // connection token to a proxy server...
+ if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) &&
+ 'Keep-Alive' == $this->_requestHeaders['connection'])
+ {
+ $this->removeHeader('connection');
+ }
+
+ $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) ||
+ (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']);
+ $sockets = &PEAR::getStaticProperty('HTTP_Request', 'sockets');
+ $sockKey = $host . ':' . $port;
+ unset($this->_sock);
+
+ // There is a connected socket in the "static" property?
+ if ($keepAlive && !empty($sockets[$sockKey]) &&
+ !empty($sockets[$sockKey]->fp))
+ {
+ $this->_sock =& $sockets[$sockKey];
+ $err = null;
+ } else {
+ $this->_notify('connect');
+ $this->_sock =& new Net_Socket();
+ $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions);
+ }
+ PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest());
+
+ if (!PEAR::isError($err)) {
+ if (!empty($this->_readTimeout)) {
+ $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);
+ }
+
+ $this->_notify('sentRequest');
+
+ // Read the response
+ $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);
+ $err = $this->_response->process(
+ $this->_saveBody && $saveBody,
+ HTTP_REQUEST_METHOD_HEAD != $this->_method
+ );
+
+ if ($keepAlive) {
+ $keepAlive = (isset($this->_response->_headers['content-length'])
+ || (isset($this->_response->_headers['transfer-encoding'])
+ && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked'));
+ if ($keepAlive) {
+ if (isset($this->_response->_headers['connection'])) {
+ $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive';
+ } else {
+ $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol;
+ }
+ }
+ }
+ }
+
+ ini_set('magic_quotes_runtime', $magicQuotes);
+
+ if (PEAR::isError($err)) {
+ return $err;
+ }
+
+ if (!$keepAlive) {
+ $this->disconnect();
+ // Store the connected socket in "static" property
+ } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) {
+ $sockets[$sockKey] =& $this->_sock;
+ }
+
+ // Check for redirection
+ if ( $this->_allowRedirects
+ AND $this->_redirects <= $this->_maxRedirects
+ AND $this->getResponseCode() > 300
+ AND $this->getResponseCode() < 399
+ AND !empty($this->_response->_headers['location'])) {
+
+
+ $redirect = $this->_response->_headers['location'];
+
+ // Absolute URL
+ if (preg_match('/^https?:\/\//i', $redirect)) {
+ $this->_url = &new Net_URL($redirect);
+ $this->addHeader('Host', $this->_generateHostHeader());
+ // Absolute path
+ } elseif ($redirect{0} == '/') {
+ $this->_url->path = $redirect;
+
+ // Relative path
+ } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {
+ if (substr($this->_url->path, -1) == '/') {
+ $redirect = $this->_url->path . $redirect;
+ } else {
+ $redirect = dirname($this->_url->path) . '/' . $redirect;
+ }
+ $redirect = Net_URL::resolvePath($redirect);
+ $this->_url->path = $redirect;
+
+ // Filename, no path
+ } else {
+ if (substr($this->_url->path, -1) == '/') {
+ $redirect = $this->_url->path . $redirect;
+ } else {
+ $redirect = dirname($this->_url->path) . '/' . $redirect;
+ }
+ $this->_url->path = $redirect;
+ }
+
+ $this->_redirects++;
+ return $this->sendRequest($saveBody);
+
+ // Too many redirects
+ } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {
+ return PEAR::raiseError('Too many redirects', HTTP_REQUEST_ERROR_REDIRECTS);
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnect the socket, if connected. Only useful if using Keep-Alive.
+ *
+ * @access public
+ */
+ function disconnect()
+ {
+ if (!empty($this->_sock) && !empty($this->_sock->fp)) {
+ $this->_notify('disconnect');
+ $this->_sock->disconnect();
+ }
+ }
+
+ /**
+ * Returns the response code
+ *
+ * @access public
+ * @return mixed Response code, false if not set
+ */
+ function getResponseCode()
+ {
+ return isset($this->_response->_code) ? $this->_response->_code : false;
+ }
+
+ /**
+ * Returns the response reason phrase
+ *
+ * @access public
+ * @return mixed Response reason phrase, false if not set
+ */
+ function getResponseReason()
+ {
+ return isset($this->_response->_reason) ? $this->_response->_reason : false;
+ }
+
+ /**
+ * Returns either the named header or all if no name given
+ *
+ * @access public
+ * @param string The header name to return, do not set to get all headers
+ * @return mixed either the value of $headername (false if header is not present)
+ * or an array of all headers
+ */
+ function getResponseHeader($headername = null)
+ {
+ if (!isset($headername)) {
+ return isset($this->_response->_headers)? $this->_response->_headers: array();
+ } else {
+ $headername = strtolower($headername);
+ return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
+ }
+ }
+
+ /**
+ * Returns the body of the response
+ *
+ * @access public
+ * @return mixed response body, false if not set
+ */
+ function getResponseBody()
+ {
+ return isset($this->_response->_body) ? $this->_response->_body : false;
+ }
+
+ /**
+ * Returns cookies set in response
+ *
+ * @access public
+ * @return mixed array of response cookies, false if none are present
+ */
+ function getResponseCookies()
+ {
+ return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
+ }
+
+ /**
+ * Builds the request string
+ *
+ * @access private
+ * @return string The request string
+ */
+ function _buildRequest()
+ {
+ $separator = ini_get('arg_separator.output');
+ ini_set('arg_separator.output', '&');
+ $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';
+ ini_set('arg_separator.output', $separator);
+
+ $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
+ $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';
+ $path = $this->_url->path . $querystring;
+ $url = $host . $port . $path;
+
+ if (!strlen($url)) {
+ $url = '/';
+ }
+
+ $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
+
+ if (in_array($this->_method, $this->_bodyDisallowed) ||
+ (0 == strlen($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method ||
+ (empty($this->_postData) && empty($this->_postFiles)))))
+ {
+ $this->removeHeader('Content-Type');
+ } else {
+ if (empty($this->_requestHeaders['content-type'])) {
+ // Add default content-type
+ $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
+ } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {
+ $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
+ $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
+ }
+ }
+
+ // Request Headers
+ if (!empty($this->_requestHeaders)) {
+ foreach ($this->_requestHeaders as $name => $value) {
+ $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
+ $request .= $canonicalName . ': ' . $value . "\r\n";
+ }
+ }
+
+ // Method does not allow a body, simply add a final CRLF
+ if (in_array($this->_method, $this->_bodyDisallowed)) {
+
+ $request .= "\r\n";
+
+ // Post data if it's an array
+ } elseif (HTTP_REQUEST_METHOD_POST == $this->_method &&
+ (!empty($this->_postData) || !empty($this->_postFiles))) {
+
+ // "normal" POST request
+ if (!isset($boundary)) {
+ $postdata = implode('&', array_map(
+ create_function('$a', 'return $a[0] . \'=\' . $a[1];'),
+ $this->_flattenArray('', $this->_postData)
+ ));
+
+ // multipart request, probably with file uploads
+ } else {
+ $postdata = '';
+ if (!empty($this->_postData)) {
+ $flatData = $this->_flattenArray('', $this->_postData);
+ foreach ($flatData as $item) {
+ $postdata .= '--' . $boundary . "\r\n";
+ $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';
+ $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";
+ }
+ }
+ foreach ($this->_postFiles as $name => $value) {
+ if (is_array($value['name'])) {
+ $varname = $name . ($this->_useBrackets? '[]': '');
+ } else {
+ $varname = $name;
+ $value['name'] = array($value['name']);
+ }
+ foreach ($value['name'] as $key => $filename) {
+ $fp = fopen($filename, 'r');
+ $basename = basename($filename);
+ $type = is_array($value['type'])? @$value['type'][$key]: $value['type'];
+
+ $postdata .= '--' . $boundary . "\r\n";
+ $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
+ $postdata .= "\r\nContent-Type: " . $type;
+ $postdata .= "\r\n\r\n" . fread($fp, filesize($filename)) . "\r\n";
+ fclose($fp);
+ }
+ }
+ $postdata .= '--' . $boundary . "--\r\n";
+ }
+ $request .= 'Content-Length: ' .
+ (HTTP_REQUEST_MBSTRING? mb_strlen($postdata, 'iso-8859-1'): strlen($postdata)) .
+ "\r\n\r\n";
+ $request .= $postdata;
+
+ // Explicitly set request body
+ } elseif (0 < strlen($this->_body)) {
+
+ $request .= 'Content-Length: ' .
+ (HTTP_REQUEST_MBSTRING? mb_strlen($this->_body, 'iso-8859-1'): strlen($this->_body)) .
+ "\r\n\r\n";
+ $request .= $this->_body;
+
+ // No body: send a Content-Length header nonetheless (request #12900),
+ // but do that only for methods that require a body (bug #14740)
+ } else {
+
+ if (in_array($this->_method, $this->_bodyRequired)) {
+ $request .= "Content-Length: 0\r\n";
+ }
+ $request .= "\r\n";
+ }
+
+ return $request;
+ }
+
+ /**
+ * Helper function to change the (probably multidimensional) associative array
+ * into the simple one.
+ *
+ * @param string name for item
+ * @param mixed item's values
+ * @return array array with the following items: array('item name', 'item value');
+ * @access private
+ */
+ function _flattenArray($name, $values)
+ {
+ if (!is_array($values)) {
+ return array(array($name, $values));
+ } else {
+ $ret = array();
+ foreach ($values as $k => $v) {
+ if (empty($name)) {
+ $newName = $k;
+ } elseif ($this->_useBrackets) {
+ $newName = $name . '[' . $k . ']';
+ } else {
+ $newName = $name;
+ }
+ $ret = array_merge($ret, $this->_flattenArray($newName, $v));
+ }
+ return $ret;
+ }
+ }
+
+
+ /**
+ * Adds a Listener to the list of listeners that are notified of
+ * the object's events
+ *
+ * Events sent by HTTP_Request object
+ * - 'connect': on connection to server
+ * - 'sentRequest': after the request was sent
+ * - 'disconnect': on disconnection from server
+ *
+ * Events sent by HTTP_Response object
+ * - 'gotHeaders': after receiving response headers (headers are passed in $data)
+ * - 'tick': on receiving a part of response body (the part is passed in $data)
+ * - 'gzTick': on receiving a gzip-encoded part of response body (ditto)
+ * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
+ *
+ * @param HTTP_Request_Listener listener to attach
+ * @return boolean whether the listener was successfully attached
+ * @access public
+ */
+ function attach(&$listener)
+ {
+ if (!is_a($listener, 'HTTP_Request_Listener')) {
+ return false;
+ }
+ $this->_listeners[$listener->getId()] =& $listener;
+ return true;
+ }
+
+
+ /**
+ * Removes a Listener from the list of listeners
+ *
+ * @param HTTP_Request_Listener listener to detach
+ * @return boolean whether the listener was successfully detached
+ * @access public
+ */
+ function detach(&$listener)
+ {
+ if (!is_a($listener, 'HTTP_Request_Listener') ||
+ !isset($this->_listeners[$listener->getId()])) {
+ return false;
+ }
+ unset($this->_listeners[$listener->getId()]);
+ return true;
+ }
+
+
+ /**
+ * Notifies all registered listeners of an event.
+ *
+ * @param string Event name
+ * @param mixed Additional data
+ * @access private
+ * @see HTTP_Request::attach()
+ */
+ function _notify($event, $data = null)
+ {
+ foreach (array_keys($this->_listeners) as $id) {
+ $this->_listeners[$id]->update($this, $event, $data);
+ }
+ }
+}
+
+
+/**
+ * Response class to complement the Request class
+ *
+ * @category HTTP
+ * @package HTTP_Request
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 1.4.4
+ */
+class HTTP_Response
+{
+ /**
+ * Socket object
+ * @var Net_Socket
+ */
+ var $_sock;
+
+ /**
+ * Protocol
+ * @var string
+ */
+ var $_protocol;
+
+ /**
+ * Return code
+ * @var string
+ */
+ var $_code;
+
+ /**
+ * Response reason phrase
+ * @var string
+ */
+ var $_reason;
+
+ /**
+ * Response headers
+ * @var array
+ */
+ var $_headers;
+
+ /**
+ * Cookies set in response
+ * @var array
+ */
+ var $_cookies;
+
+ /**
+ * Response body
+ * @var string
+ */
+ var $_body = '';
+
+ /**
+ * Used by _readChunked(): remaining length of the current chunk
+ * @var string
+ */
+ var $_chunkLength = 0;
+
+ /**
+ * Attached listeners
+ * @var array
+ */
+ var $_listeners = array();
+
+ /**
+ * Bytes left to read from message-body
+ * @var null|int
+ */
+ var $_toRead;
+
+ /**
+ * Constructor
+ *
+ * @param Net_Socket socket to read the response from
+ * @param array listeners attached to request
+ */
+ function HTTP_Response(&$sock, &$listeners)
+ {
+ $this->_sock =& $sock;
+ $this->_listeners =& $listeners;
+ }
+
+
+ /**
+ * Processes a HTTP response
+ *
+ * This extracts response code, headers, cookies and decodes body if it
+ * was encoded in some way
+ *
+ * @access public
+ * @param bool Whether to store response body in object property, set
+ * this to false if downloading a LARGE file and using a Listener.
+ * This is assumed to be true if body is gzip-encoded.
+ * @param bool Whether the response can actually have a message-body.
+ * Will be set to false for HEAD requests.
+ * @throws PEAR_Error
+ * @return mixed true on success, PEAR_Error in case of malformed response
+ */
+ function process($saveBody = true, $canHaveBody = true)
+ {
+ do {
+ $line = $this->_sock->readLine();
+ if (!preg_match('!^(HTTP/\d\.\d) (\d{3})(?: (.+))?!', $line, $s)) {
+ return PEAR::raiseError('Malformed response', HTTP_REQUEST_ERROR_RESPONSE);
+ } else {
+ $this->_protocol = $s[1];
+ $this->_code = intval($s[2]);
+ $this->_reason = empty($s[3])? null: $s[3];
+ }
+ while ('' !== ($header = $this->_sock->readLine())) {
+ $this->_processHeader($header);
+ }
+ } while (100 == $this->_code);
+
+ $this->_notify('gotHeaders', $this->_headers);
+
+ // RFC 2616, section 4.4:
+ // 1. Any response message which "MUST NOT" include a message-body ...
+ // is always terminated by the first empty line after the header fields
+ // 3. ... If a message is received with both a
+ // Transfer-Encoding header field and a Content-Length header field,
+ // the latter MUST be ignored.
+ $canHaveBody = $canHaveBody && $this->_code >= 200 &&
+ $this->_code != 204 && $this->_code != 304;
+
+ // If response body is present, read it and decode
+ $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);
+ $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);
+ $hasBody = false;
+ if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) ||
+ 0 != $this->_headers['content-length']))
+ {
+ if ($chunked || !isset($this->_headers['content-length'])) {
+ $this->_toRead = null;
+ } else {
+ $this->_toRead = $this->_headers['content-length'];
+ }
+ while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) {
+ if ($chunked) {
+ $data = $this->_readChunked();
+ } elseif (is_null($this->_toRead)) {
+ $data = $this->_sock->read(4096);
+ } else {
+ $data = $this->_sock->read(min(4096, $this->_toRead));
+ $this->_toRead -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data);
+ }
+ if ('' == $data && (!$this->_chunkLength || $this->_sock->eof())) {
+ break;
+ } else {
+ $hasBody = true;
+ if ($saveBody || $gzipped) {
+ $this->_body .= $data;
+ }
+ $this->_notify($gzipped? 'gzTick': 'tick', $data);
+ }
+ }
+ }
+
+ if ($hasBody) {
+ // Uncompress the body if needed
+ if ($gzipped) {
+ $body = $this->_decodeGzip($this->_body);
+ if (PEAR::isError($body)) {
+ return $body;
+ }
+ $this->_body = $body;
+ $this->_notify('gotBody', $this->_body);
+ } else {
+ $this->_notify('gotBody');
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Processes the response header
+ *
+ * @access private
+ * @param string HTTP header
+ */
+ function _processHeader($header)
+ {
+ if (false === strpos($header, ':')) {
+ return;
+ }
+ list($headername, $headervalue) = explode(':', $header, 2);
+ $headername = strtolower($headername);
+ $headervalue = ltrim($headervalue);
+
+ if ('set-cookie' != $headername) {
+ if (isset($this->_headers[$headername])) {
+ $this->_headers[$headername] .= ',' . $headervalue;
+ } else {
+ $this->_headers[$headername] = $headervalue;
+ }
+ } else {
+ $this->_parseCookie($headervalue);
+ }
+ }
+
+
+ /**
+ * Parse a Set-Cookie header to fill $_cookies array
+ *
+ * @access private
+ * @param string value of Set-Cookie header
+ */
+ function _parseCookie($headervalue)
+ {
+ $cookie = array(
+ 'expires' => null,
+ 'domain' => null,
+ 'path' => null,
+ 'secure' => false
+ );
+
+ // Only a name=value pair
+ if (!strpos($headervalue, ';')) {
+ $pos = strpos($headervalue, '=');
+ $cookie['name'] = trim(substr($headervalue, 0, $pos));
+ $cookie['value'] = trim(substr($headervalue, $pos + 1));
+
+ // Some optional parameters are supplied
+ } else {
+ $elements = explode(';', $headervalue);
+ $pos = strpos($elements[0], '=');
+ $cookie['name'] = trim(substr($elements[0], 0, $pos));
+ $cookie['value'] = trim(substr($elements[0], $pos + 1));
+
+ for ($i = 1; $i < count($elements); $i++) {
+ if (false === strpos($elements[$i], '=')) {
+ $elName = trim($elements[$i]);
+ $elValue = null;
+ } else {
+ list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
+ }
+ $elName = strtolower($elName);
+ if ('secure' == $elName) {
+ $cookie['secure'] = true;
+ } elseif ('expires' == $elName) {
+ $cookie['expires'] = str_replace('"', '', $elValue);
+ } elseif ('path' == $elName || 'domain' == $elName) {
+ $cookie[$elName] = urldecode($elValue);
+ } else {
+ $cookie[$elName] = $elValue;
+ }
+ }
+ }
+ $this->_cookies[] = $cookie;
+ }
+
+
+ /**
+ * Read a part of response body encoded with chunked Transfer-Encoding
+ *
+ * @access private
+ * @return string
+ */
+ function _readChunked()
+ {
+ // at start of the next chunk?
+ if (0 == $this->_chunkLength) {
+ $line = $this->_sock->readLine();
+ if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
+ $this->_chunkLength = hexdec($matches[1]);
+ // Chunk with zero length indicates the end
+ if (0 == $this->_chunkLength) {
+ $this->_sock->readLine(); // make this an eof()
+ return '';
+ }
+ } else {
+ return '';
+ }
+ }
+ $data = $this->_sock->read($this->_chunkLength);
+ $this->_chunkLength -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data);
+ if (0 == $this->_chunkLength) {
+ $this->_sock->readLine(); // Trailing CRLF
+ }
+ return $data;
+ }
+
+
+ /**
+ * Notifies all registered listeners of an event.
+ *
+ * @param string Event name
+ * @param mixed Additional data
+ * @access private
+ * @see HTTP_Request::_notify()
+ */
+ function _notify($event, $data = null)
+ {
+ foreach (array_keys($this->_listeners) as $id) {
+ $this->_listeners[$id]->update($this, $event, $data);
+ }
+ }
+
+
+ /**
+ * Decodes the message-body encoded by gzip
+ *
+ * The real decoding work is done by gzinflate() built-in function, this
+ * method only parses the header and checks data for compliance with
+ * RFC 1952
+ *
+ * @access private
+ * @param string gzip-encoded data
+ * @return string decoded data
+ */
+ function _decodeGzip($data)
+ {
+ if (HTTP_REQUEST_MBSTRING) {
+ $oldEncoding = mb_internal_encoding();
+ mb_internal_encoding('iso-8859-1');
+ }
+ $length = strlen($data);
+ // If it doesn't look like gzip-encoded data, don't bother
+ if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
+ return $data;
+ }
+ $method = ord(substr($data, 2, 1));
+ if (8 != $method) {
+ return PEAR::raiseError('_decodeGzip(): unknown compression method', HTTP_REQUEST_ERROR_GZIP_METHOD);
+ }
+ $flags = ord(substr($data, 3, 1));
+ if ($flags & 224) {
+ return PEAR::raiseError('_decodeGzip(): reserved bits are set', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+
+ // header is 10 bytes minimum. may be longer, though.
+ $headerLength = 10;
+ // extra fields, need to skip 'em
+ if ($flags & 4) {
+ if ($length - $headerLength - 2 < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $extraLength = unpack('v', substr($data, 10, 2));
+ if ($length - $headerLength - 2 - $extraLength[1] < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $headerLength += $extraLength[1] + 2;
+ }
+ // file name, need to skip that
+ if ($flags & 8) {
+ if ($length - $headerLength - 1 < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $filenameLength = strpos(substr($data, $headerLength), chr(0));
+ if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $headerLength += $filenameLength + 1;
+ }
+ // comment, need to skip that also
+ if ($flags & 16) {
+ if ($length - $headerLength - 1 < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $commentLength = strpos(substr($data, $headerLength), chr(0));
+ if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $headerLength += $commentLength + 1;
+ }
+ // have a CRC for header. let's check
+ if ($flags & 1) {
+ if ($length - $headerLength - 2 < 8) {
+ return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+ }
+ $crcReal = 0xffff & crc32(substr($data, 0, $headerLength));
+ $crcStored = unpack('v', substr($data, $headerLength, 2));
+ if ($crcReal != $crcStored[1]) {
+ return PEAR::raiseError('_decodeGzip(): header CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC);
+ }
+ $headerLength += 2;
+ }
+ // unpacked data CRC and size at the end of encoded data
+ $tmp = unpack('V2', substr($data, -8));
+ $dataCrc = $tmp[1];
+ $dataSize = $tmp[2];
+
+ // finally, call the gzinflate() function
+ // don't pass $dataSize to gzinflate, see bugs #13135, #14370
+ $unpacked = gzinflate(substr($data, $headerLength, -8));
+ if (false === $unpacked) {
+ return PEAR::raiseError('_decodeGzip(): gzinflate() call failed', HTTP_REQUEST_ERROR_GZIP_READ);
+ } elseif ($dataSize != strlen($unpacked)) {
+ return PEAR::raiseError('_decodeGzip(): data size check failed', HTTP_REQUEST_ERROR_GZIP_READ);
+ } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
+ return PEAR::raiseError('_decodeGzip(): data CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC);
+ }
+ if (HTTP_REQUEST_MBSTRING) {
+ mb_internal_encoding($oldEncoding);
+ }
+ return $unpacked;
+ }
+} // End class HTTP_Response
+?>
diff --git a/extlib/HTTP/Request/Listener.php b/extlib/HTTP/Request/Listener.php
new file mode 100644
index 000000000..b4fe444b3
--- /dev/null
+++ b/extlib/HTTP/Request/Listener.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Listener for HTTP_Request and HTTP_Response objects
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2002-2007, 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.
+ *
+ * @category HTTP
+ * @package HTTP_Request
+ * @author Alexey Borzov <avb@php.net>
+ * @copyright 2002-2007 Richard Heyes
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Listener.php,v 1.3 2007/05/18 10:33:31 avb Exp $
+ * @link http://pear.php.net/package/HTTP_Request/
+ */
+
+/**
+ * Listener for HTTP_Request and HTTP_Response objects
+ *
+ * This class implements the Observer part of a Subject-Observer
+ * design pattern.
+ *
+ * @category HTTP
+ * @package HTTP_Request
+ * @author Alexey Borzov <avb@php.net>
+ * @version Release: 1.4.4
+ */
+class HTTP_Request_Listener
+{
+ /**
+ * A listener's identifier
+ * @var string
+ */
+ var $_id;
+
+ /**
+ * Constructor, sets the object's identifier
+ *
+ * @access public
+ */
+ function HTTP_Request_Listener()
+ {
+ $this->_id = md5(uniqid('http_request_', 1));
+ }
+
+
+ /**
+ * Returns the listener's identifier
+ *
+ * @access public
+ * @return string
+ */
+ function getId()
+ {
+ return $this->_id;
+ }
+
+
+ /**
+ * This method is called when Listener is notified of an event
+ *
+ * @access public
+ * @param object an object the listener is attached to
+ * @param string Event name
+ * @param mixed Additional data
+ * @abstract
+ */
+ function update(&$subject, $event, $data = null)
+ {
+ echo "Notified of event: '$event'\n";
+ if (null !== $data) {
+ echo "Additional data: ";
+ var_dump($data);
+ }
+ }
+}
+?>
diff --git a/extlib/MIME/Type.php b/extlib/MIME/Type.php
new file mode 100644
index 000000000..c335f8d92
--- /dev/null
+++ b/extlib/MIME/Type.php
@@ -0,0 +1,523 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | PHP version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2002, 2008 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 3.0 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/3_0.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Authors: Ian Eure <ieure@php.net> |
+// +----------------------------------------------------------------------+
+//
+// $Id: Type.php,v 1.6 2009/01/16 11:49:45 cweiske Exp $
+
+require_once 'PEAR.php';
+
+$_fileCmd = &PEAR::getStaticProperty('MIME_Type', 'fileCmd');
+$_fileCmd = 'file';
+
+/**
+ * Class for working with MIME types
+ *
+ * @category MIME
+ * @package MIME_Type
+ * @license PHP License 3.0
+ * @version 1.2.0
+ * @link http://pear.php.net/package/MIME_Type
+ * @author Ian Eure <ieure@php.net>
+ */
+class MIME_Type
+{
+ /**
+ * The MIME media type
+ *
+ * @var string
+ */
+ var $media = '';
+
+ /**
+ * The MIME media sub-type
+ *
+ * @var string
+ */
+ var $subType = '';
+
+ /**
+ * Optional MIME parameters
+ *
+ * @var array
+ */
+ var $parameters = array();
+
+ /**
+ * List of valid media types.
+ * A media type is the string in front of the slash.
+ * The media type of "text/xml" would be "text".
+ *
+ * @var array
+ */
+ var $validMediaTypes = array(
+ 'text',
+ 'image',
+ 'audio',
+ 'video',
+ 'application',
+ 'multipart',
+ 'message'
+ );
+
+
+ /**
+ * Constructor.
+ *
+ * If $type is set, if will be parsed and the appropriate class vars set.
+ * If not, you get an empty class.
+ * This is useful, but not quite as useful as parsing a type.
+ *
+ * @param string $type MIME type
+ *
+ * @return void
+ */
+ function MIME_Type($type = false)
+ {
+ if ($type) {
+ $this->parse($type);
+ }
+ }
+
+
+ /**
+ * Parse a mime-type and set the class variables.
+ *
+ * @param string $type MIME type to parse
+ *
+ * @return void
+ */
+ function parse($type)
+ {
+ $this->media = $this->getMedia($type);
+ $this->subType = $this->getSubType($type);
+ $this->parameters = array();
+ if (MIME_Type::hasParameters($type)) {
+ require_once 'MIME/Type/Parameter.php';
+ foreach (MIME_Type::getParameters($type) as $param) {
+ $param = new MIME_Type_Parameter($param);
+ $this->parameters[$param->name] = $param;
+ }
+ }
+ }
+
+
+ /**
+ * Does this type have any parameters?
+ *
+ * @param string $type MIME type to check
+ *
+ * @return boolean true if $type has parameters, false otherwise
+ * @static
+ */
+ function hasParameters($type)
+ {
+ if (strstr($type, ';')) {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Get a MIME type's parameters
+ *
+ * @param string $type MIME type to get parameters of
+ *
+ * @return array $type's parameters
+ * @static
+ */
+ function getParameters($type)
+ {
+ $params = array();
+ $tmp = explode(';', $type);
+ for ($i = 1; $i < count($tmp); $i++) {
+ $params[] = trim($tmp[$i]);
+ }
+ return $params;
+ }
+
+
+ /**
+ * Strip parameters from a MIME type string.
+ *
+ * @param string $type MIME type string
+ *
+ * @return string MIME type with parameters removed
+ * @static
+ */
+ function stripParameters($type)
+ {
+ if (strstr($type, ';')) {
+ return substr($type, 0, strpos($type, ';'));
+ }
+ return $type;
+ }
+
+
+ /**
+ * Removes comments from a media type, subtype or parameter.
+ *
+ * @param string $string String to strip comments from
+ * @param string &$comment Comment is stored in there.
+ *
+ * @return string String without comments
+ * @static
+ */
+ function stripComments($string, &$comment)
+ {
+ if (strpos($string, '(') === false) {
+ return $string;
+ }
+
+ $inquote = false;
+ $quoting = false;
+ $incomment = 0;
+ $newstring = '';
+
+ for ($n = 0; $n < strlen($string); $n++) {
+ if ($quoting) {
+ if ($incomment == 0) {
+ $newstring .= $string[$n];
+ } else if ($comment !== null) {
+ $comment .= $string[$n];
+ }
+ $quoting = false;
+ } else if ($string[$n] == '\\') {
+ $quoting = true;
+ } else if (!$inquote && $incomment > 0 && $string[$n] == ')') {
+ $incomment--;
+ if ($incomment == 0 && $comment !== null) {
+ $comment .= ' ';
+ }
+ } else if (!$inquote && $string[$n] == '(') {
+ $incomment++;
+ } else if ($string[$n] == '"') {
+ if ($inquote) {
+ $inquote = false;
+ } else {
+ $inquote = true;
+ }
+ } else if ($incomment == 0) {
+ $newstring .= $string[$n];
+ } else if ($comment !== null) {
+ $comment .= $string[$n];
+ }
+ }
+
+ if ($comment !== null) {
+ $comment = trim($comment);
+ }
+
+ return $newstring;
+ }
+
+
+ /**
+ * Get a MIME type's media
+ *
+ * @note 'media' refers to the portion before the first slash
+ *
+ * @param string $type MIME type to get media of
+ *
+ * @return string $type's media
+ * @static
+ */
+ function getMedia($type)
+ {
+ $tmp = explode('/', $type);
+ return strtolower(trim(MIME_Type::stripComments($tmp[0], $null)));
+ }
+
+
+ /**
+ * Get a MIME type's subtype
+ *
+ * @param string $type MIME type to get subtype of
+ *
+ * @return string $type's subtype, null if invalid mime type
+ * @static
+ */
+ function getSubType($type)
+ {
+ $tmp = explode('/', $type);
+ if (!isset($tmp[1])) {
+ return null;
+ }
+ $tmp = explode(';', $tmp[1]);
+ return strtolower(trim(MIME_Type::stripComments($tmp[0], $null)));
+ }
+
+
+ /**
+ * Create a textual MIME type from object values
+ *
+ * This function performs the opposite function of parse().
+ *
+ * @return string MIME type string
+ */
+ function get()
+ {
+ $type = strtolower($this->media . '/' . $this->subType);
+ if (count($this->parameters)) {
+ foreach ($this->parameters as $key => $null) {
+ $type .= '; ' . $this->parameters[$key]->get();
+ }
+ }
+ return $type;
+ }
+
+
+ /**
+ * Is this type experimental?
+ *
+ * @note Experimental types are denoted by a leading 'x-' in the media or
+ * subtype, e.g. text/x-vcard or x-world/x-vrml.
+ *
+ * @param string $type MIME type to check
+ *
+ * @return boolean true if $type is experimental, false otherwise
+ * @static
+ */
+ function isExperimental($type)
+ {
+ if (substr(MIME_Type::getMedia($type), 0, 2) == 'x-' ||
+ substr(MIME_Type::getSubType($type), 0, 2) == 'x-') {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Is this a vendor MIME type?
+ *
+ * @note Vendor types are denoted with a leading 'vnd. in the subtype.
+ *
+ * @param string $type MIME type to check
+ *
+ * @return boolean true if $type is a vendor type, false otherwise
+ * @static
+ */
+ function isVendor($type)
+ {
+ if (substr(MIME_Type::getSubType($type), 0, 4) == 'vnd.') {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Is this a wildcard type?
+ *
+ * @param string $type MIME type to check
+ *
+ * @return boolean true if $type is a wildcard, false otherwise
+ * @static
+ */
+ function isWildcard($type)
+ {
+ if ($type == '*/*' || MIME_Type::getSubtype($type) == '*') {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Perform a wildcard match on a MIME type
+ *
+ * Example:
+ * MIME_Type::wildcardMatch('image/*', 'image/png')
+ *
+ * @param string $card Wildcard to check against
+ * @param string $type MIME type to check
+ *
+ * @return boolean true if there was a match, false otherwise
+ * @static
+ */
+ function wildcardMatch($card, $type)
+ {
+ if (!MIME_Type::isWildcard($card)) {
+ return false;
+ }
+
+ if ($card == '*/*') {
+ return true;
+ }
+
+ if (MIME_Type::getMedia($card) == MIME_Type::getMedia($type)) {
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Add a parameter to this type
+ *
+ * @param string $name Attribute name
+ * @param string $value Attribute value
+ * @param string $comment Comment for this parameter
+ *
+ * @return void
+ */
+ function addParameter($name, $value, $comment = false)
+ {
+ $tmp = new MIME_Type_Parameter();
+
+ $tmp->name = $name;
+ $tmp->value = $value;
+ $tmp->comment = $comment;
+ $this->parameters[$name] = $tmp;
+ }
+
+
+ /**
+ * Remove a parameter from this type
+ *
+ * @param string $name Parameter name
+ *
+ * @return void
+ */
+ function removeParameter($name)
+ {
+ unset($this->parameters[$name]);
+ }
+
+
+ /**
+ * Autodetect a file's MIME-type
+ *
+ * This function may be called staticly.
+ *
+ * @internal Tries to use fileinfo extension at first. If that
+ * does not work, mime_magic is used. If this is also not available
+ * or does not succeed, "file" command is tried to be executed with
+ * System_Command. When that fails, too, then we use our in-built
+ * extension-to-mimetype-mapping list.
+ *
+ * @param string $file Path to the file to get the type of
+ * @param bool $params Append MIME parameters if true
+ *
+ * @return string $file's MIME-type on success, PEAR_Error otherwise
+ *
+ * @since 1.0.0beta1
+ * @static
+ */
+ function autoDetect($file, $params = false)
+ {
+ // Sanity checks
+ if (!file_exists($file)) {
+ return PEAR::raiseError("File \"$file\" doesn't exist");
+ }
+
+ if (!is_readable($file)) {
+ return PEAR::raiseError("File \"$file\" is not readable");
+ }
+
+ if (function_exists('finfo_file')) {
+ $finfo = finfo_open(FILEINFO_MIME);
+ $type = finfo_file($finfo, $file);
+ finfo_close($finfo);
+ if ($type !== false && $type !== '') {
+ return MIME_Type::_handleDetection($type, $params);
+ }
+ }
+
+ if (function_exists('mime_content_type')) {
+ $type = mime_content_type($file);
+ if ($type !== false && $type !== '') {
+ return MIME_Type::_handleDetection($type, $params);
+ }
+ }
+
+ @include_once 'System/Command.php';
+ if (class_exists('System_Command')) {
+ return MIME_Type::_handleDetection(
+ MIME_Type::_fileAutoDetect($file),
+ $params
+ );
+ }
+
+ require_once 'MIME/Type/Extension.php';
+ $mte = new MIME_Type_Extension();
+ return $mte->getMIMEType($file);
+ }
+
+
+ /**
+ * Handles a detected MIME type and modifies it if necessary.
+ *
+ * @param string $type MIME Type of a file
+ * @param bool $params Append MIME parameters if true
+ *
+ * @return string $file's MIME-type on success, PEAR_Error otherwise
+ */
+ function _handleDetection($type, $params)
+ {
+ // _fileAutoDetect() may have returned an error.
+ if (PEAR::isError($type)) {
+ return $type;
+ }
+
+ // Don't return an empty string
+ if (!$type || !strlen($type)) {
+ return PEAR::raiseError("Sorry, couldn't determine file type.");
+ }
+
+ // Strip parameters if present & requested
+ if (MIME_Type::hasParameters($type) && !$params) {
+ $type = MIME_Type::stripParameters($type);
+ }
+
+ return $type;
+ }
+
+
+ /**
+ * Autodetect a file's MIME-type with 'file' and System_Command
+ *
+ * This function may be called staticly.
+ *
+ * @param string $file Path to the file to get the type of
+ *
+ * @return string $file's MIME-type
+ *
+ * @since 1.0.0beta1
+ * @static
+ */
+ function _fileAutoDetect($file)
+ {
+ $cmd = new System_Command();
+
+ // Make sure we have the 'file' command.
+ $fileCmd = PEAR::getStaticProperty('MIME_Type', 'fileCmd');
+ if (!$cmd->which($fileCmd)) {
+ unset($cmd);
+ return PEAR::raiseError("Can't find file command \"{$fileCmd}\"");
+ }
+
+ $cmd->pushCommand($fileCmd, "-bi " . escapeshellarg($file));
+ $res = $cmd->execute();
+ unset($cmd);
+
+ return $res;
+ }
+}
+
diff --git a/extlib/MIME/Type/Extension.php b/extlib/MIME/Type/Extension.php
new file mode 100644
index 000000000..1987e2a10
--- /dev/null
+++ b/extlib/MIME/Type/Extension.php
@@ -0,0 +1,298 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | PHP versions 4 and 5 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2009 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 3.0 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/3_0.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Authors: Christian Schmidt <schmidt@php.net> |
+// +----------------------------------------------------------------------+
+//
+// $Id: Extension.php,v 1.1 2009/01/16 11:49:45 cweiske Exp $
+
+require_once 'PEAR.php';
+
+/**
+ * Class for mapping file extensions to MIME types.
+ *
+ * @category MIME
+ * @package MIME_Type
+ * @author Christian Schmidt <schmidt@php.net>
+ * @license PHP License 3.0
+ * @version 1.2.0
+ * @link http://pear.php.net/package/MIME_Type
+ */
+class MIME_Type_Extension
+{
+ /**
+ * Mapping between file extension and MIME type.
+ *
+ * @internal The array is sorted alphabetically by value and with primary
+ * extension first. Be careful about not adding duplicate keys - PHP
+ * silently ignores duplicates. The following command can be used for
+ * checking for duplicates:
+ * grep "=> '" Extension.php | cut -d\' -f2 | sort | uniq -d
+ * application/octet-stream is generally used as fallback when no other
+ * MIME-type can be found, but the array does not contain a lot of such
+ * unknown extension. One entry exists, though, to allow detection of
+ * file extension for this MIME-type.
+ *
+ * @var array
+ */
+ var $extensionToType = array (
+ 'ez' => 'application/andrew-inset',
+ 'atom' => 'application/atom+xml',
+ 'jar' => 'application/java-archive',
+ 'hqx' => 'application/mac-binhex40',
+ 'cpt' => 'application/mac-compactpro',
+ 'mathml' => 'application/mathml+xml',
+ 'doc' => 'application/msword',
+ 'dat' => 'application/octet-stream',
+ 'oda' => 'application/oda',
+ 'ogg' => 'application/ogg',
+ 'pdf' => 'application/pdf',
+ 'ai' => 'application/postscript',
+ 'eps' => 'application/postscript',
+ 'ps' => 'application/postscript',
+ 'rdf' => 'application/rdf+xml',
+ 'rss' => 'application/rss+xml',
+ 'smi' => 'application/smil',
+ 'smil' => 'application/smil',
+ 'gram' => 'application/srgs',
+ 'grxml' => 'application/srgs+xml',
+ 'kml' => 'application/vnd.google-earth.kml+xml',
+ 'kmz' => 'application/vnd.google-earth.kmz',
+ 'mif' => 'application/vnd.mif',
+ 'xul' => 'application/vnd.mozilla.xul+xml',
+ 'xls' => 'application/vnd.ms-excel',
+ 'xlb' => 'application/vnd.ms-excel',
+ 'xlt' => 'application/vnd.ms-excel',
+ 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+ 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+ 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
+ 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
+ 'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
+ 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
+ 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
+ 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+ 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
+ 'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'pps' => 'application/vnd.ms-powerpoint',
+ 'odc' => 'application/vnd.oasis.opendocument.chart',
+ 'odb' => 'application/vnd.oasis.opendocument.database',
+ 'odf' => 'application/vnd.oasis.opendocument.formula',
+ 'odg' => 'application/vnd.oasis.opendocument.graphics',
+ 'otg' => 'application/vnd.oasis.opendocument.graphics-template',
+ 'odi' => 'application/vnd.oasis.opendocument.image',
+ 'odp' => 'application/vnd.oasis.opendocument.presentation',
+ 'otp' => 'application/vnd.oasis.opendocument.presentation-template',
+ 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+ 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
+ 'odt' => 'application/vnd.oasis.opendocument.text',
+ 'odm' => 'application/vnd.oasis.opendocument.text-master',
+ 'ott' => 'application/vnd.oasis.opendocument.text-template',
+ 'oth' => 'application/vnd.oasis.opendocument.text-web',
+ 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+ 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+ 'vsd' => 'application/vnd.visio',
+ 'wbxml' => 'application/vnd.wap.wbxml',
+ 'wmlc' => 'application/vnd.wap.wmlc',
+ 'wmlsc' => 'application/vnd.wap.wmlscriptc',
+ 'vxml' => 'application/voicexml+xml',
+ 'bcpio' => 'application/x-bcpio',
+ 'vcd' => 'application/x-cdlink',
+ 'pgn' => 'application/x-chess-pgn',
+ 'cpio' => 'application/x-cpio',
+ 'csh' => 'application/x-csh',
+ 'dcr' => 'application/x-director',
+ 'dir' => 'application/x-director',
+ 'dxr' => 'application/x-director',
+ 'dvi' => 'application/x-dvi',
+ 'spl' => 'application/x-futuresplash',
+ 'tgz' => 'application/x-gtar',
+ 'gtar' => 'application/x-gtar',
+ 'hdf' => 'application/x-hdf',
+ 'js' => 'application/x-javascript',
+ 'skp' => 'application/x-koan',
+ 'skd' => 'application/x-koan',
+ 'skt' => 'application/x-koan',
+ 'skm' => 'application/x-koan',
+ 'latex' => 'application/x-latex',
+ 'nc' => 'application/x-netcdf',
+ 'cdf' => 'application/x-netcdf',
+ 'sh' => 'application/x-sh',
+ 'shar' => 'application/x-shar',
+ 'swf' => 'application/x-shockwave-flash',
+ 'sit' => 'application/x-stuffit',
+ 'sv4cpio' => 'application/x-sv4cpio',
+ 'sv4crc' => 'application/x-sv4crc',
+ 'tar' => 'application/x-tar',
+ 'tcl' => 'application/x-tcl',
+ 'tex' => 'application/x-tex',
+ 'texinfo' => 'application/x-texinfo',
+ 'texi' => 'application/x-texinfo',
+ 't' => 'application/x-troff',
+ 'tr' => 'application/x-troff',
+ 'roff' => 'application/x-troff',
+ 'man' => 'application/x-troff-man',
+ 'me' => 'application/x-troff-me',
+ 'ms' => 'application/x-troff-ms',
+ 'ustar' => 'application/x-ustar',
+ 'src' => 'application/x-wais-source',
+ 'xhtml' => 'application/xhtml+xml',
+ 'xht' => 'application/xhtml+xml',
+ 'xslt' => 'application/xslt+xml',
+ 'xml' => 'application/xml',
+ 'xsl' => 'application/xml',
+ 'dtd' => 'application/xml-dtd',
+ 'zip' => 'application/zip',
+ 'au' => 'audio/basic',
+ 'snd' => 'audio/basic',
+ 'mid' => 'audio/midi',
+ 'midi' => 'audio/midi',
+ 'kar' => 'audio/midi',
+ 'mpga' => 'audio/mpeg',
+ 'mp2' => 'audio/mpeg',
+ 'mp3' => 'audio/mpeg',
+ 'aif' => 'audio/x-aiff',
+ 'aiff' => 'audio/x-aiff',
+ 'aifc' => 'audio/x-aiff',
+ 'm3u' => 'audio/x-mpegurl',
+ 'wma' => 'audio/x-ms-wma',
+ 'wax' => 'audio/x-ms-wax',
+ 'ram' => 'audio/x-pn-realaudio',
+ 'ra' => 'audio/x-pn-realaudio',
+ 'rm' => 'application/vnd.rn-realmedia',
+ 'wav' => 'audio/x-wav',
+ 'pdb' => 'chemical/x-pdb',
+ 'xyz' => 'chemical/x-xyz',
+ 'bmp' => 'image/bmp',
+ 'cgm' => 'image/cgm',
+ 'gif' => 'image/gif',
+ 'ief' => 'image/ief',
+ 'jpeg' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'jpe' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'svg' => 'image/svg+xml',
+ 'tiff' => 'image/tiff',
+ 'tif' => 'image/tiff',
+ 'djvu' => 'image/vnd.djvu',
+ 'djv' => 'image/vnd.djvu',
+ 'wbmp' => 'image/vnd.wap.wbmp',
+ 'ras' => 'image/x-cmu-raster',
+ 'ico' => 'image/x-icon',
+ 'pnm' => 'image/x-portable-anymap',
+ 'pbm' => 'image/x-portable-bitmap',
+ 'pgm' => 'image/x-portable-graymap',
+ 'ppm' => 'image/x-portable-pixmap',
+ 'rgb' => 'image/x-rgb',
+ 'xbm' => 'image/x-xbitmap',
+ 'psd' => 'image/x-photoshop',
+ 'xpm' => 'image/x-xpixmap',
+ 'xwd' => 'image/x-xwindowdump',
+ 'eml' => 'message/rfc822',
+ 'igs' => 'model/iges',
+ 'iges' => 'model/iges',
+ 'msh' => 'model/mesh',
+ 'mesh' => 'model/mesh',
+ 'silo' => 'model/mesh',
+ 'wrl' => 'model/vrml',
+ 'vrml' => 'model/vrml',
+ 'ics' => 'text/calendar',
+ 'ifb' => 'text/calendar',
+ 'css' => 'text/css',
+ 'csv' => 'text/csv',
+ 'html' => 'text/html',
+ 'htm' => 'text/html',
+ 'txt' => 'text/plain',
+ 'asc' => 'text/plain',
+ 'rtx' => 'text/richtext',
+ 'rtf' => 'text/rtf',
+ 'sgml' => 'text/sgml',
+ 'sgm' => 'text/sgml',
+ 'tsv' => 'text/tab-separated-values',
+ 'wml' => 'text/vnd.wap.wml',
+ 'wmls' => 'text/vnd.wap.wmlscript',
+ 'etx' => 'text/x-setext',
+ 'mpeg' => 'video/mpeg',
+ 'mpg' => 'video/mpeg',
+ 'mpe' => 'video/mpeg',
+ 'qt' => 'video/quicktime',
+ 'mov' => 'video/quicktime',
+ 'mxu' => 'video/vnd.mpegurl',
+ 'm4u' => 'video/vnd.mpegurl',
+ 'flv' => 'video/x-flv',
+ 'asf' => 'video/x-ms-asf',
+ 'asx' => 'video/x-ms-asf',
+ 'wmv' => 'video/x-ms-wmv',
+ 'wm' => 'video/x-ms-wm',
+ 'wmx' => 'video/x-ms-wmx',
+ 'avi' => 'video/x-msvideo',
+ 'ogv' => 'video/ogg',
+ 'movie' => 'video/x-sgi-movie',
+ 'ice' => 'x-conference/x-cooltalk',
+ );
+
+
+
+ /**
+ * Autodetect a file's MIME-type.
+ *
+ * @param string $file Path to the file to get the type of
+ *
+ * @return string $file's MIME-type on success, PEAR_Error otherwise
+ */
+ function getMIMEType($file)
+ {
+ $extension = substr(strrchr($file, '.'), 1);
+ if ($extension === false) {
+ return PEAR::raiseError("File has no extension.");
+ }
+
+ if (!isset($this->extensionToType[$extension])) {
+ return PEAR::raiseError("Sorry, couldn't determine file type.");
+ }
+
+ return $this->extensionToType[$extension];
+ }
+
+
+
+ /**
+ * Return default MIME-type for the specified extension.
+ *
+ * @param string $type MIME-type
+ *
+ * @return string A file extension without leading period.
+ */
+ function getExtension($type)
+ {
+ require_once 'MIME/Type.php';
+ // Strip parameters and comments.
+ $type = MIME_Type::getMedia($type) . '/' . MIME_Type::getSubType($type);
+
+ $extension = array_search($type, $this->extensionToType);
+ if ($extension === false) {
+ return PEAR::raiseError("Sorry, couldn't determine extension.");
+ }
+ return $extension;
+ }
+
+}
+
+?> \ No newline at end of file
diff --git a/extlib/MIME/Type/Parameter.php b/extlib/MIME/Type/Parameter.php
new file mode 100644
index 000000000..399d3dd7a
--- /dev/null
+++ b/extlib/MIME/Type/Parameter.php
@@ -0,0 +1,163 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4: */
+// +----------------------------------------------------------------------+
+// | PHP version 4 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2002 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 3.0 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/3_0.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Authors: Ian Eure <ieure@php.net> |
+// +----------------------------------------------------------------------+
+//
+// $Id: Parameter.php,v 1.1 2007/03/25 10:10:21 cweiske Exp $
+
+/**
+ * Class for working with MIME type parameters
+ *
+ * @version 1.2.0
+ * @package MIME_Type
+ * @author Ian Eure <ieure@php.net>
+ */
+class MIME_Type_Parameter {
+ /**
+ * Parameter name
+ *
+ * @var string
+ */
+ var $name;
+
+ /**
+ * Parameter value
+ *
+ * @var string
+ */
+ var $value;
+
+ /**
+ * Parameter comment
+ *
+ * @var string
+ */
+ var $comment;
+
+
+ /**
+ * Constructor.
+ *
+ * @param string $param MIME parameter to parse, if set.
+ * @return void
+ */
+ function MIME_Type_Parameter($param = false)
+ {
+ if ($param) {
+ $this->parse($param);
+ }
+ }
+
+
+ /**
+ * Parse a MIME type parameter and set object fields
+ *
+ * @param string $param MIME type parameter to parse
+ * @return void
+ */
+ function parse($param)
+ {
+ $comment = '';
+ $param = MIME_Type::stripComments($param, $comment);
+ $this->name = $this->getAttribute($param);
+ $this->value = $this->getValue($param);
+ $this->comment = $comment;
+ }
+
+
+ /**
+ * Get a parameter attribute (e.g. name)
+ *
+ * @param string MIME type parameter
+ * @return string Attribute name
+ * @static
+ */
+ function getAttribute($param)
+ {
+ $tmp = explode('=', $param);
+ return trim($tmp[0]);
+ }
+
+
+ /**
+ * Get a parameter value
+ *
+ * @param string $param MIME type parameter
+ * @return string Value
+ * @static
+ */
+ function getValue($param)
+ {
+ $tmp = explode('=', $param, 2);
+ $value = $tmp[1];
+ $value = trim($value);
+ if ($value[0] == '"' && $value[strlen($value)-1] == '"') {
+ $value = substr($value, 1, -1);
+ }
+ $value = str_replace('\\"', '"', $value);
+ return $value;
+ }
+
+
+ /**
+ * Get a parameter comment
+ *
+ * @param string $param MIME type parameter
+ * @return string Parameter comment
+ * @see getComment()
+ * @static
+ */
+ function getComment($param)
+ {
+ $cs = strpos($param, '(');
+ $comment = substr($param, $cs);
+ return trim($comment, '() ');
+ }
+
+
+ /**
+ * Does this parameter have a comment?
+ *
+ * @param string $param MIME type parameter
+ * @return boolean true if $param has a comment, false otherwise
+ * @static
+ */
+ function hasComment($param)
+ {
+ if (strstr($param, '(')) {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Get a string representation of this parameter
+ *
+ * This function performs the oppsite of parse()
+ *
+ * @return string String representation of parameter
+ */
+ function get()
+ {
+ $val = $this->name . '="' . str_replace('"', '\\"', $this->value) . '"';
+ if ($this->comment) {
+ $val .= ' (' . $this->comment . ')';
+ }
+ return $val;
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Mail/mimeDecode.php b/extlib/Mail/mimeDecode.php
new file mode 100644
index 000000000..aaa870c50
--- /dev/null
+++ b/extlib/Mail/mimeDecode.php
@@ -0,0 +1,849 @@
+<?php
+/**
+ * The Mail_mimeDecode class is used to decode mail/mime messages
+ *
+ * This class will parse a raw mime email and return
+ * the structure. Returned structure is similar to
+ * that returned by imap_fetchstructure().
+ *
+ * +----------------------------- IMPORTANT ------------------------------+
+ * | Usage of this class compared to native php extensions such as |
+ * | mailparse or imap, is slow and may be feature deficient. If available|
+ * | you are STRONGLY recommended to use the php extensions. |
+ * +----------------------------------------------------------------------+
+ *
+ * Compatible with PHP versions 4 and 5
+ *
+ * LICENSE: This LICENSE is in the BSD license style.
+ * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
+ * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
+ * 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.
+ * - Neither the name of the authors, nor the names of its contributors
+ * may 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 Mail
+ * @package Mail_Mime
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author George Schlossnagle <george@omniti.com>
+ * @author Cipriano Groenendal <cipri@php.net>
+ * @author Sean Coates <sean@php.net>
+ * @copyright 2003-2006 PEAR <pear-group@php.net>
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ * @version CVS: $Id: mimeDecode.php,v 1.48 2006/12/03 13:43:33 cipri Exp $
+ * @link http://pear.php.net/package/Mail_mime
+ */
+
+
+/**
+ * require PEAR
+ *
+ * This package depends on PEAR to raise errors.
+ */
+require_once 'PEAR.php';
+
+
+/**
+ * The Mail_mimeDecode class is used to decode mail/mime messages
+ *
+ * This class will parse a raw mime email and return the structure.
+ * Returned structure is similar to that returned by imap_fetchstructure().
+ *
+ * +----------------------------- IMPORTANT ------------------------------+
+ * | Usage of this class compared to native php extensions such as |
+ * | mailparse or imap, is slow and may be feature deficient. If available|
+ * | you are STRONGLY recommended to use the php extensions. |
+ * +----------------------------------------------------------------------+
+ *
+ * @category Mail
+ * @package Mail_Mime
+ * @author Richard Heyes <richard@phpguru.org>
+ * @author George Schlossnagle <george@omniti.com>
+ * @author Cipriano Groenendal <cipri@php.net>
+ * @author Sean Coates <sean@php.net>
+ * @copyright 2003-2006 PEAR <pear-group@php.net>
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License
+ * @version Release: @package_version@
+ * @link http://pear.php.net/package/Mail_mime
+ */
+class Mail_mimeDecode extends PEAR
+{
+ /**
+ * The raw email to decode
+ *
+ * @var string
+ * @access private
+ */
+ var $_input;
+
+ /**
+ * The header part of the input
+ *
+ * @var string
+ * @access private
+ */
+ var $_header;
+
+ /**
+ * The body part of the input
+ *
+ * @var string
+ * @access private
+ */
+ var $_body;
+
+ /**
+ * If an error occurs, this is used to store the message
+ *
+ * @var string
+ * @access private
+ */
+ var $_error;
+
+ /**
+ * Flag to determine whether to include bodies in the
+ * returned object.
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_include_bodies;
+
+ /**
+ * Flag to determine whether to decode bodies
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_decode_bodies;
+
+ /**
+ * Flag to determine whether to decode headers
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_decode_headers;
+
+ /**
+ * Constructor.
+ *
+ * Sets up the object, initialise the variables, and splits and
+ * stores the header and body of the input.
+ *
+ * @param string The input to decode
+ * @access public
+ */
+ function Mail_mimeDecode($input)
+ {
+ list($header, $body) = $this->_splitBodyHeader($input);
+
+ $this->_input = $input;
+ $this->_header = $header;
+ $this->_body = $body;
+ $this->_decode_bodies = false;
+ $this->_include_bodies = true;
+ }
+
+ /**
+ * Begins the decoding process. If called statically
+ * it will create an object and call the decode() method
+ * of it.
+ *
+ * @param array An array of various parameters that determine
+ * various things:
+ * include_bodies - Whether to include the body in the returned
+ * object.
+ * decode_bodies - Whether to decode the bodies
+ * of the parts. (Transfer encoding)
+ * decode_headers - Whether to decode headers
+ * input - If called statically, this will be treated
+ * as the input
+ * @return object Decoded results
+ * @access public
+ */
+ function decode($params = null)
+ {
+ // determine if this method has been called statically
+ $isStatic = !(isset($this) && get_class($this) == __CLASS__);
+
+ // Have we been called statically?
+ // If so, create an object and pass details to that.
+ if ($isStatic AND isset($params['input'])) {
+
+ $obj = new Mail_mimeDecode($params['input']);
+ $structure = $obj->decode($params);
+
+ // Called statically but no input
+ } elseif ($isStatic) {
+ return PEAR::raiseError('Called statically and no input given');
+
+ // Called via an object
+ } else {
+ $this->_include_bodies = isset($params['include_bodies']) ?
+ $params['include_bodies'] : false;
+ $this->_decode_bodies = isset($params['decode_bodies']) ?
+ $params['decode_bodies'] : false;
+ $this->_decode_headers = isset($params['decode_headers']) ?
+ $params['decode_headers'] : false;
+
+ $structure = $this->_decode($this->_header, $this->_body);
+ if ($structure === false) {
+ $structure = $this->raiseError($this->_error);
+ }
+ }
+
+ return $structure;
+ }
+
+ /**
+ * Performs the decoding. Decodes the body string passed to it
+ * If it finds certain content-types it will call itself in a
+ * recursive fashion
+ *
+ * @param string Header section
+ * @param string Body section
+ * @return object Results of decoding process
+ * @access private
+ */
+ function _decode($headers, $body, $default_ctype = 'text/plain')
+ {
+ $return = new stdClass;
+ $return->headers = array();
+ $headers = $this->_parseHeaders($headers);
+
+ foreach ($headers as $value) {
+ if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) {
+ $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]);
+ $return->headers[strtolower($value['name'])][] = $value['value'];
+
+ } elseif (isset($return->headers[strtolower($value['name'])])) {
+ $return->headers[strtolower($value['name'])][] = $value['value'];
+
+ } else {
+ $return->headers[strtolower($value['name'])] = $value['value'];
+ }
+ }
+
+ reset($headers);
+ while (list($key, $value) = each($headers)) {
+ $headers[$key]['name'] = strtolower($headers[$key]['name']);
+ switch ($headers[$key]['name']) {
+
+ case 'content-type':
+ $content_type = $this->_parseHeaderValue($headers[$key]['value']);
+
+ if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
+ $return->ctype_primary = $regs[1];
+ $return->ctype_secondary = $regs[2];
+ }
+
+ if (isset($content_type['other'])) {
+ while (list($p_name, $p_value) = each($content_type['other'])) {
+ $return->ctype_parameters[$p_name] = $p_value;
+ }
+ }
+ break;
+
+ case 'content-disposition':
+ $content_disposition = $this->_parseHeaderValue($headers[$key]['value']);
+ $return->disposition = $content_disposition['value'];
+ if (isset($content_disposition['other'])) {
+ while (list($p_name, $p_value) = each($content_disposition['other'])) {
+ $return->d_parameters[$p_name] = $p_value;
+ }
+ }
+ break;
+
+ case 'content-transfer-encoding':
+ $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']);
+ break;
+ }
+ }
+
+ if (isset($content_type)) {
+ switch (strtolower($content_type['value'])) {
+ case 'text/plain':
+ $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
+ $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
+ break;
+
+ case 'text/html':
+ $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
+ $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
+ break;
+
+ case 'multipart/parallel':
+ case 'multipart/appledouble': // Appledouble mail
+ case 'multipart/report': // RFC1892
+ case 'multipart/signed': // PGP
+ case 'multipart/digest':
+ case 'multipart/alternative':
+ case 'multipart/related':
+ case 'multipart/mixed':
+ if(!isset($content_type['other']['boundary'])){
+ $this->_error = 'No boundary found for ' . $content_type['value'] . ' part';
+ return false;
+ }
+
+ $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain';
+
+ $parts = $this->_boundarySplit($body, $content_type['other']['boundary']);
+ for ($i = 0; $i < count($parts); $i++) {
+ list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]);
+ $part = $this->_decode($part_header, $part_body, $default_ctype);
+ if($part === false)
+ $part = $this->raiseError($this->_error);
+ $return->parts[] = $part;
+ }
+ break;
+
+ case 'message/rfc822':
+ $obj = &new Mail_mimeDecode($body);
+ $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies,
+ 'decode_bodies' => $this->_decode_bodies,
+ 'decode_headers' => $this->_decode_headers));
+ unset($obj);
+ break;
+
+ default:
+ if(!isset($content_transfer_encoding['value']))
+ $content_transfer_encoding['value'] = '7bit';
+ $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null;
+ break;
+ }
+
+ } else {
+ $ctype = explode('/', $default_ctype);
+ $return->ctype_primary = $ctype[0];
+ $return->ctype_secondary = $ctype[1];
+ $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null;
+ }
+
+ return $return;
+ }
+
+ /**
+ * Given the output of the above function, this will return an
+ * array of references to the parts, indexed by mime number.
+ *
+ * @param object $structure The structure to go through
+ * @param string $mime_number Internal use only.
+ * @return array Mime numbers
+ */
+ function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '')
+ {
+ $return = array();
+ if (!empty($structure->parts)) {
+ if ($mime_number != '') {
+ $structure->mime_id = $prepend . $mime_number;
+ $return[$prepend . $mime_number] = &$structure;
+ }
+ for ($i = 0; $i < count($structure->parts); $i++) {
+
+
+ if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') {
+ $prepend = $prepend . $mime_number . '.';
+ $_mime_number = '';
+ } else {
+ $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1));
+ }
+
+ $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
+ foreach ($arr as $key => $val) {
+ $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key];
+ }
+ }
+ } else {
+ if ($mime_number == '') {
+ $mime_number = '1';
+ }
+ $structure->mime_id = $prepend . $mime_number;
+ $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure;
+ }
+
+ return $return;
+ }
+
+ /**
+ * Given a string containing a header and body
+ * section, this function will split them (at the first
+ * blank line) and return them.
+ *
+ * @param string Input to split apart
+ * @return array Contains header and body section
+ * @access private
+ */
+ function _splitBodyHeader($input)
+ {
+ if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) {
+ return array($match[1], $match[2]);
+ }
+ $this->_error = 'Could not split header and body';
+ return false;
+ }
+
+ /**
+ * Parse headers given in $input and return
+ * as assoc array.
+ *
+ * @param string Headers to parse
+ * @return array Contains parsed headers
+ * @access private
+ */
+ function _parseHeaders($input)
+ {
+
+ if ($input !== '') {
+ // Unfold the input
+ $input = preg_replace("/\r?\n/", "\r\n", $input);
+ $input = preg_replace("/\r\n(\t| )+/", ' ', $input);
+ $headers = explode("\r\n", trim($input));
+
+ foreach ($headers as $value) {
+ $hdr_name = substr($value, 0, $pos = strpos($value, ':'));
+ $hdr_value = substr($value, $pos+1);
+ if($hdr_value[0] == ' ')
+ $hdr_value = substr($hdr_value, 1);
+
+ $return[] = array(
+ 'name' => $hdr_name,
+ 'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value
+ );
+ }
+ } else {
+ $return = array();
+ }
+
+ return $return;
+ }
+
+ /**
+ * Function to parse a header value,
+ * extract first part, and any secondary
+ * parts (after ;) This function is not as
+ * robust as it could be. Eg. header comments
+ * in the wrong place will probably break it.
+ *
+ * @param string Header value to parse
+ * @return array Contains parsed result
+ * @access private
+ */
+ function _parseHeaderValue($input)
+ {
+
+ if (($pos = strpos($input, ';')) !== false) {
+
+ $return['value'] = trim(substr($input, 0, $pos));
+ $input = trim(substr($input, $pos+1));
+
+ if (strlen($input) > 0) {
+
+ // This splits on a semi-colon, if there's no preceeding backslash
+ // Now works with quoted values; had to glue the \; breaks in PHP
+ // the regex is already bordering on incomprehensible
+ $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/';
+ preg_match_all($splitRegex, $input, $matches);
+ $parameters = array();
+ for ($i=0; $i<count($matches[0]); $i++) {
+ $param = $matches[0][$i];
+ while (substr($param, -2) == '\;') {
+ $param .= $matches[0][++$i];
+ }
+ $parameters[] = $param;
+ }
+
+ for ($i = 0; $i < count($parameters); $i++) {
+ $param_name = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ ");
+ $param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ ");
+ if ($param_value[0] == '"') {
+ $param_value = substr($param_value, 1, -1);
+ }
+ $return['other'][$param_name] = $param_value;
+ $return['other'][strtolower($param_name)] = $param_value;
+ }
+ }
+ } else {
+ $return['value'] = trim($input);
+ }
+
+ return $return;
+ }
+
+ /**
+ * This function splits the input based
+ * on the given boundary
+ *
+ * @param string Input to parse
+ * @return array Contains array of resulting mime parts
+ * @access private
+ */
+ function _boundarySplit($input, $boundary)
+ {
+ $parts = array();
+
+ $bs_possible = substr($boundary, 2, -2);
+ $bs_check = '\"' . $bs_possible . '\"';
+
+ if ($boundary == $bs_check) {
+ $boundary = $bs_possible;
+ }
+
+ $tmp = explode('--' . $boundary, $input);
+
+ for ($i = 1; $i < count($tmp) - 1; $i++) {
+ $parts[] = $tmp[$i];
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Given a header, this function will decode it
+ * according to RFC2047. Probably not *exactly*
+ * conformant, but it does pass all the given
+ * examples (in RFC2047).
+ *
+ * @param string Input header value to decode
+ * @return string Decoded header value
+ * @access private
+ */
+ function _decodeHeader($input)
+ {
+ // Remove white space between encoded-words
+ $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
+
+ // For each encoded-word...
+ while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
+
+ $encoded = $matches[1];
+ $charset = $matches[2];
+ $encoding = $matches[3];
+ $text = $matches[4];
+
+ switch (strtolower($encoding)) {
+ case 'b':
+ $text = base64_decode($text);
+ break;
+
+ case 'q':
+ $text = str_replace('_', ' ', $text);
+ preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
+ foreach($matches[1] as $value)
+ $text = str_replace('='.$value, chr(hexdec($value)), $text);
+ break;
+ }
+
+ $input = str_replace($encoded, $text, $input);
+ }
+
+ return $input;
+ }
+
+ /**
+ * Given a body string and an encoding type,
+ * this function will decode and return it.
+ *
+ * @param string Input body to decode
+ * @param string Encoding type to use.
+ * @return string Decoded body
+ * @access private
+ */
+ function _decodeBody($input, $encoding = '7bit')
+ {
+ switch (strtolower($encoding)) {
+ case '7bit':
+ return $input;
+ break;
+
+ case 'quoted-printable':
+ return $this->_quotedPrintableDecode($input);
+ break;
+
+ case 'base64':
+ return base64_decode($input);
+ break;
+
+ default:
+ return $input;
+ }
+ }
+
+ /**
+ * Given a quoted-printable string, this
+ * function will decode and return it.
+ *
+ * @param string Input body to decode
+ * @return string Decoded body
+ * @access private
+ */
+ function _quotedPrintableDecode($input)
+ {
+ // Remove soft line breaks
+ $input = preg_replace("/=\r?\n/", '', $input);
+
+ // Replace encoded characters
+ $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input);
+
+ return $input;
+ }
+
+ /**
+ * Checks the input for uuencoded files and returns
+ * an array of them. Can be called statically, eg:
+ *
+ * $files =& Mail_mimeDecode::uudecode($some_text);
+ *
+ * It will check for the begin 666 ... end syntax
+ * however and won't just blindly decode whatever you
+ * pass it.
+ *
+ * @param string Input body to look for attahcments in
+ * @return array Decoded bodies, filenames and permissions
+ * @access public
+ * @author Unknown
+ */
+ function &uudecode($input)
+ {
+ // Find all uuencoded sections
+ preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches);
+
+ for ($j = 0; $j < count($matches[3]); $j++) {
+
+ $str = $matches[3][$j];
+ $filename = $matches[2][$j];
+ $fileperm = $matches[1][$j];
+
+ $file = '';
+ $str = preg_split("/\r?\n/", trim($str));
+ $strlen = count($str);
+
+ for ($i = 0; $i < $strlen; $i++) {
+ $pos = 1;
+ $d = 0;
+ $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077);
+
+ while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) {
+ $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
+ $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
+ $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
+ $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20);
+ $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
+
+ $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
+
+ $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077));
+
+ $pos += 4;
+ $d += 3;
+ }
+
+ if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) {
+ $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
+ $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
+ $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
+ $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
+
+ $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
+
+ $pos += 3;
+ $d += 2;
+ }
+
+ if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) {
+ $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
+ $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
+ $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
+
+ }
+ }
+ $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file);
+ }
+
+ return $files;
+ }
+
+ /**
+ * getSendArray() returns the arguments required for Mail::send()
+ * used to build the arguments for a mail::send() call
+ *
+ * Usage:
+ * $mailtext = Full email (for example generated by a template)
+ * $decoder = new Mail_mimeDecode($mailtext);
+ * $parts = $decoder->getSendArray();
+ * if (!PEAR::isError($parts) {
+ * list($recipents,$headers,$body) = $parts;
+ * $mail = Mail::factory('smtp');
+ * $mail->send($recipents,$headers,$body);
+ * } else {
+ * echo $parts->message;
+ * }
+ * @return mixed array of recipeint, headers,body or Pear_Error
+ * @access public
+ * @author Alan Knowles <alan@akbkhome.com>
+ */
+ function getSendArray()
+ {
+ // prevent warning if this is not set
+ $this->_decode_headers = FALSE;
+ $headerlist =$this->_parseHeaders($this->_header);
+ $to = "";
+ if (!$headerlist) {
+ return $this->raiseError("Message did not contain headers");
+ }
+ foreach($headerlist as $item) {
+ $header[$item['name']] = $item['value'];
+ switch (strtolower($item['name'])) {
+ case "to":
+ case "cc":
+ case "bcc":
+ $to = ",".$item['value'];
+ default:
+ break;
+ }
+ }
+ if ($to == "") {
+ return $this->raiseError("Message did not contain any recipents");
+ }
+ $to = substr($to,1);
+ return array($to,$header,$this->_body);
+ }
+
+ /**
+ * Returns a xml copy of the output of
+ * Mail_mimeDecode::decode. Pass the output in as the
+ * argument. This function can be called statically. Eg:
+ *
+ * $output = $obj->decode();
+ * $xml = Mail_mimeDecode::getXML($output);
+ *
+ * The DTD used for this should have been in the package. Or
+ * alternatively you can get it from cvs, or here:
+ * http://www.phpguru.org/xmail/xmail.dtd.
+ *
+ * @param object Input to convert to xml. This should be the
+ * output of the Mail_mimeDecode::decode function
+ * @return string XML version of input
+ * @access public
+ */
+ function getXML($input)
+ {
+ $crlf = "\r\n";
+ $output = '<?xml version=\'1.0\'?>' . $crlf .
+ '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf .
+ '<email>' . $crlf .
+ Mail_mimeDecode::_getXML($input) .
+ '</email>';
+
+ return $output;
+ }
+
+ /**
+ * Function that does the actual conversion to xml. Does a single
+ * mimepart at a time.
+ *
+ * @param object Input to convert to xml. This is a mimepart object.
+ * It may or may not contain subparts.
+ * @param integer Number of tabs to indent
+ * @return string XML version of input
+ * @access private
+ */
+ function _getXML($input, $indent = 1)
+ {
+ $htab = "\t";
+ $crlf = "\r\n";
+ $output = '';
+ $headers = @(array)$input->headers;
+
+ foreach ($headers as $hdr_name => $hdr_value) {
+
+ // Multiple headers with this name
+ if (is_array($headers[$hdr_name])) {
+ for ($i = 0; $i < count($hdr_value); $i++) {
+ $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent);
+ }
+
+ // Only one header of this sort
+ } else {
+ $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent);
+ }
+ }
+
+ if (!empty($input->parts)) {
+ for ($i = 0; $i < count($input->parts); $i++) {
+ $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf .
+ Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) .
+ str_repeat($htab, $indent) . '</mimepart>' . $crlf;
+ }
+ } elseif (isset($input->body)) {
+ $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' .
+ $input->body . ']]></body>' . $crlf;
+ }
+
+ return $output;
+ }
+
+ /**
+ * Helper function to _getXML(). Returns xml of a header.
+ *
+ * @param string Name of header
+ * @param string Value of header
+ * @param integer Number of tabs to indent
+ * @return string XML version of input
+ * @access private
+ */
+ function _getXML_helper($hdr_name, $hdr_value, $indent)
+ {
+ $htab = "\t";
+ $crlf = "\r\n";
+ $return = '';
+
+ $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value);
+ $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name)));
+
+ // Sort out any parameters
+ if (!empty($new_hdr_value['other'])) {
+ foreach ($new_hdr_value['other'] as $paramname => $paramvalue) {
+ $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf .
+ str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf .
+ str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf .
+ str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf;
+ }
+
+ $params = implode('', $params);
+ } else {
+ $params = '';
+ }
+
+ $return = str_repeat($htab, $indent) . '<header>' . $crlf .
+ str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf .
+ str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf .
+ $params .
+ str_repeat($htab, $indent) . '</header>' . $crlf;
+
+ return $return;
+ }
+
+} // End of class
diff --git a/extlib/Net/URL2.php b/extlib/Net/URL2.php
new file mode 100644
index 000000000..7a654aed8
--- /dev/null
+++ b/extlib/Net/URL2.php
@@ -0,0 +1,813 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Copyright (c) 2007-2008, Christian Schmidt, Peytz & Co. A/S |
+// | 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: Christian Schmidt <schmidt at php dot net> |
+// +-----------------------------------------------------------------------+
+//
+// $Id: URL2.php,v 1.10 2008/04/26 21:57:08 schmidt Exp $
+//
+// Net_URL2 Class (PHP5 Only)
+
+// This code is released under the BSD License - http://www.opensource.org/licenses/bsd-license.php
+/**
+ * @license BSD License
+ */
+class Net_URL2
+{
+ /**
+ * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default
+ * is true.
+ */
+ const OPTION_STRICT = 'strict';
+
+ /**
+ * Represent arrays in query using PHP's [] notation. Default is true.
+ */
+ const OPTION_USE_BRACKETS = 'use_brackets';
+
+ /**
+ * URL-encode query variable keys. Default is true.
+ */
+ const OPTION_ENCODE_KEYS = 'encode_keys';
+
+ /**
+ * Query variable separators when parsing the query string. Every character
+ * is considered a separator. Default is specified by the
+ * arg_separator.input php.ini setting (this defaults to "&").
+ */
+ const OPTION_SEPARATOR_INPUT = 'input_separator';
+
+ /**
+ * Query variable separator used when generating the query string. Default
+ * is specified by the arg_separator.output php.ini setting (this defaults
+ * to "&").
+ */
+ const OPTION_SEPARATOR_OUTPUT = 'output_separator';
+
+ /**
+ * Default options corresponds to how PHP handles $_GET.
+ */
+ private $options = array(
+ self::OPTION_STRICT => true,
+ self::OPTION_USE_BRACKETS => true,
+ self::OPTION_ENCODE_KEYS => true,
+ self::OPTION_SEPARATOR_INPUT => 'x&',
+ self::OPTION_SEPARATOR_OUTPUT => 'x&',
+ );
+
+ /**
+ * @var string|bool
+ */
+ private $scheme = false;
+
+ /**
+ * @var string|bool
+ */
+ private $userinfo = false;
+
+ /**
+ * @var string|bool
+ */
+ private $host = false;
+
+ /**
+ * @var int|bool
+ */
+ private $port = false;
+
+ /**
+ * @var string
+ */
+ private $path = '';
+
+ /**
+ * @var string|bool
+ */
+ private $query = false;
+
+ /**
+ * @var string|bool
+ */
+ private $fragment = false;
+
+ /**
+ * @param string $url an absolute or relative URL
+ * @param array $options
+ */
+ public function __construct($url, $options = null)
+ {
+ $this->setOption(self::OPTION_SEPARATOR_INPUT,
+ ini_get('arg_separator.input'));
+ $this->setOption(self::OPTION_SEPARATOR_OUTPUT,
+ ini_get('arg_separator.output'));
+ if (is_array($options)) {
+ foreach ($options as $optionName => $value) {
+ $this->setOption($optionName);
+ }
+ }
+
+ if (preg_match('@^([a-z][a-z0-9.+-]*):@i', $url, $reg)) {
+ $this->scheme = $reg[1];
+ $url = substr($url, strlen($reg[0]));
+ }
+
+ if (preg_match('@^//([^/#?]+)@', $url, $reg)) {
+ $this->setAuthority($reg[1]);
+ $url = substr($url, strlen($reg[0]));
+ }
+
+ $i = strcspn($url, '?#');
+ $this->path = substr($url, 0, $i);
+ $url = substr($url, $i);
+
+ if (preg_match('@^\?([^#]*)@', $url, $reg)) {
+ $this->query = $reg[1];
+ $url = substr($url, strlen($reg[0]));
+ }
+
+ if ($url) {
+ $this->fragment = substr($url, 1);
+ }
+ }
+
+ /**
+ * Returns the scheme, e.g. "http" or "urn", or false if there is no
+ * scheme specified, i.e. if this is a relative URL.
+ *
+ * @return string|bool
+ */
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ /**
+ * @param string|bool $scheme
+ *
+ * @return void
+ * @see getScheme()
+ */
+ public function setScheme($scheme)
+ {
+ $this->scheme = $scheme;
+ }
+
+ /**
+ * Returns the user part of the userinfo part (the part preceding the first
+ * ":"), or false if there is no userinfo part.
+ *
+ * @return string|bool
+ */
+ public function getUser()
+ {
+ return $this->userinfo !== false ? preg_replace('@:.*$@', '', $this->userinfo) : false;
+ }
+
+ /**
+ * Returns the password part of the userinfo part (the part after the first
+ * ":"), or false if there is no userinfo part (i.e. the URL does not
+ * contain "@" in front of the hostname) or the userinfo part does not
+ * contain ":".
+ *
+ * @return string|bool
+ */
+ public function getPassword()
+ {
+ return $this->userinfo !== false ? substr(strstr($this->userinfo, ':'), 1) : false;
+ }
+
+ /**
+ * Returns the userinfo part, or false if there is none, i.e. if the
+ * authority part does not contain "@".
+ *
+ * @return string|bool
+ */
+ public function getUserinfo()
+ {
+ return $this->userinfo;
+ }
+
+ /**
+ * Sets the userinfo part. If two arguments are passed, they are combined
+ * in the userinfo part as username ":" password.
+ *
+ * @param string|bool $userinfo userinfo or username
+ * @param string|bool $password
+ *
+ * @return void
+ */
+ public function setUserinfo($userinfo, $password = false)
+ {
+ $this->userinfo = $userinfo;
+ if ($password !== false) {
+ $this->userinfo .= ':' . $password;
+ }
+ }
+
+ /**
+ * Returns the host part, or false if there is no authority part, e.g.
+ * relative URLs.
+ *
+ * @return string|bool
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * @param string|bool $host
+ *
+ * @return void
+ */
+ public function setHost($host)
+ {
+ $this->host = $host;
+ }
+
+ /**
+ * Returns the port number, or false if there is no port number specified,
+ * i.e. if the default port is to be used.
+ *
+ * @return int|bool
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * @param int|bool $port
+ *
+ * @return void
+ */
+ public function setPort($port)
+ {
+ $this->port = intval($port);
+ }
+
+ /**
+ * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or
+ * false if there is no authority none.
+ *
+ * @return string|bool
+ */
+ public function getAuthority()
+ {
+ if (!$this->host) {
+ return false;
+ }
+
+ $authority = '';
+
+ if ($this->userinfo !== false) {
+ $authority .= $this->userinfo . '@';
+ }
+
+ $authority .= $this->host;
+
+ if ($this->port !== false) {
+ $authority .= ':' . $this->port;
+ }
+
+ return $authority;
+ }
+
+ /**
+ * @param string|false $authority
+ *
+ * @return void
+ */
+ public function setAuthority($authority)
+ {
+ $this->user = false;
+ $this->pass = false;
+ $this->host = false;
+ $this->port = false;
+ if (preg_match('@^(([^\@]+)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) {
+ if ($reg[1]) {
+ $this->userinfo = $reg[2];
+ }
+
+ $this->host = $reg[3];
+ if (isset($reg[5])) {
+ $this->port = intval($reg[5]);
+ }
+ }
+ }
+
+ /**
+ * Returns the path part (possibly an empty string).
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * @param string $path
+ *
+ * @return void
+ */
+ public function setPath($path)
+ {
+ $this->path = $path;
+ }
+
+ /**
+ * Returns the query string (excluding the leading "?"), or false if "?"
+ * isn't present in the URL.
+ *
+ * @return string|bool
+ * @see self::getQueryVariables()
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * @param string|bool $query
+ *
+ * @return void
+ * @see self::setQueryVariables()
+ */
+ public function setQuery($query)
+ {
+ $this->query = $query;
+ }
+
+ /**
+ * Returns the fragment name, or false if "#" isn't present in the URL.
+ *
+ * @return string|bool
+ */
+ public function getFragment()
+ {
+ return $this->fragment;
+ }
+
+ /**
+ * @param string|bool $fragment
+ *
+ * @return void
+ */
+ public function setFragment($fragment)
+ {
+ $this->fragment = $fragment;
+ }
+
+ /**
+ * Returns the query string like an array as the variables would appear in
+ * $_GET in a PHP script.
+ *
+ * @return array
+ */
+ public function getQueryVariables()
+ {
+ $pattern = '/[' .
+ preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') .
+ ']/';
+ $parts = preg_split($pattern, $this->query, -1, PREG_SPLIT_NO_EMPTY);
+ $return = array();
+
+ foreach ($parts as $part) {
+ if (strpos($part, '=') !== false) {
+ list($key, $value) = explode('=', $part, 2);
+ } else {
+ $key = $part;
+ $value = null;
+ }
+
+ if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
+ $key = rawurldecode($key);
+ }
+ $value = rawurldecode($value);
+
+ if ($this->getOption(self::OPTION_USE_BRACKETS) &&
+ 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->getOption(self::OPTION_USE_BRACKETS)
+ && !empty($return[$key])
+ ) {
+ $return[$key] = (array) $return[$key];
+ $return[$key][] = $value;
+ } else {
+ $return[$key] = $value;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * @param array $array (name => value) array
+ *
+ * @return void
+ */
+ public function setQueryVariables(array $array)
+ {
+ if (!$array) {
+ $this->query = false;
+ } else {
+ foreach ($array as $name => $value) {
+ if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
+ $name = rawurlencode($name);
+ }
+
+ if (is_array($value)) {
+ foreach ($value as $k => $v) {
+ $parts[] = $this->getOption(self::OPTION_USE_BRACKETS)
+ ? sprintf('%s[%s]=%s', $name, $k, $v)
+ : ($name . '=' . $v);
+ }
+ } elseif (!is_null($value)) {
+ $parts[] = $name . '=' . $value;
+ } else {
+ $parts[] = $name;
+ }
+ }
+ $this->query = implode($this->getOption(self::OPTION_SEPARATOR_OUTPUT),
+ $parts);
+ }
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return array
+ */
+ public function setQueryVariable($name, $value)
+ {
+ $array = $this->getQueryVariables();
+ $array[$name] = $value;
+ $this->setQueryVariables($array);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return void
+ */
+ public function unsetQueryVariable($name)
+ {
+ $array = $this->getQueryVariables();
+ unset($array[$name]);
+ $this->setQueryVariables($array);
+ }
+
+ /**
+ * Returns a string representation of this URL.
+ *
+ * @return string
+ */
+ public function getURL()
+ {
+ // See RFC 3986, section 5.3
+ $url = "";
+
+ if ($this->scheme !== false) {
+ $url .= $this->scheme . ':';
+ }
+
+ $authority = $this->getAuthority();
+ if ($authority !== false) {
+ $url .= '//' . $authority;
+ }
+ $url .= $this->path;
+
+ if ($this->query !== false) {
+ $url .= '?' . $this->query;
+ }
+
+ if ($this->fragment !== false) {
+ $url .= '#' . $this->fragment;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns a normalized string representation of this URL. This is useful
+ * for comparison of URLs.
+ *
+ * @return string
+ */
+ public function getNormalizedURL()
+ {
+ $url = clone $this;
+ $url->normalize();
+ return $url->getUrl();
+ }
+
+ /**
+ * Returns a normalized Net_URL2 instance.
+ *
+ * @return Net_URL2
+ */
+ public function normalize()
+ {
+ // See RFC 3886, section 6
+
+ // Schemes are case-insensitive
+ if ($this->scheme) {
+ $this->scheme = strtolower($this->scheme);
+ }
+
+ // Hostnames are case-insensitive
+ if ($this->host) {
+ $this->host = strtolower($this->host);
+ }
+
+ // Remove default port number for known schemes (RFC 3986, section 6.2.3)
+ if ($this->port &&
+ $this->scheme &&
+ $this->port == getservbyname($this->scheme, 'tcp')) {
+
+ $this->port = false;
+ }
+
+ // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
+ foreach (array('userinfo', 'host', 'path') as $part) {
+ if ($this->$part) {
+ $this->$part = preg_replace('/%[0-9a-f]{2}/ie', 'strtoupper("\0")', $this->$part);
+ }
+ }
+
+ // Path segment normalization (RFC 3986, section 6.2.2.3)
+ $this->path = self::removeDotSegments($this->path);
+
+ // Scheme based normalization (RFC 3986, section 6.2.3)
+ if ($this->host && !$this->path) {
+ $this->path = '/';
+ }
+ }
+
+ /**
+ * Returns whether this instance represents an absolute URL.
+ *
+ * @return bool
+ */
+ public function isAbsolute()
+ {
+ return (bool) $this->scheme;
+ }
+
+ /**
+ * Returns an Net_URL2 instance representing an absolute URL relative to
+ * this URL.
+ *
+ * @param Net_URL2|string $reference relative URL
+ *
+ * @return Net_URL2
+ */
+ public function resolve($reference)
+ {
+ if (is_string($reference)) {
+ $reference = new self($reference);
+ }
+ if (!$this->isAbsolute()) {
+ throw new Exception('Base-URL must be absolute');
+ }
+
+ // A non-strict parser may ignore a scheme in the reference if it is
+ // identical to the base URI's scheme.
+ if (!$this->getOption(self::OPTION_STRICT) && $reference->scheme == $this->scheme) {
+ $reference->scheme = false;
+ }
+
+ $target = new self('');
+ if ($reference->scheme !== false) {
+ $target->scheme = $reference->scheme;
+ $target->setAuthority($reference->getAuthority());
+ $target->path = self::removeDotSegments($reference->path);
+ $target->query = $reference->query;
+ } else {
+ $authority = $reference->getAuthority();
+ if ($authority !== false) {
+ $target->setAuthority($authority);
+ $target->path = self::removeDotSegments($reference->path);
+ $target->query = $reference->query;
+ } else {
+ if ($reference->path == '') {
+ $target->path = $this->path;
+ if ($reference->query !== false) {
+ $target->query = $reference->query;
+ } else {
+ $target->query = $this->query;
+ }
+ } else {
+ if (substr($reference->path, 0, 1) == '/') {
+ $target->path = self::removeDotSegments($reference->path);
+ } else {
+ // Merge paths (RFC 3986, section 5.2.3)
+ if ($this->host !== false && $this->path == '') {
+ $target->path = '/' . $this->path;
+ } else {
+ $i = strrpos($this->path, '/');
+ if ($i !== false) {
+ $target->path = substr($this->path, 0, $i + 1);
+ }
+ $target->path .= $reference->path;
+ }
+ $target->path = self::removeDotSegments($target->path);
+ }
+ $target->query = $reference->query;
+ }
+ $target->setAuthority($this->getAuthority());
+ }
+ $target->scheme = $this->scheme;
+ }
+
+ $target->fragment = $reference->fragment;
+
+ return $target;
+ }
+
+ /**
+ * Removes dots as described in RFC 3986, section 5.2.4, e.g.
+ * "/foo/../bar/baz" => "/bar/baz"
+ *
+ * @param string $path a path
+ *
+ * @return string a path
+ */
+ private static function removeDotSegments($path)
+ {
+ $output = '';
+
+ // Make sure not to be trapped in an infinite loop due to a bug in this
+ // method
+ $j = 0;
+ while ($path && $j++ < 100) {
+ // Step A
+ if (substr($path, 0, 2) == './') {
+ $path = substr($path, 2);
+ } elseif (substr($path, 0, 3) == '../') {
+ $path = substr($path, 3);
+
+ // Step B
+ } elseif (substr($path, 0, 3) == '/./' || $path == '/.') {
+ $path = '/' . substr($path, 3);
+
+ // Step C
+ } elseif (substr($path, 0, 4) == '/../' || $path == '/..') {
+ $path = '/' . substr($path, 4);
+ $i = strrpos($output, '/');
+ $output = $i === false ? '' : substr($output, 0, $i);
+
+ // Step D
+ } elseif ($path == '.' || $path == '..') {
+ $path = '';
+
+ // Step E
+ } else {
+ $i = strpos($path, '/');
+ if ($i === 0) {
+ $i = strpos($path, '/', 1);
+ }
+ if ($i === false) {
+ $i = strlen($path);
+ }
+ $output .= substr($path, 0, $i);
+ $path = substr($path, $i);
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Returns a Net_URL2 instance representing the canonical URL of the
+ * currently executing PHP script.
+ *
+ * @return string
+ */
+ public static function getCanonical()
+ {
+ if (!isset($_SERVER['REQUEST_METHOD'])) {
+ // ALERT - no current URL
+ throw new Exception('Script was not called through a webserver');
+ }
+
+ // Begin with a relative URL
+ $url = new self($_SERVER['PHP_SELF']);
+ $url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
+ $url->host = $_SERVER['SERVER_NAME'];
+ $port = intval($_SERVER['SERVER_PORT']);
+ if ($url->scheme == 'http' && $port != 80 ||
+ $url->scheme == 'https' && $port != 443) {
+
+ $url->port = $port;
+ }
+ return $url;
+ }
+
+ /**
+ * Returns the URL used to retrieve the current request.
+ *
+ * @return string
+ */
+ public static function getRequestedURL()
+ {
+ return self::getRequested()->getUrl();
+ }
+
+ /**
+ * Returns a Net_URL2 instance representing the URL used to retrieve the
+ * current request.
+ *
+ * @return Net_URL2
+ */
+ public static function getRequested()
+ {
+ if (!isset($_SERVER['REQUEST_METHOD'])) {
+ // ALERT - no current URL
+ throw new Exception('Script was not called through a webserver');
+ }
+
+ // Begin with a relative URL
+ $url = new self($_SERVER['REQUEST_URI']);
+ $url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
+ // Set host and possibly port
+ $url->setAuthority($_SERVER['HTTP_HOST']);
+ return $url;
+ }
+
+ /**
+ * Sets the specified option.
+ *
+ * @param string $optionName a self::OPTION_ constant
+ * @param mixed $value option value
+ *
+ * @return void
+ * @see self::OPTION_STRICT
+ * @see self::OPTION_USE_BRACKETS
+ * @see self::OPTION_ENCODE_KEYS
+ */
+ function setOption($optionName, $value)
+ {
+ if (!array_key_exists($optionName, $this->options)) {
+ return false;
+ }
+ $this->options[$optionName] = $value;
+ }
+
+ /**
+ * Returns the value of the specified option.
+ *
+ * @param string $optionName The name of the option to retrieve
+ *
+ * @return mixed
+ */
+ function getOption($optionName)
+ {
+ return isset($this->options[$optionName])
+ ? $this->options[$optionName] : false;
+ }
+}
diff --git a/extlib/Services/oEmbed.php b/extlib/Services/oEmbed.php
new file mode 100644
index 000000000..5d38ed883
--- /dev/null
+++ b/extlib/Services/oEmbed.php
@@ -0,0 +1,357 @@
+<?php
+
+/**
+ * An interface for oEmbed consumption
+ *
+ * PHP version 5.1.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Validate.php';
+require_once 'Net/URL2.php';
+require_once 'HTTP/Request.php';
+require_once 'Services/oEmbed/Exception.php';
+require_once 'Services/oEmbed/Exception/NoSupport.php';
+require_once 'Services/oEmbed/Object.php';
+
+/**
+ * Base class for consuming oEmbed objects
+ *
+ * <code>
+ * <?php
+ *
+ * require_once 'Services/oEmbed.php';
+ *
+ * // The URL that we'd like to find out more information about.
+ * $url = 'http://flickr.com/photos/joestump/2848795611/';
+ *
+ * // The oEmbed API URI. Not all providers support discovery yet so we're
+ * // explicitly providing one here. If one is not provided Services_oEmbed
+ * // attempts to discover it. If none is found an exception is thrown.
+ * $oEmbed = new Services_oEmbed($url, array(
+ * Services_oEmbed::OPTION_API => 'http://www.flickr.com/services/oembed/'
+ * ));
+ * $object = $oEmbed->getObject();
+ *
+ * // All of the objects have somewhat sane __toString() methods that allow
+ * // you to output them directly.
+ * echo (string)$object;
+ *
+ * ?>
+ * </code>
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed
+{
+ /**
+ * HTTP timeout in seconds
+ *
+ * All HTTP requests made by Services_oEmbed will respect this timeout.
+ * This can be passed to {@link Services_oEmbed::setOption()} or to the
+ * options parameter in {@link Services_oEmbed::__construct()}.
+ *
+ * @var string OPTION_TIMEOUT Timeout in seconds
+ */
+ const OPTION_TIMEOUT = 'http_timeout';
+
+ /**
+ * HTTP User-Agent
+ *
+ * All HTTP requests made by Services_oEmbed will be sent with the
+ * string set by this option.
+ *
+ * @var string OPTION_USER_AGENT The HTTP User-Agent string
+ */
+ const OPTION_USER_AGENT = 'http_user_agent';
+
+ /**
+ * The API's URI
+ *
+ * If the API is known ahead of time this option can be used to explicitly
+ * set it. If not present then the API is attempted to be discovered
+ * through the auto-discovery mechanism.
+ *
+ * @var string OPTION_API
+ */
+ const OPTION_API = 'oembed_api';
+
+ /**
+ * Options for oEmbed requests
+ *
+ * @var array $options The options for making requests
+ */
+ protected $options = array(
+ self::OPTION_TIMEOUT => 3,
+ self::OPTION_API => null,
+ self::OPTION_USER_AGENT => 'Services_oEmbed 0.1.0'
+ );
+
+ /**
+ * URL of object to get embed information for
+ *
+ * @var object $url {@link Net_URL2} instance of URL of object
+ */
+ protected $url = null;
+
+ /**
+ * Constructor
+ *
+ * @param string $url The URL to fetch an oEmbed for
+ * @param array $options A list of options for the oEmbed lookup
+ *
+ * @throws {@link Services_oEmbed_Exception} if the $url is invalid
+ * @throws {@link Services_oEmbed_Exception} when no valid API is found
+ * @return void
+ */
+ public function __construct($url, array $options = array())
+ {
+ if (Validate::uri($url)) {
+ $this->url = new Net_URL2($url);
+ } else {
+ throw new Services_oEmbed_Exception('URL is invalid');
+ }
+
+ if (count($options)) {
+ foreach ($options as $key => $val) {
+ $this->setOption($key, $val);
+ }
+ }
+
+ if ($this->options[self::OPTION_API] === null) {
+ $this->options[self::OPTION_API] = $this->discover();
+ }
+ }
+
+ /**
+ * Set an option for the oEmbed request
+ *
+ * @param mixed $option The option name
+ * @param mixed $value The option value
+ *
+ * @see Services_oEmbed::OPTION_API, Services_oEmbed::OPTION_TIMEOUT
+ * @throws {@link Services_oEmbed_Exception} on invalid option
+ * @access public
+ * @return void
+ */
+ public function setOption($option, $value)
+ {
+ switch ($option) {
+ case self::OPTION_API:
+ case self::OPTION_TIMEOUT:
+ break;
+ default:
+ throw new Services_oEmbed_Exception(
+ 'Invalid option "' . $option . '"'
+ );
+ }
+
+ $func = '_set_' . $option;
+ if (method_exists($this, $func)) {
+ $this->options[$option] = $this->$func($value);
+ } else {
+ $this->options[$option] = $value;
+ }
+ }
+
+ /**
+ * Set the API option
+ *
+ * @param string $value The API's URI
+ *
+ * @throws {@link Services_oEmbed_Exception} on invalid API URI
+ * @see Validate::uri()
+ * @return string
+ */
+ protected function _set_oembed_api($value)
+ {
+ if (!Validate::uri($value)) {
+ throw new Services_oEmbed_Exception(
+ 'API URI provided is invalid'
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the oEmbed response
+ *
+ * @param array $params Optional parameters for
+ *
+ * @throws {@link Services_oEmbed_Exception} on cURL errors
+ * @throws {@link Services_oEmbed_Exception} on HTTP errors
+ * @throws {@link Services_oEmbed_Exception} when result is not parsable
+ * @return object The oEmbed response as an object
+ */
+ public function getObject(array $params = array())
+ {
+ $params['url'] = $this->url->getURL();
+ if (!isset($params['format'])) {
+ $params['format'] = 'json';
+ }
+
+ $sets = array();
+ foreach ($params as $var => $val) {
+ $sets[] = $var . '=' . urlencode($val);
+ }
+
+ $url = $this->options[self::OPTION_API] . '?' . implode('&', $sets);
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_HEADER, false);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]);
+ $result = curl_exec($ch);
+
+ if (curl_errno($ch)) {
+ throw new Services_oEmbed_Exception(
+ curl_error($ch), curl_errno($ch)
+ );
+ }
+
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if (substr($code, 0, 1) != '2') {
+ throw new Services_oEmbed_Exception('Non-200 code returned');
+ }
+
+ curl_close($ch);
+
+ switch ($params['format']) {
+ case 'json':
+ $res = json_decode($result);
+ if (!is_object($res)) {
+ throw new Services_oEmbed_Exception(
+ 'Could not parse JSON response'
+ );
+ }
+ break;
+ case 'xml':
+ libxml_use_internal_errors(true);
+ $res = simplexml_load_string($result);
+ if (!$res instanceof SimpleXMLElement) {
+ $errors = libxml_get_errors();
+ $err = array_shift($errors);
+ libxml_clear_errors();
+ libxml_use_internal_errors(false);
+ throw new Services_oEmbed_Exception(
+ $err->message, $error->code
+ );
+ }
+ break;
+ }
+
+ return Services_oEmbed_Object::factory($res);
+ }
+
+ /**
+ * Discover an oEmbed API
+ *
+ * @param string $url The URL to attempt to discover oEmbed for
+ *
+ * @throws {@link Services_oEmbed_Exception} if the $url is invalid
+ * @return string The oEmbed API endpoint discovered
+ */
+ protected function discover($url)
+ {
+ $body = $this->sendRequest($url);
+
+ // Find all <link /> tags that have a valid oembed type set. We then
+ // extract the href attribute for each type.
+ $regexp = '#<link([^>]*)type="' .
+ '(application/json|text/xml)\+oembed"([^>]*)>#i';
+
+ $m = $ret = array();
+ if (!preg_match_all($regexp, $body, $m)) {
+ throw new Services_oEmbed_Exception_NoSupport(
+ 'No valid oEmbed links found on page'
+ );
+ }
+
+ foreach ($m[0] as $i => $link) {
+ $h = array();
+ if (preg_match('/href="([^"]+)"/i', $link, $h)) {
+ $ret[$m[2][$i]] = $h[1];
+ }
+ }
+
+ return (isset($ret['json']) ? $ret['json'] : array_pop($ret));
+ }
+
+ /**
+ * Send a GET request to the provider
+ *
+ * @param mixed $url The URL to send the request to
+ *
+ * @throws {@link Services_oEmbed_Exception} on HTTP errors
+ * @return string The contents of the response
+ */
+ private function sendRequest($url)
+ {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_HEADER, false);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]);
+ curl_setopt($ch, CURLOPT_USERAGENT, $this->options[self::OPTION_USER_AGENT]);
+ $result = curl_exec($ch);
+ if (curl_errno($ch)) {
+ throw new Services_oEmbed_Exception(
+ curl_error($ch), curl_errno($ch)
+ );
+ }
+
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ if (substr($code, 0, 1) != '2') {
+ throw new Services_oEmbed_Exception('Non-200 code returned');
+ }
+
+ return $result;
+ }
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Exception.php b/extlib/Services/oEmbed/Exception.php
new file mode 100644
index 000000000..446ac2a70
--- /dev/null
+++ b/extlib/Services/oEmbed/Exception.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Base exception class for {@link Services_oEmbed}
+ *
+ * PHP version 5.1.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'PEAR/Exception.php';
+
+/**
+ * Base exception class for {@link Services_oEmbed}
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Exception extends PEAR_Exception
+{
+
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Exception/NoSupport.php b/extlib/Services/oEmbed/Exception/NoSupport.php
new file mode 100644
index 000000000..384c7191f
--- /dev/null
+++ b/extlib/Services/oEmbed/Exception/NoSupport.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Exception class when no oEmbed support is discovered
+ *
+ * PHP version 5.2.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+/**
+ * Exception class when no oEmbed support is discovered
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Exception_NoSupport extends Services_oEmbed_Exception
+{
+
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object.php b/extlib/Services/oEmbed/Object.php
new file mode 100644
index 000000000..9eedd7efb
--- /dev/null
+++ b/extlib/Services/oEmbed/Object.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * An interface for oEmbed consumption
+ *
+ * PHP version 5.1.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Services/oEmbed/Object/Exception.php';
+
+/**
+ * Base class for consuming oEmbed objects
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+abstract class Services_oEmbed_Object
+{
+
+ /**
+ * Valid oEmbed object types
+ *
+ * @var array $types Array of valid object types
+ * @see Services_oEmbed_Object::factory()
+ */
+ static protected $types = array(
+ 'photo' => 'Photo',
+ 'video' => 'Video',
+ 'link' => 'Link',
+ 'rich' => 'Rich'
+ );
+
+ /**
+ * Create an oEmbed object from result
+ *
+ * @param object $object Raw object returned from API
+ *
+ * @throws {@link Services_oEmbed_Object_Exception} on object error
+ * @return object Instance of object driver
+ * @see Services_oEmbed_Object_Link, Services_oEmbed_Object_Photo
+ * @see Services_oEmbed_Object_Rich, Services_oEmbed_Object_Video
+ */
+ static public function factory($object)
+ {
+ if (!isset($object->type)) {
+ throw new Services_oEmbed_Object_Exception(
+ 'Object has no type'
+ );
+ }
+
+ $type = (string)$object->type;
+ if (!isset(self::$types[$type])) {
+ throw new Services_oEmbed_Object_Exception(
+ 'Object type is unknown or invalid: ' . $type
+ );
+ }
+
+ $file = 'Services/oEmbed/Object/' . self::$types[$type] . '.php';
+ include_once $file;
+
+ $class = 'Services_oEmbed_Object_' . self::$types[$type];
+ if (!class_exists($class)) {
+ throw new Services_oEmbed_Object_Exception(
+ 'Object class is invalid or not present'
+ );
+ }
+
+ $instance = new $class($object);
+ return $instance;
+ }
+
+ /**
+ * Instantiation is not allowed
+ *
+ * @return void
+ */
+ private function __construct()
+ {
+
+ }
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object/Common.php b/extlib/Services/oEmbed/Object/Common.php
new file mode 100644
index 000000000..f568ec89f
--- /dev/null
+++ b/extlib/Services/oEmbed/Object/Common.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * Base class for oEmbed objects
+ *
+ * PHP version 5.1.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+/**
+ * Base class for oEmbed objects
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+abstract class Services_oEmbed_Object_Common
+{
+ /**
+ * Raw object returned from API
+ *
+ * @var object $object The raw object from the API
+ */
+ protected $object = null;
+
+ /**
+ * Required fields per the specification
+ *
+ * @var array $required Array of required fields
+ * @link http://oembed.com
+ */
+ protected $required = array();
+
+ /**
+ * Constructor
+ *
+ * @param object $object Raw object returned from the API
+ *
+ * @throws {@link Services_oEmbed_Object_Exception} on missing fields
+ * @return void
+ */
+ public function __construct($object)
+ {
+ $this->object = $object;
+
+ $this->required[] = 'version';
+ foreach ($this->required as $field) {
+ if (!isset($this->$field)) {
+ throw new Services_oEmbed_Object_Exception(
+ 'Object is missing required ' . $field . ' attribute'
+ );
+ }
+ }
+ }
+
+ /**
+ * Get object variable
+ *
+ * @param string $var Variable to get
+ *
+ * @see Services_oEmbed_Object_Common::$object
+ * @return mixed Attribute's value or null if it's not set/exists
+ */
+ public function __get($var)
+ {
+ if (property_exists($this->object, $var)) {
+ return $this->object->$var;
+ }
+
+ return null;
+ }
+
+ /**
+ * Is variable set?
+ *
+ * @param string $var Variable name to check
+ *
+ * @return boolean True if set, false if not
+ * @see Services_oEmbed_Object_Common::$object
+ */
+ public function __isset($var)
+ {
+ if (property_exists($this->object, $var)) {
+ return (isset($this->object->$var));
+ }
+
+ return false;
+ }
+
+ /**
+ * Require a sane __toString for all objects
+ *
+ * @return string
+ */
+ abstract public function __toString();
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object/Exception.php b/extlib/Services/oEmbed/Object/Exception.php
new file mode 100644
index 000000000..6025ffd49
--- /dev/null
+++ b/extlib/Services/oEmbed/Object/Exception.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Exception for {@link Services_oEmbed_Object}
+ *
+ * PHP version 5.1.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Services/oEmbed/Exception.php';
+
+/**
+ * Exception for {@link Services_oEmbed_Object}
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Object_Exception extends Services_oEmbed_Exception
+{
+
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object/Link.php b/extlib/Services/oEmbed/Object/Link.php
new file mode 100644
index 000000000..9b627a89a
--- /dev/null
+++ b/extlib/Services/oEmbed/Object/Link.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Link object for {@link Services_oEmbed}
+ *
+ * PHP version 5.2.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Services/oEmbed/Object/Common.php';
+
+/**
+ * Link object for {@link Services_oEmbed}
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Object_Link extends Services_oEmbed_Object_Common
+{
+ /**
+ * Output a sane link
+ *
+ * @return string An HTML link of the object
+ */
+ public function __toString()
+ {
+ return '<a href="' . $this->url . '">' . $this->title . '</a>';
+ }
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object/Photo.php b/extlib/Services/oEmbed/Object/Photo.php
new file mode 100644
index 000000000..5fbf4292f
--- /dev/null
+++ b/extlib/Services/oEmbed/Object/Photo.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * Photo object for {@link Services_oEmbed}
+ *
+ * PHP version 5.2.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Services/oEmbed/Object/Common.php';
+
+/**
+ * Photo object for {@link Services_oEmbed}
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Object_Photo extends Services_oEmbed_Object_Common
+{
+ /**
+ * Required fields for photo objects
+ *
+ * @var array $required Required fields
+ */
+ protected $required = array(
+ 'url', 'width', 'height'
+ );
+
+ /**
+ * Output a valid HTML tag for image
+ *
+ * @return string HTML <img /> tag for Photo
+ */
+ public function __toString()
+ {
+ $img = '<img src="' . $this->url . '" width="' . $this->width . '" ' .
+ 'height="' . $this->height . '"';
+
+ if (isset($this->title)) {
+ $img .= ' alt="' . $this->title . '"';
+ }
+
+ return $img . ' />';
+ }
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object/Rich.php b/extlib/Services/oEmbed/Object/Rich.php
new file mode 100644
index 000000000..dbf6933ac
--- /dev/null
+++ b/extlib/Services/oEmbed/Object/Rich.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Photo object for {@link Services_oEmbed}
+ *
+ * PHP version 5.2.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Services/oEmbed/Object/Common.php';
+
+/**
+ * Photo object for {@link Services_oEmbed}
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Object_Rich extends Services_oEmbed_Object_Common
+{
+ /**
+ * Required fields for rich objects
+ *
+ * @var array $required Required fields
+ */
+ protected $required = array(
+ 'html', 'width', 'height'
+ );
+
+ /**
+ * Output a the HTML tag for rich object
+ *
+ * @return string HTML for rich object
+ */
+ public function __toString()
+ {
+ return $this->html;
+ }
+}
+
+?>
diff --git a/extlib/Services/oEmbed/Object/Video.php b/extlib/Services/oEmbed/Object/Video.php
new file mode 100644
index 000000000..746108115
--- /dev/null
+++ b/extlib/Services/oEmbed/Object/Video.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Photo object for {@link Services_oEmbed}
+ *
+ * PHP version 5.2.0+
+ *
+ * Copyright (c) 2008, Digg.com, Inc.
+ *
+ * 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.
+ * - Neither the name of Digg.com, Inc. nor the names of its contributors
+ * may 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 Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version SVN: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+
+require_once 'Services/oEmbed/Object/Common.php';
+
+/**
+ * Photo object for {@link Services_oEmbed}
+ *
+ * @category Services
+ * @package Services_oEmbed
+ * @author Joe Stump <joe@joestump.net>
+ * @copyright 2008 Digg.com, Inc.
+ * @license http://tinyurl.com/42zef New BSD License
+ * @version Release: @version@
+ * @link http://code.google.com/p/digg
+ * @link http://oembed.com
+ */
+class Services_oEmbed_Object_Video extends Services_oEmbed_Object_Common
+{
+ /**
+ * Required fields for video objects
+ *
+ * @var array $required Required fields
+ */
+ protected $required = array(
+ 'html', 'width', 'height'
+ );
+
+ /**
+ * Output a valid embed tag for video
+ *
+ * @return string HTML for video
+ */
+ public function __toString()
+ {
+ return $this->html;
+ }
+}
+
+?>
diff --git a/extlib/Stomp.php b/extlib/Stomp.php
new file mode 100644
index 000000000..9e1c97b3b
--- /dev/null
+++ b/extlib/Stomp.php
@@ -0,0 +1,594 @@
+<?php
+/**
+ *
+ * Copyright 2005-2006 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* vim: set expandtab tabstop=3 shiftwidth=3: */
+
+require_once 'Stomp/Frame.php';
+
+/**
+ * A Stomp Connection
+ *
+ *
+ * @package Stomp
+ * @author Hiram Chirino <hiram@hiramchirino.com>
+ * @author Dejan Bosanac <dejan@nighttale.net>
+ * @author Michael Caplan <mcaplan@labnet.net>
+ * @version $Revision: 43 $
+ */
+class Stomp
+{
+ /**
+ * Perform request synchronously
+ *
+ * @var boolean
+ */
+ public $sync = false;
+
+ /**
+ * Default prefetch size
+ *
+ * @var int
+ */
+ public $prefetchSize = 1;
+
+ /**
+ * Client id used for durable subscriptions
+ *
+ * @var string
+ */
+ public $clientId = null;
+
+ protected $_brokerUri = null;
+ protected $_socket = null;
+ protected $_hosts = array();
+ protected $_params = array();
+ protected $_subscriptions = array();
+ protected $_defaultPort = 61613;
+ protected $_currentHost = - 1;
+ protected $_attempts = 10;
+ protected $_username = '';
+ protected $_password = '';
+ protected $_sessionId;
+ protected $_read_timeout_seconds = 60;
+ protected $_read_timeout_milliseconds = 0;
+
+ /**
+ * Constructor
+ *
+ * @param string $brokerUri Broker URL
+ * @throws Stomp_Exception
+ */
+ public function __construct ($brokerUri)
+ {
+ $this->_brokerUri = $brokerUri;
+ $this->_init();
+ }
+ /**
+ * Initialize connection
+ *
+ * @throws Stomp_Exception
+ */
+ protected function _init ()
+ {
+ $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i";
+ if (preg_match($pattern, $this->_brokerUri, $regs)) {
+ $scheme = $regs[2];
+ $hosts = $regs[3];
+ $params = $regs[4];
+ if ($scheme != "failover") {
+ $this->_processUrl($this->_brokerUri);
+ } else {
+ $urls = explode(",", $hosts);
+ foreach ($urls as $url) {
+ $this->_processUrl($url);
+ }
+ }
+ if ($params != null) {
+ parse_str($params, $this->_params);
+ }
+ } else {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception("Bad Broker URL {$this->_brokerUri}");
+ }
+ }
+ /**
+ * Process broker URL
+ *
+ * @param string $url Broker URL
+ * @throws Stomp_Exception
+ * @return boolean
+ */
+ protected function _processUrl ($url)
+ {
+ $parsed = parse_url($url);
+ if ($parsed) {
+ array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme']));
+ } else {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception("Bad Broker URL $url");
+ }
+ }
+ /**
+ * Make socket connection to the server
+ *
+ * @throws Stomp_Exception
+ */
+ protected function _makeConnection ()
+ {
+ if (count($this->_hosts) == 0) {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception("No broker defined");
+ }
+
+ // force disconnect, if previous established connection exists
+ $this->disconnect();
+
+ $i = $this->_currentHost;
+ $att = 0;
+ $connected = false;
+ while (! $connected && $att ++ < $this->_attempts) {
+ if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') {
+ $i = rand(0, count($this->_hosts) - 1);
+ } else {
+ $i = ($i + 1) % count($this->_hosts);
+ }
+ $broker = $this->_hosts[$i];
+ $host = $broker[0];
+ $port = $broker[1];
+ $scheme = $broker[2];
+ if ($port == null) {
+ $port = $this->_defaultPort;
+ }
+ if ($this->_socket != null) {
+ fclose($this->_socket);
+ $this->_socket = null;
+ }
+ $this->_socket = @fsockopen($scheme . '://' . $host, $port);
+ if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception("Could not connect to $host:$port ($att/{$this->_attempts})");
+ } else if (is_resource($this->_socket)) {
+ $connected = true;
+ $this->_currentHost = $i;
+ break;
+ }
+ }
+ if (! $connected) {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception("Could not connect to a broker");
+ }
+ }
+ /**
+ * Connect to server
+ *
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ public function connect ($username = '', $password = '')
+ {
+ $this->_makeConnection();
+ if ($username != '') {
+ $this->_username = $username;
+ }
+ if ($password != '') {
+ $this->_password = $password;
+ }
+ $headers = array('login' => $this->_username , 'passcode' => $this->_password);
+ if ($this->clientId != null) {
+ $headers["client-id"] = $this->clientId;
+ }
+ $frame = new Stomp_Frame("CONNECT", $headers);
+ $this->_writeFrame($frame);
+ $frame = $this->readFrame();
+ if ($frame instanceof Stomp_Frame && $frame->command == 'CONNECTED') {
+ $this->_sessionId = $frame->headers["session"];
+ return true;
+ } else {
+ require_once 'Stomp/Exception.php';
+ if ($frame instanceof Stomp_Frame) {
+ throw new Stomp_Exception("Unexpected command: {$frame->command}", 0, $frame->body);
+ } else {
+ throw new Stomp_Exception("Connection not acknowledged");
+ }
+ }
+ }
+
+ /**
+ * Check if client session has ben established
+ *
+ * @return boolean
+ */
+ public function isConnected ()
+ {
+ return !empty($this->_sessionId) && is_resource($this->_socket);
+ }
+ /**
+ * Current stomp session ID
+ *
+ * @return string
+ */
+ public function getSessionId()
+ {
+ return $this->_sessionId;
+ }
+ /**
+ * Send a message to a destination in the messaging system
+ *
+ * @param string $destination Destination queue
+ * @param string|Stomp_Frame $msg Message
+ * @param array $properties
+ * @param boolean $sync Perform request synchronously
+ * @return boolean
+ */
+ public function send ($destination, $msg, $properties = null, $sync = null)
+ {
+ if ($msg instanceof Stomp_Frame) {
+ $msg->headers['destination'] = $destination;
+ $msg->headers = array_merge($msg->headers, $properties);
+ $frame = $msg;
+ } else {
+ $headers = $properties;
+ $headers['destination'] = $destination;
+ $frame = new Stomp_Frame('SEND', $headers, $msg);
+ }
+ $this->_prepareReceipt($frame, $sync);
+ $this->_writeFrame($frame);
+ return $this->_waitForReceipt($frame, $sync);
+ }
+ /**
+ * Prepair frame receipt
+ *
+ * @param Stomp_Frame $frame
+ * @param boolean $sync
+ */
+ protected function _prepareReceipt (Stomp_Frame $frame, $sync)
+ {
+ $receive = $this->sync;
+ if ($sync !== null) {
+ $receive = $sync;
+ }
+ if ($receive == true) {
+ $frame->headers['receipt'] = md5(microtime());
+ }
+ }
+ /**
+ * Wait for receipt
+ *
+ * @param Stomp_Frame $frame
+ * @param boolean $sync
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ protected function _waitForReceipt (Stomp_Frame $frame, $sync)
+ {
+
+ $receive = $this->sync;
+ if ($sync !== null) {
+ $receive = $sync;
+ }
+ if ($receive == true) {
+ $id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null;
+ if ($id == null) {
+ return true;
+ }
+ $frame = $this->readFrame();
+ if ($frame instanceof Stomp_Frame && $frame->command == 'RECEIPT') {
+ if ($frame->headers['receipt-id'] == $id) {
+ return true;
+ } else {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body);
+ }
+ } else {
+ require_once 'Stomp/Exception.php';
+ if ($frame instanceof Stomp_Frame) {
+ throw new Stomp_Exception("Unexpected command {$frame->command}", 0, $frame->body);
+ } else {
+ throw new Stomp_Exception("Receipt not received");
+ }
+ }
+ }
+ return true;
+ }
+ /**
+ * Register to listen to a given destination
+ *
+ * @param string $destination Destination queue
+ * @param array $properties
+ * @param boolean $sync Perform request synchronously
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ public function subscribe ($destination, $properties = null, $sync = null)
+ {
+ $headers = array('ack' => 'client');
+ $headers['activemq.prefetchSize'] = $this->prefetchSize;
+ if ($this->clientId != null) {
+ $headers["activemq.subcriptionName"] = $this->clientId;
+ }
+ if (isset($properties)) {
+ foreach ($properties as $name => $value) {
+ $headers[$name] = $value;
+ }
+ }
+ $headers['destination'] = $destination;
+ $frame = new Stomp_Frame('SUBSCRIBE', $headers);
+ $this->_prepareReceipt($frame, $sync);
+ $this->_writeFrame($frame);
+ if ($this->_waitForReceipt($frame, $sync) == true) {
+ $this->_subscriptions[$destination] = $properties;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ /**
+ * Remove an existing subscription
+ *
+ * @param string $destination
+ * @param array $properties
+ * @param boolean $sync Perform request synchronously
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ public function unsubscribe ($destination, $properties = null, $sync = null)
+ {
+ $headers = array();
+ if (isset($properties)) {
+ foreach ($properties as $name => $value) {
+ $headers[$name] = $value;
+ }
+ }
+ $headers['destination'] = $destination;
+ $frame = new Stomp_Frame('UNSUBSCRIBE', $headers);
+ $this->_prepareReceipt($frame, $sync);
+ $this->_writeFrame($frame);
+ if ($this->_waitForReceipt($frame, $sync) == true) {
+ unset($this->_subscriptions[$destination]);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ /**
+ * Start a transaction
+ *
+ * @param string $transactionId
+ * @param boolean $sync Perform request synchronously
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ public function begin ($transactionId = null, $sync = null)
+ {
+ $headers = array();
+ if (isset($transactionId)) {
+ $headers['transaction'] = $transactionId;
+ }
+ $frame = new Stomp_Frame('BEGIN', $headers);
+ $this->_prepareReceipt($frame, $sync);
+ $this->_writeFrame($frame);
+ return $this->_waitForReceipt($frame, $sync);
+ }
+ /**
+ * Commit a transaction in progress
+ *
+ * @param string $transactionId
+ * @param boolean $sync Perform request synchronously
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ public function commit ($transactionId = null, $sync = null)
+ {
+ $headers = array();
+ if (isset($transactionId)) {
+ $headers['transaction'] = $transactionId;
+ }
+ $frame = new Stomp_Frame('COMMIT', $headers);
+ $this->_prepareReceipt($frame, $sync);
+ $this->_writeFrame($frame);
+ return $this->_waitForReceipt($frame, $sync);
+ }
+ /**
+ * Roll back a transaction in progress
+ *
+ * @param string $transactionId
+ * @param boolean $sync Perform request synchronously
+ */
+ public function abort ($transactionId = null, $sync = null)
+ {
+ $headers = array();
+ if (isset($transactionId)) {
+ $headers['transaction'] = $transactionId;
+ }
+ $frame = new Stomp_Frame('ABORT', $headers);
+ $this->_prepareReceipt($frame, $sync);
+ $this->_writeFrame($frame);
+ return $this->_waitForReceipt($frame, $sync);
+ }
+ /**
+ * Acknowledge consumption of a message from a subscription
+ * Note: This operation is always asynchronous
+ *
+ * @param string|Stomp_Frame $messageMessage ID
+ * @param string $transactionId
+ * @return boolean
+ * @throws Stomp_Exception
+ */
+ public function ack ($message, $transactionId = null)
+ {
+ if ($message instanceof Stomp_Frame) {
+ $frame = new Stomp_Frame('ACK', $message->headers);
+ $this->_writeFrame($frame);
+ return true;
+ } else {
+ $headers = array();
+ if (isset($transactionId)) {
+ $headers['transaction'] = $transactionId;
+ }
+ $headers['message-id'] = $message;
+ $frame = new Stomp_Frame('ACK', $headers);
+ $this->_writeFrame($frame);
+ return true;
+ }
+ }
+ /**
+ * Graceful disconnect from the server
+ *
+ */
+ public function disconnect ()
+ {
+ $header = array();
+
+ if ($this->clientId != null) {
+ $headers["client-id"] = $this->clientId;
+ }
+
+ if (is_resource($this->_socket)) {
+ $this->_writeFrame(new Stomp_Frame('DISCONNECT', $headers));
+ fclose($this->_socket);
+ }
+ $this->_socket = null;
+ $this->_sessionId = null;
+ $this->_currentHost = -1;
+ $this->_subscriptions = array();
+ $this->_username = '';
+ $this->_password = '';
+ }
+ /**
+ * Write frame to server
+ *
+ * @param Stomp_Frame $stompFrame
+ */
+ protected function _writeFrame (Stomp_Frame $stompFrame)
+ {
+ if (!is_resource($this->_socket)) {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception('Socket connection hasn\'t been established');
+ }
+
+ $data = $stompFrame->__toString();
+ $r = fwrite($this->_socket, $data, strlen($data));
+ if ($r === false || $r == 0) {
+ $this->_reconnect();
+ $this->_writeFrame($stompFrame);
+ }
+ }
+
+ /**
+ * Set timeout to wait for content to read
+ *
+ * @param int $seconds_to_wait Seconds to wait for a frame
+ * @param int $milliseconds Milliseconds to wait for a frame
+ */
+ public function setReadTimeout($seconds, $milliseconds = 0)
+ {
+ $this->_read_timeout_seconds = $seconds;
+ $this->_read_timeout_milliseconds = $milliseconds;
+ }
+
+ /**
+ * Read responce frame from server
+ *
+ * @return Stomp_Frame|Stomp_Message_Map|boolean False when no frame to read
+ */
+ public function readFrame ()
+ {
+ if (!$this->hasFrameToRead()) {
+ return false;
+ }
+
+ $rb = 1024;
+ $data = '';
+ do {
+ $read = fgets($this->_socket, $rb);
+ if ($read === false) {
+ $this->_reconnect();
+ return $this->readFrame();
+ }
+ $data .= $read;
+ $len = strlen($data);
+ } while (($len < 2 || ! ($data[$len - 2] == "\x00" && $data[$len - 1] == "\n")));
+
+ list ($header, $body) = explode("\n\n", $data, 2);
+ $header = explode("\n", $header);
+ $headers = array();
+ $command = null;
+ foreach ($header as $v) {
+ if (isset($command)) {
+ list ($name, $value) = explode(':', $v, 2);
+ $headers[$name] = $value;
+ } else {
+ $command = $v;
+ }
+ }
+ $frame = new Stomp_Frame($command, $headers, trim($body));
+ if (isset($frame->headers['amq-msg-type']) && $frame->headers['amq-msg-type'] == 'MapMessage') {
+ require_once 'Stomp/Message/Map.php';
+ return new Stomp_Message_Map($frame);
+ } else {
+ return $frame;
+ }
+ }
+
+ /**
+ * Check if there is a frame to read
+ *
+ * @return boolean
+ */
+ public function hasFrameToRead()
+ {
+ $read = array($this->_socket);
+ $write = null;
+ $except = null;
+
+ $has_frame_to_read = stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds);
+
+ if ($has_frame_to_read === false) {
+ throw new Stomp_Exception('Check failed to determin if the socket is readable');
+ } else if ($has_frame_to_read > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Reconnects and renews subscriptions (if there were any)
+ * Call this method when you detect connection problems
+ */
+ protected function _reconnect ()
+ {
+ $subscriptions = $this->_subscriptions;
+
+ $this->connect($this->_username, $this->_password);
+ foreach ($subscriptions as $dest => $properties) {
+ $this->subscribe($dest, $properties);
+ }
+ }
+ /**
+ * Graceful object desruction
+ *
+ */
+ public function __destruct()
+ {
+ $this->disconnect();
+ }
+}
+?>
diff --git a/extlib/Stomp/Exception.php b/extlib/Stomp/Exception.php
new file mode 100644
index 000000000..e6870bc15
--- /dev/null
+++ b/extlib/Stomp/Exception.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ *
+ * Copyright 2005-2006 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* vim: set expandtab tabstop=3 shiftwidth=3: */
+
+/**
+ * A Stomp Connection
+ *
+ *
+ * @package Stomp
+ * @author Michael Caplan <mcaplan@labnet.net>
+ * @version $Revision: 23 $
+ */
+class Stomp_Exception extends Exception
+{
+ protected $_details;
+
+ /**
+ * Constructor
+ *
+ * @param string $message Error message
+ * @param int $code Error code
+ * @param string $details Stomp server error details
+ */
+ public function __construct($message = null, $code = 0, $details = '')
+ {
+ $this->_details = $details;
+
+ parent::__construct($message, $code);
+ }
+
+ /**
+ * Stomp server error details
+ *
+ * @return string
+ */
+ public function getDetails()
+ {
+ return $this->_details;
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Stomp/Frame.php b/extlib/Stomp/Frame.php
new file mode 100644
index 000000000..dc59c1cb7
--- /dev/null
+++ b/extlib/Stomp/Frame.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ *
+ * Copyright 2005-2006 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* vim: set expandtab tabstop=3 shiftwidth=3: */
+
+/**
+ * Stomp Frames are messages that are sent and received on a StompConnection.
+ *
+ * @package Stomp
+ * @author Hiram Chirino <hiram@hiramchirino.com>
+ * @author Dejan Bosanac <dejan@nighttale.net>
+ * @author Michael Caplan <mcaplan@labnet.net>
+ * @version $Revision: 36 $
+ */
+class Stomp_Frame
+{
+ public $command;
+ public $headers = array();
+ public $body;
+
+ /**
+ * Constructor
+ *
+ * @param string $command
+ * @param array $headers
+ * @param string $body
+ */
+ public function __construct ($command = null, $headers = null, $body = null)
+ {
+ $this->_init($command, $headers, $body);
+ }
+
+ protected function _init ($command = null, $headers = null, $body = null)
+ {
+ $this->command = $command;
+ if ($headers != null) {
+ $this->headers = $headers;
+ }
+ $this->body = $body;
+
+ if ($this->command == 'ERROR') {
+ require_once 'Stomp/Exception.php';
+ throw new Stomp_Exception($this->headers['message'], 0, $this->body);
+ }
+ }
+
+ /**
+ * Convert frame to transportable string
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $data = $this->command . "\n";
+
+ foreach ($this->headers as $name => $value) {
+ $data .= $name . ": " . $value . "\n";
+ }
+
+ $data .= "\n";
+ $data .= $this->body;
+ return $data .= "\x00\n";
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Stomp/Message.php b/extlib/Stomp/Message.php
new file mode 100644
index 000000000..6bcad3efd
--- /dev/null
+++ b/extlib/Stomp/Message.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ *
+ * Copyright 2005-2006 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* vim: set expandtab tabstop=3 shiftwidth=3: */
+
+require_once 'Stomp/Frame.php';
+
+/**
+ * Basic text stomp message
+ *
+ * @package Stomp
+ * @author Dejan Bosanac <dejan@nighttale.net>
+ * @version $Revision: 23 $
+ */
+class Stomp_Message extends Stomp_Frame
+{
+ public function __construct ($body, $headers = null)
+ {
+ $this->_init("SEND", $headers, $body);
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Stomp/Message/Bytes.php b/extlib/Stomp/Message/Bytes.php
new file mode 100644
index 000000000..c75f23e43
--- /dev/null
+++ b/extlib/Stomp/Message/Bytes.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ *
+ * Copyright 2005-2006 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* vim: set expandtab tabstop=3 shiftwidth=3: */
+
+require_once 'Stomp/Message.php';
+
+/**
+ * Message that contains a stream of uninterpreted bytes
+ *
+ * @package Stomp
+ * @author Dejan Bosanac <dejan@nighttale.net>
+ * @version $Revision: 23 $
+ */
+class Stomp_Message_Bytes extends Stomp_Message
+{
+ /**
+ * Constructor
+ *
+ * @param string $body
+ * @param array $headers
+ */
+ function __construct ($body, $headers = null)
+ {
+ $this->_init("SEND", $headers, $body);
+ if ($this->headers == null) {
+ $this->headers = array();
+ }
+ $this->headers['content-length'] = count($body);
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/Stomp/Message/Map.php b/extlib/Stomp/Message/Map.php
new file mode 100644
index 000000000..288456a84
--- /dev/null
+++ b/extlib/Stomp/Message/Map.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ *
+ * Copyright 2005-2006 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* vim: set expandtab tabstop=3 shiftwidth=3: */
+
+require_once 'Stomp/Message.php';
+
+/**
+ * Message that contains a set of name-value pairs
+ *
+ * @package Stomp
+ * @author Dejan Bosanac <dejan@nighttale.net>
+ * @version $Revision: 23 $
+ */
+class Stomp_Message_Map extends Stomp_Message
+{
+ public $map;
+
+ /**
+ * Constructor
+ *
+ * @param Stomp_Frame|string $msg
+ * @param array $headers
+ */
+ function __construct ($msg, $headers = null)
+ {
+ if ($msg instanceof Stomp_Frame) {
+ $this->_init($msg->command, $msg->headers, $msg->body);
+ $this->map = json_decode($msg->body);
+ } else {
+ $this->_init("SEND", $headers, $msg);
+ if ($this->headers == null) {
+ $this->headers = array();
+ }
+ $this->headers['amq-msg-type'] = 'MapMessage';
+ $this->body = json_encode($msg);
+ }
+ }
+}
+?> \ No newline at end of file
diff --git a/extlib/System/Command.php b/extlib/System/Command.php
new file mode 100644
index 000000000..f5c3ec6b9
--- /dev/null
+++ b/extlib/System/Command.php
@@ -0,0 +1,587 @@
+<?php
+// {{{ license
+
+// +----------------------------------------------------------------------+
+// | PHP Version 4.0 |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.02 of the PHP license, |
+// | that is bundled with this package in the file LICENSE, and is |
+// | available at through the world-wide-web at |
+// | http://www.php.net/license/2_02.txt. |
+// | If you did not receive a copy of the PHP license and are unable to |
+// | obtain it through the world-wide-web, please send a note to |
+// | license@php.net so we can mail you a copy immediately. |
+// +----------------------------------------------------------------------+
+// | Author: Anders Johannsen <anders@johannsen.com> |
+// | Author: Dan Allen <dan@mojavelinux.com>
+// +----------------------------------------------------------------------+
+
+// $Id: Command.php,v 1.9 2007/04/20 21:08:48 cconstantine Exp $
+
+// }}}
+// {{{ includes
+
+require_once 'PEAR.php';
+require_once 'System.php';
+
+// }}}
+// {{{ constants
+
+define('SYSTEM_COMMAND_OK', 1);
+define('SYSTEM_COMMAND_ERROR', -1);
+define('SYSTEM_COMMAND_NO_SHELL', -2);
+define('SYSTEM_COMMAND_INVALID_SHELL', -3);
+define('SYSTEM_COMMAND_TMPDIR_ERROR', -4);
+define('SYSTEM_COMMAND_INVALID_OPERATOR', -5);
+define('SYSTEM_COMMAND_INVALID_COMMAND', -6);
+define('SYSTEM_COMMAND_OPERATOR_PLACEMENT',-7);
+define('SYSTEM_COMMAND_COMMAND_PLACEMENT', -8);
+define('SYSTEM_COMMAND_NOHUP_MISSING', -9);
+define('SYSTEM_COMMAND_NO_OUTPUT', -10);
+define('SYSTEM_COMMAND_STDERR', -11);
+define('SYSTEM_COMMAND_NONZERO_EXIT', -12);
+
+// }}}
+
+// {{{ class System_Command
+
+/**
+ * The System_Command:: class implements an abstraction for various ways
+ * of executing commands (directly using the backtick operator,
+ * as a background task after the script has terminated using
+ * register_shutdown_function() or as a detached process using nohup).
+ *
+ * @author Anders Johannsen <anders@johannsen.com>
+ * @author Dan Allen <dan@mojavelinux.com>
+ * @version $Revision: 1.9 $
+ */
+
+// }}}
+class System_Command {
+ // {{{ properties
+
+ /**
+ * Array of settings used when creating the shell command
+ *
+ * @var array
+ * @access private
+ */
+ var $options = array();
+
+ /**
+ * Array of available shells to use to execute the command
+ *
+ * @var array
+ * @access private
+ */
+ var $shells = array();
+
+ /**
+ * Array of available control operators used between commands
+ *
+ * @var array
+ * @access private
+ */
+ var $controlOperators = array();
+
+ /**
+ * The system command to be executed
+ *
+ * @var string
+ * @access private
+ */
+ var $systemCommand = null;
+
+ /**
+ * Previously added part to the command string
+ *
+ * @var string
+ * @access private
+ */
+ var $previousElement = null;
+
+ /**
+ * Directory for writing stderr output
+ *
+ * @var string
+ * @access private
+ */
+ var $tmpDir = null;
+
+ /**
+ * To allow the pear error object to accumulate when building
+ * the command, we use the command status to keep track when
+ * a pear error is raised
+ *
+ * @var int
+ * @access private
+ */
+ var $commandStatus = 0;
+
+ /**
+ * Hold initialization PEAR_Error
+ *
+ * @var object
+ * @access private
+ **/
+ var $_initError = null;
+
+ // }}}
+ // {{{ constructor
+
+ /**
+ * Class constructor
+ *
+ * Defines all necessary constants and sets defaults
+ *
+ * @access public
+ */
+ function System_Command($in_shell = null)
+ {
+ // Defining constants
+ $this->options = array(
+ 'SEQUENCE' => true,
+ 'SHUTDOWN' => false,
+ 'SHELL' => $this->which($in_shell),
+ 'OUTPUT' => true,
+ 'NOHUP' => false,
+ 'BACKGROUND' => false,
+ 'STDERR' => false
+ );
+
+ // prepare the available control operators
+ $this->controlOperators = array(
+ 'PIPE' => '|',
+ 'AND' => '&&',
+ 'OR' => '||',
+ 'GROUP' => ';',
+ 'LFIFO' => '<',
+ 'RFIFO' => '>',
+ );
+
+ // List of allowed/available shells
+ $this->shells = array(
+ 'sh',
+ 'bash',
+ 'zsh',
+ 'tcsh',
+ 'csh',
+ 'ash',
+ 'sash',
+ 'esh',
+ 'ksh'
+ );
+
+ // Find the first available shell
+ if (empty($this->options['SHELL'])) {
+ foreach ($this->shells as $shell) {
+ if ($this->options['SHELL'] = $this->which($shell)) {
+ break;
+ }
+ }
+
+ // see if we still have no shell
+ if (empty($this->options['SHELL'])) {
+ $this->_initError =& PEAR::raiseError(null, SYSTEM_COMMAND_NO_SHELL, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ return;
+ }
+ }
+
+ // Caputre a temporary directory for capturing stderr from commands
+ $this->tmpDir = System::tmpdir();
+ if (!System::mkDir("-p {$this->tmpDir}")) {
+ $this->_initError =& PEAR::raiseError(null, SYSTEM_COMMAND_TMPDIR_ERROR, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ return;
+ }
+ }
+
+ // }}}
+ // {{{ setOption()
+
+ /**
+ * Sets the value for an option. Each option should be set to true
+ * or false; except the 'SHELL' option which should be a string
+ * naming a shell. The options are:
+ *
+ * 'SEQUENCE' Allow a sequence command or not (right now this is always on);
+ *
+ * 'SHUTDOWN' Execute commands via a shutdown function;
+ *
+ * 'SHELL' Path to shell;
+ *
+ * 'OUTPUT' Output stdout from process;
+ *
+ * 'NOHUP' Use nohup to detach process;
+ *
+ * 'BACKGROUND' Run as a background process with &;
+ *
+ * 'STDERR' Output on stderr will raise an error, even if
+ * the command's exit value is zero. The output from
+ * stderr can be retrieved using the getDebugInfo()
+ * method of the Pear_ERROR object returned by
+ * execute().;
+ *
+ * @param string $in_option is a case-sensitive string,
+ * corresponding to the option
+ * that should be changed
+ * @param mixed $in_setting is the new value for the option
+ * @access public
+ * @return bool true if succes, else false
+ */
+ function setOption($in_option, $in_setting)
+ {
+ if ($this->_initError) {
+ return $this->_initError;
+ }
+
+ $option = strtoupper($in_option);
+
+ if (!isset($this->options[$option])) {
+ PEAR::raiseError(null, SYSTEM_COMMAND_ERROR, null, E_USER_NOTICE, null, 'System_Command_Error', true);
+ return false;
+ }
+
+ switch ($option) {
+ case 'OUTPUT':
+ case 'SHUTDOWN':
+ case 'SEQUENCE':
+ case 'BACKGROUND':
+ case 'STDERR':
+ $this->options[$option] = !empty($in_setting);
+ return true;
+ break;
+
+ case 'SHELL':
+ if (($shell = $this->which($in_setting)) !== false) {
+ $this->options[$option] = $shell;
+ return true;
+ }
+ else {
+ PEAR::raiseError(null, SYSTEM_COMMAND_NO_SHELL, null, E_USER_NOTICE, $in_setting, 'System_Command_Error', true);
+ return false;
+ }
+ break;
+
+ case 'NOHUP':
+ if (empty($in_setting)) {
+ $this->options[$option] = false;
+ }
+ else if ($location = $this->which('nohup')) {
+ $this->options[$option] = $location;
+ }
+ else {
+ PEAR::raiseError(null, SYSTEM_COMMAND_NOHUP_MISSING, null, E_USER_NOTICE, null, 'System_Command_Error', true);
+ return false;
+ }
+ break;
+ }
+ }
+
+ // }}}
+ // {{{ pushCommand()
+
+ /**
+ * Used to push a command onto the running command to be executed
+ *
+ * @param string $in_command binary to be run
+ * @param string $in_argument either an option or argument value, to be handled appropriately
+ * @param string $in_argument
+ * @param ...
+ *
+ * @access public
+ * @return boolean true on success {or System_Command_Error Exception}
+ */
+ function pushCommand($in_command)
+ {
+ if ($this->_initError) {
+ return $this->_initError;
+ }
+
+ if (!is_null($this->previousElement) && !in_array($this->previousElement, $this->controlOperators)) {
+ $this->commandStatus = -1;
+ $error = PEAR::raiseError(null, SYSTEM_COMMAND_COMMAND_PLACEMENT, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ }
+
+ // check for error here
+ $command = escapeshellcmd($this->which($in_command));
+ if ($command === false) {
+ $error = PEAR::raiseError(null, SYSTEM_COMMAND_INVALID_COMMAND, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ }
+
+ $argv = func_get_args();
+ array_shift($argv);
+ foreach($argv as $arg) {
+ if (strpos($arg, '-') === 0) {
+ $command .= ' ' . $arg;
+ }
+ elseif ($arg != '') {
+ $command .= ' ' . escapeshellarg($arg);
+ }
+ }
+
+ $this->previousElement = $command;
+ $this->systemCommand .= $command;
+
+ return isset($error) ? $error : true;
+ }
+
+ // }}}
+ // {{{ pushOperator()
+
+ /**
+ * Used to push an operator onto the running command to be executed
+ *
+ * @param string $in_operator Either string reprentation of operator or system character
+ *
+ * @access public
+ * @return boolean true on success {or System_Command_Error Exception}
+ */
+ function pushOperator($in_operator)
+ {
+ if ($this->_initError) {
+ return $this->_initError;
+ }
+
+ $operator = isset($this->controlOperators[$in_operator]) ? $this->controlOperators[$in_operator] : $in_operator;
+
+ if (is_null($this->previousElement) || in_array($this->previousElement, $this->controlOperators)) {
+ $this->commandStatus = -1;
+ $error = PEAR::raiseError(null, SYSTEM_COMMAND_OPERATOR_PLACEMENT, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ }
+ elseif (!in_array($operator, $this->controlOperators)) {
+ $this->commandStatus = -1;
+ $error = PEAR::raiseError(null, SYSTEM_COMMAND_INVALID_OPERATOR, null, E_USER_WARNING, $operator, 'System_Command_Error', true);
+ }
+
+ $this->previousElement = $operator;
+ $this->systemCommand .= ' ' . $operator . ' ';
+ return isset($error) ? $error : true;
+ }
+
+ // }}}
+ // {{{ execute()
+
+ /**
+ * Executes the code according to given options
+ *
+ * @return bool true if success {or System_Command_Exception}
+ *
+ * @access public
+ */
+ function execute()
+ {
+ if ($this->_initError) {
+ return $this->_initError;
+ }
+
+ // if the command is empty or if the last element was a control operator, we can't continue
+ if (is_null($this->previousElement) || $this->commandStatus == -1 || in_array($this->previousElement, $this->controlOperators)) {
+ return PEAR::raiseError(null, SYSTEM_COMMAND_INVALID_COMMAND, null, E_USER_WARNING, $this->systemCommand, 'System_Command_Error', true);
+ }
+
+ // Warning about impossible mix of options
+ if (!empty($this->options['OUTPUT'])) {
+ if (!empty($this->options['SHUTDOWN']) || !empty($this->options['NOHUP'])) {
+ return PEAR::raiseError(null, SYSTEM_COMMAND_NO_OUTPUT, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ }
+ }
+
+ // if this is not going to stdout, then redirect to /dev/null
+ if (empty($this->options['OUTPUT'])) {
+ $this->systemCommand .= ' >/dev/null';
+ }
+
+ $suffix = '';
+ // run a command immune to hangups, with output to a non-tty
+ if (!empty($this->options['NOHUP'])) {
+ $this->systemCommand = $this->options['NOHUP'] . $this->systemCommand;
+ }
+ // run a background process (only if not nohup)
+ elseif (!empty($this->options['BACKGROUND'])) {
+ $suffix = ' &';
+ }
+
+ // Register to be run on shutdown
+ if (!empty($this->options['SHUTDOWN'])) {
+ $line = "system(\"{$this->systemCommand}$suffix\");";
+ $function = create_function('', $line);
+ register_shutdown_function($function);
+ return true;
+ }
+ else {
+ // send stderr to a file so that we can reap the error message
+ $tmpFile = tempnam($this->tmpDir, 'System_Command-');
+ $this->systemCommand .= ' 2>' . $tmpFile . $suffix;
+ $shellPipe = $this->which('echo') . ' ' . escapeshellarg($this->systemCommand) . ' | ' . $this->options['SHELL'];
+ exec($shellPipe, $result, $returnVal);
+
+ if ($returnVal !== 0) {
+ // command returned nonzero; that's always an error
+ $return = PEAR::raiseError(null, SYSTEM_COMMAND_NONZERO_EXIT, null, E_USER_WARNING, null, 'System_Command_Error', true);
+ }
+ else if (!$this->options['STDERR']) {
+ // caller does not care about stderr; return success
+ $return = implode("\n", $result);
+ }
+ else {
+ // our caller cares about stderr; check stderr output
+ clearstatcache();
+ if (filesize($tmpFile) > 0) {
+ // the command actually wrote to stderr
+ $stderr_output = file_get_contents($tmpFile);
+ $return = PEAR::raiseError(null, SYSTEM_COMMAND_STDERR, null, E_USER_WARNING, $stderr_output, 'System_Command_Error', true);
+ } else {
+ // total success; return stdout gathered by exec()
+ $return = implode("\n", $result);
+ }
+ }
+
+ unlink($tmpFile);
+ return $return;
+ }
+ }
+
+ // }}}
+ // {{{ which()
+
+ /**
+ * Functionality similiar to unix 'which'. Searches the path
+ * for the specified program.
+ *
+ * @param $cmd name of the executable to search for
+ *
+ * @access private
+ * @return string returns the full path if found, false if not
+ */
+ function which($in_cmd)
+ {
+ // only pass non-empty strings to System::which()
+ if (!is_string($in_cmd) || '' === $in_cmd) {
+ return(false);
+ }
+
+ // explicitly pass false as fallback value
+ return System::which($in_cmd, false);
+ }
+
+ // }}}
+ // {{{ reset()
+
+ /**
+ * Prepare for a new command to be built
+ *
+ * @access public
+ * @return void
+ */
+ function reset()
+ {
+ $this->previousElement = null;
+ $this->systemCommand = null;
+ $this->commandStatus = 0;
+ }
+
+ // }}}
+ // {{{ errorMessage()
+
+ /**
+ * Return a textual error message for a System_Command error code
+ *
+ * @param integer error code
+ *
+ * @return string error message, or false if the error code was
+ * not recognized
+ */
+ function errorMessage($in_value)
+ {
+ static $errorMessages;
+ if (!isset($errorMessages)) {
+ $errorMessages = array(
+ SYSTEM_COMMAND_OK => 'no error',
+ SYSTEM_COMMAND_ERROR => 'unknown error',
+ SYSTEM_COMMAND_NO_SHELL => 'no shell found',
+ SYSTEM_COMMAND_INVALID_SHELL => 'invalid shell',
+ SYSTEM_COMMAND_TMPDIR_ERROR => 'could not create temporary directory',
+ SYSTEM_COMMAND_INVALID_OPERATOR => 'control operator invalid',
+ SYSTEM_COMMAND_INVALID_COMMAND => 'invalid system command',
+ SYSTEM_COMMAND_OPERATOR_PLACEMENT => 'invalid placement of control operator',
+ SYSTEM_COMMAND_COMMAND_PLACEMENT => 'invalid placement of command',
+ SYSTEM_COMMAND_NOHUP_MISSING => 'nohup not found on system',
+ SYSTEM_COMMAND_NO_OUTPUT => 'output not allowed',
+ SYSTEM_COMMAND_STDERR => 'command wrote to stderr',
+ SYSTEM_COMMAND_NONZERO_EXIT => 'non-zero exit value from command',
+ );
+ }
+
+ if (System_Command::isError($in_value)) {
+ $in_value = $in_value->getCode();
+ }
+
+ return isset($errorMessages[$in_value]) ? $errorMessages[$in_value] : $errorMessages[SYSTEM_COMMAND_ERROR];
+ }
+
+ // }}}
+ // {{{ isError()
+
+ /**
+ * Tell whether a result code from a System_Command method is an error
+ *
+ * @param int result code
+ *
+ * @return bool whether $in_value is an error
+ *
+ * @access public
+ */
+ function isError($in_value)
+ {
+ return (is_object($in_value) &&
+ (strtolower(get_class($in_value)) == 'system_command_error' ||
+ is_subclass_of($in_value, 'system_command_error')));
+ }
+
+ // }}}
+}
+
+// {{{ class System_Command_Error
+
+/**
+ * System_Command_Error constructor.
+ *
+ * @param mixed System_Command error code, or string with error message.
+ * @param integer what "error mode" to operate in
+ * @param integer what error level to use for $mode & PEAR_ERROR_TRIGGER
+ * @param mixed additional debug info, such as the last query
+ *
+ * @access public
+ *
+ * @see PEAR_Error
+ */
+
+// }}}
+class System_Command_Error extends PEAR_Error
+{
+ // {{{ properties
+
+ /**
+ * Message in front of the error message
+ * @var string $error_message_prefix
+ */
+ var $error_message_prefix = 'System_Command Error: ';
+
+ // }}}
+ // {{{ constructor
+
+ function System_Command_Error($code = SYSTEM_COMMAND_ERROR, $mode = PEAR_ERROR_RETURN,
+ $level = E_USER_NOTICE, $debuginfo = null)
+ {
+ if (is_int($code)) {
+ $this->PEAR_Error(System_Command::errorMessage($code), $code, $mode, $level, $debuginfo);
+ } else {
+ $this->PEAR_Error("Invalid error code: $code", SYSTEM_COMMAND_ERROR, $mode, $level, $debuginfo);
+ }
+ }
+
+ // }}}
+}
+?>
diff --git a/extlib/Validate.php b/extlib/Validate.php
index 4c05506b3..3d8bc23f2 100644
--- a/extlib/Validate.php
+++ b/extlib/Validate.php
@@ -1,1051 +1,1116 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4: */
-// +----------------------------------------------------------------------+
-// | Copyright (c) 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox, Amir Saied |
-// +----------------------------------------------------------------------+
-// | This source file is subject to the New BSD license, That is bundled |
-// | with this package in the file LICENSE, and is available through |
-// | the world-wide-web at |
-// | http://www.opensource.org/licenses/bsd-license.php |
-// | If you did not receive a copy of the new BSDlicense and are unable |
-// | to obtain it through the world-wide-web, please send a note to |
-// | pajoye@php.net so we can mail you a copy immediately. |
-// +----------------------------------------------------------------------+
-// | Author: Tomas V.V.Cox <cox@idecnet.com> |
-// | Pierre-Alain Joye <pajoye@php.net> |
-// | Amir Mohammad Saied <amir@php.net> |
-// +----------------------------------------------------------------------+
-//
-/**
- * Validation class
- *
- * Package to validate various datas. It includes :
- * - numbers (min/max, decimal or not)
- * - email (syntax, domain check)
- * - string (predifined type alpha upper and/or lowercase, numeric,...)
- * - date (min, max, rfc822 compliant)
- * - uri (RFC2396)
- * - possibility valid multiple data with a single method call (::multiple)
- *
- * @category Validate
- * @package Validate
- * @author Tomas V.V.Cox <cox@idecnet.com>
- * @author Pierre-Alain Joye <pajoye@php.net>
- * @author Amir Mohammad Saied <amir@php.net>
- * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied
- * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
- * @version CVS: $Id: Validate.php,v 1.123 2007/12/12 16:45:51 davidc Exp $
- * @link http://pear.php.net/package/Validate
- */
-
-/**
- * Methods for common data validations
- */
-define('VALIDATE_NUM', '0-9');
-define('VALIDATE_SPACE', '\s');
-define('VALIDATE_ALPHA_LOWER', 'a-z');
-define('VALIDATE_ALPHA_UPPER', 'A-Z');
-define('VALIDATE_ALPHA', VALIDATE_ALPHA_LOWER . VALIDATE_ALPHA_UPPER);
-define('VALIDATE_EALPHA_LOWER', VALIDATE_ALPHA_LOWER . 'áéíóúýàèìòùäëïöüÿâêîôûãñõ¨åæç½ðøþß');
-define('VALIDATE_EALPHA_UPPER', VALIDATE_ALPHA_UPPER . 'ÁÉÍÓÚÝÀÈÌÒÙÄËÏÖܾÂÊÎÔÛÃÑÕ¦ÅÆǼÐØÞ');
-define('VALIDATE_EALPHA', VALIDATE_EALPHA_LOWER . VALIDATE_EALPHA_UPPER);
-define('VALIDATE_PUNCTUATION', VALIDATE_SPACE . '\.,;\:&"\'\?\!\(\)');
-define('VALIDATE_NAME', VALIDATE_EALPHA . VALIDATE_SPACE . "'" . "-");
-define('VALIDATE_STREET', VALIDATE_NUM . VALIDATE_NAME . "/\\ºª\.");
-
-define('VALIDATE_ITLD_EMAILS', 1);
-define('VALIDATE_GTLD_EMAILS', 2);
-define('VALIDATE_CCTLD_EMAILS', 4);
-define('VALIDATE_ALL_EMAILS', 8);
-
-/**
- * Validation class
- *
- * Package to validate various datas. It includes :
- * - numbers (min/max, decimal or not)
- * - email (syntax, domain check)
- * - string (predifined type alpha upper and/or lowercase, numeric,...)
- * - date (min, max)
- * - uri (RFC2396)
- * - possibility valid multiple data with a single method call (::multiple)
- *
- * @category Validate
- * @package Validate
- * @author Tomas V.V.Cox <cox@idecnet.com>
- * @author Pierre-Alain Joye <pajoye@php.net>
- * @author Amir Mohammad Saied <amir@php.net>
- * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied
- * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
- * @version Release: @package_version@
- * @link http://pear.php.net/package/Validate
- */
-class Validate
-{
- /**
- * International Top-Level Domain
- *
- * This is an array of the known international
- * top-level domain names.
- *
- * @access protected
- * @var array $_iTld (International top-level domains)
- */
- var $_itld = array(
- 'arpa',
- 'root',
- );
-
- /**
- * Generic top-level domain
- *
- * This is an array of the official
- * generic top-level domains.
- *
- * @access protected
- * @var array $_gTld (Generic top-level domains)
- */
- var $_gtld = array(
- 'aero',
- 'biz',
- 'cat',
- 'com',
- 'coop',
- 'edu',
- 'gov',
- 'info',
- 'int',
- 'jobs',
- 'mil',
- 'mobi',
- 'museum',
- 'name',
- 'net',
- 'org',
- 'pro',
- 'travel',
- 'asia',
- 'post',
- 'tel',
- 'geo',
- );
-
- /**
- * Country code top-level domains
- *
- * This is an array of the official country
- * codes top-level domains
- *
- * @access protected
- * @var array $_ccTld (Country Code Top-Level Domain)
- */
- var $_cctld = array(
- 'ac',
- 'ad','ae','af','ag',
- 'ai','al','am','an',
- 'ao','aq','ar','as',
- 'at','au','aw','ax',
- 'az','ba','bb','bd',
- 'be','bf','bg','bh',
- 'bi','bj','bm','bn',
- 'bo','br','bs','bt',
- 'bu','bv','bw','by',
- 'bz','ca','cc','cd',
- 'cf','cg','ch','ci',
- 'ck','cl','cm','cn',
- 'co','cr','cs','cu',
- 'cv','cx','cy','cz',
- 'de','dj','dk','dm',
- 'do','dz','ec','ee',
- 'eg','eh','er','es',
- 'et','eu','fi','fj',
- 'fk','fm','fo','fr',
- 'ga','gb','gd','ge',
- 'gf','gg','gh','gi',
- 'gl','gm','gn','gp',
- 'gq','gr','gs','gt',
- 'gu','gw','gy','hk',
- 'hm','hn','hr','ht',
- 'hu','id','ie','il',
- 'im','in','io','iq',
- 'ir','is','it','je',
- 'jm','jo','jp','ke',
- 'kg','kh','ki','km',
- 'kn','kp','kr','kw',
- 'ky','kz','la','lb',
- 'lc','li','lk','lr',
- 'ls','lt','lu','lv',
- 'ly','ma','mc','md',
- 'me','mg','mh','mk',
- 'ml','mm','mn','mo',
- 'mp','mq','mr','ms',
- 'mt','mu','mv','mw',
- 'mx','my','mz','na',
- 'nc','ne','nf','ng',
- 'ni','nl','no','np',
- 'nr','nu','nz','om',
- 'pa','pe','pf','pg',
- 'ph','pk','pl','pm',
- 'pn','pr','ps','pt',
- 'pw','py','qa','re',
- 'ro','rs','ru','rw',
- 'sa','sb','sc','sd',
- 'se','sg','sh','si',
- 'sj','sk','sl','sm',
- 'sn','so','sr','st',
- 'su','sv','sy','sz',
- 'tc','td','tf','tg',
- 'th','tj','tk','tl',
- 'tm','tn','to','tp',
- 'tr','tt','tv','tw',
- 'tz','ua','ug','uk',
- 'us','uy','uz','va',
- 'vc','ve','vg','vi',
- 'vn','vu','wf','ws',
- 'ye','yt','yu','za',
- 'zm','zw',
- );
-
-
- /**
- * Validate a number
- *
- * @param string $number Number to validate
- * @param array $options array where:
- * 'decimal' is the decimal char or false when decimal not allowed
- * i.e. ',.' to allow both ',' and '.'
- * 'dec_prec' Number of allowed decimals
- * 'min' minimum value
- * 'max' maximum value
- *
- * @return boolean true if valid number, false if not
- *
- * @access public
- */
- function number($number, $options = array())
- {
- $decimal = $dec_prec = $min = $max = null;
- if (is_array($options)) {
- extract($options);
- }
-
- $dec_prec = $dec_prec ? "{1,$dec_prec}" : '+';
- $dec_regex = $decimal ? "[$decimal][0-9]$dec_prec" : '';
-
- if (!preg_match("|^[-+]?\s*[0-9]+($dec_regex)?\$|", $number)) {
- return false;
- }
-
- if ($decimal != '.') {
- $number = strtr($number, $decimal, '.');
- }
-
- $number = (float)str_replace(' ', '', $number);
- if ($min !== null && $min > $number) {
- return false;
- }
-
- if ($max !== null && $max < $number) {
- return false;
- }
- return true;
- }
-
- /**
- * Converting a string to UTF-7 (RFC 2152)
- *
- * @param $string string to be converted
- *
- * @return string converted string
- *
- * @access private
- */
- function __stringToUtf7($string) {
- $return = '';
- $utf7 = array(
- 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
- 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
- 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
- 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
- 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
- '3', '4', '5', '6', '7', '8', '9', '+', ','
- );
-
- $state = 0;
- if (!empty($string)) {
- $i = 0;
- while ($i <= strlen($string)) {
- $char = substr($string, $i, 1);
- if ($state == 0) {
- if ((ord($char) >= 0x7F) || (ord($char) <= 0x1F)) {
- if ($char) {
- $return .= '&';
- }
- $state = 1;
- } elseif ($char == '&') {
- $return .= '&-';
- } else {
- $return .= $char;
- }
- } elseif (($i == strlen($string) ||
- !((ord($char) >= 0x7F)) || (ord($char) <= 0x1F))) {
- if ($state != 1) {
- if (ord($char) > 64) {
- $return .= '';
- } else {
- $return .= $utf7[ord($char)];
- }
- }
- $return .= '-';
- $state = 0;
- } else {
- switch($state) {
- case 1:
- $return .= $utf7[ord($char) >> 2];
- $residue = (ord($char) & 0x03) << 4;
- $state = 2;
- break;
- case 2:
- $return .= $utf7[$residue | (ord($char) >> 4)];
- $residue = (ord($char) & 0x0F) << 2;
- $state = 3;
- break;
- case 3:
- $return .= $utf7[$residue | (ord($char) >> 6)];
- $return .= $utf7[ord($char) & 0x3F];
- $state = 1;
- break;
- }
- }
- $i++;
- }
- return $return;
- }
- return '';
- }
-
- /**
- * Validate an email according to full RFC822 (inclusive human readable part)
- *
- * @param string $email email to validate,
- * will return the address for optional dns validation
- * @param array $options email() options
- *
- * @return boolean true if valid email, false if not
- *
- * @access private
- */
- function __emailRFC822(&$email, &$options)
- {
- if (Validate::__stringToUtf7($email) != $email) {
- return false;
- }
- static $address = null;
- static $uncomment = null;
- if (!$address) {
- // atom = 1*<any CHAR except specials, SPACE and CTLs>
- $atom = '[^][()<>@,;:\\".\s\000-\037\177-\377]+\s*';
- // qtext = <any CHAR excepting <">, ; => may be folded
- // "\" & CR, and including linear-white-space>
- $qtext = '[^"\\\\\r]';
- // quoted-pair = "\" CHAR ; may quote any char
- $quoted_pair = '\\\\.';
- // quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or
- // ; quoted chars.
- $quoted_string = '"(?:' . $qtext . '|' . $quoted_pair . ')*"\s*';
- // word = atom / quoted-string
- $word = '(?:' . $atom . '|' . $quoted_string . ')';
- // local-part = word *("." word) ; uninterpreted
- // ; case-preserved
- $local_part = $word . '(?:\.\s*' . $word . ')*';
- // dtext = <any CHAR excluding "[", ; => may be folded
- // "]", "\" & CR, & including linear-white-space>
- $dtext = '[^][\\\\\r]';
- // domain-literal = "[" *(dtext / quoted-pair) "]"
- $domain_literal = '\[(?:' . $dtext . '|' . $quoted_pair . ')*\]\s*';
- // sub-domain = domain-ref / domain-literal
- // domain-ref = atom ; symbolic reference
- $sub_domain = '(?:' . $atom . '|' . $domain_literal . ')';
- // domain = sub-domain *("." sub-domain)
- $domain = $sub_domain . '(?:\.\s*' . $sub_domain . ')*';
- // addr-spec = local-part "@" domain ; global address
- $addr_spec = $local_part . '@\s*' . $domain;
- // route = 1#("@" domain) ":" ; path-relative
- $route = '@' . $domain . '(?:,@\s*' . $domain . ')*:\s*';
- // route-addr = "<" [route] addr-spec ">"
- $route_addr = '<\s*(?:' . $route . ')?' . $addr_spec . '>\s*';
- // phrase = 1*word ; Sequence of words
- $phrase = $word . '+';
- // mailbox = addr-spec ; simple address
- // / phrase route-addr ; name & addr-spec
- $mailbox = '(?:' . $addr_spec . '|' . $phrase . $route_addr . ')';
- // group = phrase ":" [#mailbox] ";"
- $group = $phrase . ':\s*(?:' . $mailbox . '(?:,\s*' . $mailbox . ')*)?;\s*';
- // address = mailbox ; one addressee
- // / group ; named list
- $address = '/^\s*(?:' . $mailbox . '|' . $group . ')$/';
- $uncomment =
- '/((?:(?:\\\\"|[^("])*(?:' . $quoted_string .
- ')?)*)((?<!\\\\)\((?:(?2)|.)*?(?<!\\\\)\))/';
- }
- // strip comments
- $email = preg_replace($uncomment, '$1 ', $email);
- return preg_match($address, $email);
- }
-
- /**
- * Full TLD Validation function
- *
- * This function is used to make a much more proficient validation
- * against all types of official domain names.
- *
- * @access protected
- * @param string $email The email address to check.
- * @param array $options The options for validation
- * @return bool True if validating succeeds
- */
- function _fullTLDValidation($email, $options)
- {
- $validate = array();
-
- switch ($options) {
- /** 1 */
- case VALIDATE_ITLD_EMAILS:
- array_push($validate, 'itld');
- break;
-
- /** 2 */
- case VALIDATE_GTLD_EMAILS:
- array_push($validate, 'gtld');
- break;
-
- /** 3 */
- case VALIDATE_ITLD_EMAILS | VALIDATE_GTLD_EMAILS:
- array_push($validate, 'itld');
- array_push($validate, 'gtld');
- break;
-
- /** 4 */
- case VALIDATE_CCTLD_EMAILS:
- array_push($validate, 'cctld');
- break;
-
- /** 5 */
- case VALIDATE_CCTLD_EMAILS | VALIDATE_ITLD_EMAILS:
- array_push($validate, 'cctld');
- array_push($validate, 'itld');
- break;
-
- /** 6 */
- case VALIDATE_CCTLD_EMAILS ^ VALIDATE_ITLD_EMAILS:
- array_push($validate, 'cctld');
- array_push($validate, 'itld');
- break;
-
- /** 7 - 8 */
- case VALIDATE_CCTLD_EMAILS | VALIDATE_ITLD_EMAILS | VALIDATE_GTLD_EMAILS:
- case VALIDATE_ALL_EMAILS:
- array_push($validate, 'cctld');
- array_push($validate, 'itld');
- array_push($validate, 'gtld');
- break;
- }
-
- /**
- * Debugging still, not implemented but code is somewhat here.
- */
-
- $self = new Validate;
-
- $toValidate = array();
-
- foreach ($validate as $valid) {
- $tmpVar = '_' . (string)$valid;
- $toValidate[$valid] = $self->{$tmpVar};
- }
-
- $e = $self->executeFullEmailValidation($email, $toValidate);
-
- return $e;
- }
- // {{{ protected function executeFullEmailValidation
- /**
- * Execute the validation
- *
- * This function will execute the full email vs tld
- * validation using an array of tlds passed to it.
- *
- * @access public
- * @param string $email The email to validate.
- * @param array $arrayOfTLDs The array of the TLDs to validate
- * @return true or false (Depending on if it validates or if it does not)
- */
- function executeFullEmailValidation($email, $arrayOfTLDs)
- {
- $emailEnding = explode('.', $email);
- $emailEnding = $emailEnding[count($emailEnding)-1];
-
- foreach ($arrayOfTLDs as $validator => $keys) {
- if (in_array($emailEnding, $keys)) {
- return true;
- }
- }
- return false;
- }
- // }}}
-
- /**
- * Validate an email
- *
- * @param string $email email to validate
- * @param mixed boolean (BC) $check_domain Check or not if the domain exists
- * array $options associative array of options
- * 'check_domain' boolean Check or not if the domain exists
- * 'use_rfc822' boolean Apply the full RFC822 grammar
- *
- * @return boolean true if valid email, false if not
- *
- * @access public
- */
- function email($email, $options = null)
- {
- $check_domain = false;
- $use_rfc822 = false;
- if (is_bool($options)) {
- $check_domain = $options;
- } elseif (is_array($options)) {
- extract($options);
- }
-
- /**
- * @todo Fix bug here.. even if it passes this, it won't be passing
- * The regular expression below
- */
- if (isset($fullTLDValidation)) {
- $valid = Validate::_fullTLDValidation($email, $fullTLDValidation);
-
- if (!$valid) {
- return false;
- }
- }
-
- // the base regexp for address
- $regex = '&^(?: # recipient:
- ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
- ([-\w!\#\$%\&\'*+~/^`|{}]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}]+)*)) #2 OR dot-atom
- @(((\[)? #3 domain, 4 as IPv4, 5 optionally bracketed
- (?:(?:(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))\.){3}
- (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))))(?(5)\])|
- ((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z0-9](?:[-a-z0-9]*[a-z0-9])?) #6 domain as hostname
- \.((?:([^- ])[-a-z]*[-a-z]))) #7 TLD
- $&xi';
-
- if ($use_rfc822? Validate::__emailRFC822($email, $options) :
- preg_match($regex, $email)) {
- if ($check_domain && function_exists('checkdnsrr')) {
- list (, $domain) = explode('@', $email);
- if (checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A')) {
- return true;
- }
- return false;
- }
- return true;
- }
- return false;
- }
-
- /**
- * Validate a string using the given format 'format'
- *
- * @param string $string String to validate
- * @param array $options Options array where:
- * 'format' is the format of the string
- * Ex: VALIDATE_NUM . VALIDATE_ALPHA (see constants)
- * 'min_length' minimum length
- * 'max_length' maximum length
- *
- * @return boolean true if valid string, false if not
- *
- * @access public
- */
- function string($string, $options)
- {
- $format = null;
- $min_length = $max_length = 0;
- if (is_array($options)) {
- extract($options);
- }
- if ($format && !preg_match("|^[$format]*\$|s", $string)) {
- return false;
- }
- if ($min_length && strlen($string) < $min_length) {
- return false;
- }
- if ($max_length && strlen($string) > $max_length) {
- return false;
- }
- return true;
- }
-
- /**
- * Validate an URI (RFC2396)
- * This function will validate 'foobarstring' by default, to get it to validate
- * only http, https, ftp and such you have to pass it in the allowed_schemes
- * option, like this:
- * <code>
- * $options = array('allowed_schemes' => array('http', 'https', 'ftp'))
- * var_dump(Validate::uri('http://www.example.org', $options));
- * </code>
- *
- * NOTE 1: The rfc2396 normally allows middle '-' in the top domain
- * e.g. http://example.co-m should be valid
- * However, as '-' is not used in any known TLD, it is invalid
- * NOTE 2: As double shlashes // are allowed in the path part, only full URIs
- * including an authority can be valid, no relative URIs
- * the // are mandatory (optionally preceeded by the 'sheme:' )
- * NOTE 3: the full complience to rfc2396 is not achieved by default
- * the characters ';/?:@$,' will not be accepted in the query part
- * if not urlencoded, refer to the option "strict'"
- *
- * @param string $url URI to validate
- * @param array $options Options used by the validation method.
- * key => type
- * 'domain_check' => boolean
- * Whether to check the DNS entry or not
- * 'allowed_schemes' => array, list of protocols
- * List of allowed schemes ('http',
- * 'ssh+svn', 'mms')
- * 'strict' => string the refused chars
- * in query and fragment parts
- * default: ';/?:@$,'
- * empty: accept all rfc2396 foreseen chars
- *
- * @return boolean true if valid uri, false if not
- *
- * @access public
- */
- function uri($url, $options = null)
- {
- $strict = ';/?:@$,';
- $domain_check = false;
- $allowed_schemes = null;
- if (is_array($options)) {
- extract($options);
- }
- if (preg_match(
- '&^(?:([a-z][-+.a-z0-9]*):)? # 1. scheme
- (?:// # authority start
- (?:((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();:\&=+$,])*)@)? # 2. authority-userinfo
- (?:((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[a-z0-9]+)?\.?) # 3. authority-hostname OR
- |([0-9]{1,3}(?:\.[0-9]{1,3}){3})) # 4. authority-ipv4
- (?::([0-9]*))?) # 5. authority-port
- ((?:/(?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'():@\&=+$,;])*)*/?)? # 6. path
- (?:\?([^#]*))? # 7. query
- (?:\#((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();/?:@\&=+$,])*))? # 8. fragment
- $&xi', $url, $matches)) {
- $scheme = isset($matches[1]) ? $matches[1] : '';
- $authority = isset($matches[3]) ? $matches[3] : '' ;
- if (is_array($allowed_schemes) &&
- !in_array($scheme,$allowed_schemes)
- ) {
- return false;
- }
- if (!empty($matches[4])) {
- $parts = explode('.', $matches[4]);
- foreach ($parts as $part) {
- if ($part > 255) {
- return false;
- }
- }
- } elseif ($domain_check && function_exists('checkdnsrr')) {
- if (!checkdnsrr($authority, 'A')) {
- return false;
- }
- }
- if ($strict) {
- $strict = '#[' . preg_quote($strict, '#') . ']#';
- if ((!empty($matches[7]) && preg_match($strict, $matches[7]))
- || (!empty($matches[8]) && preg_match($strict, $matches[8]))) {
- return false;
- }
- }
- return true;
- }
- return false;
- }
-
- /**
- * Validate date and times. Note that this method need the Date_Calc class
- *
- * @param string $date Date to validate
- * @param array $options array options where :
- * 'format' The format of the date (%d-%m-%Y)
- * or rfc822_compliant
- * 'min' The date has to be greater
- * than this array($day, $month, $year)
- * or PEAR::Date object
- * 'max' The date has to be smaller than
- * this array($day, $month, $year)
- * or PEAR::Date object
- *
- * @return boolean true if valid date/time, false if not
- *
- * @access public
- */
- function date($date, $options)
- {
- $max = $min = false;
- $format = '';
- if (is_array($options)) {
- extract($options);
- }
-
- if (strtolower($format) == 'rfc822_compliant') {
- $preg = '&^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),) \s+
- (?:(\d{2})?) \s+
- (?:(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?) \s+
- (?:(\d{2}(\d{2})?)?) \s+
- (?:(\d{2}?)):(?:(\d{2}?))(:(?:(\d{2}?)))? \s+
- (?:[+-]\d{4}|UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Za-ik-z])$&xi';
-
- if (!preg_match($preg, $date, $matches)) {
- return false;
- }
-
- $year = (int)$matches[4];
- $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
- $month = array_keys($months, $matches[3]);
- $month = (int)$month[0]+1;
- $day = (int)$matches[2];
- $weekday= $matches[1];
- $hour = (int)$matches[6];
- $minute = (int)$matches[7];
- isset($matches[9]) ? $second = (int)$matches[9] : $second = 0;
-
- if ((strlen($year) != 4) ||
- ($day > 31 || $day < 1)||
- ($hour > 23) ||
- ($minute > 59) ||
- ($second > 59)) {
- return false;
- }
- } else {
- $date_len = strlen($format);
- for ($i = 0; $i < $date_len; $i++) {
- $c = $format{$i};
- if ($c == '%') {
- $next = $format{$i + 1};
- switch ($next) {
- case 'j':
- case 'd':
- if ($next == 'j') {
- $day = (int)Validate::_substr($date, 1, 2);
- } else {
- $day = (int)Validate::_substr($date, 0, 2);
- }
- if ($day < 1 || $day > 31) {
- return false;
- }
- break;
- case 'm':
- case 'n':
- if ($next == 'm') {
- $month = (int)Validate::_substr($date, 0, 2);
- } else {
- $month = (int)Validate::_substr($date, 1, 2);
- }
- if ($month < 1 || $month > 12) {
- return false;
- }
- break;
- case 'Y':
- case 'y':
- if ($next == 'Y') {
- $year = Validate::_substr($date, 4);
- $year = (int)$year?$year:'';
- } else {
- $year = (int)(substr(date('Y'), 0, 2) .
- Validate::_substr($date, 2));
- }
- if (strlen($year) != 4 || $year < 0 || $year > 9999) {
- return false;
- }
- break;
- case 'g':
- case 'h':
- if ($next == 'g') {
- $hour = Validate::_substr($date, 1, 2);
- } else {
- $hour = Validate::_substr($date, 2);
- }
- if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 12) {
- return false;
- }
- break;
- case 'G':
- case 'H':
- if ($next == 'G') {
- $hour = Validate::_substr($date, 1, 2);
- } else {
- $hour = Validate::_substr($date, 2);
- }
- if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 24) {
- return false;
- }
- break;
- case 's':
- case 'i':
- $t = Validate::_substr($date, 2);
- if (!preg_match('/^\d+$/', $t) || $t < 0 || $t > 59) {
- return false;
- }
- break;
- default:
- trigger_error("Not supported char `$next' after % in offset " . ($i+2), E_USER_WARNING);
- }
- $i++;
- } else {
- //literal
- if (Validate::_substr($date, 1) != $c) {
- return false;
- }
- }
- }
- }
- // there is remaing data, we don't want it
- if (strlen($date) && (strtolower($format) != 'rfc822_compliant')) {
- return false;
- }
-
- if (isset($day) && isset($month) && isset($year)) {
- if (!checkdate($month, $day, $year)) {
- return false;
- }
-
- if (strtolower($format) == 'rfc822_compliant') {
- if ($weekday != date("D", mktime(0, 0, 0, $month, $day, $year))) {
- return false;
- }
- }
-
- if ($min) {
- include_once 'Date/Calc.php';
- if (is_a($min, 'Date') &&
- (Date_Calc::compareDates($day, $month, $year,
- $min->getDay(), $min->getMonth(), $min->getYear()) < 0))
- {
- return false;
- } elseif (is_array($min) &&
- (Date_Calc::compareDates($day, $month, $year,
- $min[0], $min[1], $min[2]) < 0))
- {
- return false;
- }
- }
-
- if ($max) {
- include_once 'Date/Calc.php';
- if (is_a($max, 'Date') &&
- (Date_Calc::compareDates($day, $month, $year,
- $max->getDay(), $max->getMonth(), $max->getYear()) > 0))
- {
- return false;
- } elseif (is_array($max) &&
- (Date_Calc::compareDates($day, $month, $year,
- $max[0], $max[1], $max[2]) > 0))
- {
- return false;
- }
- }
- }
-
- return true;
- }
-
- function _substr(&$date, $num, $opt = false)
- {
- if ($opt && strlen($date) >= $opt && preg_match('/^[0-9]{'.$opt.'}/', $date, $m)) {
- $ret = $m[0];
- } else {
- $ret = substr($date, 0, $num);
- }
- $date = substr($date, strlen($ret));
- return $ret;
- }
-
- function _modf($val, $div) {
- if (function_exists('bcmod')) {
- return bcmod($val, $div);
- } elseif (function_exists('fmod')) {
- return fmod($val, $div);
- }
- $r = $val / $div;
- $i = intval($r);
- return intval($val - $i * $div + .1);
- }
-
- /**
- * Calculates sum of product of number digits with weights
- *
- * @param string $number number string
- * @param array $weights reference to array of weights
- *
- * @returns int returns product of number digits with weights
- *
- * @access protected
- */
- function _multWeights($number, &$weights) {
- if (!is_array($weights)) {
- return -1;
- }
- $sum = 0;
-
- $count = min(count($weights), strlen($number));
- if ($count == 0) { // empty string or weights array
- return -1;
- }
- for ($i = 0; $i < $count; ++$i) {
- $sum += intval(substr($number, $i, 1)) * $weights[$i];
- }
-
- return $sum;
- }
-
- /**
- * Calculates control digit for a given number
- *
- * @param string $number number string
- * @param array $weights reference to array of weights
- * @param int $modulo (optionsl) number
- * @param int $subtract (optional) number
- * @param bool $allow_high (optional) true if function can return number higher than 10
- *
- * @returns int -1 calculated control number is returned
- *
- * @access protected
- */
- function _getControlNumber($number, &$weights, $modulo = 10, $subtract = 0, $allow_high = false) {
- // calc sum
- $sum = Validate::_multWeights($number, $weights);
- if ($sum == -1) {
- return -1;
- }
- $mod = Validate::_modf($sum, $modulo); // calculate control digit
-
- if ($subtract > $mod && $mod > 0) {
- $mod = $subtract - $mod;
- }
- if ($allow_high === false) {
- $mod %= 10; // change 10 to zero
- }
- return $mod;
- }
-
- /**
- * Validates a number
- *
- * @param string $number number to validate
- * @param array $weights reference to array of weights
- * @param int $modulo (optionsl) number
- * @param int $subtract (optional) numbier
- *
- * @returns bool true if valid, false if not
- *
- * @access protected
- */
- function _checkControlNumber($number, &$weights, $modulo = 10, $subtract = 0) {
- if (strlen($number) < count($weights)) {
- return false;
- }
- $target_digit = substr($number, count($weights), 1);
- $control_digit = Validate::_getControlNumber($number, $weights, $modulo, $subtract, $modulo > 10);
-
- if ($control_digit == -1) {
- return false;
- }
- if ($target_digit === 'X' && $control_digit == 10) {
- return true;
- }
- if ($control_digit != $target_digit) {
- return false;
- }
- return true;
- }
-
- /**
- * Bulk data validation for data introduced in the form of an
- * assoc array in the form $var_name => $value.
- * Can be used on any of Validate subpackages
- *
- * @param array $data Ex: array('name' => 'toto', 'email' => 'toto@thing.info');
- * @param array $val_type Contains the validation type and all parameters used in.
- * 'val_type' is not optional
- * others validations properties must have the same name as the function
- * parameters.
- * Ex: array('toto'=>array('type'=>'string','format'='toto@thing.info','min_length'=>5));
- * @param boolean $remove if set, the elements not listed in data will be removed
- *
- * @return array value name => true|false the value name comes from the data key
- *
- * @access public
- */
- function multiple(&$data, &$val_type, $remove = false)
- {
- $keys = array_keys($data);
- $valid = array();
- foreach ($keys as $var_name) {
- if (!isset($val_type[$var_name])) {
- if ($remove) {
- unset($data[$var_name]);
- }
- continue;
- }
- $opt = $val_type[$var_name];
- $methods = get_class_methods('Validate');
- $val2check = $data[$var_name];
- // core validation method
- if (in_array(strtolower($opt['type']), $methods)) {
- //$opt[$opt['type']] = $data[$var_name];
- $method = $opt['type'];
- unset($opt['type']);
-
- if (sizeof($opt) == 1 && is_array(reset($opt))) {
- $opt = array_pop($opt);
- }
- $valid[$var_name] = call_user_func(array('Validate', $method), $val2check, $opt);
-
- /**
- * external validation method in the form:
- * "<class name><underscore><method name>"
- * Ex: us_ssn will include class Validate/US.php and call method ssn()
- */
- } elseif (strpos($opt['type'], '_') !== false) {
- $validateType = explode('_', $opt['type']);
- $method = array_pop($validateType);
- $class = implode('_', $validateType);
- $classPath = str_replace('_', DIRECTORY_SEPARATOR, $class);
- $class = 'Validate_' . $class;
- if (!@include_once "Validate/$classPath.php") {
- trigger_error("$class isn't installed or you may have some permissoin issues", E_USER_ERROR);
- }
-
- $ce = substr(phpversion(), 0, 1) > 4 ? class_exists($class, false) : class_exists($class);
- if (!$ce ||
- !in_array($method, get_class_methods($class)))
- {
- trigger_error("Invalid validation type $class::$method", E_USER_WARNING);
- continue;
- }
- unset($opt['type']);
- if (sizeof($opt) == 1) {
- $opt = array_pop($opt);
- }
- $valid[$var_name] = call_user_func(array($class, $method), $data[$var_name], $opt);
- } else {
- trigger_error("Invalid validation type {$opt['type']}", E_USER_WARNING);
- }
- }
- return $valid;
- }
-}
-
+<?php
+/**
+ * Validation class
+ *
+ * Copyright (c) 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox, Amir Saied
+ *
+ * This source file is subject to the New BSD license, That is bundled
+ * with this package in the file LICENSE, and is available through
+ * the world-wide-web at
+ * http://www.opensource.org/licenses/bsd-license.php
+ * If you did not receive a copy of the new BSDlicense and are unable
+ * to obtain it through the world-wide-web, please send a note to
+ * pajoye@php.net so we can mail you a copy immediately.
+ *
+ * Author: Tomas V.V.Cox <cox@idecnet.com>
+ * Pierre-Alain Joye <pajoye@php.net>
+ * Amir Mohammad Saied <amir@php.net>
+ *
+ *
+ * Package to validate various datas. It includes :
+ * - numbers (min/max, decimal or not)
+ * - email (syntax, domain check)
+ * - string (predifined type alpha upper and/or lowercase, numeric,...)
+ * - date (min, max, rfc822 compliant)
+ * - uri (RFC2396)
+ * - possibility valid multiple data with a single method call (::multiple)
+ *
+ * @category Validate
+ * @package Validate
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Pierre-Alain Joye <pajoye@php.net>
+ * @author Amir Mohammad Saied <amir@php.net>
+ * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version CVS: $Id: Validate.php,v 1.134 2009/01/28 12:27:33 davidc Exp $
+ * @link http://pear.php.net/package/Validate
+ */
+
+/**
+ * Methods for common data validations
+ */
+define('VALIDATE_NUM', '0-9');
+define('VALIDATE_SPACE', '\s');
+define('VALIDATE_ALPHA_LOWER', 'a-z');
+define('VALIDATE_ALPHA_UPPER', 'A-Z');
+define('VALIDATE_ALPHA', VALIDATE_ALPHA_LOWER . VALIDATE_ALPHA_UPPER);
+define('VALIDATE_EALPHA_LOWER', VALIDATE_ALPHA_LOWER . 'áéíóúýàèìòùäëïöüÿâêîôûãñõ¨åæç½ðøþß');
+define('VALIDATE_EALPHA_UPPER', VALIDATE_ALPHA_UPPER . 'ÁÉÍÓÚÝÀÈÌÒÙÄËÏÖܾÂÊÎÔÛÃÑÕ¦ÅÆǼÐØÞ');
+define('VALIDATE_EALPHA', VALIDATE_EALPHA_LOWER . VALIDATE_EALPHA_UPPER);
+define('VALIDATE_PUNCTUATION', VALIDATE_SPACE . '\.,;\:&"\'\?\!\(\)');
+define('VALIDATE_NAME', VALIDATE_EALPHA . VALIDATE_SPACE . "'" . "-");
+define('VALIDATE_STREET', VALIDATE_NUM . VALIDATE_NAME . "/\\ºª\.");
+
+define('VALIDATE_ITLD_EMAILS', 1);
+define('VALIDATE_GTLD_EMAILS', 2);
+define('VALIDATE_CCTLD_EMAILS', 4);
+define('VALIDATE_ALL_EMAILS', 8);
+
+/**
+ * Validation class
+ *
+ * Package to validate various datas. It includes :
+ * - numbers (min/max, decimal or not)
+ * - email (syntax, domain check)
+ * - string (predifined type alpha upper and/or lowercase, numeric,...)
+ * - date (min, max)
+ * - uri (RFC2396)
+ * - possibility valid multiple data with a single method call (::multiple)
+ *
+ * @category Validate
+ * @package Validate
+ * @author Tomas V.V.Cox <cox@idecnet.com>
+ * @author Pierre-Alain Joye <pajoye@php.net>
+ * @author Amir Mohammad Saied <amir@php.net>
+ * @copyright 1997-2006 Pierre-Alain Joye,Tomas V.V.Cox,Amir Mohammad Saied
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version Release: @package_version@
+ * @link http://pear.php.net/package/Validate
+ */
+class Validate
+{
+ /**
+ * International Top-Level Domain
+ *
+ * This is an array of the known international
+ * top-level domain names.
+ *
+ * @access protected
+ * @var array $_iTld (International top-level domains)
+ */
+ var $_itld = array(
+ 'arpa',
+ 'root',
+ );
+
+ /**
+ * Generic top-level domain
+ *
+ * This is an array of the official
+ * generic top-level domains.
+ *
+ * @access protected
+ * @var array $_gTld (Generic top-level domains)
+ */
+ var $_gtld = array(
+ 'aero',
+ 'biz',
+ 'cat',
+ 'com',
+ 'coop',
+ 'edu',
+ 'gov',
+ 'info',
+ 'int',
+ 'jobs',
+ 'mil',
+ 'mobi',
+ 'museum',
+ 'name',
+ 'net',
+ 'org',
+ 'pro',
+ 'travel',
+ 'asia',
+ 'post',
+ 'tel',
+ 'geo',
+ );
+
+ /**
+ * Country code top-level domains
+ *
+ * This is an array of the official country
+ * codes top-level domains
+ *
+ * @access protected
+ * @var array $_ccTld (Country Code Top-Level Domain)
+ */
+ var $_cctld = array(
+ 'ac',
+ 'ad','ae','af','ag',
+ 'ai','al','am','an',
+ 'ao','aq','ar','as',
+ 'at','au','aw','ax',
+ 'az','ba','bb','bd',
+ 'be','bf','bg','bh',
+ 'bi','bj','bm','bn',
+ 'bo','br','bs','bt',
+ 'bu','bv','bw','by',
+ 'bz','ca','cc','cd',
+ 'cf','cg','ch','ci',
+ 'ck','cl','cm','cn',
+ 'co','cr','cs','cu',
+ 'cv','cx','cy','cz',
+ 'de','dj','dk','dm',
+ 'do','dz','ec','ee',
+ 'eg','eh','er','es',
+ 'et','eu','fi','fj',
+ 'fk','fm','fo','fr',
+ 'ga','gb','gd','ge',
+ 'gf','gg','gh','gi',
+ 'gl','gm','gn','gp',
+ 'gq','gr','gs','gt',
+ 'gu','gw','gy','hk',
+ 'hm','hn','hr','ht',
+ 'hu','id','ie','il',
+ 'im','in','io','iq',
+ 'ir','is','it','je',
+ 'jm','jo','jp','ke',
+ 'kg','kh','ki','km',
+ 'kn','kp','kr','kw',
+ 'ky','kz','la','lb',
+ 'lc','li','lk','lr',
+ 'ls','lt','lu','lv',
+ 'ly','ma','mc','md',
+ 'me','mg','mh','mk',
+ 'ml','mm','mn','mo',
+ 'mp','mq','mr','ms',
+ 'mt','mu','mv','mw',
+ 'mx','my','mz','na',
+ 'nc','ne','nf','ng',
+ 'ni','nl','no','np',
+ 'nr','nu','nz','om',
+ 'pa','pe','pf','pg',
+ 'ph','pk','pl','pm',
+ 'pn','pr','ps','pt',
+ 'pw','py','qa','re',
+ 'ro','rs','ru','rw',
+ 'sa','sb','sc','sd',
+ 'se','sg','sh','si',
+ 'sj','sk','sl','sm',
+ 'sn','so','sr','st',
+ 'su','sv','sy','sz',
+ 'tc','td','tf','tg',
+ 'th','tj','tk','tl',
+ 'tm','tn','to','tp',
+ 'tr','tt','tv','tw',
+ 'tz','ua','ug','uk',
+ 'us','uy','uz','va',
+ 'vc','ve','vg','vi',
+ 'vn','vu','wf','ws',
+ 'ye','yt','yu','za',
+ 'zm','zw',
+ );
+
+ /**
+ * Validate a tag URI (RFC4151)
+ *
+ * @param string $uri tag URI to validate
+ *
+ * @return boolean true if valid tag URI, false if not
+ *
+ * @access private
+ */
+ function __uriRFC4151($uri)
+ {
+ $datevalid = false;
+ if (preg_match(
+ '/^tag:(?<name>.*),(?<date>\d{4}-?\d{0,2}-?\d{0,2}):(?<specific>.*)(.*:)*$/', $uri, $matches)) {
+ $date = $matches['date'];
+ $date6 = strtotime($date);
+ if ((strlen($date) == 4) && $date <= date('Y')) {
+ $datevalid = true;
+ } elseif ((strlen($date) == 7) && ($date6 < strtotime("now"))) {
+ $datevalid = true;
+ } elseif ((strlen($date) == 10) && ($date6 < strtotime("now"))) {
+ $datevalid = true;
+ }
+ if (self::email($matches['name'])) {
+ $namevalid = true;
+ } else {
+ $namevalid = self::email('info@' . $matches['name']);
+ }
+ return $datevalid && $namevalid;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Validate a number
+ *
+ * @param string $number Number to validate
+ * @param array $options array where:
+ * 'decimal' is the decimal char or false when decimal
+ * not allowed.
+ * i.e. ',.' to allow both ',' and '.'
+ * 'dec_prec' Number of allowed decimals
+ * 'min' minimum value
+ * 'max' maximum value
+ *
+ * @return boolean true if valid number, false if not
+ *
+ * @access public
+ */
+ function number($number, $options = array())
+ {
+ $decimal = $dec_prec = $min = $max = null;
+ if (is_array($options)) {
+ extract($options);
+ }
+
+ $dec_prec = $dec_prec ? "{1,$dec_prec}" : '+';
+ $dec_regex = $decimal ? "[$decimal][0-9]$dec_prec" : '';
+
+ if (!preg_match("|^[-+]?\s*[0-9]+($dec_regex)?\$|", $number)) {
+ return false;
+ }
+
+ if ($decimal != '.') {
+ $number = strtr($number, $decimal, '.');
+ }
+
+ $number = (float)str_replace(' ', '', $number);
+ if ($min !== null && $min > $number) {
+ return false;
+ }
+
+ if ($max !== null && $max < $number) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Converting a string to UTF-7 (RFC 2152)
+ *
+ * @param string $string string to be converted
+ *
+ * @return string converted string
+ *
+ * @access private
+ */
+ function __stringToUtf7($string)
+ {
+ $return = '';
+ $utf7 = array(
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
+ 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
+ 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
+ '3', '4', '5', '6', '7', '8', '9', '+', ','
+ );
+
+
+ $state = 0;
+
+ if (!empty($string)) {
+ $i = 0;
+ while ($i <= strlen($string)) {
+ $char = substr($string, $i, 1);
+ if ($state == 0) {
+ if ((ord($char) >= 0x7F) || (ord($char) <= 0x1F)) {
+ if ($char) {
+ $return .= '&';
+ }
+ $state = 1;
+ } elseif ($char == '&') {
+ $return .= '&-';
+ } else {
+ $return .= $char;
+ }
+ } elseif (($i == strlen($string) ||
+ !((ord($char) >= 0x7F)) || (ord($char) <= 0x1F))) {
+ if ($state != 1) {
+ if (ord($char) > 64) {
+ $return .= '';
+ } else {
+ $return .= $utf7[ord($char)];
+ }
+ }
+ $return .= '-';
+ $state = 0;
+ } else {
+ switch($state) {
+ case 1:
+ $return .= $utf7[ord($char) >> 2];
+ $residue = (ord($char) & 0x03) << 4;
+ $state = 2;
+ break;
+ case 2:
+ $return .= $utf7[$residue | (ord($char) >> 4)];
+ $residue = (ord($char) & 0x0F) << 2;
+ $state = 3;
+ break;
+ case 3:
+ $return .= $utf7[$residue | (ord($char) >> 6)];
+ $return .= $utf7[ord($char) & 0x3F];
+ $state = 1;
+ break;
+ }
+ }
+ $i++;
+ }
+ return $return;
+ }
+ return '';
+ }
+
+ /**
+ * Validate an email according to full RFC822 (inclusive human readable part)
+ *
+ * @param string $email email to validate,
+ * will return the address for optional dns validation
+ * @param array $options email() options
+ *
+ * @return boolean true if valid email, false if not
+ *
+ * @access private
+ */
+ function __emailRFC822(&$email, &$options)
+ {
+ static $address = null;
+ static $uncomment = null;
+ if (!$address) {
+ // atom = 1*<any CHAR except specials, SPACE and CTLs>
+ $atom = '[^][()<>@,;:\\".\s\000-\037\177-\377]+\s*';
+ // qtext = <any CHAR excepting <">, ; => may be folded
+ // "\" & CR, and including linear-white-space>
+ $qtext = '[^"\\\\\r]';
+ // quoted-pair = "\" CHAR ; may quote any char
+ $quoted_pair = '\\\\.';
+ // quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or
+ // ; quoted chars.
+ $quoted_string = '"(?:' . $qtext . '|' . $quoted_pair . ')*"\s*';
+ // word = atom / quoted-string
+ $word = '(?:' . $atom . '|' . $quoted_string . ')';
+ // local-part = word *("." word) ; uninterpreted
+ // ; case-preserved
+ $local_part = $word . '(?:\.\s*' . $word . ')*';
+ // dtext = <any CHAR excluding "[", ; => may be folded
+ // "]", "\" & CR, & including linear-white-space>
+ $dtext = '[^][\\\\\r]';
+ // domain-literal = "[" *(dtext / quoted-pair) "]"
+ $domain_literal = '\[(?:' . $dtext . '|' . $quoted_pair . ')*\]\s*';
+ // sub-domain = domain-ref / domain-literal
+ // domain-ref = atom ; symbolic reference
+ $sub_domain = '(?:' . $atom . '|' . $domain_literal . ')';
+ // domain = sub-domain *("." sub-domain)
+ $domain = $sub_domain . '(?:\.\s*' . $sub_domain . ')*';
+ // addr-spec = local-part "@" domain ; global address
+ $addr_spec = $local_part . '@\s*' . $domain;
+ // route = 1#("@" domain) ":" ; path-relative
+ $route = '@' . $domain . '(?:,@\s*' . $domain . ')*:\s*';
+ // route-addr = "<" [route] addr-spec ">"
+ $route_addr = '<\s*(?:' . $route . ')?' . $addr_spec . '>\s*';
+ // phrase = 1*word ; Sequence of words
+ $phrase = $word . '+';
+ // mailbox = addr-spec ; simple address
+ // / phrase route-addr ; name & addr-spec
+ $mailbox = '(?:' . $addr_spec . '|' . $phrase . $route_addr . ')';
+ // group = phrase ":" [#mailbox] ";"
+ $group = $phrase . ':\s*(?:' . $mailbox . '(?:,\s*' . $mailbox . ')*)?;\s*';
+ // address = mailbox ; one addressee
+ // / group ; named list
+ $address = '/^\s*(?:' . $mailbox . '|' . $group . ')$/';
+
+ $uncomment =
+ '/((?:(?:\\\\"|[^("])*(?:' . $quoted_string .
+ ')?)*)((?<!\\\\)\((?:(?2)|.)*?(?<!\\\\)\))/';
+ }
+ // strip comments
+ $email = preg_replace($uncomment, '$1 ', $email);
+ return preg_match($address, $email);
+ }
+
+ /**
+ * Full TLD Validation function
+ *
+ * This function is used to make a much more proficient validation
+ * against all types of official domain names.
+ *
+ * @param string $email The email address to check.
+ * @param array $options The options for validation
+ *
+ * @access protected
+ *
+ * @return bool True if validating succeeds
+ */
+ function _fullTLDValidation($email, $options)
+ {
+ $validate = array();
+ if(!empty($options["VALIDATE_ITLD_EMAILS"])) array_push($validate, 'itld');
+ if(!empty($options["VALIDATE_GTLD_EMAILS"])) array_push($validate, 'gtld');
+ if(!empty($options["VALIDATE_CCTLD_EMAILS"])) array_push($validate, 'cctld');
+
+ $self = new Validate;
+
+ $toValidate = array();
+
+ foreach ($validate as $valid) {
+ $tmpVar = '_' . (string)$valid;
+
+ $toValidate[$valid] = $self->{$tmpVar};
+ }
+
+ $e = $self->executeFullEmailValidation($email, $toValidate);
+
+ return $e;
+ }
+
+ /**
+ * Execute the validation
+ *
+ * This function will execute the full email vs tld
+ * validation using an array of tlds passed to it.
+ *
+ * @param string $email The email to validate.
+ * @param array $arrayOfTLDs The array of the TLDs to validate
+ *
+ * @access public
+ *
+ * @return true or false (Depending on if it validates or if it does not)
+ */
+ function executeFullEmailValidation($email, $arrayOfTLDs)
+ {
+ $emailEnding = explode('.', $email);
+ $emailEnding = $emailEnding[count($emailEnding)-1];
+ foreach ($arrayOfTLDs as $validator => $keys) {
+ if (in_array($emailEnding, $keys)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Validate an email
+ *
+ * @param string $email email to validate
+ * @param mixed boolean (BC) $check_domain Check or not if the domain exists
+ * array $options associative array of options
+ * 'check_domain' boolean Check or not if the domain exists
+ * 'use_rfc822' boolean Apply the full RFC822 grammar
+ *
+ * Ex.
+ * $options = array(
+ * 'check_domain' => 'true',
+ * 'fullTLDValidation' => 'true',
+ * 'use_rfc822' => 'true',
+ * 'VALIDATE_GTLD_EMAILS' => 'true',
+ * 'VALIDATE_CCTLD_EMAILS' => 'true',
+ * 'VALIDATE_ITLD_EMAILS' => 'true',
+ * );
+ *
+ * @return boolean true if valid email, false if not
+ *
+ * @access public
+ */
+ function email($email, $options = null)
+ {
+ $check_domain = false;
+ $use_rfc822 = false;
+ if (is_bool($options)) {
+ $check_domain = $options;
+ } elseif (is_array($options)) {
+ extract($options);
+ }
+
+ /**
+ * Check for IDN usage so we can encode the domain as Punycode
+ * before continuing.
+ */
+ $hasIDNA = false;
+
+ if (@include_once('Net/IDNA.php')) {
+ $hasIDNA = true;
+ }
+
+ if ($hasIDNA === true) {
+ if (strpos($email, '@') !== false) {
+ list($name, $domain) = explode('@', $email, 2);
+
+ // Check if the domain contains characters > 127 which means
+ // it's an idn domain name.
+ $chars = count_chars($domain, 1);
+ if (!empty($chars) && max(array_keys($chars)) > 127) {
+ $idna =& Net_IDNA::singleton();
+ $domain = $idna->encode($domain);
+ }
+
+ $email = "$name@$domain";
+ }
+ }
+
+ /**
+ * @todo Fix bug here.. even if it passes this, it won't be passing
+ * The regular expression below
+ */
+ if (isset($fullTLDValidation)) {
+ //$valid = Validate::_fullTLDValidation($email, $fullTLDValidation);
+ $valid = Validate::_fullTLDValidation($email, $options);
+
+ if (!$valid) {
+ return false;
+ }
+ }
+
+ // the base regexp for address
+ $regex = '&^(?: # recipient:
+ ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
+ ([-\w!\#\$%\&\'*+~/^`|{}]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}]+)*)) #2 OR dot-atom
+ @(((\[)? #3 domain, 4 as IPv4, 5 optionally bracketed
+ (?:(?:(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))\.){3}
+ (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:[0-1]?[0-9]?[0-9]))))(?(5)\])|
+ ((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z0-9](?:[-a-z0-9]*[a-z0-9])?) #6 domain as hostname
+ \.((?:([^- ])[-a-z]*[-a-z]))) #7 TLD
+ $&xi';
+
+ //checks if exists the domain (MX or A)
+ if ($use_rfc822? Validate::__emailRFC822($email, $options) :
+ preg_match($regex, $email)) {
+ if ($check_domain && function_exists('checkdnsrr')) {
+ list ($account, $domain) = explode('@', $email);
+ if (checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A')) {
+ return true;
+ }
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Validate a string using the given format 'format'
+ *
+ * @param string $string String to validate
+ * @param array $options Options array where:
+ * 'format' is the format of the string
+ * Ex:VALIDATE_NUM . VALIDATE_ALPHA (see constants)
+ * 'min_length' minimum length
+ * 'max_length' maximum length
+ *
+ * @return boolean true if valid string, false if not
+ *
+ * @access public
+ */
+ function string($string, $options)
+ {
+ $format = null;
+ $min_length = 0;
+ $max_length = 0;
+
+ if (is_array($options)) {
+ extract($options);
+ }
+
+ if ($format && !preg_match("|^[$format]*\$|s", $string)) {
+ return false;
+ }
+
+ if ($min_length && strlen($string) < $min_length) {
+ return false;
+ }
+
+ if ($max_length && strlen($string) > $max_length) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate an URI (RFC2396)
+ * This function will validate 'foobarstring' by default, to get it to validate
+ * only http, https, ftp and such you have to pass it in the allowed_schemes
+ * option, like this:
+ * <code>
+ * $options = array('allowed_schemes' => array('http', 'https', 'ftp'))
+ * var_dump(Validate::uri('http://www.example.org', $options));
+ * </code>
+ *
+ * NOTE 1: The rfc2396 normally allows middle '-' in the top domain
+ * e.g. http://example.co-m should be valid
+ * However, as '-' is not used in any known TLD, it is invalid
+ * NOTE 2: As double shlashes // are allowed in the path part, only full URIs
+ * including an authority can be valid, no relative URIs
+ * the // are mandatory (optionally preceeded by the 'sheme:' )
+ * NOTE 3: the full complience to rfc2396 is not achieved by default
+ * the characters ';/?:@$,' will not be accepted in the query part
+ * if not urlencoded, refer to the option "strict'"
+ *
+ * @param string $url URI to validate
+ * @param array $options Options used by the validation method.
+ * key => type
+ * 'domain_check' => boolean
+ * Whether to check the DNS entry or not
+ * 'allowed_schemes' => array, list of protocols
+ * List of allowed schemes ('http',
+ * 'ssh+svn', 'mms')
+ * 'strict' => string the refused chars
+ * in query and fragment parts
+ * default: ';/?:@$,'
+ * empty: accept all rfc2396 foreseen chars
+ *
+ * @return boolean true if valid uri, false if not
+ *
+ * @access public
+ */
+ function uri($url, $options = null)
+ {
+ $strict = ';/?:@$,';
+ $domain_check = false;
+ $allowed_schemes = null;
+ if (is_array($options)) {
+ extract($options);
+ }
+ if (is_array($allowed_schemes) &&
+ in_array("tag", $allowed_schemes)
+ ) {
+ if (strpos($url, "tag:") === 0) {
+ return self::__uriRFC4151($url);
+ }
+ }
+
+ if (preg_match(
+ '&^(?:([a-z][-+.a-z0-9]*):)? # 1. scheme
+ (?:// # authority start
+ (?:((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();:\&=+$,])*)@)? # 2. authority-userinfo
+ (?:((?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[a-z0-9]+)?\.?) # 3. authority-hostname OR
+ |([0-9]{1,3}(?:\.[0-9]{1,3}){3})) # 4. authority-ipv4
+ (?::([0-9]*))?) # 5. authority-port
+ ((?:/(?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'():@\&=+$,;])*)*/?)? # 6. path
+ (?:\?([^#]*))? # 7. query
+ (?:\#((?:%[0-9a-f]{2}|[-a-z0-9_.!~*\'();/?:@\&=+$,])*))? # 8. fragment
+ $&xi', $url, $matches)) {
+ $scheme = isset($matches[1]) ? $matches[1] : '';
+ $authority = isset($matches[3]) ? $matches[3] : '' ;
+ if (is_array($allowed_schemes) &&
+ !in_array($scheme, $allowed_schemes)
+ ) {
+ return false;
+ }
+ if (!empty($matches[4])) {
+ $parts = explode('.', $matches[4]);
+ foreach ($parts as $part) {
+ if ($part > 255) {
+ return false;
+ }
+ }
+ } elseif ($domain_check && function_exists('checkdnsrr')) {
+ if (!checkdnsrr($authority, 'A')) {
+ return false;
+ }
+ }
+ if ($strict) {
+ $strict = '#[' . preg_quote($strict, '#') . ']#';
+ if ((!empty($matches[7]) && preg_match($strict, $matches[7]))
+ || (!empty($matches[8]) && preg_match($strict, $matches[8]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Validate date and times. Note that this method need the Date_Calc class
+ *
+ * @param string $date Date to validate
+ * @param array $options array options where :
+ * 'format' The format of the date (%d-%m-%Y)
+ * or rfc822_compliant
+ * 'min' The date has to be greater
+ * than this array($day, $month, $year)
+ * or PEAR::Date object
+ * 'max' The date has to be smaller than
+ * this array($day, $month, $year)
+ * or PEAR::Date object
+ *
+ * @return boolean true if valid date/time, false if not
+ *
+ * @access public
+ */
+ function date($date, $options)
+ {
+ $max = false;
+ $min = false;
+ $format = '';
+
+ if (is_array($options)) {
+ extract($options);
+ }
+
+ if (strtolower($format) == 'rfc822_compliant') {
+ $preg = '&^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),) \s+
+ (?:(\d{2})?) \s+
+ (?:(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?) \s+
+ (?:(\d{2}(\d{2})?)?) \s+
+ (?:(\d{2}?)):(?:(\d{2}?))(:(?:(\d{2}?)))? \s+
+ (?:[+-]\d{4}|UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Za-ik-z])$&xi';
+
+ if (!preg_match($preg, $date, $matches)) {
+ return false;
+ }
+
+ $year = (int)$matches[4];
+ $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
+ $month = array_keys($months, $matches[3]);
+ $month = (int)$month[0]+1;
+ $day = (int)$matches[2];
+ $weekday = $matches[1];
+ $hour = (int)$matches[6];
+ $minute = (int)$matches[7];
+ isset($matches[9]) ? $second = (int)$matches[9] : $second = 0;
+
+ if ((strlen($year) != 4) ||
+ ($day > 31 || $day < 1)||
+ ($hour > 23) ||
+ ($minute > 59) ||
+ ($second > 59)) {
+ return false;
+ }
+ } else {
+ $date_len = strlen($format);
+ for ($i = 0; $i < $date_len; $i++) {
+ $c = $format{$i};
+ if ($c == '%') {
+ $next = $format{$i + 1};
+ switch ($next) {
+ case 'j':
+ case 'd':
+ if ($next == 'j') {
+ $day = (int)Validate::_substr($date, 1, 2);
+ } else {
+ $day = (int)Validate::_substr($date, 0, 2);
+ }
+ if ($day < 1 || $day > 31) {
+ return false;
+ }
+ break;
+ case 'm':
+ case 'n':
+ if ($next == 'm') {
+ $month = (int)Validate::_substr($date, 0, 2);
+ } else {
+ $month = (int)Validate::_substr($date, 1, 2);
+ }
+ if ($month < 1 || $month > 12) {
+ return false;
+ }
+ break;
+ case 'Y':
+ case 'y':
+ if ($next == 'Y') {
+ $year = Validate::_substr($date, 4);
+ $year = (int)$year?$year:'';
+ } else {
+ $year = (int)(substr(date('Y'), 0, 2) .
+ Validate::_substr($date, 2));
+ }
+ if (strlen($year) != 4 || $year < 0 || $year > 9999) {
+ return false;
+ }
+ break;
+ case 'g':
+ case 'h':
+ if ($next == 'g') {
+ $hour = Validate::_substr($date, 1, 2);
+ } else {
+ $hour = Validate::_substr($date, 2);
+ }
+ if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 12) {
+ return false;
+ }
+ break;
+ case 'G':
+ case 'H':
+ if ($next == 'G') {
+ $hour = Validate::_substr($date, 1, 2);
+ } else {
+ $hour = Validate::_substr($date, 2);
+ }
+ if (!preg_match('/^\d+$/', $hour) || $hour < 0 || $hour > 24) {
+ return false;
+ }
+ break;
+ case 's':
+ case 'i':
+ $t = Validate::_substr($date, 2);
+ if (!preg_match('/^\d+$/', $t) || $t < 0 || $t > 59) {
+ return false;
+ }
+ break;
+ default:
+ trigger_error("Not supported char `$next' after % in offset " . ($i+2), E_USER_WARNING);
+ }
+ $i++;
+ } else {
+ //literal
+ if (Validate::_substr($date, 1) != $c) {
+ return false;
+ }
+ }
+ }
+ }
+ // there is remaing data, we don't want it
+ if (strlen($date) && (strtolower($format) != 'rfc822_compliant')) {
+ return false;
+ }
+
+ if (isset($day) && isset($month) && isset($year)) {
+ if (!checkdate($month, $day, $year)) {
+ return false;
+ }
+
+ if (strtolower($format) == 'rfc822_compliant') {
+ if ($weekday != date("D", mktime(0, 0, 0, $month, $day, $year))) {
+ return false;
+ }
+ }
+
+ if ($min) {
+ include_once 'Date/Calc.php';
+ if (is_a($min, 'Date') &&
+ (Date_Calc::compareDates($day, $month, $year,
+ $min->getDay(), $min->getMonth(), $min->getYear()) < 0)
+ ) {
+ return false;
+ } elseif (is_array($min) &&
+ (Date_Calc::compareDates($day, $month, $year,
+ $min[0], $min[1], $min[2]) < 0)
+ ) {
+ return false;
+ }
+ }
+
+ if ($max) {
+ include_once 'Date/Calc.php';
+ if (is_a($max, 'Date') &&
+ (Date_Calc::compareDates($day, $month, $year,
+ $max->getDay(), $max->getMonth(), $max->getYear()) > 0)
+ ) {
+ return false;
+ } elseif (is_array($max) &&
+ (Date_Calc::compareDates($day, $month, $year,
+ $max[0], $max[1], $max[2]) > 0)
+ ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Substr
+ *
+ * @param string &$date Date
+ * @param string $num Length
+ * @param string $opt Unknown
+ *
+ * @access private
+ * @return string
+ */
+ function _substr(&$date, $num, $opt = false)
+ {
+ if ($opt && strlen($date) >= $opt && preg_match('/^[0-9]{'.$opt.'}/', $date, $m)) {
+ $ret = $m[0];
+ } else {
+ $ret = substr($date, 0, $num);
+ }
+ $date = substr($date, strlen($ret));
+ return $ret;
+ }
+
+ function _modf($val, $div)
+ {
+ if (function_exists('bcmod')) {
+ return bcmod($val, $div);
+ } elseif (function_exists('fmod')) {
+ return fmod($val, $div);
+ }
+ $r = $val / $div;
+ $i = intval($r);
+ return intval($val - $i * $div + .1);
+ }
+
+ /**
+ * Calculates sum of product of number digits with weights
+ *
+ * @param string $number number string
+ * @param array $weights reference to array of weights
+ *
+ * @access protected
+ *
+ * @return int returns product of number digits with weights
+ */
+ function _multWeights($number, &$weights)
+ {
+ if (!is_array($weights)) {
+ return -1;
+ }
+ $sum = 0;
+
+ $count = min(count($weights), strlen($number));
+ if ($count == 0) { // empty string or weights array
+ return -1;
+ }
+ for ($i = 0; $i < $count; ++$i) {
+ $sum += intval(substr($number, $i, 1)) * $weights[$i];
+ }
+
+ return $sum;
+ }
+
+ /**
+ * Calculates control digit for a given number
+ *
+ * @param string $number number string
+ * @param array $weights reference to array of weights
+ * @param int $modulo (optionsl) number
+ * @param int $subtract (optional) number
+ * @param bool $allow_high (optional) true if function can return number higher than 10
+ *
+ * @access protected
+ *
+ * @return int -1 calculated control number is returned
+ */
+ function _getControlNumber($number, &$weights, $modulo = 10, $subtract = 0, $allow_high = false)
+ {
+ // calc sum
+ $sum = Validate::_multWeights($number, $weights);
+ if ($sum == -1) {
+ return -1;
+ }
+ $mod = Validate::_modf($sum, $modulo); // calculate control digit
+
+ if ($subtract > $mod && $mod > 0) {
+ $mod = $subtract - $mod;
+ }
+ if ($allow_high === false) {
+ $mod %= 10; // change 10 to zero
+ }
+ return $mod;
+ }
+
+ /**
+ * Validates a number
+ *
+ * @param string $number number to validate
+ * @param array $weights reference to array of weights
+ * @param int $modulo (optional) number
+ * @param int $subtract (optional) number
+ *
+ * @access protected
+ *
+ * @return bool true if valid, false if not
+ */
+ function _checkControlNumber($number, &$weights, $modulo = 10, $subtract = 0)
+ {
+ if (strlen($number) < count($weights)) {
+ return false;
+ }
+ $target_digit = substr($number, count($weights), 1);
+ $control_digit = Validate::_getControlNumber($number, $weights, $modulo, $subtract, $modulo > 10);
+
+ if ($control_digit == -1) {
+ return false;
+ }
+ if ($target_digit === 'X' && $control_digit == 10) {
+ return true;
+ }
+ if ($control_digit != $target_digit) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Bulk data validation for data introduced in the form of an
+ * assoc array in the form $var_name => $value.
+ * Can be used on any of Validate subpackages
+ *
+ * @param array $data Ex: array('name' => 'toto', 'email' => 'toto@thing.info');
+ * @param array $val_type Contains the validation type and all parameters used in.
+ * 'val_type' is not optional
+ * others validations properties must have the same name as the function
+ * parameters.
+ * Ex: array('toto'=>array('type'=>'string','format'='toto@thing.info','min_length'=>5));
+ * @param boolean $remove if set, the elements not listed in data will be removed
+ *
+ * @return array value name => true|false the value name comes from the data key
+ *
+ * @access public
+ */
+ function multiple(&$data, &$val_type, $remove = false)
+ {
+ $keys = array_keys($data);
+ $valid = array();
+
+ foreach ($keys as $var_name) {
+ if (!isset($val_type[$var_name])) {
+ if ($remove) {
+ unset($data[$var_name]);
+ }
+ continue;
+ }
+ $opt = $val_type[$var_name];
+ $methods = get_class_methods('Validate');
+ $val2check = $data[$var_name];
+ // core validation method
+ if (in_array(strtolower($opt['type']), $methods)) {
+ //$opt[$opt['type']] = $data[$var_name];
+ $method = $opt['type'];
+ unset($opt['type']);
+
+ if (sizeof($opt) == 1 && is_array(reset($opt))) {
+ $opt = array_pop($opt);
+ }
+ $valid[$var_name] = call_user_func(array('Validate', $method), $val2check, $opt);
+
+ /**
+ * external validation method in the form:
+ * "<class name><underscore><method name>"
+ * Ex: us_ssn will include class Validate/US.php and call method ssn()
+ */
+ } elseif (strpos($opt['type'], '_') !== false) {
+ $validateType = explode('_', $opt['type']);
+ $method = array_pop($validateType);
+ $class = implode('_', $validateType);
+ $classPath = str_replace('_', DIRECTORY_SEPARATOR, $class);
+ $class = 'Validate_' . $class;
+ if (!@include_once "Validate/$classPath.php") {
+ trigger_error("$class isn't installed or you may have some permissoin issues", E_USER_ERROR);
+ }
+
+ $ce = substr(phpversion(), 0, 1) > 4 ?
+ class_exists($class, false) : class_exists($class);
+ if (!$ce ||
+ !in_array($method, get_class_methods($class))
+ ) {
+ trigger_error("Invalid validation type $class::$method",
+ E_USER_WARNING);
+ continue;
+ }
+ unset($opt['type']);
+ if (sizeof($opt) == 1) {
+ $opt = array_pop($opt);
+ }
+ $valid[$var_name] = call_user_func(array($class, $method),
+ $data[$var_name], $opt);
+ } else {
+ trigger_error("Invalid validation type {$opt['type']}",
+ E_USER_WARNING);
+ }
+ }
+ return $valid;
+ }
+}
+
diff --git a/extlib/XMPPHP/BOSH.php b/extlib/XMPPHP/BOSH.php
index b147443d7..befaf60a7 100644
--- a/extlib/XMPPHP/BOSH.php
+++ b/extlib/XMPPHP/BOSH.php
@@ -27,7 +27,7 @@
*/
/** XMPPHP_XMLStream */
-require_once "XMPP.php";
+require_once dirname(__FILE__) . "/XMPP.php";
/**
* XMPPHP Main Class
diff --git a/extlib/XMPPHP/XMLStream.php b/extlib/XMPPHP/XMLStream.php
index 0fcfea375..d33411ec5 100644
--- a/extlib/XMPPHP/XMLStream.php
+++ b/extlib/XMPPHP/XMLStream.php
@@ -27,13 +27,13 @@
*/
/** XMPPHP_Exception */
-require_once 'Exception.php';
+require_once dirname(__FILE__) . '/Exception.php';
/** XMPPHP_XMLObj */
-require_once 'XMLObj.php';
+require_once dirname(__FILE__) . '/XMLObj.php';
/** XMPPHP_Log */
-require_once 'Log.php';
+require_once dirname(__FILE__) . '/Log.php';
/**
* XMPPHP XML Stream
@@ -375,7 +375,7 @@ class XMPPHP_XMLStream {
* integer -> process for this amount of time
*/
- private function __process($maximum=0) {
+ private function __process($maximum=5) {
$remaining = $maximum;
diff --git a/extlib/XMPPHP/XMPP.php b/extlib/XMPPHP/XMPP.php
index 73fbd2658..429f45e56 100644
--- a/extlib/XMPPHP/XMPP.php
+++ b/extlib/XMPPHP/XMPP.php
@@ -27,8 +27,8 @@
*/
/** XMPPHP_XMLStream */
-require_once "XMLStream.php";
-require_once "Roster.php";
+require_once dirname(__FILE__) . "/XMLStream.php";
+require_once dirname(__FILE__) . "/Roster.php";
/**
* XMPPHP Main Class
@@ -208,6 +208,15 @@ class XMPPHP_XMPP extends XMPPHP_XMLStream {
$this->send($out);
}
+ /**
+ * Send Auth request
+ *
+ * @param string $jid
+ */
+ public function subscribe($jid) {
+ $this->send("<presence type='subscribe' to='{$jid}' from='{$this->fulljid}' />");
+ #$this->send("<presence type='subscribed' to='{$jid}' from='{$this->fulljid}' />");
+ }
/**
* Message handler
diff --git a/extlib/facebook/facebook.php b/extlib/facebook/facebook.php
index 35de6be50..fee1dd086 100644
--- a/extlib/facebook/facebook.php
+++ b/extlib/facebook/facebook.php
@@ -1,5 +1,5 @@
<?php
-// Copyright 2004-2008 Facebook. All Rights Reserved.
+// Copyright 2004-2009 Facebook. All Rights Reserved.
//
// +---------------------------------------------------------------------------+
// | Facebook Platform PHP5 client |
@@ -30,13 +30,12 @@
// +---------------------------------------------------------------------------+
// | For help with this library, contact developers-help@facebook.com |
// +---------------------------------------------------------------------------+
-//
+
include_once 'facebookapi_php5_restlib.php';
define('FACEBOOK_API_VALIDATION_ERROR', 1);
class Facebook {
public $api_client;
-
public $api_key;
public $secret;
public $generate_session_secret;
@@ -213,28 +212,55 @@ class Facebook {
}
}
- // Invalidate the session currently being used, and clear any state associated with it
+ // Invalidate the session currently being used, and clear any state associated
+ // with it. Note that the user will still remain logged into Facebook.
public function expire_session() {
if ($this->api_client->auth_expireSession()) {
- if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
- $cookies = array('user', 'session_key', 'expires', 'ss');
- foreach ($cookies as $name) {
- setcookie($this->api_key . '_' . $name, false, time() - 3600);
- unset($_COOKIE[$this->api_key . '_' . $name]);
- }
- setcookie($this->api_key, false, time() - 3600);
- unset($_COOKIE[$this->api_key]);
- }
-
- // now, clear the rest of the stored state
- $this->user = 0;
- $this->api_client->session_key = 0;
+ $this->clear_cookie_state();
return true;
} else {
return false;
}
}
+ /** Logs the user out of all temporary application sessions as well as their
+ * Facebook session. Note this will only work if the user has a valid current
+ * session with the application.
+ *
+ * @param string $next URL to redirect to upon logging out
+ *
+ */
+ public function logout($next) {
+ $logout_url = $this->get_logout_url($next);
+
+ // Clear any stored state
+ $this->clear_cookie_state();
+
+ $this->redirect($logout_url);
+ }
+
+ /**
+ * Clears any persistent state stored about the user, including
+ * cookies and information related to the current session in the
+ * client.
+ *
+ */
+ public function clear_cookie_state() {
+ if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
+ $cookies = array('user', 'session_key', 'expires', 'ss');
+ foreach ($cookies as $name) {
+ setcookie($this->api_key . '_' . $name, false, time() - 3600);
+ unset($_COOKIE[$this->api_key . '_' . $name]);
+ }
+ setcookie($this->api_key, false, time() - 3600);
+ unset($_COOKIE[$this->api_key]);
+ }
+
+ // now, clear the rest of the stored state
+ $this->user = 0;
+ $this->api_client->session_key = 0;
+ }
+
public function redirect($url) {
if ($this->in_fb_canvas()) {
echo '<fb:redirect url="' . $url . '"/>';
@@ -249,7 +275,8 @@ class Facebook {
}
public function in_frame() {
- return isset($this->fb_params['in_canvas']) || isset($this->fb_params['in_iframe']);
+ return isset($this->fb_params['in_canvas'])
+ || isset($this->fb_params['in_iframe']);
}
public function in_fb_canvas() {
return isset($this->fb_params['in_canvas']);
@@ -296,14 +323,42 @@ class Facebook {
}
public function get_add_url($next=null) {
- return self::get_facebook_url().'/add.php?api_key='.$this->api_key .
- ($next ? '&next=' . urlencode($next) : '');
+ $page = self::get_facebook_url().'/add.php';
+ $params = array('api_key' => $this->api_key);
+
+ if ($next) {
+ $params['next'] = $next;
+ }
+
+ return $page . '?' . http_build_query($params);
}
public function get_login_url($next, $canvas) {
- return self::get_facebook_url().'/login.php?v=1.0&api_key=' . $this->api_key .
- ($next ? '&next=' . urlencode($next) : '') .
- ($canvas ? '&canvas' : '');
+ $page = self::get_facebook_url().'/login.php';
+ $params = array('api_key' => $this->api_key,
+ 'v' => '1.0');
+
+ if ($next) {
+ $params['next'] = $next;
+ }
+ if ($canvas) {
+ $params['canvas'] = '1';
+ }
+
+ return $page . '?' . http_build_query($params);
+ }
+
+ public function get_logout_url($next) {
+ $page = self::get_facebook_url().'/logout.php';
+ $params = array('app_key' => $this->api_key,
+ 'session_key' => $this->api_client->session_key);
+
+ if ($next) {
+ $params['connect_next'] = 1;
+ $params['next'] = $next;
+ }
+
+ return $page . '?' . http_build_query($params);
}
public function set_user($user, $session_key, $expires=null, $session_secret=null) {
@@ -410,7 +465,20 @@ class Facebook {
return $fb_params;
}
- /*
+ /**
+ * Validates the account that a user was trying to set up an
+ * independent account through Facebook Connect.
+ *
+ * @param user The user attempting to set up an independent account.
+ * @param hash The hash passed to the reclamation URL used.
+ * @return bool True if the user is the one that selected the
+ * reclamation link.
+ */
+ public function verify_account_reclamation($user, $hash) {
+ return $hash == md5($user . $this->secret);
+ }
+
+ /**
* Validates that a given set of parameters match their signature.
* Parameters all match a given input prefix, such as "fb_sig".
*
@@ -422,6 +490,37 @@ class Facebook {
return self::generate_sig($fb_params, $this->secret) == $expected_sig;
}
+ /**
+ * Validate the given signed public session data structure with
+ * public key of the app that
+ * the session proof belongs to.
+ *
+ * @param $signed_data the session info that is passed by another app
+ * @param string $public_key Optional public key of the app. If this
+ * is not passed, function will make an API call to get it.
+ * return true if the session proof passed verification.
+ */
+ public function verify_signed_public_session_data($signed_data,
+ $public_key = null) {
+
+ // If public key is not already provided, we need to get it through API
+ if (!$public_key) {
+ $public_key = $this->api_client->auth_getAppPublicKey(
+ $signed_data['api_key']);
+ }
+
+ // Create data to verify
+ $data_to_serialize = $signed_data;
+ unset($data_to_serialize['sig']);
+ $serialized_data = implode('_', $data_to_serialize);
+
+ // Decode signature
+ $signature = base64_decode($signed_data['sig']);
+ $result = openssl_verify($serialized_data, $signature, $public_key,
+ OPENSSL_ALGO_SHA1);
+ return $result == 1;
+ }
+
/*
* Generate a signature using the application secret key.
*
diff --git a/extlib/facebook/facebook_desktop.php b/extlib/facebook/facebook_desktop.php
index 90cdf66bd..e79a2ca34 100644
--- a/extlib/facebook/facebook_desktop.php
+++ b/extlib/facebook/facebook_desktop.php
@@ -1,5 +1,5 @@
<?php
-// Copyright 2004-2008 Facebook. All Rights Reserved.
+// Copyright 2004-2009 Facebook. All Rights Reserved.
//
// +---------------------------------------------------------------------------+
// | Facebook Platform PHP5 client |
diff --git a/extlib/facebook/facebookapi_php5_restlib.php b/extlib/facebook/facebookapi_php5_restlib.php
index 389f40a9d..3fec06e8a 100644..100755
--- a/extlib/facebook/facebookapi_php5_restlib.php
+++ b/extlib/facebook/facebookapi_php5_restlib.php
@@ -1,9 +1,10 @@
<?php
+// Copyright 2004-2009 Facebook. All Rights Reserved.
//
// +---------------------------------------------------------------------------+
// | Facebook Platform PHP5 client |
// +---------------------------------------------------------------------------+
-// | Copyright (c) 2007-2008 Facebook, Inc. |
+// | Copyright (c) 2007-2009 Facebook, Inc. |
// | All rights reserved. |
// | |
// | Redistribution and use in source and binary forms, with or without |
@@ -32,6 +33,7 @@
//
include_once 'jsonwrapper/jsonwrapper.php';
+
class FacebookRestClient {
public $secret;
public $session_key;
@@ -50,7 +52,9 @@ class FacebookRestClient {
public $canvas_user;
public $batch_mode;
private $batch_queue;
+ private $pending_batch;
private $call_as_apikey;
+ private $use_curl_if_available;
const BATCH_MODE_DEFAULT = 0;
const BATCH_MODE_SERVER_PARALLEL = 0;
@@ -70,7 +74,8 @@ class FacebookRestClient {
$this->batch_mode = FacebookRestClient::BATCH_MODE_DEFAULT;
$this->last_call_id = 0;
$this->call_as_apikey = '';
- $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php';
+ $this->use_curl_if_available = true;
+ $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php';
if (!empty($GLOBALS['facebook_config']['debug'])) {
$this->cur_id = 0;
@@ -123,39 +128,61 @@ function toggleDisplay(id, type) {
}
/**
+ * Normally, if the cURL library/PHP extension is available, it is used for
+ * HTTP transactions. This allows that behavior to be overridden, falling
+ * back to a vanilla-PHP implementation even if cURL is installed.
+ *
+ * @param $use_curl_if_available bool whether or not to use cURL if available
+ */
+ public function set_use_curl_if_available($use_curl_if_available) {
+ $this->use_curl_if_available = $use_curl_if_available;
+ }
+
+ /**
* Start a batch operation.
*/
public function begin_batch() {
- if($this->batch_queue !== null) {
+ if ($this->pending_batch()) {
$code = FacebookAPIErrorCodes::API_EC_BATCH_ALREADY_STARTED;
- throw new FacebookRestClientException($code,
- FacebookAPIErrorCodes::$api_error_descriptions[$code]);
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
}
$this->batch_queue = array();
+ $this->pending_batch = true;
}
/*
* End current batch operation
*/
public function end_batch() {
- if($this->batch_queue === null) {
+ if (!$this->pending_batch()) {
$code = FacebookAPIErrorCodes::API_EC_BATCH_NOT_STARTED;
- throw new FacebookRestClientException($code,
- FacebookAPIErrorCodes::$api_error_descriptions[$code]);
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
}
- $this->execute_server_side_batch();
+ $this->pending_batch = false;
+ $this->execute_server_side_batch();
$this->batch_queue = null;
}
+ /**
+ * are we currently queueing up calls for a batch?
+ */
+ public function pending_batch() {
+ return $this->pending_batch;
+ }
+
private function execute_server_side_batch() {
$item_count = count($this->batch_queue);
$method_feed = array();
foreach($this->batch_queue as $batch_item) {
- $method_feed[] = $this->create_post_string($batch_item['m'],
- $batch_item['p']);
+ $method = $batch_item['m'];
+ $params = $batch_item['p'];
+ $this->finalize_params($method, $params);
+ $method_feed[] = $this->create_post_string($method, $params);
}
$method_feed_json = json_encode($method_feed);
@@ -202,6 +229,18 @@ function toggleDisplay(id, type) {
$this->call_as_apikey = '';
}
+
+ /*
+ * If a page is loaded via HTTPS, then all images and static
+ * resources need to be printed with HTTPS urls to avoid
+ * mixed content warnings. If your page loads with an HTTPS
+ * url, then call set_use_ssl_resources to retrieve the correct
+ * urls.
+ */
+ public function set_use_ssl_resources($is_ssl = true) {
+ $this->use_ssl_resources = $is_ssl;
+ }
+
/**
* Returns public information for an application (as shown in the application
* directory) by either application ID, API key, or canvas page name.
@@ -231,7 +270,7 @@ function toggleDisplay(id, type) {
* @return string An authentication token.
*/
public function auth_createToken() {
- return $this->call_method('facebook.auth.createToken', array());
+ return $this->call_method('facebook.auth.createToken');
}
/**
@@ -246,8 +285,7 @@ function toggleDisplay(id, type) {
* @return array An assoc array containing session_key, uid
*/
public function auth_getSession($auth_token, $generate_session_secret=false) {
- //Check if we are in batch mode
- if($this->batch_queue === null) {
+ if (!$this->pending_batch()) {
$result = $this->call_method('facebook.auth.getSession',
array('auth_token' => $auth_token,
'generate_session_secret' => $generate_session_secret));
@@ -271,7 +309,7 @@ function toggleDisplay(id, type) {
* API_EC_PARAM_UNKNOWN
*/
public function auth_promoteSession() {
- return $this->call_method('facebook.auth.promoteSession', array());
+ return $this->call_method('facebook.auth.promoteSession');
}
/**
@@ -282,7 +320,20 @@ function toggleDisplay(id, type) {
* @return bool true if session expiration was successful, false otherwise
*/
public function auth_expireSession() {
- return $this->call_method('facebook.auth.expireSession', array());
+ return $this->call_method('facebook.auth.expireSession');
+ }
+
+ /**
+ * Revokes the given extended permission that the user granted at some
+ * prior time (for instance, offline_access or email). If no user is
+ * provided, it will be revoked for the user of the current session.
+ *
+ * @param string $perm The permission to revoke
+ * @param int $uid The user for whom to revoke the permission.
+ */
+ public function auth_revokeExtendedPermission($perm, $uid=null) {
+ return $this->call_method('facebook.auth.revokeExtendedPermission',
+ array('perm' => $perm, 'uid' => $uid));
}
/**
@@ -303,6 +354,30 @@ function toggleDisplay(id, type) {
}
/**
+ * Get public key that is needed to verify digital signature
+ * an app may pass to other apps. The public key is only used by
+ * other apps for verification purposes.
+ * @param string API key of an app
+ * @return string The public key for the app.
+ */
+ public function auth_getAppPublicKey($target_app_key) {
+ return $this->call_method('facebook.auth.getAppPublicKey',
+ array('target_app_key' => $target_app_key));
+ }
+
+ /**
+ * Get a structure that can be passed to another app
+ * as proof of session. The other app can verify it using public
+ * key of this app.
+ *
+ * @return signed public session data structure.
+ */
+ public function auth_getSignedPublicSessionData() {
+ return $this->call_method('facebook.auth.getSignedPublicSessionData',
+ array());
+ }
+
+ /**
* Returns the number of unconnected friends that exist in this application.
* This number is determined based on the accounts registered through
* connect.registerUsers() (see below).
@@ -363,8 +438,9 @@ function toggleDisplay(id, type) {
*
* @param int $uid (Optional) User associated with events. A null
* parameter will default to the session user.
- * @param array $eids (Optional) Filter by these event ids. A null
- * parameter will get all events for the user.
+ * @param array/string $eids (Optional) Filter by these event
+ * ids. A null parameter will get all events for
+ * the user. (A csv list will work but is deprecated)
* @param int $start_time (Optional) Filter with this unix time as lower
* bound. A null or zero parameter indicates no
* lower bound.
@@ -718,12 +794,15 @@ function toggleDisplay(id, type) {
* @param string $body_general (Optional) Additional markup that extends
* the body of a short story.
* @param int $story_size (Optional) A story size (see above)
+ * @param string $user_message (Optional) A user message for a short
+ * story.
*
* @return bool true on success
*/
public function &feed_publishUserAction(
$template_bundle_id, $template_data, $target_ids='', $body_general='',
- $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE) {
+ $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE,
+ $user_message='') {
if (is_array($template_data)) {
$template_data = json_encode($template_data);
@@ -739,7 +818,107 @@ function toggleDisplay(id, type) {
'template_data' => $template_data,
'target_ids' => $target_ids,
'body_general' => $body_general,
- 'story_size' => $story_size));
+ 'story_size' => $story_size,
+ 'user_message' => $user_message));
+ }
+
+
+ /**
+ * Publish a post to the user's stream.
+ *
+ * @param $message the user's message
+ * @param $attachment the post's attachment (optional)
+ * @param $action links the post's action links (optional)
+ * @param $target_id the user on whose wall the post will be posted
+ * (optional)
+ * @param $uid the actor (defaults to session user)
+ * @return string the post id
+ */
+ public function stream_publish(
+ $message, $attachment = null, $action_links = null, $target_id = null,
+ $uid = null) {
+
+ return $this->call_method(
+ 'facebook.stream.publish',
+ array('message' => $message,
+ 'attachment' => $attachment,
+ 'action_links' => $action_links,
+ 'target_id' => $target_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Remove a post from the user's stream.
+ * Currently, you may only remove stories you application created.
+ *
+ * @param $post_id the post id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_remove($post_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.remove',
+ array('post_id' => $post_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Add a comment to a stream post
+ *
+ * @param $post_id the post id
+ * @param $comment the comment text
+ * @param $uid the actor (defaults to session user)
+ * @return string the id of the created comment
+ */
+ public function stream_addComment($post_id, $comment, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.addComment',
+ array('post_id' => $post_id,
+ 'comment' => $comment,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+
+ /**
+ * Remove a comment from a stream post
+ *
+ * @param $comment_id the comment id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_removeComment($comment_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.removeComment',
+ array('comment_id' => $comment_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Add a like to a stream post
+ *
+ * @param $post_id the post id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_addLike($post_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.addLike',
+ array('post_id' => $post_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Remove a like from a stream post
+ *
+ * @param $post_id the post id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_removeLike($post_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.removeLike',
+ array('post_id' => $post_id,
+ 'uid' => $this->get_uid($uid)));
}
/**
@@ -750,7 +929,7 @@ function toggleDisplay(id, type) {
* @return array An array of feed story objects.
*/
public function &feed_getAppFriendStories() {
- return $this->call_method('facebook.feed.getAppFriendStories', array());
+ return $this->call_method('facebook.feed.getAppFriendStories');
}
/**
@@ -771,33 +950,42 @@ function toggleDisplay(id, type) {
* Returns whether or not pairs of users are friends.
* Note that the Facebook friend relationship is symmetric.
*
- * @param array $uids1 array of ids (id_1, id_2,...) of some length X
- * @param array $uids2 array of ids (id_A, id_B,...) of SAME length X
+ * @param array/string $uids1 list of ids (id_1, id_2,...)
+ * of some length X (csv is deprecated)
+ * @param array/string $uids2 list of ids (id_A, id_B,...)
+ * of SAME length X (csv is deprecated)
*
* @return array An array with uid1, uid2, and bool if friends, e.g.:
* array(0 => array('uid1' => id_1, 'uid2' => id_A, 'are_friends' => 1),
* 1 => array('uid1' => id_2, 'uid2' => id_B, 'are_friends' => 0)
* ...)
+ * @error
+ * API_EC_PARAM_USER_ID_LIST
*/
public function &friends_areFriends($uids1, $uids2) {
return $this->call_method('facebook.friends.areFriends',
- array('uids1' => $uids1, 'uids2' => $uids2));
+ array('uids1' => $uids1,
+ 'uids2' => $uids2));
}
/**
* Returns the friends of the current session user.
*
* @param int $flid (Optional) Only return friends on this friend list.
+ * @param int $uid (Optional) Return friends for this user.
*
* @return array An array of friends
*/
- public function &friends_get($flid=null) {
+ public function &friends_get($flid=null, $uid = null) {
if (isset($this->friends_list)) {
return $this->friends_list;
}
$params = array();
- if (isset($this->canvas_user)) {
- $params['uid'] = $this->canvas_user;
+ if (!$uid && isset($this->canvas_user)) {
+ $uid = $this->canvas_user;
+ }
+ if ($uid) {
+ $params['uid'] = $uid;
}
if ($flid) {
$params['flid'] = $flid;
@@ -812,7 +1000,7 @@ function toggleDisplay(id, type) {
* @return array An array of friend list objects
*/
public function &friends_getLists() {
- return $this->call_method('facebook.friends.getLists', array());
+ return $this->call_method('facebook.friends.getLists');
}
/**
@@ -822,7 +1010,7 @@ function toggleDisplay(id, type) {
* @return array An array of friends also using the app
*/
public function &friends_getAppUsers() {
- return $this->call_method('facebook.friends.getAppUsers', array());
+ return $this->call_method('facebook.friends.getAppUsers');
}
/**
@@ -830,8 +1018,9 @@ function toggleDisplay(id, type) {
*
* @param int $uid (Optional) User associated with groups. A null
* parameter will default to the session user.
- * @param array $gids (Optional) Group ids to query. A null parameter will
- * get all groups for the user.
+ * @param array/string $gids (Optional) Array of group ids to query. A null
+ * parameter will get all groups for the user.
+ * (csv is deprecated)
*
* @return array An array of group objects
*/
@@ -890,6 +1079,40 @@ function toggleDisplay(id, type) {
}
/**
+ * Retrieves links posted by the given user.
+ *
+ * @param int $uid The user whose links you wish to retrieve
+ * @param int $limit The maximimum number of links to retrieve
+ * @param array $link_ids (Optional) Array of specific link
+ * IDs to retrieve by this user
+ *
+ * @return array An array of links.
+ */
+ public function &links_get($uid, $limit, $link_ids = null) {
+ return $this->call_method('links.get',
+ array('uid' => $uid,
+ 'limit' => $limit,
+ 'link_ids' => $link_ids));
+ }
+
+ /**
+ * Posts a link on Facebook.
+ *
+ * @param string $url URL/link you wish to post
+ * @param string $comment (Optional) A comment about this link
+ * @param int $uid (Optional) User ID that is posting this link;
+ * defaults to current session user
+ *
+ * @return bool
+ */
+ public function &links_post($url, $comment='', $uid = null) {
+ return $this->call_method('links.post',
+ array('uid' => $uid,
+ 'url' => $url,
+ 'comment' => $comment));
+ }
+
+ /**
* Permissions API
*/
@@ -946,6 +1169,78 @@ function toggleDisplay(id, type) {
}
/**
+ * Creates a note with the specified title and content.
+ *
+ * @param string $title Title of the note.
+ * @param string $content Content of the note.
+ * @param int $uid (Optional) The user for whom you are creating a
+ * note; defaults to current session user
+ *
+ * @return int The ID of the note that was just created.
+ */
+ public function &notes_create($title, $content, $uid = null) {
+ return $this->call_method('notes.create',
+ array('uid' => $uid,
+ 'title' => $title,
+ 'content' => $content));
+ }
+
+ /**
+ * Deletes the specified note.
+ *
+ * @param int $note_id ID of the note you wish to delete
+ * @param int $uid (Optional) Owner of the note you wish to delete;
+ * defaults to current session user
+ *
+ * @return bool
+ */
+ public function &notes_delete($note_id, $uid = null) {
+ return $this->call_method('notes.delete',
+ array('uid' => $uid,
+ 'note_id' => $note_id));
+ }
+
+ /**
+ * Edits a note, replacing its title and contents with the title
+ * and contents specified.
+ *
+ * @param int $note_id ID of the note you wish to edit
+ * @param string $title Replacement title for the note
+ * @param string $content Replacement content for the note
+ * @param int $uid (Optional) Owner of the note you wish to edit;
+ * defaults to current session user
+ *
+ * @return bool
+ */
+ public function &notes_edit($note_id, $title, $content, $uid = null) {
+ return $this->call_method('notes.edit',
+ array('uid' => $uid,
+ 'note_id' => $note_id,
+ 'title' => $title,
+ 'content' => $content));
+ }
+
+ /**
+ * Retrieves all notes by a user. If note_ids are specified,
+ * retrieves only those specific notes by that user.
+ *
+ * @param int $uid User whose notes you wish to retrieve
+ * @param array $note_ids (Optional) List of specific note
+ * IDs by this user to retrieve
+ *
+ * @return array A list of all of the given user's notes, or an empty list
+ * if the viewer lacks permissions or if there are no visible
+ * notes.
+ */
+ public function &notes_get($uid, $note_ids = null) {
+
+ return $this->call_method('notes.get',
+ array('uid' => $uid,
+ 'note_ids' => $note_ids));
+ }
+
+
+ /**
* Returns the outstanding notifications for the session user.
*
* @return array An assoc array of notification count objects for
@@ -954,13 +1249,15 @@ function toggleDisplay(id, type) {
* and an eid list of 'event_invites'
*/
public function &notifications_get() {
- return $this->call_method('facebook.notifications.get', array());
+ return $this->call_method('facebook.notifications.get');
}
/**
* Sends a notification to the specified users.
*
* @return A comma separated list of successful recipients
+ * @error
+ * API_EC_PARAM_USER_ID_LIST
*/
public function &notifications_send($to_ids, $notification, $type) {
return $this->call_method('facebook.notifications.send',
@@ -972,12 +1269,14 @@ function toggleDisplay(id, type) {
/**
* Sends an email to the specified user of the application.
*
- * @param array $recipients id of the recipients
+ * @param array/string $recipients array of ids of the recipients (csv is deprecated)
* @param string $subject subject of the email
* @param string $text (plain text) body of the email
* @param string $fbml fbml markup for an html version of the email
*
* @return string A comma separated list of successful recipients
+ * @error
+ * API_EC_PARAM_USER_ID_LIST
*/
public function &notifications_sendEmail($recipients,
$subject,
@@ -993,9 +1292,9 @@ function toggleDisplay(id, type) {
/**
* Returns the requested info fields for the requested set of pages.
*
- * @param array $page_ids an array of page ids
- * @param array $fields an array of strings describing the info fields
- * desired
+ * @param array/string $page_ids an array of page ids (csv is deprecated)
+ * @param array/string $fields an array of strings describing the
+ * info fields desired (csv is deprecated)
* @param int $uid (Optional) limit results to pages of which this
* user is a fan.
* @param string type limits results to a particular type of page.
@@ -1090,7 +1389,7 @@ function toggleDisplay(id, type) {
'tag_text' => $tag_text,
'x' => $x,
'y' => $y,
- 'tags' => json_encode($tags),
+ 'tags' => (is_array($tags)) ? json_encode($tags) : null,
'owner_uid' => $this->get_uid($owner_uid)));
}
@@ -1128,7 +1427,8 @@ function toggleDisplay(id, type) {
* @param int $subj_id (Optional) Filter by uid of user tagged in the photos.
* @param int $aid (Optional) Filter by an album, as returned by
* photos_getAlbums.
- * @param array $pids (Optional) Restrict to a list of pids
+ * @param array/string $pids (Optional) Restrict to an array of pids
+ * (csv is deprecated)
*
* Note that at least one of these parameters needs to be specified, or an
* error is returned.
@@ -1143,9 +1443,10 @@ function toggleDisplay(id, type) {
/**
* Returns the albums created by the given user.
*
- * @param int $uid (Optional) The uid of the user whose albums you want.
- * A null will return the albums of the session user.
- * @param array $aids (Optional) A list of aids to restrict the query.
+ * @param int $uid (Optional) The uid of the user whose albums you want.
+ * A null will return the albums of the session user.
+ * @param string $aids (Optional) An array of aids to restrict
+ * the query. (csv is deprecated)
*
* Note that at least one of the (uid, aids) parameters must be specified.
*
@@ -1172,16 +1473,66 @@ function toggleDisplay(id, type) {
}
/**
+ * Uploads a photo.
+ *
+ * @param string $file The location of the photo on the local filesystem.
+ * @param int $aid (Optional) The album into which to upload the
+ * photo.
+ * @param string $caption (Optional) A caption for the photo.
+ * @param int uid (Optional) The user ID of the user whose photo you
+ * are uploading
+ *
+ * @return array An array of user objects
+ */
+ public function photos_upload($file, $aid=null, $caption=null, $uid=null) {
+ return $this->call_upload_method('facebook.photos.upload',
+ array('aid' => $aid,
+ 'caption' => $caption,
+ 'uid' => $uid),
+ $file);
+ }
+
+
+ /**
+ * Uploads a video.
+ *
+ * @param string $file The location of the video on the local filesystem.
+ * @param string $title (Optional) A title for the video. Titles over 65 characters in length will be truncated.
+ * @param string $description (Optional) A description for the video.
+ *
+ * @return array An array with the video's ID, title, description, and a link to view it on Facebook.
+ */
+ public function video_upload($file, $title=null, $description=null) {
+ return $this->call_upload_method('facebook.video.upload',
+ array('title' => $title,
+ 'description' => $description),
+ $file,
+ Facebook::get_facebook_url('api-video') . '/restserver.php');
+ }
+
+ /**
+ * Returns an array with the video limitations imposed on the current session's
+ * associated user. Maximum length is measured in seconds; maximum size is
+ * measured in bytes.
+ *
+ * @return array Array with "length" and "size" keys
+ */
+ public function &video_getUploadLimits() {
+ return $this->call_method('facebook.video.getUploadLimits');
+ }
+
+ /**
* Returns the requested info fields for the requested set of users.
*
- * @param array $uids An array of user ids
- * @param array $fields An array of info field names desired
+ * @param array/string $uids An array of user ids (csv is deprecated)
+ * @param array/string $fields An array of info field names desired (csv is deprecated)
*
* @return array An array of user objects
*/
public function &users_getInfo($uids, $fields) {
return $this->call_method('facebook.users.getInfo',
- array('uids' => $uids, 'fields' => $fields));
+ array('uids' => $uids,
+ 'fields' => $fields));
}
/**
@@ -1194,14 +1545,15 @@ function toggleDisplay(id, type) {
* users, use users.getInfo instead, so that proper privacy rules will be
* applied.
*
- * @param array $uids An array of user ids
- * @param array $fields An array of info field names desired
+ * @param array/string $uids An array of user ids (csv is deprecated)
+ * @param array/string $fields An array of info field names desired (csv is deprecated)
*
* @return array An array of user objects
*/
public function &users_getStandardInfo($uids, $fields) {
return $this->call_method('facebook.users.getStandardInfo',
- array('uids' => $uids, 'fields' => $fields));
+ array('uids' => $uids,
+ 'fields' => $fields));
}
/**
@@ -1210,7 +1562,7 @@ function toggleDisplay(id, type) {
* @return integer User id
*/
public function &users_getLoggedInUser() {
- return $this->call_method('facebook.users.getLoggedInUser', array());
+ return $this->call_method('facebook.users.getLoggedInUser');
}
/**
@@ -1239,6 +1591,17 @@ function toggleDisplay(id, type) {
}
/**
+ * Returns whether or not the user corresponding to the current
+ * session object is verified by Facebook. See the documentation
+ * for Users.isVerified for details.
+ *
+ * @return boolean true if the user is verified
+ */
+ public function &users_isVerified() {
+ return $this->call_method('facebook.users.isVerified');
+ }
+
+ /**
* Sets the users' current status message. Message does NOT contain the
* word "is" , so make sure to include a verb.
*
@@ -1269,6 +1632,69 @@ function toggleDisplay(id, type) {
}
/**
+ * Gets the stream on behalf of a user using a set of users. This
+ * call will return the latest $limit queries between $start_time
+ * and $end_time.
+ *
+ * @param int $viewer_id user making the call (def: session)
+ * @param array $source_ids users/pages to look at (def: all connections)
+ * @param int $start_time start time to look for stories (def: 1 day ago)
+ * @param int $end_time end time to look for stories (def: now)
+ * @param int $limit number of stories to attempt to fetch (def: 30)
+ * @param string $filter_key key returned by stream.getFilters to fetch
+ *
+ * @return array(
+ * 'posts' => array of posts,
+ * 'profiles' => array of profile metadata of users/pages in posts
+ * 'albums' => array of album metadata in posts
+ * )
+ */
+ public function &stream_get($viewer_id = null,
+ $source_ids = null,
+ $start_time = 0,
+ $end_time = 0,
+ $limit = 30,
+ $filter_key = '') {
+ $args = array(
+ 'viewer_id' => $viewer_id,
+ 'source_ids' => $source_ids,
+ 'start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'limit' => $limit,
+ 'filter_key' => $filter_key);
+ return $this->call_method('facebook.stream.get', $args);
+ }
+
+ /**
+ * Gets the filters (with relevant filter keys for stream.get) for a
+ * particular user. These filters are typical things like news feed,
+ * friend lists, networks. They can be used to filter the stream
+ * without complex queries to determine which ids belong in which groups.
+ *
+ * @param int $uid user to get filters for
+ *
+ * @return array of stream filter objects
+ */
+ public function &stream_getFilters($uid = null) {
+ $args = array('uid' => $uid);
+ return $this->call_method('facebook.stream.getFilters', $args);
+ }
+
+ /**
+ * Gets the full comments given a post_id from stream.get or the
+ * stream FQL table. Initially, only a set of preview comments are
+ * returned because some posts can have many comments.
+ *
+ * @param string $post_id id of the post to get comments for
+ *
+ * @return array of comment objects
+ */
+ public function &stream_getComments($post_id) {
+ $args = array('post_id' => $post_id);
+ return $this->call_method('facebook.stream.getComments', $args);
+ }
+
+ /**
* Sets the FBML for the profile of the user attached to this session.
*
* @param string $markup The FBML that describes the profile
@@ -1690,7 +2116,7 @@ function toggleDisplay(id, type) {
* API_EC_DATA_UNKNOWN_ERROR
*/
public function &data_getObjectTypes() {
- return $this->call_method('facebook.data.getObjectTypes', array());
+ return $this->call_method('facebook.data.getObjectTypes');
}
/**
@@ -2315,12 +2741,14 @@ function toggleDisplay(id, type) {
*
* @param string $integration_point_name Name of an integration point
* (see developer wiki for list).
+ * @param int $uid Specific user to check the limit.
*
* @return int Integration point allocation value
*/
- public function &admin_getAllocation($integration_point_name) {
+ public function &admin_getAllocation($integration_point_name, $uid=null) {
return $this->call_method('facebook.admin.getAllocation',
- array('integration_point_name' => $integration_point_name));
+ array('integration_point_name' => $integration_point_name,
+ 'uid' => $uid));
}
/**
@@ -2376,28 +2804,75 @@ function toggleDisplay(id, type) {
*/
public function admin_getRestrictionInfo() {
return json_decode(
- $this->call_method('admin.getRestrictionInfo', array()),
+ $this->call_method('admin.getRestrictionInfo'),
true);
}
+
+ /**
+ * Bans a list of users from the app. Banned users can't
+ * access the app's canvas page and forums.
+ *
+ * @param array $uids an array of user ids
+ * @return bool true on success
+ */
+ public function admin_banUsers($uids) {
+ return $this->call_method(
+ 'admin.banUsers', array('uids' => json_encode($uids)));
+ }
+
+ /**
+ * Unban users that have been previously banned with
+ * admin_banUsers().
+ *
+ * @param array $uids an array of user ids
+ * @return bool true on success
+ */
+ public function admin_unbanUsers($uids) {
+ return $this->call_method(
+ 'admin.unbanUsers', array('uids' => json_encode($uids)));
+ }
+
+ /**
+ * Gets the list of users that have been banned from the application.
+ * $uids is an optional parameter that filters the result with the list
+ * of provided user ids. If $uids is provided,
+ * only banned user ids that are contained in $uids are returned.
+ *
+ * @param array $uids an array of user ids to filter by
+ * @return bool true on success
+ */
+
+ public function admin_getBannedUsers($uids = null) {
+ return $this->call_method(
+ 'admin.getBannedUsers',
+ array('uids' => $uids ? json_encode($uids) : null));
+ }
+
/* UTILITY FUNCTIONS */
/**
- * Calls the specified method with the specified parameters.
+ * Calls the specified normal POST method with the specified parameters.
*
* @param string $method Name of the Facebook method to invoke
* @param array $params A map of param names => param values
*
- * @return mixed Result of method call
+ * @return mixed Result of method call; this returns a reference to support
+ * 'delayed returns' when in a batch context.
+ * See: http://wiki.developers.facebook.com/index.php/Using_batching_API
*/
- public function & call_method($method, $params) {
- //Check if we are in batch mode
- if($this->batch_queue === null) {
+ public function &call_method($method, $params = array()) {
+ if (!$this->pending_batch()) {
if ($this->call_as_apikey) {
$params['call_as_apikey'] = $this->call_as_apikey;
}
- $xml = $this->post_request($method, $params);
- $result = $this->convert_xml_to_result($xml, $method, $params);
+ $data = $this->post_request($method, $params);
+ if (empty($params['format']) || strtolower($params['format']) != 'json') {
+ $result = $this->convert_xml_to_result($data, $method, $params);
+ }
+ else {
+ $result = json_decode($data, true);
+ }
if (is_array($result) && isset($result['error_code'])) {
throw new FacebookRestClientException($result['error_msg'],
@@ -2413,11 +2888,46 @@ function toggleDisplay(id, type) {
return $result;
}
- private function convert_xml_to_result($xml, $method, $params) {
+ /**
+ * Calls the specified file-upload POST method with the specified parameters
+ *
+ * @param string $method Name of the Facebook method to invoke
+ * @param array $params A map of param names => param values
+ * @param string $file A path to the file to upload (required)
+ *
+ * @return array A dictionary representing the response.
+ */
+ public function call_upload_method($method, $params, $file, $server_addr = null) {
+ if (!$this->pending_batch()) {
+ if (!file_exists($file)) {
+ $code =
+ FacebookAPIErrorCodes::API_EC_PARAM;
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
+ }
+
+ $xml = $this->post_upload_request($method, $params, $file, $server_addr);
+ $result = $this->convert_xml_to_result($xml, $method, $params);
+
+ if (is_array($result) && isset($result['error_code'])) {
+ throw new FacebookRestClientException($result['error_msg'],
+ $result['error_code']);
+ }
+ }
+ else {
+ $code =
+ FacebookAPIErrorCodes::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE;
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
+ }
+
+ return $result;
+ }
+
+ protected function convert_xml_to_result($xml, $method, $params) {
$sxml = simplexml_load_string($xml);
$result = self::convert_simplexml_to_array($sxml);
-
if (!empty($GLOBALS['facebook_config']['debug'])) {
// output the raw xml and its corresponding php object, for debugging:
print '<div style="margin: 10px 30px; padding: 5px; border: 2px solid black; background: gray; color: white; font-size: 12px; font-weight: bold;">';
@@ -2436,7 +2946,25 @@ function toggleDisplay(id, type) {
return $result;
}
- private function create_post_string($method, $params) {
+ private function finalize_params($method, &$params) {
+ $this->add_standard_params($method, $params);
+ // we need to do this before signing the params
+ $this->convert_array_values_to_json($params);
+ $params['sig'] = Facebook::generate_sig($params, $this->secret);
+ }
+
+ private function convert_array_values_to_json(&$params) {
+ foreach ($params as $key => &$val) {
+ if (is_array($val)) {
+ $val = json_encode($val);
+ }
+ }
+ }
+
+ private function add_standard_params($method, &$params) {
+ if ($this->call_as_apikey) {
+ $params['call_as_apikey'] = $this->call_as_apikey;
+ }
$params['method'] = $method;
$params['session_key'] = $this->session_key;
$params['api_key'] = $this->api_key;
@@ -2448,50 +2976,118 @@ function toggleDisplay(id, type) {
if (!isset($params['v'])) {
$params['v'] = '1.0';
}
+ if (isset($this->use_ssl_resources) &&
+ $this->use_ssl_resources) {
+ $params['return_ssl_resources'] = true;
+ }
+ }
+
+ private function create_post_string($method, $params) {
$post_params = array();
foreach ($params as $key => &$val) {
- if (is_array($val)) $val = implode(',', $val);
$post_params[] = $key.'='.urlencode($val);
}
- $secret = $this->secret;
- $post_params[] = 'sig='.Facebook::generate_sig($params, $secret);
return implode('&', $post_params);
}
- public function post_request($method, $params) {
+ private function run_multipart_http_transaction($method, $params, $file, $server_addr) {
- $post_string = $this->create_post_string($method, $params);
+ // the format of this message is specified in RFC1867/RFC1341.
+ // we add twenty pseudo-random digits to the end of the boundary string.
+ $boundary = '--------------------------FbMuLtIpArT' .
+ sprintf("%010d", mt_rand()) .
+ sprintf("%010d", mt_rand());
+ $content_type = 'multipart/form-data; boundary=' . $boundary;
+ // within the message, we prepend two extra hyphens.
+ $delimiter = '--' . $boundary;
+ $close_delimiter = $delimiter . '--';
+ $content_lines = array();
+ foreach ($params as $key => &$val) {
+ $content_lines[] = $delimiter;
+ $content_lines[] = 'Content-Disposition: form-data; name="' . $key . '"';
+ $content_lines[] = '';
+ $content_lines[] = $val;
+ }
+ // now add the file data
+ $content_lines[] = $delimiter;
+ $content_lines[] =
+ 'Content-Disposition: form-data; filename="' . $file . '"';
+ $content_lines[] = 'Content-Type: application/octet-stream';
+ $content_lines[] = '';
+ $content_lines[] = file_get_contents($file);
+ $content_lines[] = $close_delimiter;
+ $content_lines[] = '';
+ $content = implode("\r\n", $content_lines);
+ return $this->run_http_post_transaction($content_type, $content, $server_addr);
+ }
- if (function_exists('curl_init')) {
- // Use CURL if installed...
+ public function post_request($method, $params) {
+ $this->finalize_params($method, $params);
+ $post_string = $this->create_post_string($method, $params);
+ if ($this->use_curl_if_available && function_exists('curl_init')) {
$useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->server_addr);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$result = curl_exec($ch);
curl_close($ch);
} else {
- // Non-CURL based version...
$content_type = 'application/x-www-form-urlencoded';
- $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) '.phpversion();
- $context =
- array('http' =>
+ $content = $post_string;
+ $result = $this->run_http_post_transaction($content_type,
+ $content,
+ $this->server_addr);
+ }
+ return $result;
+ }
+
+ private function post_upload_request($method, $params, $file, $server_addr = null) {
+ $server_addr = $server_addr ? $server_addr : $this->server_addr;
+ $this->finalize_params($method, $params);
+ if ($this->use_curl_if_available && function_exists('curl_init')) {
+ // prepending '@' causes cURL to upload the file; the key is ignored.
+ $params['_file'] = '@' . $file;
+ $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $server_addr);
+ // this has to come before the POSTFIELDS set!
+ curl_setopt($ch, CURLOPT_POST, 1 );
+ // passing an array gets curl to use the multipart/form-data content type
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+ $result = curl_exec($ch);
+ curl_close($ch);
+ } else {
+ $result = $this->run_multipart_http_transaction($method, $params, $file, $server_addr);
+ }
+ return $result;
+ }
+
+ private function run_http_post_transaction($content_type, $content, $server_addr) {
+
+ $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) ' . phpversion();
+ $content_length = strlen($content);
+ $context =
+ array('http' =>
array('method' => 'POST',
- 'header' => 'Content-type: '.$content_type."\r\n".
- 'User-Agent: '.$user_agent."\r\n".
- 'Content-length: ' . strlen($post_string),
- 'content' => $post_string));
- $contextid=stream_context_create($context);
- $sock=fopen($this->server_addr, 'r', false, $contextid);
- if ($sock) {
- $result='';
- while (!feof($sock))
- $result.=fgets($sock, 4096);
-
- fclose($sock);
+ 'user_agent' => $user_agent,
+ 'header' => 'Content-Type: ' . $content_type . "\r\n" .
+ 'Content-Length: ' . $content_length,
+ 'content' => $content));
+ $context_id = stream_context_create($context);
+ $sock = fopen($server_addr, 'r', false, $context_id);
+
+ $result = '';
+ if ($sock) {
+ while (!feof($sock)) {
+ $result .= fgets($sock, 4096);
}
+ fclose($sock);
}
return $result;
}
@@ -2541,6 +3137,14 @@ class FacebookAPIErrorCodes {
const API_EC_METHOD = 3;
const API_EC_TOO_MANY_CALLS = 4;
const API_EC_BAD_IP = 5;
+ const API_EC_HOST_API = 6;
+ const API_EC_HOST_UP = 7;
+ const API_EC_SECURE = 8;
+ const API_EC_RATE = 9;
+ const API_EC_PERMISSION_DENIED = 10;
+ const API_EC_DEPRECATED = 11;
+ const API_EC_VERSION = 12;
+ const API_EC_INTERNAL_FQL_ERROR = 13;
/*
* PARAMETER ERRORS
@@ -2550,27 +3154,121 @@ class FacebookAPIErrorCodes {
const API_EC_PARAM_SESSION_KEY = 102;
const API_EC_PARAM_CALL_ID = 103;
const API_EC_PARAM_SIGNATURE = 104;
+ const API_EC_PARAM_TOO_MANY = 105;
const API_EC_PARAM_USER_ID = 110;
const API_EC_PARAM_USER_FIELD = 111;
const API_EC_PARAM_SOCIAL_FIELD = 112;
+ const API_EC_PARAM_EMAIL = 113;
+ const API_EC_PARAM_USER_ID_LIST = 114;
+ const API_EC_PARAM_FIELD_LIST = 115;
const API_EC_PARAM_ALBUM_ID = 120;
+ const API_EC_PARAM_PHOTO_ID = 121;
+ const API_EC_PARAM_FEED_PRIORITY = 130;
+ const API_EC_PARAM_CATEGORY = 140;
+ const API_EC_PARAM_SUBCATEGORY = 141;
+ const API_EC_PARAM_TITLE = 142;
+ const API_EC_PARAM_DESCRIPTION = 143;
+ const API_EC_PARAM_BAD_JSON = 144;
const API_EC_PARAM_BAD_EID = 150;
const API_EC_PARAM_UNKNOWN_CITY = 151;
+ const API_EC_PARAM_BAD_PAGE_TYPE = 152;
/*
* USER PERMISSIONS ERRORS
*/
const API_EC_PERMISSION = 200;
const API_EC_PERMISSION_USER = 210;
+ const API_EC_PERMISSION_NO_DEVELOPERS = 211;
const API_EC_PERMISSION_ALBUM = 220;
const API_EC_PERMISSION_PHOTO = 221;
+ const API_EC_PERMISSION_MESSAGE = 230;
+ const API_EC_PERMISSION_OTHER_USER = 240;
+ const API_EC_PERMISSION_STATUS_UPDATE = 250;
+ const API_EC_PERMISSION_PHOTO_UPLOAD = 260;
+ const API_EC_PERMISSION_VIDEO_UPLOAD = 261;
+ const API_EC_PERMISSION_SMS = 270;
+ const API_EC_PERMISSION_CREATE_LISTING = 280;
+ const API_EC_PERMISSION_CREATE_NOTE = 281;
+ const API_EC_PERMISSION_SHARE_ITEM = 282;
const API_EC_PERMISSION_EVENT = 290;
+ const API_EC_PERMISSION_LARGE_FBML_TEMPLATE = 291;
+ const API_EC_PERMISSION_LIVEMESSAGE = 292;
const API_EC_PERMISSION_RSVP_EVENT = 299;
- const FQL_EC_PARSER = 601;
+ /*
+ * DATA EDIT ERRORS
+ */
+ const API_EC_EDIT = 300;
+ const API_EC_EDIT_USER_DATA = 310;
+ const API_EC_EDIT_PHOTO = 320;
+ const API_EC_EDIT_ALBUM_SIZE = 321;
+ const API_EC_EDIT_PHOTO_TAG_SUBJECT = 322;
+ const API_EC_EDIT_PHOTO_TAG_PHOTO = 323;
+ const API_EC_EDIT_PHOTO_FILE = 324;
+ const API_EC_EDIT_PHOTO_PENDING_LIMIT = 325;
+ const API_EC_EDIT_PHOTO_TAG_LIMIT = 326;
+ const API_EC_EDIT_ALBUM_REORDER_PHOTO_NOT_IN_ALBUM = 327;
+ const API_EC_EDIT_ALBUM_REORDER_TOO_FEW_PHOTOS = 328;
+
+ const API_EC_MALFORMED_MARKUP = 329;
+ const API_EC_EDIT_MARKUP = 330;
+
+ const API_EC_EDIT_FEED_TOO_MANY_USER_CALLS = 340;
+ const API_EC_EDIT_FEED_TOO_MANY_USER_ACTION_CALLS = 341;
+ const API_EC_EDIT_FEED_TITLE_LINK = 342;
+ const API_EC_EDIT_FEED_TITLE_LENGTH = 343;
+ const API_EC_EDIT_FEED_TITLE_NAME = 344;
+ const API_EC_EDIT_FEED_TITLE_BLANK = 345;
+ const API_EC_EDIT_FEED_BODY_LENGTH = 346;
+ const API_EC_EDIT_FEED_PHOTO_SRC = 347;
+ const API_EC_EDIT_FEED_PHOTO_LINK = 348;
+
+ const API_EC_EDIT_VIDEO_SIZE = 350;
+ const API_EC_EDIT_VIDEO_INVALID_FILE = 351;
+ const API_EC_EDIT_VIDEO_INVALID_TYPE = 352;
+ const API_EC_EDIT_VIDEO_FILE = 353;
+
+ const API_EC_EDIT_FEED_TITLE_ARRAY = 360;
+ const API_EC_EDIT_FEED_TITLE_PARAMS = 361;
+ const API_EC_EDIT_FEED_BODY_ARRAY = 362;
+ const API_EC_EDIT_FEED_BODY_PARAMS = 363;
+ const API_EC_EDIT_FEED_PHOTO = 364;
+ const API_EC_EDIT_FEED_TEMPLATE = 365;
+ const API_EC_EDIT_FEED_TARGET = 366;
+ const API_EC_EDIT_FEED_MARKUP = 367;
+
+ /**
+ * SESSION ERRORS
+ */
+ const API_EC_SESSION_TIMED_OUT = 450;
+ const API_EC_SESSION_METHOD = 451;
+ const API_EC_SESSION_INVALID = 452;
+ const API_EC_SESSION_REQUIRED = 453;
+ const API_EC_SESSION_REQUIRED_FOR_SECRET = 454;
+ const API_EC_SESSION_CANNOT_USE_SESSION_SECRET = 455;
+
+
+ /**
+ * FQL ERRORS
+ */
+ const FQL_EC_UNKNOWN_ERROR = 600;
+ const FQL_EC_PARSER = 601; // backwards compatibility
+ const FQL_EC_PARSER_ERROR = 601;
const FQL_EC_UNKNOWN_FIELD = 602;
const FQL_EC_UNKNOWN_TABLE = 603;
- const FQL_EC_NOT_INDEXABLE = 604;
+ const FQL_EC_NOT_INDEXABLE = 604; // backwards compatibility
+ const FQL_EC_NO_INDEX = 604;
+ const FQL_EC_UNKNOWN_FUNCTION = 605;
+ const FQL_EC_INVALID_PARAM = 606;
+ const FQL_EC_INVALID_FIELD = 607;
+ const FQL_EC_INVALID_SESSION = 608;
+ const FQL_EC_UNSUPPORTED_APP_TYPE = 609;
+ const FQL_EC_SESSION_SECRET_NOT_ALLOWED = 610;
+ const FQL_EC_DEPRECATED_TABLE = 611;
+ const FQL_EC_EXTENDED_PERMISSION = 612;
+ const FQL_EC_RATE_LIMIT_EXCEEDED = 613;
+
+ const API_EC_REF_SET_FAILED = 700;
/**
* DATA STORE API ERRORS
@@ -2581,52 +3279,122 @@ class FacebookAPIErrorCodes {
const API_EC_DATA_OBJECT_NOT_FOUND = 803;
const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804;
const API_EC_DATA_DATABASE_ERROR = 805;
+ const API_EC_DATA_CREATE_TEMPLATE_ERROR = 806;
+ const API_EC_DATA_TEMPLATE_EXISTS_ERROR = 807;
+ const API_EC_DATA_TEMPLATE_HANDLE_TOO_LONG = 808;
+ const API_EC_DATA_TEMPLATE_HANDLE_ALREADY_IN_USE = 809;
+ const API_EC_DATA_TOO_MANY_TEMPLATE_BUNDLES = 810;
+ const API_EC_DATA_MALFORMED_ACTION_LINK = 811;
+ const API_EC_DATA_TEMPLATE_USES_RESERVED_TOKEN = 812;
/*
- * Batch ERROR
+ * APPLICATION INFO ERRORS
*/
- const API_EC_BATCH_ALREADY_STARTED = 900;
- const API_EC_BATCH_NOT_STARTED = 901;
- const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 902;
+ const API_EC_NO_SUCH_APP = 900;
+ /*
+ * BATCH ERRORS
+ */
+ const API_EC_BATCH_TOO_MANY_ITEMS = 950;
+ const API_EC_BATCH_ALREADY_STARTED = 951;
+ const API_EC_BATCH_NOT_STARTED = 952;
+ const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 953;
+
+ /*
+ * EVENT API ERRORS
+ */
+ const API_EC_EVENT_INVALID_TIME = 1000;
+
+ /*
+ * INFO BOX ERRORS
+ */
+ const API_EC_INFO_NO_INFORMATION = 1050;
+ const API_EC_INFO_SET_FAILED = 1051;
+
+ /*
+ * LIVEMESSAGE API ERRORS
+ */
+ const API_EC_LIVEMESSAGE_SEND_FAILED = 1100;
+ const API_EC_LIVEMESSAGE_EVENT_NAME_TOO_LONG = 1101;
+ const API_EC_LIVEMESSAGE_MESSAGE_TOO_LONG = 1102;
+
+ /*
+ * CONNECT SESSION ERRORS
+ */
+ const API_EC_CONNECT_FEED_DISABLED = 1300;
+
+ /*
+ * Platform tag bundles errors
+ */
+ const API_EC_TAG_BUNDLE_QUOTA = 1400;
+
+ /*
+ * SHARE
+ */
+ const API_EC_SHARE_BAD_URL = 1500;
+
+ /*
+ * NOTES
+ */
+ const API_EC_NOTE_CANNOT_MODIFY = 1600;
+
+ /*
+ * COMMENTS
+ */
+ const API_EC_COMMENTS_UNKNOWN = 1700;
+ const API_EC_COMMENTS_POST_TOO_LONG = 1701;
+ const API_EC_COMMENTS_DB_DOWN = 1702;
+ const API_EC_COMMENTS_INVALID_XID = 1703;
+ const API_EC_COMMENTS_INVALID_UID = 1704;
+ const API_EC_COMMENTS_INVALID_POST = 1705;
+
+ /**
+ * This array is no longer maintained; to view the description of an error
+ * code, please look at the message element of the API response or visit
+ * the developer wiki at http://wiki.developers.facebook.com/.
+ */
public static $api_error_descriptions = array(
- API_EC_SUCCESS => 'Success',
- API_EC_UNKNOWN => 'An unknown error occurred',
- API_EC_SERVICE => 'Service temporarily unavailable',
- API_EC_METHOD => 'Unknown method',
- API_EC_TOO_MANY_CALLS => 'Application request limit reached',
- API_EC_BAD_IP => 'Unauthorized source IP address',
- API_EC_PARAM => 'Invalid parameter',
- API_EC_PARAM_API_KEY => 'Invalid API key',
- API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
- API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous',
- API_EC_PARAM_SIGNATURE => 'Incorrect signature',
- API_EC_PARAM_USER_ID => 'Invalid user id',
- API_EC_PARAM_USER_FIELD => 'Invalid user info field',
- API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
- API_EC_PARAM_ALBUM_ID => 'Invalid album id',
- API_EC_PARAM_BAD_EID => 'Invalid eid',
- API_EC_PARAM_UNKNOWN_CITY => 'Unknown city',
- API_EC_PERMISSION => 'Permissions error',
- API_EC_PERMISSION_USER => 'User not visible',
- API_EC_PERMISSION_ALBUM => 'Album not visible',
- API_EC_PERMISSION_PHOTO => 'Photo not visible',
- API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event',
- API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event',
- FQL_EC_PARSER => 'FQL: Parser Error',
- FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field',
- FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table',
- FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable',
- FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function',
- FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in',
- API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
- API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
- API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
- API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
- API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
- API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
- API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
- API_EC_BATCH_NOT_STARTED => 'end_batch called before start_batch',
- API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode',
+ self::API_EC_SUCCESS => 'Success',
+ self::API_EC_UNKNOWN => 'An unknown error occurred',
+ self::API_EC_SERVICE => 'Service temporarily unavailable',
+ self::API_EC_METHOD => 'Unknown method',
+ self::API_EC_TOO_MANY_CALLS => 'Application request limit reached',
+ self::API_EC_BAD_IP => 'Unauthorized source IP address',
+ self::API_EC_PARAM => 'Invalid parameter',
+ self::API_EC_PARAM_API_KEY => 'Invalid API key',
+ self::API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
+ self::API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous',
+ self::API_EC_PARAM_SIGNATURE => 'Incorrect signature',
+ self::API_EC_PARAM_USER_ID => 'Invalid user id',
+ self::API_EC_PARAM_USER_FIELD => 'Invalid user info field',
+ self::API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
+ self::API_EC_PARAM_USER_ID_LIST => 'Invalid user id list',
+ self::API_EC_PARAM_FIELD_LIST => 'Invalid field list',
+ self::API_EC_PARAM_ALBUM_ID => 'Invalid album id',
+ self::API_EC_PARAM_BAD_EID => 'Invalid eid',
+ self::API_EC_PARAM_UNKNOWN_CITY => 'Unknown city',
+ self::API_EC_PERMISSION => 'Permissions error',
+ self::API_EC_PERMISSION_USER => 'User not visible',
+ self::API_EC_PERMISSION_NO_DEVELOPERS => 'Application has no developers',
+ self::API_EC_PERMISSION_ALBUM => 'Album not visible',
+ self::API_EC_PERMISSION_PHOTO => 'Photo not visible',
+ self::API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event',
+ self::API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event',
+ self::API_EC_EDIT_ALBUM_SIZE => 'Album is full',
+ self::FQL_EC_PARSER => 'FQL: Parser Error',
+ self::FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field',
+ self::FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table',
+ self::FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable',
+ self::FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function',
+ self::FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in',
+ self::API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
+ self::API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
+ self::API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
+ self::API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
+ self::API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
+ self::API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
+ self::API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
+ self::API_EC_BATCH_NOT_STARTED => 'end_batch called before begin_batch',
+ self::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode'
);
}
diff --git a/index.php b/index.php
index 0c69e226f..5f9a048f2 100644
--- a/index.php
+++ b/index.php
@@ -1,7 +1,7 @@
<?php
/**
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -48,22 +48,72 @@ function handleError($error)
$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'));
+ if(common_config('site', 'logdebug')) {
+ $bt = $error->getBacktrace();
+ foreach ($bt as $line) {
+ common_log(LOG_ERR, $line);
+ }
+ }
+ if ($error instanceof DB_DataObject_Error ||
+ $error instanceof DB_Error) {
+ $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'));
+ } else {
+ $msg = _('An important error occured, probably related to email setup. '.
+ 'Check logfiles for more info..');
+ }
$dac = new DBErrorAction($msg, 500);
$dac->showPage();
exit(-1);
}
+function checkMirror($action_obj)
+{
+ global $config;
+
+ static $alwaysRW = array('session', 'remember_me');
+
+ if (common_config('db', 'mirror') && $action_obj->isReadOnly($args)) {
+ if (is_array(common_config('db', 'mirror'))) {
+ // "load balancing", ha ha
+ $arr = common_config('db', 'mirror');
+ $k = array_rand($arr);
+ $mirror = $arr[$k];
+ } else {
+ $mirror = common_config('db', 'mirror');
+ }
+
+ // We ensure that these tables always are used
+ // on the master DB
+
+ $config['db']['database_rw'] = $config['db']['database'];
+ $config['db']['ini_rw'] = INSTALLDIR.'/classes/laconica.ini';
+
+ foreach ($alwaysRW as $table) {
+ $config['db']['table_'.$table] = 'rw';
+ }
+
+ // everyone else uses the mirror
+
+ $config['db']['database'] = $mirror;
+ }
+}
+
function main()
{
- global $user, $action, $config;
+ // quick check for fancy URL auto-detection support in installer.
+ if (isset($_SERVER['REDIRECT_URL']) && ((dirname($_SERVER['REQUEST_URI']) . '/check-fancy') === $_SERVER['REDIRECT_URL'])) {
+ die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs.");
+ }
+ global $user, $action;
+
+ Snapshot::check();
if (!_have_config()) {
$msg = sprintf(_("No configuration file found. Try running ".
@@ -128,19 +178,7 @@ function main()
} else {
$action_obj = new $action_class();
- // XXX: find somewhere for this little block to live
-
- if (common_config('db', 'mirror') && $action_obj->isReadOnly($args)) {
- if (is_array(common_config('db', 'mirror'))) {
- // "load balancing", ha ha
- $arr = common_config('db', 'mirror');
- $k = array_rand($arr);
- $mirror = $arr[$k];
- } else {
- $mirror = common_config('db', 'mirror');
- }
- $config['db']['database'] = $mirror;
- }
+ checkMirror($action_obj);
try {
if ($action_obj->prepare($args)) {
diff --git a/install.php b/install.php
index 87a99a650..570b08edf 100644
--- a/install.php
+++ b/install.php
@@ -1,7 +1,7 @@
<?php
/**
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2009, Controlez-Vous, Inc.
+ * Copyright (C) 2009, Control Yourself, 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
@@ -35,15 +35,17 @@ function main()
function checkPrereqs()
{
+ $pass = true;
+
if (file_exists(INSTALLDIR.'/config.php')) {
?><p class="error">Config file &quot;config.php&quot; already exists.</p>
<?php
- return false;
+ $pass = false;
}
if (version_compare(PHP_VERSION, '5.0.0', '<')) {
?><p class="error">Require PHP version 5 or greater.</p><?php
- return false;
+ $pass = false;
}
$reqs = array('gd', 'mysql', 'curl',
@@ -52,28 +54,32 @@ function checkPrereqs()
foreach ($reqs as $req) {
if (!checkExtension($req)) {
- ?><p class="error">Cannot load required extension &quot;<?php echo $req; ?>&quot;.</p><?php
- return false;
+ ?><p class="error">Cannot load required extension: <code><?php echo $req; ?></code></p><?php
+ $pass = false;
}
}
if (!is_writable(INSTALLDIR)) {
- ?><p class="error">Cannot write config file to &quot;<?php echo INSTALLDIR; ?>&quot;.</p>
- <p>On your server, try this command:</p>
- <blockquote>chmod a+w <?php echo INSTALLDIR; ?></blockquote>
+ ?><p class="error">Cannot write config file to: <code><?php echo INSTALLDIR; ?></code></p>
+ <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?></code>
<?php
- return false;
+ $pass = false;
}
if (!is_writable(INSTALLDIR.'/avatar/')) {
- ?><p class="error">Cannot write avatar directory &quot;<?php echo INSTALLDIR; ?>/avatar/&quot;.</p>
- <p>On your server, try this command:</p>
- <blockquote>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</blockquote>
+ ?><p class="error">Cannot write avatar directory: <code><?php echo INSTALLDIR; ?>/avatar/</code></p>
+ <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</code></p>
+ <?
+ $pass = false;
+ }
+ if (!is_writable(INSTALLDIR.'/background/')) {
+ ?><p class="error">Cannot write background directory: <code><?php echo INSTALLDIR; ?>/background/</code></p>
+ <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?>/background/</code></p>
<?
- return false;
+ $pass = false;
}
- return true;
+ return $pass;
}
function checkExtension($name)
@@ -88,96 +94,118 @@ function checkExtension($name)
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>
+ echo<<<E_O_T
+ </ul>
+ </dd>
+</dl>
+<dl id="page_notice" class="system_notice">
+ <dt>Page notice</dt>
+ <dd>
+ <div class="instructions">
+ <p>Enter your database connection information below to initialize the database.</p>
+ </div>
+ </dd>
+</dl>
+<form method="post" action="install.php" class="form_settings" id="form_install">
+ <fieldset>
+ <legend>Connection settings</legend>
+ <ul class="form_data">
+ <li>
+ <label for="sitename">Site name</label>
+ <input type="text" id="sitename" name="sitename" />
+ <p class="form_guide">The name of your site</p>
+ </li>
+ <li>
+ <label for="fancy-enable">Fancy URLs</label>
+ <input type="radio" name="fancy" id="fancy-enable" value="enable" checked='checked' /> enable<br />
+ <input type="radio" name="fancy" id="fancy-disable" value="" /> disable<br />
+ <p class="form_guide" id='fancy-form_guide'>Enable fancy (pretty) URLs. Auto-detection failed, it depends on Javascript.</p>
+ </li>
+ <li>
+ <label for="host">Hostname</label>
+ <input type="text" id="host" name="host" />
+ <p class="form_guide">Database hostname</p>
+ </li>
+ <li>
+ <label for="host">Database</label>
+ <input type="text" id="database" name="database" />
+ <p class="form_guide">Database name</p>
+ </li>
+ <li>
+ <label for="username">Username</label>
+ <input type="text" id="username" name="username" />
+ <p class="form_guide">Database username</p>
+ </li>
+ <li>
+ <label for="password">Password</label>
+ <input type="password" id="password" name="password" />
+ <p class="form_guide">Database password</p>
+ </li>
+ </ul>
+ <input type="submit" name="submit" class="submit" value="Submit" />
+ </fieldset>
</form>
-<?
+
+E_O_T;
}
function updateStatus($status, $error=false)
{
?>
- <li>
-<?
- print $status;
-?>
- </li>
-<?
+ <li <?php echo ($error) ? 'class="error"': ''; ?>><?print $status;?></li>
+
+<?php
}
function handlePost()
{
?>
- <ul>
-<?
- $host = $_POST['host'];
+
+<?php
+ $host = $_POST['host'];
$database = $_POST['database'];
$username = $_POST['username'];
$password = $_POST['password'];
$sitename = $_POST['sitename'];
+ $fancy = !empty($_POST['fancy']);
+?>
+ <dl class="system_notice">
+ <dt>Page notice</dt>
+ <dd>
+ <ul>
+<?php
+ $fail = false;
if (empty($host)) {
updateStatus("No hostname specified.", true);
- showForm();
- return;
+ $fail = true;
}
if (empty($database)) {
updateStatus("No database specified.", true);
- showForm();
- return;
+ $fail = true;
}
if (empty($username)) {
updateStatus("No username specified.", true);
- showForm();
- return;
+ $fail = true;
}
if (empty($password)) {
updateStatus("No password specified.", true);
- showForm();
- return;
+ $fail = true;
}
if (empty($sitename)) {
updateStatus("No sitename specified.", true);
- showForm();
- return;
+ $fail = true;
}
+ if($fail){
+ showForm();
+ return;
+ }
+
updateStatus("Starting installation...");
updateStatus("Checking database...");
$conn = mysql_connect($host, $username, $password);
@@ -214,24 +242,29 @@ function handlePost()
}
updateStatus("Writing config file...");
$sqlUrl = "mysqli://$username:$password@$host/$database";
- $res = writeConf($sitename, $sqlUrl);
+ $res = writeConf($sitename, $sqlUrl, $fancy);
if (!$res) {
updateStatus("Can't write config file.", true);
showForm();
return;
}
updateStatus("Done!");
+ if ($path) $path .= '/';
+ updateStatus("You can visit your <a href='/$path'>new Laconica site</a>.");
?>
- </ul>
-<?
+
+<?php
}
-function writeConf($sitename, $sqlUrl)
+function writeConf($sitename, $sqlUrl, $fancy)
{
$res = file_put_contents(INSTALLDIR.'/config.php',
"<?php\n".
+ "if (!defined('LACONICA')) { exit(1); }\n\n".
"\$config['site']['name'] = \"$sitename\";\n\n".
- "\$config['db']['database'] = \"$sqlUrl\";\n\n");
+ ($fancy ? "\$config['site']['fancy'] = true;\n\n":'').
+ "\$config['db']['database'] = \"$sqlUrl\";\n\n".
+ "?>");
return $res;
}
@@ -253,21 +286,37 @@ function runDbScript($filename, $conn)
}
?>
-<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>
+<?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?>
+<!DOCTYPE html
+PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
+ <head>
+ <title>Install Laconica</title>
+ <link rel="shortcut icon" href="favicon.ico"/>
+ <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.8" media="screen, projection, tv"/>
+ <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/base/css/ie.css?version=0.8" /><![endif]-->
+ <!--[if lte IE 6]><link rel="stylesheet" type="text/css" theme/base/css/ie6.css?version=0.8" /><![endif]-->
+ <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/default/css/ie.css?version=0.8" /><![endif]-->
+ <script src="js/jquery.min.js"></script>
+ <script src="js/install.js"></script>
+ </head>
+ <body id="install">
+ <div id="wrap">
+ <div id="header">
+ <address id="site_contact" class="vcard">
+ <a class="url home bookmark" href=".">
+ <img class="logo photo" src="theme/default/logo.png" alt="Laconica"/>
+ <span class="fn org">Laconica</span>
+ </a>
+ </address>
+ </div>
+ <div id="core">
+ <div id="content">
+ <h1>Install Laconica</h1>
<?php main(); ?>
- </div>
- </div>
- </div>
-</body>
+ </div>
+ </div>
+ </div>
+ </body>
</html>
diff --git a/js/farbtastic/LICENSE.txt b/js/farbtastic/LICENSE.txt
new file mode 100644
index 000000000..5a3cc209a
--- /dev/null
+++ b/js/farbtastic/LICENSE.txt
@@ -0,0 +1,341 @@
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/js/farbtastic/farbtastic.js b/js/farbtastic/farbtastic.js
new file mode 100644
index 000000000..d8b5ad9cd
--- /dev/null
+++ b/js/farbtastic/farbtastic.js
@@ -0,0 +1,345 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+jQuery.fn.farbtastic = function (callback) {
+ $.farbtastic(this, callback);
+ return this;
+};
+
+jQuery.farbtastic = function (container, callback) {
+ var container = $(container).get(0);
+ return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
+}
+
+jQuery._farbtastic = function (container, callback) {
+ // Store farbtastic object
+ var fb = this;
+
+ // Insert markup
+ $(container).html('<div class="farbtastic"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>');
+ var e = $('.farbtastic', container);
+ fb.wheel = $('.wheel', container).get(0);
+ // Dimensions
+ fb.radius = 84;
+ fb.square = 100;
+ fb.width = 194;
+
+ // Fix background PNGs in IE6
+ if (navigator.appVersion.match(/MSIE [0-6]\./)) {
+ $('*', e).each(function () {
+ if (this.currentStyle.backgroundImage != 'none') {
+ var image = this.currentStyle.backgroundImage;
+ image = this.currentStyle.backgroundImage.substring(5, image.length - 2);
+ $(this).css({
+ 'backgroundImage': 'none',
+ 'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
+ });
+ }
+ });
+ }
+
+ /**
+ * Link to the given element(s) or callback.
+ */
+ fb.linkTo = function (callback) {
+ // Unbind previous nodes
+ if (typeof fb.callback == 'object') {
+ $(fb.callback).unbind('keyup', fb.updateValue);
+ }
+
+ // Reset color
+ fb.color = null;
+
+ // Bind callback or elements
+ if (typeof callback == 'function') {
+ fb.callback = callback;
+ }
+ else if (typeof callback == 'object' || typeof callback == 'string') {
+ fb.callback = $(callback);
+ fb.callback.bind('keyup', fb.updateValue);
+ if (fb.callback.get(0).value) {
+ fb.setColor(fb.callback.get(0).value);
+ }
+ }
+ return this;
+ }
+ fb.updateValue = function (event) {
+ if (this.value && this.value != fb.color) {
+ fb.setColor(this.value);
+ }
+ }
+
+ /**
+ * Change color with HTML syntax #123456
+ */
+ fb.setColor = function (color) {
+ var unpack = fb.unpack(color);
+ if (fb.color != color && unpack) {
+ fb.color = color;
+ fb.rgb = unpack;
+ fb.hsl = fb.RGBToHSL(fb.rgb);
+ fb.updateDisplay();
+ }
+ return this;
+ }
+
+ /**
+ * Change color with HSL triplet [0..1, 0..1, 0..1]
+ */
+ fb.setHSL = function (hsl) {
+ fb.hsl = hsl;
+ fb.rgb = fb.HSLToRGB(hsl);
+ fb.color = fb.pack(fb.rgb);
+ fb.updateDisplay();
+ return this;
+ }
+
+ /////////////////////////////////////////////////////
+
+ /**
+ * Retrieve the coordinates of the given event relative to the center
+ * of the widget.
+ */
+ fb.widgetCoords = function (event) {
+ var x, y;
+ var el = event.target || event.srcElement;
+ var reference = fb.wheel;
+
+ if (typeof event.offsetX != 'undefined') {
+ // Use offset coordinates and find common offsetParent
+ var pos = { x: event.offsetX, y: event.offsetY };
+
+ // Send the coordinates upwards through the offsetParent chain.
+ var e = el;
+ while (e) {
+ e.mouseX = pos.x;
+ e.mouseY = pos.y;
+ pos.x += e.offsetLeft;
+ pos.y += e.offsetTop;
+ e = e.offsetParent;
+ }
+
+ // Look for the coordinates starting from the wheel widget.
+ var e = reference;
+ var offset = { x: 0, y: 0 }
+ while (e) {
+ if (typeof e.mouseX != 'undefined') {
+ x = e.mouseX - offset.x;
+ y = e.mouseY - offset.y;
+ break;
+ }
+ offset.x += e.offsetLeft;
+ offset.y += e.offsetTop;
+ e = e.offsetParent;
+ }
+
+ // Reset stored coordinates
+ e = el;
+ while (e) {
+ e.mouseX = undefined;
+ e.mouseY = undefined;
+ e = e.offsetParent;
+ }
+ }
+ else {
+ // Use absolute coordinates
+ var pos = fb.absolutePosition(reference);
+ x = (event.pageX || 0*(event.clientX + $('html').get(0).scrollLeft)) - pos.x;
+ y = (event.pageY || 0*(event.clientY + $('html').get(0).scrollTop)) - pos.y;
+ }
+ // Subtract distance to middle
+ return { x: x - fb.width / 2, y: y - fb.width / 2 };
+ }
+
+ /**
+ * Mousedown handler
+ */
+ fb.mousedown = function (event) {
+ // Capture mouse
+ if (!document.dragging) {
+ $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
+ document.dragging = true;
+ }
+
+ // Check which area is being dragged
+ var pos = fb.widgetCoords(event);
+ fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;
+
+ // Process
+ fb.mousemove(event);
+ return false;
+ }
+
+ /**
+ * Mousemove handler
+ */
+ fb.mousemove = function (event) {
+ // Get coordinates relative to color picker center
+ var pos = fb.widgetCoords(event);
+
+ // Set new HSL parameters
+ if (fb.circleDrag) {
+ var hue = Math.atan2(pos.x, -pos.y) / 6.28;
+ if (hue < 0) hue += 1;
+ fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
+ }
+ else {
+ var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
+ var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
+ fb.setHSL([fb.hsl[0], sat, lum]);
+ }
+ return false;
+ }
+
+ /**
+ * Mouseup handler
+ */
+ fb.mouseup = function () {
+ // Uncapture mouse
+ $(document).unbind('mousemove', fb.mousemove);
+ $(document).unbind('mouseup', fb.mouseup);
+ document.dragging = false;
+ }
+
+ /**
+ * Update the markers and styles
+ */
+ fb.updateDisplay = function () {
+ // Markers
+ var angle = fb.hsl[0] * 6.28;
+ $('.h-marker', e).css({
+ left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
+ top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
+ });
+
+ $('.sl-marker', e).css({
+ left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
+ top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
+ });
+
+ // Saturation/Luminance gradient
+ $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
+
+ // Linked elements or callback
+ if (typeof fb.callback == 'object') {
+ // Set background/foreground color
+ $(fb.callback).css({
+ backgroundColor: fb.color,
+ color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
+ });
+
+ // Change linked value
+ $(fb.callback).each(function() {
+ if (this.value && this.value != fb.color) {
+ this.value = fb.color;
+ }
+ });
+ }
+ else if (typeof fb.callback == 'function') {
+ fb.callback.call(fb, fb.color);
+ }
+ }
+
+ /**
+ * Get absolute position of element
+ */
+ fb.absolutePosition = function (el) {
+ var r = { x: el.offsetLeft, y: el.offsetTop };
+ // Resolve relative to offsetParent
+ if (el.offsetParent) {
+ var tmp = fb.absolutePosition(el.offsetParent);
+ r.x += tmp.x;
+ r.y += tmp.y;
+ }
+ return r;
+ };
+
+ /* Various color utility functions */
+ fb.pack = function (rgb) {
+ var r = Math.round(rgb[0] * 255);
+ var g = Math.round(rgb[1] * 255);
+ var b = Math.round(rgb[2] * 255);
+ return '#' + (r < 16 ? '0' : '') + r.toString(16) +
+ (g < 16 ? '0' : '') + g.toString(16) +
+ (b < 16 ? '0' : '') + b.toString(16);
+ }
+
+ fb.unpack = function (color) {
+ if (color.length == 7) {
+ return [parseInt('0x' + color.substring(1, 3)) / 255,
+ parseInt('0x' + color.substring(3, 5)) / 255,
+ parseInt('0x' + color.substring(5, 7)) / 255];
+ }
+ else if (color.length == 4) {
+ return [parseInt('0x' + color.substring(1, 2)) / 15,
+ parseInt('0x' + color.substring(2, 3)) / 15,
+ parseInt('0x' + color.substring(3, 4)) / 15];
+ }
+ }
+
+ fb.HSLToRGB = function (hsl) {
+ var m1, m2, r, g, b;
+ var h = hsl[0], s = hsl[1], l = hsl[2];
+ m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
+ m1 = l * 2 - m2;
+ return [this.hueToRGB(m1, m2, h+0.33333),
+ this.hueToRGB(m1, m2, h),
+ this.hueToRGB(m1, m2, h-0.33333)];
+ }
+
+ fb.hueToRGB = function (m1, m2, h) {
+ h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
+ if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+ if (h * 2 < 1) return m2;
+ if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
+ return m1;
+ }
+
+ fb.RGBToHSL = function (rgb) {
+ var min, max, delta, h, s, l;
+ var r = rgb[0], g = rgb[1], b = rgb[2];
+ min = Math.min(r, Math.min(g, b));
+ max = Math.max(r, Math.max(g, b));
+ delta = max - min;
+ l = (min + max) / 2;
+ s = 0;
+ if (l > 0 && l < 1) {
+ s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
+ }
+ h = 0;
+ if (delta > 0) {
+ if (max == r && max != g) h += (g - b) / delta;
+ if (max == g && max != b) h += (2 + (b - r) / delta);
+ if (max == b && max != r) h += (4 + (r - g) / delta);
+ h /= 6;
+ }
+ return [h, s, l];
+ }
+
+ // Install mousedown handler (the others are set on the document on-demand)
+ $('*', e).mousedown(fb.mousedown);
+
+ // Init color
+ fb.setColor('#000000');
+
+ // Set linked elements/callback
+ if (callback) {
+ fb.linkTo(callback);
+ }
+} \ No newline at end of file
diff --git a/js/farbtastic/marker.png b/js/farbtastic/marker.png
new file mode 100755
index 000000000..3929bbb51
--- /dev/null
+++ b/js/farbtastic/marker.png
Binary files differ
diff --git a/js/farbtastic/mask.png b/js/farbtastic/mask.png
new file mode 100644
index 000000000..b0a4d406f
--- /dev/null
+++ b/js/farbtastic/mask.png
Binary files differ
diff --git a/js/farbtastic/wheel.png b/js/farbtastic/wheel.png
new file mode 100644
index 000000000..97b343d98
--- /dev/null
+++ b/js/farbtastic/wheel.png
Binary files differ
diff --git a/js/install.js b/js/install.js
new file mode 100644
index 000000000..32a54111e
--- /dev/null
+++ b/js/install.js
@@ -0,0 +1,18 @@
+$(document).ready(function(){
+ $.ajax({url:'check-fancy',
+ type:'GET',
+ success:function(data, textStatus) {
+ $('#fancy-enable').attr('checked', true);
+ $('#fancy-disable').attr('checked', false);
+ $('#fancy-form_guide').text(data);
+ },
+ error:function(XMLHttpRequest, textStatus, errorThrown) {
+ $('#fancy-enable').attr('checked', false);
+ $('#fancy-disable').attr('checked', true);
+ $('#fancy-enable').attr('disabled', true);
+ $('#fancy-disable').attr('disabled', true);
+ $('#fancy-form_guide').text("Fancy URL support detection failed, disabling this option. Make sure you renamed htaccess.sample to .htaccess.");
+ }
+ });
+});
+
diff --git a/js/jcrop/jquery.Jcrop.go.js b/js/jcrop/jquery.Jcrop.go.js
index a0399d540..4e1cbfd1e 100644
--- a/js/jcrop/jquery.Jcrop.go.js
+++ b/js/jcrop/jquery.Jcrop.go.js
@@ -1,39 +1,48 @@
- $(function(){
- var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0;
- var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0;
- var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width");
- var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height");
+/** Init for Jcrop library and page setup
+ *
+ * @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/
+ */
- jQuery("#avatar_original img").Jcrop({
- onChange: showPreview,
- setSelect: [ x, y, w, h ],
- onSelect: updateCoords,
- aspectRatio: 1,
- boxWidth: 480,
- boxHeight: 480,
- bgColor: '#000',
- bgOpacity: .4
- });
- });
+$(function(){
+ var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0;
+ var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0;
+ var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width");
+ var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height");
- function showPreview(coords) {
- var rx = 96 / coords.w;
- var ry = 96 / coords.h;
+ jQuery("#avatar_original img").Jcrop({
+ onChange: showPreview,
+ setSelect: [ x, y, w, h ],
+ onSelect: updateCoords,
+ aspectRatio: 1,
+ boxWidth: 480,
+ boxHeight: 480,
+ bgColor: '#000',
+ bgOpacity: .4
+ });
+});
- var img_width = $("#avatar_original img").attr("width");
- var img_height = $("#avatar_original img").attr("height");
+function showPreview(coords) {
+ var rx = 96 / coords.w;
+ var ry = 96 / coords.h;
- $('#avatar_preview img').css({
- width: Math.round(rx *img_width) + 'px',
- height: Math.round(ry * img_height) + 'px',
- marginLeft: '-' + Math.round(rx * coords.x) + 'px',
- marginTop: '-' + Math.round(ry * coords.y) + 'px'
- });
- };
+ var img_width = $("#avatar_original img").attr("width");
+ var img_height = $("#avatar_original img").attr("height");
- function updateCoords(c) {
- $('#avatar_crop_x').val(c.x);
- $('#avatar_crop_y').val(c.y);
- $('#avatar_crop_w').val(c.w);
- $('#avatar_crop_h').val(c.h);
- };
+ $('#avatar_preview img').css({
+ width: Math.round(rx *img_width) + 'px',
+ height: Math.round(ry * img_height) + 'px',
+ marginLeft: '-' + Math.round(rx * coords.x) + 'px',
+ marginTop: '-' + Math.round(ry * coords.y) + 'px'
+ });
+};
+
+function updateCoords(c) {
+ $('#avatar_crop_x').val(c.x);
+ $('#avatar_crop_y').val(c.y);
+ $('#avatar_crop_w').val(c.w);
+ $('#avatar_crop_h').val(c.h);
+};
diff --git a/js/jquery.form.js b/js/jquery.form.js
index cb8b5a660..936b847ab 100644
--- a/js/jquery.form.js
+++ b/js/jquery.form.js
@@ -157,7 +157,7 @@ $.fn.ajaxSubmit = function(options) {
function fileUpload() {
var form = $form[0];
- if ($(':input[@name=submit]', form).length) {
+ if ($(':input[name=submit]', form).length) {
alert('Error: Form elements must not be named "submit".');
return;
}
@@ -570,7 +570,7 @@ $.fn.clearForm = function() {
$.fn.clearFields = $.fn.clearInputs = function() {
return this.each(function() {
var t = this.type, tag = this.tagName.toLowerCase();
- if (t == 'text' || t == 'password' || tag == 'textarea')
+ if (t == 'file' || t == 'text' || t == 'password' || tag == 'textarea')
this.value = '';
else if (t == 'checkbox' || t == 'radio')
this.checked = false;
diff --git a/js/jquery.joverlay.js b/js/jquery.joverlay.js
new file mode 100644
index 000000000..e4effec8e
--- /dev/null
+++ b/js/jquery.joverlay.js
@@ -0,0 +1,271 @@
+/* Copyright (c) 2009 Alvaro A. Lima Jr http://alvarojunior.com/jquery/joverlay.html
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * Version: 0.7.1 (JUN 15, 2009)
+ * Requires: jQuery 1.3+
+ */
+
+(function($) {
+
+ // Global vars
+ var isIE6 = $.browser.msie && $.browser.version == 6.0; // =(
+ var JOVERLAY_TIMER = null;
+ var JOVERLAY_ELEMENT_PREV = null;
+
+ $.fn.jOverlay = function(options) {
+
+ // Element exist?
+ if ( $('#jOverlay').length ) {$.closeOverlay();}
+
+ // Clear Element Prev
+ JOVERLAY_ELEMENT_PREV = null;
+
+ // Clear Timer
+ if (JOVERLAY_TIMER !== null) {
+ clearTimeout( JOVERLAY_TIMER );
+ }
+
+ // Set Options
+ var options = $.extend({}, $.fn.jOverlay.options, options);
+
+ // private function
+ function center(id) {
+ if (options.center) {
+ $.center(id);
+ }
+ }
+
+ var element = this.is('*') ? this : '#jOverlayContent';
+ var position = isIE6 ? 'absolute' : 'fixed';
+ var isImage = /([^\/\\]+)\.(png|gif|jpeg|jpg|bmp)$/i.test( options.url );
+
+ var imgLoading = options.imgLoading ? "<img id='jOverlayLoading' src='"+options.imgLoading+"' style='position:"+position+"; z-index:"+(options.zIndex + 9)+";'/>" : '';
+
+ $('body').prepend(imgLoading + "<div id='jOverlay' />"
+ + "<div id='jOverlayContent' style='position:"+position+"; z-index:"+(options.zIndex + 5)+"; display:none;'/>"
+ );
+
+ // Loading Centered
+ $('#jOverlayLoading').load(function(){
+ center(this);
+ });
+
+ //IE 6 FIX
+ if ( isIE6 ) {
+ $('select').hide();
+ $('#jOverlayContent select').show();
+ }
+
+ // Overlay Style
+ $('#jOverlay').css({
+ backgroundColor : options.color,
+ position : position,
+ top : '0px',
+ left : '0px',
+ filter : 'alpha(opacity='+ (options.opacity * 100) +')', // IE =(
+ opacity : options.opacity, // Good Browser =D
+ zIndex : options.zIndex,
+ width : !isIE6 ? '100%' : $(window).width() + 'px',
+ height : !isIE6 ? '100%' : $(document).height() + 'px'
+ }).show();
+
+ // ELEMENT
+ if ( this.is('*') ) {
+
+ JOVERLAY_ELEMENT_PREV = this.prev();
+
+ $('#jOverlayContent').html(
+ this.show().attr('display', options.autoHide ? 'none' : this.css('display') )
+ );
+
+ if ( !isImage ) {
+
+ center('#jOverlayContent');
+
+ $('#jOverlayContent').show();
+
+ // Execute callback
+ if ( !options.url && $.isFunction( options.success ) ) {
+ options.success( this );
+ }
+
+ }
+
+ }
+
+ // IMAGE
+ if ( isImage ) {
+
+ $('<img/>').load(function(){
+ var resize = $.resize(this.width, this.height);
+
+ $(this).css({
+ width : resize.width,
+ height : resize.height
+ });
+
+ $( element ).html(this);
+
+ center('#jOverlayContent');
+
+ $('#jOverlayLoading').fadeOut(500);
+ $('#jOverlayContent').show();
+
+ // Execute callback
+ if ( $.isFunction( options.success ) ) {
+ options.success( this );
+ }
+
+ }).error(function(){
+ alert('Image ('+options.url+') not found.');
+ $.closeOverlay();
+ }).attr({'src' : options.url, 'alt' : options.url});
+
+ }
+
+ // AJAX
+ if ( options.url && !isImage ) {
+
+ $.ajax({
+ type: options.method,
+ data: options.data,
+ url: options.url,
+ success: function(responseText) {
+
+ $('#jOverlayLoading').fadeOut(500);
+
+ $( element ).html(responseText).show();
+
+ center('#jOverlayContent');
+
+ // Execute callback
+ if ($.isFunction( options.success )) {
+ options.success(responseText);
+ }
+
+ },
+ error : function() {
+ alert('URL ('+options.url+') not found.');
+ $.closeOverlay();
+ }
+ });
+
+ }
+
+ // :(
+ if ( isIE6 ) {
+
+ // Window scroll
+ $(window).scroll(function(){
+ center('#jOverlayContent');
+ });
+
+ // Window resize
+ $(window).resize(function(){
+
+ $('#jOverlay').css({
+ width: $(window).width() + 'px',
+ height: $(document).height() + 'px'
+ });
+
+ center('#jOverlayContent');
+
+ });
+
+ }
+
+ // Press ESC to close
+ $(document).keydown(function(event){
+ if (event.keyCode == 27) {
+ $.closeOverlay();
+ }
+ });
+
+ // Click to close
+ if ( options.bgClickToClose ) {
+ $('#jOverlay').click($.closeOverlay);
+ }
+
+ // Timeout (auto-close)
+ // time in millis to wait before auto-close
+ // set to 0 to disable
+ if ( Number(options.timeout) > 0 ) {
+ jOverlayTimer = setTimeout( $.closeOverlay, Number(options.timeout) );
+ }
+
+ // ADD CSS
+ $('#jOverlayContent').css(options.css || {});
+ };
+
+ // Resizing large images - orginal by Christian Montoya.
+ // Edited by - Cody Lindley (http://www.codylindley.com) (Thickbox 3.1)
+ $.resize = function(imageWidth, imageHeight) {
+ var x = $(window).width() - 150;
+ var y = $(window).height() - 150;
+ if (imageWidth > x) {
+ imageHeight = imageHeight * (x / imageWidth);
+ imageWidth = x;
+ if (imageHeight > y) {
+ imageWidth = imageWidth * (y / imageHeight);
+ imageHeight = y;
+ }
+ } else if (imageHeight > y) {
+ imageWidth = imageWidth * (y / imageHeight);
+ imageHeight = y;
+ if (imageWidth > x) {
+ imageHeight = imageHeight * (x / imageWidth);
+ imageWidth = x;
+ }
+ }
+ return {width:imageWidth, height:imageHeight};
+ };
+
+ // Centered Element
+ $.center = function(element) {
+ var element = $(element);
+ var elemWidth = element.width();
+
+ element.css({
+ width : elemWidth + 'px',
+ marginLeft : '-' + (elemWidth / 2) + 'px',
+ marginTop : '-' + element.height() / 2 + 'px',
+ height : 'auto',
+ top : !isIE6 ? '50%' : $(window).scrollTop() + ($(window).height() / 2) + 'px',
+ left : '50%'
+ });
+ };
+
+ // Options default
+ $.fn.jOverlay.options = {
+ method : 'GET',
+ data : '',
+ url : '',
+ color : '#000',
+ opacity : '0.6',
+ zIndex : 9999,
+ center : true,
+ imgLoading : '',
+ bgClickToClose : true,
+ success : null,
+ timeout : 0,
+ autoHide : true,
+ css : {}
+ };
+
+ // Close
+ $.closeOverlay = function() {
+
+ if (isIE6) { $("select").show(); }
+
+ if ( JOVERLAY_ELEMENT_PREV !== null ) {
+ if ( JOVERLAY_ELEMENT_PREV !== null ) {
+ var element = $('#jOverlayContent').children();
+ JOVERLAY_ELEMENT_PREV.after( element.css('display', element.attr('display') ) );
+ element.removeAttr('display');
+ }
+ }
+
+ $('#jOverlayLoading, #jOverlayContent, #jOverlay').remove();
+
+ };
+
+})(jQuery); \ No newline at end of file
diff --git a/js/jquery.joverlay.min.js b/js/jquery.joverlay.min.js
new file mode 100644
index 000000000..44cd46043
--- /dev/null
+++ b/js/jquery.joverlay.min.js
@@ -0,0 +1,7 @@
+/* Copyright (c) 2009 Alvaro A. Lima Jr http://alvarojunior.com/jquery/joverlay.html
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * Version: 0.7.1 (JUN 15, 2009
+ * Requires: jQuery 1.3+
+ * Packer from http://dean.edwards.name/packer/
+ */
+(function($){var g=$.browser.msie&&$.browser.version==6.0;var h=null;var i=null;$.fn.jOverlay=function(b){if($('#jOverlay').length){$.closeOverlay()}i=null;if(h!==null){clearTimeout(h)}var b=$.extend({},$.fn.jOverlay.options,b);function center(a){if(b.center){$.center(a)}}var c=this.is('*')?this:'#jOverlayContent';var d=g?'absolute':'fixed';var e=/([^\/\\]+)\.(png|gif|jpeg|jpg|bmp)$/i.test(b.url);var f=b.imgLoading?"<img id='jOverlayLoading' src='"+b.imgLoading+"' style='position:"+d+"; z-index:"+(b.zIndex+9)+";'/>":'';$('body').prepend(f+"<div id='jOverlay' />"+"<div id='jOverlayContent' style='position:"+d+"; z-index:"+(b.zIndex+5)+"; display:none;'/>");$('#jOverlayLoading').load(function(){center(this)});if(g){$('select').hide();$('#jOverlayContent select').show()}$('#jOverlay').css({backgroundColor:b.color,position:d,top:'0px',left:'0px',filter:'alpha(opacity='+(b.opacity*100)+')',opacity:b.opacity,zIndex:b.zIndex,width:!g?'100%':$(window).width()+'px',height:!g?'100%':$(document).height()+'px'}).show();if(this.is('*')){i=this.prev();$('#jOverlayContent').html(this.show().attr('display',b.autoHide?'none':this.css('display')));if(!e){center('#jOverlayContent');$('#jOverlayContent').show();if(!b.url&&$.isFunction(b.success)){b.success(this)}}}if(e){$('<img/>').load(function(){var a=$.resize(this.width,this.height);$(this).css({width:a.width,height:a.height});$(c).html(this);center('#jOverlayContent');$('#jOverlayLoading').fadeOut(500);$('#jOverlayContent').show();if($.isFunction(b.success)){b.success(this)}}).error(function(){alert('Image ('+b.url+') not found.');$.closeOverlay()}).attr({'src':b.url,'alt':b.url})}if(b.url&&!e){$.ajax({type:b.method,data:b.data,url:b.url,success:function(a){$('#jOverlayLoading').fadeOut(500);$(c).html(a).show();center('#jOverlayContent');if($.isFunction(b.success)){b.success(a)}},error:function(){alert('URL ('+b.url+') not found.');$.closeOverlay()}})}if(g){$(window).scroll(function(){center('#jOverlayContent')});$(window).resize(function(){$('#jOverlay').css({width:$(window).width()+'px',height:$(document).height()+'px'});center('#jOverlayContent')})}$(document).keydown(function(a){if(a.keyCode==27){$.closeOverlay()}});if(b.bgClickToClose){$('#jOverlay').click($.closeOverlay)}if(Number(b.timeout)>0){jOverlayTimer=setTimeout($.closeOverlay,Number(b.timeout))}$('#jOverlayContent').css(b.css||{})};$.resize=function(a,b){var x=$(window).width()-150;var y=$(window).height()-150;if(a>x){b=b*(x/a);a=x;if(b>y){a=a*(y/b);b=y}}else if(b>y){a=a*(y/b);b=y;if(a>x){b=b*(x/a);a=x}}return{width:a,height:b}};$.center=function(a){var a=$(a);var b=a.width();a.css({width:b+'px',marginLeft:'-'+(b/2)+'px',marginTop:'-'+a.height()/2+'px',height:'auto',top:!g?'50%':$(window).scrollTop()+($(window).height()/2)+'px',left:'50%'})};$.fn.jOverlay.options={method:'GET',data:'',url:'',color:'#000',opacity:'0.6',zIndex:9999,center:true,imgLoading:'',bgClickToClose:true,success:null,timeout:0,autoHide:true,css:{}};$.closeOverlay=function(){if(g){$("select").show()}if(i!==null){if(i!==null){var a=$('#jOverlayContent').children();i.after(a.css('display',a.attr('display')));a.removeAttr('display')}}$('#jOverlayLoading, #jOverlayContent, #jOverlay').remove()}})(jQuery);
diff --git a/js/userdesign.go.js b/js/userdesign.go.js
new file mode 100644
index 000000000..dda86294e
--- /dev/null
+++ b/js/userdesign.go.js
@@ -0,0 +1,99 @@
+/** Init for Farbtastic library and page setup
+ *
+ * @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/
+ */
+$(document).ready(function() {
+ function UpdateColors(S) {
+ C = $(S).val();
+ switch (parseInt(S.id.slice(-1))) {
+ case 1: default:
+ $('body').css({'background-color':C});
+ break;
+ case 2:
+ $('#content, #site_nav_local_views .current a').css({'background-color':C});
+ break;
+ case 3:
+ $('#aside_primary').css({'background-color':C});
+ break;
+ case 4:
+ $('html body').css({'color':C});
+ break;
+ case 5:
+ $('a').css({'color':C});
+ break;
+ }
+ }
+
+ function UpdateFarbtastic(e) {
+ f.linked = e;
+ f.setColor(e.value);
+ }
+
+ function UpdateSwatch(e) {
+ $(e).css({"background-color": e.value,
+ "color": f.hsl[2] > 0.5 ? "#000": "#fff"});
+ }
+
+ function SynchColors(e) {
+ var S = f.linked;
+ var C = f.color;
+
+ if (S && S.value && S.value != C) {
+ S.value = C;
+ UpdateSwatch(S);
+ UpdateColors(S);
+ }
+ }
+
+ function InitFarbtastic() {
+ $('#settings_design_color').append('<div id="color-picker"></div>');
+ $('#color-picker').hide();
+
+ f = $.farbtastic('#color-picker', SynchColors);
+ swatches = $('#settings_design_color .swatch');
+
+ swatches
+ .each(SynchColors)
+ .blur(function() {
+ tv = $(this).val();
+ $(this).val(tv.toUpperCase());
+ (tv.length == 4) ? ((tv[0] == '#') ? $(this).val('#'+tv[1]+tv[1]+tv[2]+tv[2]+tv[3]+tv[3]) : '') : '';
+ })
+ .focus(function() {
+ $('#color-picker').show();
+ UpdateFarbtastic(this);
+ })
+ .change(function() {
+ UpdateFarbtastic(this);
+ UpdateSwatch(this);
+ UpdateColors(this);
+ }).change();
+ }
+
+ var f, swatches;
+ InitFarbtastic();
+ $('#form_settings_design').bind('reset', function(){
+ setTimeout(function(){
+ swatches.each(function(){UpdateColors(this);});
+ $('#color-picker').remove();
+ swatches.unbind();
+ InitFarbtastic();
+ },10);
+ });
+
+ $('#design_background-image_off').focus(function() {
+ $('body').css({'background-image':'none'});
+ });
+ $('#design_background-image_on').focus(function() {
+ $('body').css({'background-image':'url('+$('#design_background-image_onoff img')[0].src+')'});
+ $('body').css({'background-attachment': 'fixed'});
+ });
+
+ $('#design_background-image_repeat').click(function() {
+ ($(this)[0].checked) ? $('body').css({'background-repeat':'repeat'}) : $('body').css({'background-repeat':'no-repeat'});
+ });
+});
diff --git a/js/util.js b/js/util.js
index 23abba6c2..bbcbc0bbb 100644
--- a/js/util.js
+++ b/js/util.js
@@ -49,8 +49,9 @@ $(document).ready(function(){
// run once in case there's something in there
counter();
- // set the focus
- $("#notice_data-text").focus();
+ if($('body')[0].id != 'conversation') {
+ $("#notice_data-text").focus();
+ }
}
// XXX: refactor this code
@@ -203,14 +204,27 @@ $(document).ready(function(){
else {
li = $("li", xml).get(0);
if ($("#"+li.id).length == 0) {
- $("#notices_primary .notices").prepend(document._importNode(li, true));
- $("#notices_primary .notice:first").css({display:"none"});
- $("#notices_primary .notice:first").fadeIn(2500);
- NoticeHover();
- NoticeReply();
+ var notice_irt_value = $('#notice_in-reply-to').val();
+ var notice_irt = '#notices_primary #notice-'+notice_irt_value;
+ if($('body')[0].id == 'conversation') {
+ if(notice_irt_value.length > 0 && $(notice_irt+' .notices').length < 1) {
+ $(notice_irt).append('<ul class="notices"></ul>');
+ }
+ $($(notice_irt+' .notices')[0]).append(document._importNode(li, true));
+ }
+ else {
+ $("#notices_primary .notices").prepend(document._importNode(li, true));
+ }
+ $('#'+li.id).css({display:'none'});
+ $('#'+li.id).fadeIn(2500);
+ NoticeReply();
+ NoticeAttachments();
}
}
$("#notice_data-text").val("");
+ $("#notice_data-attach").val("");
+ $("#notice_in-reply-to").val("");
+ $('#notice_data-attach_selected').remove();
counter();
}
$("#form_notice").removeClass("processing");
@@ -220,28 +234,18 @@ $(document).ready(function(){
};
$("#form_notice").ajaxForm(PostNotice);
$("#form_notice").each(addAjaxHidden);
- NoticeHover();
NoticeReply();
+ NoticeAttachments();
+ NoticeDataAttach();
});
-function NoticeHover() {
- $("#content .notice").hover(
- function () {
- $(this).addClass('hover');
- },
- function () {
- $(this).removeClass('hover');
- }
- );
-}
-
function NoticeReply() {
- if ($('#notice_data-text').length > 0) {
+ if ($('#notice_data-text').length > 0 && $('#content .notice_reply').length > 0) {
$('#content .notice').each(function() {
- var notice = $(this);
- $('.notice_reply', $(this)).click(function() {
- var nickname = ($('.author .nickname', notice).length > 0) ? $('.author .nickname', notice) : $('.author .nickname');
- NoticeReplySet(nickname.text(), $('.notice_id', notice).text());
+ var notice = $(this)[0];
+ $($('.notice_reply', notice)[0]).click(function() {
+ var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname');
+ NoticeReplySet(nickname.text(), $($('.notice_id', notice)[0]).text());
return false;
});
});
@@ -261,3 +265,67 @@ function NoticeReplySet(nick,id) {
}
return true;
}
+
+function NoticeAttachments() {
+ $.fn.jOverlay.options = {
+ method : 'GET',
+ data : '',
+ url : '',
+ color : '#000',
+ opacity : '0.6',
+ zIndex : 99,
+ center : false,
+ imgLoading : $('address .url')[0].href+'theme/base/images/illustrations/illu_progress_loading-01.gif',
+ bgClickToClose : true,
+ success : function() {
+ $('#jOverlayContent').append('<button>&#215;</button>');
+ $('#jOverlayContent button').click($.closeOverlay);
+ },
+ timeout : 0,
+ autoHide : true,
+ css : {'max-width':'542px', 'top':'22.5%', 'left':'32.5%'}
+ };
+
+ $('#content .notice a.attachment').click(function() {
+ $().jOverlay({url: $('address .url')[0].href+'attachment/' + ($(this).attr('id').substring('attachment'.length + 1)) + '/ajax'});
+ return false;
+ });
+
+ var t;
+ $("body:not(#shownotice) #content .notice a.thumbnail").hover(
+ function() {
+ var anchor = $(this);
+ $("a.thumbnail").children('img').hide();
+ anchor.closest(".entry-title").addClass('ov');
+
+ if (anchor.children('img').length == 0) {
+ t = setTimeout(function() {
+ $.get($('address .url')[0].href+'attachment/' + (anchor.attr('id').substring('attachment'.length + 1)) + '/thumbnail', null, function(data) {
+ anchor.append(data);
+ });
+ }, 500);
+ }
+ else {
+ anchor.children('img').show();
+ }
+ },
+ function() {
+ clearTimeout(t);
+ $("a.thumbnail").children('img').hide();
+ $(this).closest(".entry-title").removeClass('ov');
+ }
+ );
+}
+
+function NoticeDataAttach() {
+ NDA = $('#notice_data-attach');
+ NDA.change(function() {
+ S = '<div id="notice_data-attach_selected" class="success"><code>'+$(this).val()+'</code> <button>&#215;</button></div>';
+ NDAS = $('#notice_data-attach_selected');
+ (NDAS.length > 0) ? NDAS.replaceWith(S) : $('#form_notice').append(S);
+ $('#notice_data-attach_selected button').click(function(){
+ $('#notice_data-attach_selected').remove();
+ NDA.val('');
+ });
+ });
+}
diff --git a/lib/Shorturl_api.php b/lib/Shorturl_api.php
index d1fc5eb6d..22d5b4cb5 100644
--- a/lib/Shorturl_api.php
+++ b/lib/Shorturl_api.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -22,6 +22,7 @@ if (!defined('LACONICA')) { exit(1); }
class ShortUrlApi
{
protected $service_url;
+ protected $long_limit = 27;
function __construct($service_url)
{
diff --git a/lib/accountsettingsaction.php b/lib/accountsettingsaction.php
index 46090b8c1..4ab50abce 100644
--- a/lib/accountsettingsaction.php
+++ b/lib/accountsettingsaction.php
@@ -115,6 +115,9 @@ class AccountSettingsNav extends Widget
'openidsettings' =>
array(_('OpenID'),
_('Add or remove OpenIDs')),
+ 'userdesignsettings' =>
+ array(_('Design'),
+ _('Design your profile')),
'othersettings' =>
array(_('Other'),
_('Other options')));
diff --git a/lib/action.php b/lib/action.php
index 7c7c52c2c..95ee10c64 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -242,6 +242,11 @@ class Action extends HTMLOutputter // lawsuit
$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.joverlay.min.js')),
+ ' ');
+
Event::handle('EndShowJQueryScripts', array($this));
}
if (Event::handle('StartShowLaconicaScripts', array($this))) {
@@ -378,15 +383,18 @@ class Action extends HTMLOutputter // lawsuit
{
$this->elementStart('address', array('id' => 'site_contact',
'class' => 'vcard'));
- $this->elementStart('a', array('class' => 'url home bookmark',
- 'href' => common_local_url('public')));
- if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) {
- $this->element('img', array('class' => 'logo photo',
- 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'),
- 'alt' => common_config('site', 'name')));
+ if (Event::handle('StartAddressData', array($this))) {
+ $this->elementStart('a', array('class' => 'url home bookmark',
+ 'href' => common_local_url('public')));
+ if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) {
+ $this->element('img', array('class' => 'logo photo',
+ 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'),
+ 'alt' => common_config('site', 'name')));
+ }
+ $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
+ $this->elementEnd('a');
+ Event::handle('EndAddressData', array($this));
}
- $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
- $this->elementEnd('a');
$this->elementEnd('address');
}
@@ -416,11 +424,13 @@ class Action extends HTMLOutputter // lawsuit
$this->menuItem(common_local_url('smssettings'),
_('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
}
- $this->menuItem(common_local_url('invite'),
- _('Invite'),
- sprintf(_('Invite friends and colleagues to join you on %s'),
- common_config('site', 'name')),
- false, 'nav_invitecontact');
+ if (common_config('invite', 'enabled')) {
+ $this->menuItem(common_local_url('invite'),
+ _('Invite'),
+ sprintf(_('Invite friends and colleagues to join you on %s'),
+ common_config('site', 'name')),
+ false, 'nav_invitecontact');
+ }
$this->menuItem(common_local_url('logout'),
_('Logout'), _('Logout from the site'), false, 'nav_logout');
}
@@ -429,8 +439,6 @@ class Action extends HTMLOutputter // lawsuit
$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('login'),
_('Login'), _('Login to the site'), false, 'nav_login');
}
@@ -569,20 +577,32 @@ class Action extends HTMLOutputter // lawsuit
/**
* Show page notice block.
*
+ * Only show the block if a subclassed action has overrided
+ * Action::showPageNotice(), or an event handler is registered for
+ * the StartShowPageNotice event, in which case we assume the
+ * 'page_notice' definition list is desired. This is to prevent
+ * empty 'page_notice' definition lists from being output everywhere.
+ *
* @return nothing
*/
function showPageNoticeBlock()
{
- $this->elementStart('dl', array('id' => 'page_notice',
- 'class' => 'system_notice'));
- $this->element('dt', null, _('Page notice'));
- $this->elementStart('dd');
- if (Event::handle('StartShowPageNotice', array($this))) {
- $this->showPageNotice();
- Event::handle('EndShowPageNotice', array($this));
+ $rmethod = new ReflectionMethod($this, 'showPageNotice');
+ $dclass = $rmethod->getDeclaringClass()->getName();
+
+ if ($dclass != 'Action' || Event::hasHandler('StartShowPageNotice')) {
+
+ $this->elementStart('dl', array('id' => 'page_notice',
+ 'class' => 'system_notice'));
+ $this->element('dt', null, _('Page notice'));
+ $this->elementStart('dd');
+ if (Event::handle('StartShowPageNotice', array($this))) {
+ $this->showPageNotice();
+ Event::handle('EndShowPageNotice', array($this));
+ }
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
}
- $this->elementEnd('dd');
- $this->elementEnd('dl');
}
/**
@@ -686,6 +706,11 @@ class Action extends HTMLOutputter // lawsuit
_('About'));
$this->menuItem(common_local_url('doc', array('title' => 'faq')),
_('FAQ'));
+ $bb = common_config('site', 'broughtby');
+ if (!empty($bb)) {
+ $this->menuItem(common_local_url('doc', array('title' => 'tos')),
+ _('TOS'));
+ }
$this->menuItem(common_local_url('doc', array('title' => 'privacy')),
_('Privacy'));
$this->menuItem(common_local_url('doc', array('title' => 'source')),
@@ -747,7 +772,9 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('p');
$this->element('img', array('id' => 'license_cc',
'src' => common_config('license', 'image'),
- 'alt' => common_config('license', 'title')));
+ 'alt' => common_config('license', 'title'),
+ 'width' => '80',
+ 'height' => '15'));
//TODO: This is dirty: i18n
$this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
$this->element('a', array('class' => 'license',
@@ -847,10 +874,12 @@ class Action extends HTMLOutputter // lawsuit
}
if ($lm) {
header('Last-Modified: ' . date(DATE_RFC1123, $lm));
- if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
- $ims = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+ if (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
+ $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+ $ims = strtotime($if_modified_since);
if ($lm <= $ims) {
- $if_none_match = $_SERVER['HTTP_IF_NONE_MATCH'];
+ $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
+ $_SERVER['HTTP_IF_NONE_MATCH'] : null;
if (!$if_none_match ||
!$etag ||
$this->_hasEtag($etag, $if_none_match)) {
@@ -944,12 +973,16 @@ class Action extends HTMLOutputter // lawsuit
$action = $this->trimmed('action');
$args = $this->args;
unset($args['action']);
+ if (common_config('site', 'fancy')) {
+ unset($args['p']);
+ }
if (array_key_exists('submit', $args)) {
unset($args['submit']);
}
foreach (array_keys($_COOKIE) as $cookie) {
unset($args[$cookie]);
}
+
return common_local_url($action, $args);
}
diff --git a/lib/arraywrapper.php b/lib/arraywrapper.php
index ef0eeffa5..a8a12b3bb 100644
--- a/lib/arraywrapper.php
+++ b/lib/arraywrapper.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php
new file mode 100644
index 000000000..f6a1b59d0
--- /dev/null
+++ b/lib/attachmentlist.php
@@ -0,0 +1,357 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * widget for displaying a list of notice attachments
+ *
+ * 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 UI
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@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);
+}
+
+/**
+ * widget for displaying a list of notice attachments
+ *
+ * There are a number of actions that display a list of notices, in
+ * reverse chronological order. This widget abstracts out most of the
+ * code for UI for notice lists. It's overridden to hide some
+ * data for e.g. the profile page.
+ *
+ * @category UI
+ * @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 Notice
+ * @see NoticeListItem
+ * @see ProfileNoticeList
+ */
+
+class AttachmentList extends Widget
+{
+ /** the current stream of notices being displayed. */
+
+ var $notice = null;
+
+ /**
+ * constructor
+ *
+ * @param Notice $notice stream of notices from DB_DataObject
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($out);
+ $this->notice = $notice;
+ }
+
+ /**
+ * show the list of notices
+ *
+ * "Uses up" the stream by looping through it. So, probably can't
+ * be called twice on the same list.
+ *
+ * @return int count of notices listed.
+ */
+
+ function show()
+ {
+ $atts = new File;
+ $att = $atts->getAttachments($this->notice->id);
+ if (empty($att)) return 0;
+ $this->out->elementStart('dl', array('id' =>'attachments',
+ 'class' => 'entry-content'));
+ $this->out->element('dt', null, _('Attachments'));
+ $this->out->elementStart('dd');
+ $this->out->elementStart('ol', array('class' => 'attachments'));
+
+ foreach ($att as $n=>$attachment) {
+ $item = $this->newListItem($attachment);
+ $item->show();
+ }
+
+ $this->out->elementEnd('dd');
+ $this->out->elementEnd('ol');
+ $this->out->elementEnd('dl');
+
+ return count($att);
+ }
+
+ /**
+ * returns a new list item for the current notice
+ *
+ * Recipe (factory?) method; overridden by sub-classes to give
+ * a different list item class.
+ *
+ * @param Notice $notice the current notice
+ *
+ * @return NoticeListItem a list item for displaying the notice
+ */
+
+ function newListItem($attachment)
+ {
+ return new AttachmentListItem($attachment, $this->out);
+ }
+}
+
+/**
+ * widget for displaying a single notice
+ *
+ * This widget has the core smarts for showing a single notice: what to display,
+ * where, and under which circumstances. Its key method is show(); this is a recipe
+ * that calls all the other show*() methods to build up a single notice. The
+ * ProfileNoticeListItem subclass, for example, overrides showAuthor() to skip
+ * author info (since that's implicit by the data in the page).
+ *
+ * @category UI
+ * @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 NoticeList
+ * @see ProfileNoticeListItem
+ */
+
+class AttachmentListItem extends Widget
+{
+ /** The attachment this item will show. */
+
+ var $attachment = null;
+
+ var $oembed = null;
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($attachment, $out=null)
+ {
+ parent::__construct($out);
+ $this->attachment = $attachment;
+ $this->oembed = File_oembed::staticGet('file_id', $this->attachment->id);
+ }
+
+ function title() {
+ if (empty($this->attachment->title)) {
+ if (empty($this->oembed->title)) {
+ $title = $this->attachment->url;
+ } else {
+ $title = $this->oembed->title;
+ }
+ } else {
+ $title = $this->attachment->title;
+ }
+
+ return $title;
+ }
+
+ function linkTitle() {
+ return $this->title();
+ }
+
+ /**
+ * recipe function for displaying a single notice.
+ *
+ * This uses all the other methods to correctly display a notice. Override
+ * it or one of the others to fine-tune the output.
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $this->showStart();
+ $this->showNoticeAttachment();
+ $this->showEnd();
+ }
+
+ function linkAttr() {
+ return array('class' => 'attachment', 'href' => $this->attachment->url, 'id' => 'attachment-' . $this->attachment->id);
+ }
+
+ function showLink() {
+ $this->out->elementStart('a', $this->linkAttr());
+ $this->out->element('span', null, $this->linkTitle());
+ $this->showRepresentation();
+ $this->out->elementEnd('a');
+ }
+
+ function showNoticeAttachment()
+ {
+ $this->showLink();
+ }
+
+ function showRepresentation() {
+ $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id);
+ if (!empty($thumbnail)) {
+ $this->out->element('img', array('alt' => '', 'src' => $thumbnail->url, 'width' => $thumbnail->width, 'height' => $thumbnail->height));
+ }
+ }
+
+ /**
+ * start a single notice.
+ *
+ * @return void
+ */
+
+ function showStart()
+ {
+ // XXX: RDFa
+ // TODO: add notice_type class e.g., notice_video, notice_image
+ $this->out->elementStart('li');
+ }
+
+ /**
+ * finish the notice
+ *
+ * Close the last elements in the notice list item
+ *
+ * @return void
+ */
+
+ function showEnd()
+ {
+ $this->out->elementEnd('li');
+ }
+}
+
+class Attachment extends AttachmentListItem
+{
+ function showLink() {
+ $this->out->elementStart('div', array('id' => 'attachment_view',
+ 'class' => 'hentry'));
+ $this->out->elementStart('div', 'entry-title');
+ $this->out->elementStart('a', $this->linkAttr());
+ $this->out->element('span', null, $this->linkTitle());
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('div');
+
+ $this->out->elementStart('div', 'entry-content');
+ $this->showRepresentation();
+ $this->out->elementEnd('div');
+
+ if (!empty($this->oembed->author_name) || !empty($this->oembed->provider)) {
+ $this->out->elementStart('div', array('id' => 'oembed_info',
+ 'class' => 'entry-content'));
+ if (!empty($this->oembed->author_name)) {
+ $this->out->elementStart('dl', 'vcard author');
+ $this->out->element('dt', null, _('Author'));
+ $this->out->elementStart('dd', 'fn');
+ if (empty($this->oembed->author_url)) {
+ $this->out->text($this->oembed->author_name);
+ } else {
+ $this->out->element('a', array('href' => $this->oembed->author_url,
+ 'class' => 'url'), $this->oembed->author_name);
+ }
+ $this->out->elementEnd('dd');
+ $this->out->elementEnd('dl');
+ }
+ if (!empty($this->oembed->provider)) {
+ $this->out->elementStart('dl', 'vcard');
+ $this->out->element('dt', null, _('Provider'));
+ $this->out->elementStart('dd', 'fn');
+ if (empty($this->oembed->provider_url)) {
+ $this->out->text($this->oembed->provider);
+ } else {
+ $this->out->element('a', array('href' => $this->oembed->provider_url,
+ 'class' => 'url'), $this->oembed->provider);
+ }
+ $this->out->elementEnd('dd');
+ $this->out->elementEnd('dl');
+ }
+ $this->out->elementEnd('div');
+ }
+ $this->out->elementEnd('div');
+ }
+
+ function show() {
+ $this->showNoticeAttachment();
+ }
+
+ function linkAttr() {
+ return array('class' => 'external', 'href' => $this->attachment->url);
+ }
+
+ function linkTitle() {
+ return $this->attachment->url;
+ }
+
+ function showRepresentation() {
+ if (empty($this->oembed->type)) {
+ if (empty($this->attachment->mimetype)) {
+ $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true));
+ } else {
+ switch ($this->attachment->mimetype) {
+ case 'image/gif':
+ case 'image/png':
+ case 'image/jpg':
+ case 'image/jpeg':
+ $this->out->element('img', array('src' => $this->attachment->url, 'alt' => 'alt'));
+ break;
+
+ case 'application/ogg':
+ case 'audio/x-speex':
+ case 'video/mpeg':
+ case 'audio/mpeg':
+ case 'video/mp4':
+ case 'video/quicktime':
+ $arr = array('type' => $this->attachment->mimetype,
+ 'data' => $this->attachment->url,
+ 'width' => 320,
+ 'height' => 240
+ );
+ $this->out->elementStart('object', $arr);
+ $this->out->element('param', array('name' => 'src', 'value' => $this->attachment->url));
+ $this->out->element('param', array('name' => 'autoStart', 'value' => 1));
+ $this->out->elementEnd('object');
+ break;
+ }
+ }
+ } else {
+ switch ($this->oembed->type) {
+ case 'rich':
+ case 'video':
+ case 'link':
+ if (!empty($this->oembed->html)) {
+ $this->out->raw($this->oembed->html);
+ }
+ break;
+
+ case 'photo':
+ $this->out->element('img', array('src' => $this->oembed->url, 'width' => $this->oembed->width, 'height' => $this->oembed->height, 'alt' => 'alt'));
+ break;
+
+ default:
+ $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true));
+ }
+ }
+ }
+}
+
diff --git a/lib/personal.php b/lib/attachmentnoticesection.php
index f92732375..eb3176376 100644
--- a/lib/personal.php
+++ b/lib/attachmentnoticesection.php
@@ -2,7 +2,7 @@
/**
* Laconica, the distributed open-source microblogging tool
*
- * User profile page
+ * FIXME
*
* PHP version 5
*
@@ -19,11 +19,10 @@
* 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 Personal
+ * @category Widget
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
- * @author Sarven Capadisli <csarven@controlyourself.ca>
- * @copyright 2008-2009 Control Yourself, Inc.
+ * @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/
*/
@@ -33,28 +32,44 @@ if (!defined('LACONICA')) {
}
/**
- * Base class for user profile page
+ * FIXME
*
- * @category Personal
+ * These are the widgets that show interesting data about a person * group, or site.
+ *
+ * @category Widget
* @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 PersonalAction extends Action
+class AttachmentNoticeSection extends NoticeSection
{
+ function showContent() {
+ parent::showContent();
+ return false;
+ }
- var $user = null;
-
- function isReadOnly($args)
+ function getNotices()
{
- return true;
+ $notice = new Notice;
+ $f2p = new File_to_post;
+ $f2p->file_id = $this->out->attachment->id;
+ $notice->joinAdd($f2p);
+ $notice->orderBy('created desc');
+ $notice->selectAdd('post_id as id');
+ $notice->find();
+ return $notice;
}
- function handle($args)
+ function title()
{
- parent::handle($args);
+ return _('Notices where this attachment appears');
}
+ function divId()
+ {
+ return 'popular_notices';
+ }
}
+
diff --git a/lib/attachmenttagcloudsection.php b/lib/attachmenttagcloudsection.php
new file mode 100644
index 000000000..50bfceccb
--- /dev/null
+++ b/lib/attachmenttagcloudsection.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Attachment tag cloud section
+ *
+ * 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 Widget
+ * @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);
+}
+
+/**
+ * Attachment tag cloud section
+ *
+ * @category Widget
+ * @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 AttachmentTagCloudSection extends TagCloudSection
+{
+ function title()
+ {
+ return _('Tags for this attachment');
+ }
+
+ function showTag($tag, $weight, $relative)
+ {
+ if ($relative > 0.5) {
+ $rel = 'tag-cloud-7';
+ } else if ($relative > 0.4) {
+ $rel = 'tag-cloud-6';
+ } else if ($relative > 0.3) {
+ $rel = 'tag-cloud-5';
+ } else if ($relative > 0.2) {
+ $rel = 'tag-cloud-4';
+ } else if ($relative > 0.1) {
+ $rel = 'tag-cloud-3';
+ } else if ($relative > 0.05) {
+ $rel = 'tag-cloud-2';
+ } else {
+ $rel = 'tag-cloud-1';
+ }
+
+ $this->out->elementStart('li', $rel);
+ $this->out->element('a', array('href' => $this->tagUrl($tag)),
+ $tag);
+ $this->out->elementEnd('li');
+ }
+
+ function getTags()
+ {
+ $notice_tag = new Notice_tag;
+ $query = 'select tag,count(tag) as weight from notice_tag join file_to_post on (notice_tag.notice_id=post_id) join notice on notice_id = notice.id where file_id=' . $notice_tag->escape($this->out->attachment->id) . ' group by tag order by weight desc';
+ $notice_tag->query($query);
+ return $notice_tag;
+ }
+}
+
diff --git a/lib/channel.php b/lib/channel.php
index f1e205546..38c1d4d67 100644
--- a/lib/channel.php
+++ b/lib/channel.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/clienterroraction.php b/lib/clienterroraction.php
index 0c48414d5..7ddc35eb6 100644
--- a/lib/clienterroraction.php
+++ b/lib/clienterroraction.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/command.php b/lib/command.php
index 507990a0b..4e2280bc8 100644
--- a/lib/command.php
+++ b/lib/command.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -97,18 +97,11 @@ class StatsCommand extends Command
{
function execute($channel)
{
+ $profile = $this->user->getProfile();
- $subs = new Subscription();
- $subs->subscriber = $this->user->id;
- $subs_count = (int) $subs->count() - 1;
-
- $subbed = new Subscription();
- $subbed->subscribed = $this->user->id;
- $subbed_count = (int) $subbed->count() - 1;
-
- $notices = new Notice();
- $notices->profile_id = $this->user->id;
- $notice_count = (int) $notices->count();
+ $subs_count = $profile->subscriptionCount();
+ $subbed_count = $profile->subscriberCount();
+ $notice_count = $profile->noticeCount();
$channel->output($this->user, sprintf(_("Subscriptions: %1\$s\n".
"Subscribers: %2\$s\n".
diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php
index 49c733c03..6538d4442 100644
--- a/lib/commandinterpreter.php
+++ b/lib/commandinterpreter.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/common.php b/lib/common.php
index 151b31d80..c47702779 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -19,7 +19,7 @@
if (!defined('LACONICA')) { exit(1); }
-define('LACONICA_VERSION', '0.7.4');
+define('LACONICA_VERSION', '0.8.0');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
@@ -55,14 +55,37 @@ require_once(INSTALLDIR.'/lib/language.php');
require_once(INSTALLDIR.'/lib/event.php');
require_once(INSTALLDIR.'/lib/plugin.php');
-// try to figure out where we are
+function _sn_to_path($sn)
+{
+ $past_root = substr($sn, 1);
+ $last_slash = strrpos($past_root, '/');
+ if ($last_slash > 0) {
+ $p = substr($past_root, 0, $last_slash);
+ } else {
+ $p = '';
+ }
+ return $p;
+}
+
+// try to figure out where we are. $server and $path
+// can be set by including module, else we guess based
+// on HTTP info.
+
+if (isset($server)) {
+ $_server = $server;
+} else {
+ $_server = array_key_exists('SERVER_NAME', $_SERVER) ?
+ strtolower($_SERVER['SERVER_NAME']) :
+ null;
+}
-$_server = array_key_exists('SERVER_NAME', $_SERVER) ?
- strtolower($_SERVER['SERVER_NAME']) :
- null;
-$_path = array_key_exists('SCRIPT_NAME', $_SERVER) ?
- substr($_SERVER['SCRIPT_NAME'], 1, strrpos($_SERVER['SCRIPT_NAME'], '/') - 1) :
- null;
+if (isset($path)) {
+ $_path = $path;
+} else {
+ $_path = array_key_exists('SCRIPT_NAME', $_SERVER) ?
+ _sn_to_path($_SERVER['SCRIPT_NAME']) :
+ null;
+}
// default configuration, overwritten in config.php
@@ -71,6 +94,14 @@ $config =
array('name' => 'Just another Laconica microblog',
'server' => $_server,
'theme' => 'default',
+ 'design' =>
+ array('backgroundcolor' => '#CEE1E9',
+ 'contentcolor' => '#FFFFFF',
+ 'sidebarcolor' => '#C8D1D5',
+ 'textcolor' => '#000000',
+ 'linkcolor' => '#002E6E',
+ 'backgroundimage' => null,
+ 'disposition' => 1),
'path' => $_path,
'logfile' => null,
'logo' => null,
@@ -93,9 +124,16 @@ $config =
'dupelimit' => 60), # default for same person saying the same thing
'syslog' =>
array('appname' => 'laconica', # for syslog
- 'priority' => 'debug'), # XXX: currently ignored
+ 'priority' => 'debug', # XXX: currently ignored
+ 'facility' => LOG_USER),
'queue' =>
- array('enabled' => false),
+ array('enabled' => false,
+ 'subsystem' => 'db', # default to database, or 'stomp'
+ 'stomp_server' => null,
+ 'queue_basename' => 'laconica',
+ 'stomp_username' => null,
+ 'stomp_password' => null,
+ ),
'license' =>
array('url' => 'http://creativecommons.org/licenses/by/3.0/',
'title' => 'Creative Commons Attribution 3.0',
@@ -109,13 +147,21 @@ $config =
'profile' =>
array('banned' => array()),
'avatar' =>
- array('server' => null),
+ array('server' => null,
+ 'dir' => INSTALLDIR . '/avatar/',
+ 'path' => $_path . '/avatar/'),
+ 'background' =>
+ array('server' => null,
+ 'dir' => INSTALLDIR . '/background/',
+ 'path' => $_path . '/background/'),
'public' =>
array('localonly' => true,
'blacklist' => array(),
'autosource' => array()),
'theme' =>
- array('server' => null),
+ array('server' => null,
+ 'dir' => null,
+ 'path'=> null),
'throttle' =>
array('enabled' => false, // whether to throttle edits; false by default
'count' => 20, // number of allowed messages in timespan
@@ -131,6 +177,8 @@ $config =
'host' => null, # only set if != server
'debug' => false, # print extra debug info
'public' => array()), # JIDs of users who want to receive the public stream
+ 'invite' =>
+ array('enabled' => true),
'sphinx' =>
array('enabled' => false,
'server' => 'localhost',
@@ -143,20 +191,76 @@ $config =
array('piddir' => '/var/run',
'user' => false,
'group' => false),
+ 'twitterbridge' =>
+ array('enabled' => false),
'integration' =>
array('source' => 'Laconica', # source attribute for Twitter
'taguri' => $_server.',2009'), # base for tag URIs
'memcached' =>
array('enabled' => false,
'server' => 'localhost',
+ 'base' => null,
'port' => 11211),
'ping' =>
array('notify' => array()),
'inboxes' =>
array('enabled' => true), # on by default for new sites
'newuser' =>
- array('subscribe' => null,
+ array('default' => null,
'welcome' => null),
+ 'snapshot' =>
+ array('run' => 'web',
+ 'frequency' => 10000,
+ 'reporturl' => 'http://laconi.ca/stats/report'),
+ 'attachments' =>
+ array('server' => null,
+ 'dir' => INSTALLDIR . '/file/',
+ 'path' => $_path . '/file/',
+ 'supported' => array('image/png',
+ 'image/jpeg',
+ 'image/gif',
+ 'image/svg+xml',
+ 'audio/mpeg',
+ 'audio/x-speex',
+ 'application/ogg',
+ 'application/pdf',
+ 'application/vnd.oasis.opendocument.text',
+ 'application/vnd.oasis.opendocument.text-template',
+ 'application/vnd.oasis.opendocument.graphics',
+ 'application/vnd.oasis.opendocument.graphics-template',
+ 'application/vnd.oasis.opendocument.presentation',
+ 'application/vnd.oasis.opendocument.presentation-template',
+ 'application/vnd.oasis.opendocument.spreadsheet',
+ 'application/vnd.oasis.opendocument.spreadsheet-template',
+ 'application/vnd.oasis.opendocument.chart',
+ 'application/vnd.oasis.opendocument.chart-template',
+ 'application/vnd.oasis.opendocument.image',
+ 'application/vnd.oasis.opendocument.image-template',
+ 'application/vnd.oasis.opendocument.formula',
+ 'application/vnd.oasis.opendocument.formula-template',
+ 'application/vnd.oasis.opendocument.text-master',
+ 'application/vnd.oasis.opendocument.text-web',
+ 'application/x-zip',
+ 'application/zip',
+ 'text/plain',
+ 'video/mpeg',
+ 'video/mp4',
+ 'video/quicktime',
+ 'video/mpeg'),
+ 'file_quota' => 5000000,
+ 'user_quota' => 50000000,
+ 'monthly_quota' => 15000000,
+ 'uploads' => true,
+ 'filecommand' => '/usr/bin/file',
+ ),
+ 'group' =>
+ array('maxaliases' => 3),
+ 'oohembed' => array('endpoint' => 'http://oohembed.com/oohembed/'),
+ 'search' =>
+ array('type' => 'fulltext'),
+ 'sessions' =>
+ array('handle' => false, // whether to handle sessions ourselves
+ 'debug' => false), // debugging output for sessions
);
$config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
@@ -178,18 +282,55 @@ if (function_exists('date_default_timezone_set')) {
date_default_timezone_set('UTC');
}
+function addPlugin($name, $attrs = null)
+{
+ $name = ucfirst($name);
+ $pluginclass = "{$name}Plugin";
+
+ if (!class_exists($pluginclass)) {
+
+ $files = array("local/plugins/{$pluginclass}.php",
+ "local/plugins/{$name}/{$pluginclass}.php",
+ "local/{$pluginclass}.php",
+ "local/{$name}/{$pluginclass}.php",
+ "plugins/{$pluginclass}.php",
+ "plugins/{$name}/{$pluginclass}.php");
+
+ foreach ($files as $file) {
+ $fullpath = INSTALLDIR.'/'.$file;
+ if (@file_exists($fullpath)) {
+ include_once($fullpath);
+ break;
+ }
+ }
+ }
+
+ $inst = new $pluginclass();
+
+ if (!empty($attrs)) {
+ foreach ($attrs as $aname => $avalue) {
+ $inst->$aname = $avalue;
+ }
+ }
+ return $inst;
+}
+
// From most general to most specific:
// server-wide, then vhost-wide, then for a path,
// finally for a dir (usually only need one of the last two).
-$_config_files = array('/etc/laconica/laconica.php',
- '/etc/laconica/'.$_server.'.php');
+if (isset($conffile)) {
+ $_config_files = array($conffile);
+} else {
+ $_config_files = array('/etc/laconica/laconica.php',
+ '/etc/laconica/'.$_server.'.php');
-if (strlen($_path) > 0) {
- $_config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php';
-}
+ if (strlen($_path) > 0) {
+ $_config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php';
+ }
-$_config_files[] = INSTALLDIR.'/config.php';
+ $_config_files[] = INSTALLDIR.'/config.php';
+}
$_have_a_config = false;
@@ -218,19 +359,19 @@ if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db']
// XXX: how many of these could be auto-loaded on use?
-require_once('Validate.php');
-require_once('markdown.php');
+require_once 'Validate.php';
+require_once 'markdown.php';
-require_once(INSTALLDIR.'/lib/util.php');
-require_once(INSTALLDIR.'/lib/action.php');
-require_once(INSTALLDIR.'/lib/theme.php');
-require_once(INSTALLDIR.'/lib/mail.php');
-require_once(INSTALLDIR.'/lib/subs.php');
-require_once(INSTALLDIR.'/lib/Shorturl_api.php');
-require_once(INSTALLDIR.'/lib/twitter.php');
+require_once INSTALLDIR.'/lib/util.php';
+require_once INSTALLDIR.'/lib/action.php';
+require_once INSTALLDIR.'/lib/theme.php';
+require_once INSTALLDIR.'/lib/mail.php';
+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');
+require_once INSTALLDIR.'/lib/clientexception.php';
+require_once INSTALLDIR.'/lib/serverexception.php';
// XXX: other formats here
diff --git a/lib/currentuserdesignaction.php b/lib/currentuserdesignaction.php
new file mode 100644
index 000000000..4c7e15a8b
--- /dev/null
+++ b/lib/currentuserdesignaction.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for actions that use the current user's design
+ *
+ * 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 Action
+ * @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);
+}
+
+/**
+ * Base class for actions that use the current user's design
+ *
+ * Some pages (settings in particular) use the current user's chosen
+ * design. This superclass returns that design.
+ *
+ * @category Action
+ * @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 CurrentUserDesignAction extends Action
+{
+
+ /**
+ * Show the user's design stylesheet
+ *
+ * @return nothing
+ */
+
+ function showStylesheets()
+ {
+ parent::showStylesheets();
+
+ $user = common_current_user();
+
+ if (empty($user) || $user->viewdesigns) {
+ $design = $this->getDesign();
+
+ if (!empty($design)) {
+ $design->showCSS($this);
+ }
+ }
+ }
+
+ /**
+ * A design for this action
+ *
+ * if the user attribute has been set, returns that user's
+ * design.
+ *
+ * @return Design a design object to use
+ */
+
+ function getDesign()
+ {
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ return null;
+ }
+
+ return $cur->getDesign();
+ }
+
+}
diff --git a/lib/daemon.php b/lib/daemon.php
index 9c1ae50a0..9d89c63e7 100644
--- a/lib/daemon.php
+++ b/lib/daemon.php
@@ -1,7 +1,7 @@
<?php
/**
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -23,6 +23,13 @@ if (!defined('LACONICA')) {
class Daemon
{
+ var $daemonize = true;
+
+ function __construct($daemonize = true)
+ {
+ $this->daemonize = $daemonize;
+ }
+
function name()
{
return null;
@@ -129,12 +136,16 @@ class Daemon
common_log(LOG_INFO, $this->name() . ' already running. Exiting.');
exit(0);
}
- if ($this->background()) {
- $this->writePidFile();
- $this->changeUser();
- $this->run();
- $this->clearPidFile();
+
+ if ($this->daemonize) {
+ common_log(LOG_INFO, 'Backgrounding daemon "'.$this->name().'"');
+ $this->background();
}
+
+ $this->writePidFile();
+ $this->changeUser();
+ $this->run();
+ $this->clearPidFile();
}
function run()
diff --git a/lib/dberroraction.php b/lib/dberroraction.php
index 0dc92490c..a04e5f74f 100644
--- a/lib/dberroraction.php
+++ b/lib/dberroraction.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/dbqueuemanager.php b/lib/dbqueuemanager.php
new file mode 100644
index 000000000..6e7172de0
--- /dev/null
+++ b/lib/dbqueuemanager.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Simple-minded queue manager for storing items in the database
+ *
+ * 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 QueueManager
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @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/
+ */
+
+class DBQueueManager extends QueueManager
+{
+ var $qis = array();
+
+ function enqueue($object, $queue)
+ {
+ $notice = $object;
+
+ $qi = new Queue_item();
+
+ $qi->notice_id = $notice->id;
+ $qi->transport = $queue;
+ $qi->created = $notice->created;
+ $result = $qi->insert();
+
+ if (!$result) {
+ common_log_db_error($qi, 'INSERT', __FILE__);
+ throw new ServerException('DB error inserting queue item');
+ }
+
+ return true;
+ }
+
+ function service($queue, $handler)
+ {
+ while (true) {
+ $this->_log(LOG_DEBUG, 'Checking for notices...');
+ $notice = $this->_nextItem($queue, null);
+ if (empty($notice)) {
+ $this->_log(LOG_DEBUG, 'No notices waiting; idling.');
+ // Nothing in the queue. Do you
+ // have other tasks, like servicing your
+ // XMPP connection, to do?
+ $handler->idle(QUEUE_HANDLER_MISS_IDLE);
+ } else {
+ $this->_log(LOG_INFO, 'Got notice '. $notice->id);
+ // Yay! Got one!
+ if ($handler->handle_notice($notice)) {
+ $this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id);
+ $this->_done($notice, $queue);
+ } else {
+ $this->_log(LOG_INFO, 'Failed to handle notice '. $notice->id);
+ $this->_fail($notice, $queue);
+ }
+ // Chance to e.g. service your XMPP connection
+ $this->_log(LOG_DEBUG, 'Idling after success.');
+ $handler->idle(QUEUE_HANDLER_HIT_IDLE);
+ }
+ // XXX: when do we give up?
+ }
+ }
+
+ function _nextItem($queue, $timeout=null)
+ {
+ $start = time();
+ $result = null;
+
+ do {
+ $qi = Queue_item::top($queue);
+ if (!empty($qi)) {
+ $notice = Notice::staticGet('id', $qi->notice_id);
+ if (!empty($notice)) {
+ $result = $notice;
+ } else {
+ $this->_log(LOG_INFO, 'dequeued non-existent notice ' . $notice->id);
+ $qi->delete();
+ $qi->free();
+ $qi = null;
+ }
+ }
+ } while (empty($result) && (is_null($timeout) || (time() - $start) < $timeout));
+
+ return $result;
+ }
+
+ function _done($object, $queue)
+ {
+ // XXX: right now, we only handle notices
+
+ $notice = $object;
+
+ $qi = Queue_item::pkeyGet(array('notice_id' => $notice->id,
+ 'transport' => $queue));
+
+ if (empty($qi)) {
+ $this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue);
+ } else {
+ if (empty($qi->claimed)) {
+ $this->_log(LOG_WARNING, 'Reluctantly releasing unclaimed queue item '.
+ 'for '.$notice->id.', queue '.$queue);
+ }
+ $qi->delete();
+ $qi->free();
+ $qi = null;
+ }
+
+ $this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id);
+
+ $notice->free();
+ $notice = null;
+ }
+
+ function _fail($object, $queue)
+ {
+ // XXX: right now, we only handle notices
+
+ $notice = $object;
+
+ $qi = Queue_item::pkeyGet(array('notice_id' => $notice->id,
+ 'transport' => $queue));
+
+ if (empty($qi)) {
+ $this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue);
+ } else {
+ if (empty($qi->claimed)) {
+ $this->_log(LOG_WARNING, 'Ignoring failure for unclaimed queue item '.
+ 'for '.$notice->id.', queue '.$queue);
+ } else {
+ $orig = clone($qi);
+ $qi->claimed = null;
+ $qi->update($orig);
+ $qi = null;
+ }
+ }
+
+ $this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id);
+
+ $notice->free();
+ $notice = null;
+ }
+
+ function _log($level, $msg)
+ {
+ common_log($level, 'DBQueueManager: '.$msg);
+ }
+}
diff --git a/lib/designsettings.php b/lib/designsettings.php
new file mode 100644
index 000000000..fbffdb208
--- /dev/null
+++ b/lib/designsettings.php
@@ -0,0 +1,477 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Change user password
+ *
+ * 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 Settings
+ * @package Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @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/accountsettingsaction.php';
+require_once INSTALLDIR . '/lib/webcolor.php';
+
+/**
+ * Base class for setting a user or group design
+ *
+ * Shows the design setting form and also handles some things like saving
+ * background images, and fetching a default design
+ *
+ * @category Settings
+ * @package Laconica
+ * @author Zach Copley <zach@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 DesignSettingsAction extends AccountSettingsAction
+{
+
+ var $submitaction = null;
+
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('Profile design');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('Customize the way your profile looks ' .
+ 'with a background image and a colour palette of your choice.');
+ }
+
+ /**
+ * Shows the design settings form
+ *
+ * @param Design $design a working design to show
+ *
+ * @return nothing
+ */
+
+ function showDesignForm($design)
+ {
+
+ $this->elementStart('form', array('method' => 'post',
+ 'enctype' => 'multipart/form-data',
+ 'id' => 'form_settings_design',
+ 'class' => 'form_settings',
+ 'action' => $this->submitaction));
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset', array('id' =>
+ 'settings_design_background-image'));
+ $this->element('legend', null, _('Change background image'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'design_background-image_file'),
+ _('Upload file'));
+ $this->element('input', array('name' => 'design_background-image_file',
+ 'type' => 'file',
+ 'id' => 'design_background-image_file'));
+ $this->element('p', 'form_guide', _('You can upload your personal ' .
+ 'background image. The maximum file size is 2Mb.'));
+ $this->element('input', array('name' => 'MAX_FILE_SIZE',
+ 'type' => 'hidden',
+ 'id' => 'MAX_FILE_SIZE',
+ 'value' => ImageFile::maxFileSizeInt()));
+ $this->elementEnd('li');
+
+ if (!empty($design->backgroundimage)) {
+
+ $this->elementStart('li', array('id' =>
+ 'design_background-image_onoff'));
+
+ $this->element('img', array('src' =>
+ Design::url($design->backgroundimage)));
+
+ $attrs = array('name' => 'design_background-image_onoff',
+ 'type' => 'radio',
+ 'id' => 'design_background-image_on',
+ 'class' => 'radio',
+ 'value' => 'on');
+
+ if ($design->disposition & BACKGROUND_ON) {
+ $attrs['checked'] = 'checked';
+ }
+
+ $this->element('input', $attrs);
+
+ $this->element('label', array('for' => 'design_background-image_on',
+ 'class' => 'radio'),
+ _('On'));
+
+ $attrs = array('name' => 'design_background-image_onoff',
+ 'type' => 'radio',
+ 'id' => 'design_background-image_off',
+ 'class' => 'radio',
+ 'value' => 'off');
+
+ if ($design->disposition & BACKGROUND_OFF) {
+ $attrs['checked'] = 'checked';
+ }
+
+ $this->element('input', $attrs);
+
+ $this->element('label', array('for' => 'design_background-image_off',
+ 'class' => 'radio'),
+ _('Off'));
+ $this->element('p', 'form_guide', _('Turn background image on or off.'));
+ $this->elementEnd('li');
+
+ $this->elementStart('li');
+ $this->checkbox('design_background-image_repeat',
+ _('Tile background image'),
+ ($design->disposition & BACKGROUND_TILE) ? true : false);
+ $this->elementEnd('li');
+ }
+
+ $this->elementEnd('ul');
+ $this->elementEnd('fieldset');
+
+ $this->elementStart('fieldset', array('id' => 'settings_design_color'));
+ $this->element('legend', null, _('Change colours'));
+ $this->elementStart('ul', 'form_data');
+
+ try {
+
+ $bgcolor = new WebColor($design->backgroundcolor);
+
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'swatch-1'), _('Background'));
+ $this->element('input', array('name' => 'design_background',
+ 'type' => 'text',
+ 'id' => 'swatch-1',
+ 'class' => 'swatch',
+ 'maxlength' => '7',
+ 'size' => '7',
+ 'value' => '#' . $bgcolor->hexValue()));
+ $this->elementEnd('li');
+
+ $ccolor = new WebColor($design->contentcolor);
+
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'swatch-2'), _('Content'));
+ $this->element('input', array('name' => 'design_content',
+ 'type' => 'text',
+ 'id' => 'swatch-2',
+ 'class' => 'swatch',
+ 'maxlength' => '7',
+ 'size' => '7',
+ 'value' => '#' . $ccolor->hexValue()));
+ $this->elementEnd('li');
+
+ $sbcolor = new WebColor($design->sidebarcolor);
+
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'swatch-3'), _('Sidebar'));
+ $this->element('input', array('name' => 'design_sidebar',
+ 'type' => 'text',
+ 'id' => 'swatch-3',
+ 'class' => 'swatch',
+ 'maxlength' => '7',
+ 'size' => '7',
+ 'value' => '#' . $sbcolor->hexValue()));
+ $this->elementEnd('li');
+
+ $tcolor = new WebColor($design->textcolor);
+
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'swatch-4'), _('Text'));
+ $this->element('input', array('name' => 'design_text',
+ 'type' => 'text',
+ 'id' => 'swatch-4',
+ 'class' => 'swatch',
+ 'maxlength' => '7',
+ 'size' => '7',
+ 'value' => '#' . $tcolor->hexValue()));
+ $this->elementEnd('li');
+
+ $lcolor = new WebColor($design->linkcolor);
+
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'swatch-5'), _('Links'));
+ $this->element('input', array('name' => 'design_links',
+ 'type' => 'text',
+ 'id' => 'swatch-5',
+ 'class' => 'swatch',
+ 'maxlength' => '7',
+ 'size' => '7',
+ 'value' => '#' . $lcolor->hexValue()));
+ $this->elementEnd('li');
+
+ } catch (WebColorException $e) {
+ common_log(LOG_ERR, 'Bad color values in design ID: ' .$design->id);
+ }
+
+ $this->elementEnd('ul');
+ $this->elementEnd('fieldset');
+
+ $this->submit('defaults', _('Use defaults'), 'submit form_action-default',
+ 'defaults', _('Restore default designs'));
+
+ $this->element('input', array('id' => 'settings_design_reset',
+ 'type' => 'reset',
+ 'value' => 'Reset',
+ 'class' => 'submit form_action-primary',
+ 'title' => _('Reset back to default')));
+
+ $this->submit('save', _('Save'), 'submit form_action-secondary',
+ 'save', _('Save design'));
+
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ /**
+ * Handle a post
+ *
+ * Validate input and save changes. Reload the form with a success
+ * or error message.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // XXX: Robin's workaround for a bug in PHP where $_POST
+ // and $_FILE are empty in the case that the uploaded
+ // file is bigger than PHP is configured to handle.
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (empty($_POST) && $_SERVER['CONTENT_LENGTH']) {
+
+ $msg = _('The server was unable to handle that much POST ' .
+ 'data (%s bytes) due to its current configuration.');
+
+ $this->showForm(sprintf($msg, $_SERVER['CONTENT_LENGTH']));
+ }
+ }
+
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('save')) {
+ $this->saveDesign();
+ } else if ($this->arg('defaults')) {
+ $this->restoreDefaults();
+ } else {
+ $this->showForm(_('Unexpected form submission.'));
+ }
+ }
+
+ /**
+ * Add the Farbtastic stylesheet
+ *
+ * @return void
+ */
+
+ function showStylesheets()
+ {
+ parent::showStylesheets();
+ $farbtasticStyle =
+ common_path('theme/base/css/farbtastic.css?version='.LACONICA_VERSION);
+
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => $farbtasticStyle,
+ 'media' => 'screen, projection, tv'));
+ }
+
+ /**
+ * Add the Farbtastic scripts
+ *
+ * @return void
+ */
+
+ function showScripts()
+ {
+ parent::showScripts();
+
+ $farbtasticPack = common_path('js/farbtastic/farbtastic.js');
+ $userDesignGo = common_path('js/userdesign.go.js');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => $farbtasticPack));
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => $userDesignGo));
+ }
+
+ /**
+ * Get a default design
+ *
+ * @return Design design
+ */
+
+ function defaultDesign()
+ {
+ $defaults = common_config('site', 'design');
+
+ $design = new Design();
+
+ try {
+
+ $color = new WebColor();
+
+ $color->parseColor($defaults['backgroundcolor']);
+ $design->backgroundcolor = $color->intValue();
+
+ $color->parseColor($defaults['contentcolor']);
+ $design->contentcolor = $color->intValue();
+
+ $color->parseColor($defaults['sidebarcolor']);
+ $design->sidebarcolor = $color->intValue();
+
+ $color->parseColor($defaults['textcolor']);
+ $design->textcolor = $color->intValue();
+
+ $color->parseColor($defaults['linkcolor']);
+ $design->linkcolor = $color->intValue();
+
+ $design->backgroundimage = $defaults['backgroundimage'];
+
+ $design->disposition = $defaults['disposition'];
+
+ } catch (WebColorException $e) {
+ common_log(LOG_ERR, _('Bad default color settings: ' .
+ $e->getMessage()));
+ }
+
+ return $design;
+ }
+
+ /**
+ * Save the background image, if any, and set its disposition
+ *
+ * @param Design $design a working design to attach the img to
+ *
+ * @return nothing
+ */
+
+ function saveBackgroundImage($design)
+ {
+
+ // Now that we have a Design ID we can add a file to the design.
+ // XXX: This is an additional DB hit, but figured having the image
+ // associated with the Design rather than the User was worth
+ // it. -- Zach
+
+ if ($_FILES['design_background-image_file']['error'] ==
+ UPLOAD_ERR_OK) {
+
+ $filepath = null;
+
+ try {
+ $imagefile =
+ ImageFile::fromUpload('design_background-image_file');
+ } catch (Exception $e) {
+ $this->showForm($e->getMessage());
+ return;
+ }
+
+ $filename = Design::filename($design->id,
+ image_type_to_extension($imagefile->type),
+ common_timestamp());
+
+ $filepath = Design::path($filename);
+
+ move_uploaded_file($imagefile->filepath, $filepath);
+
+ // delete any old backround img laying around
+
+ if (isset($design->backgroundimage)) {
+ @unlink(Design::path($design->backgroundimage));
+ }
+
+ $original = clone($design);
+
+ $design->backgroundimage = $filename;
+
+ // default to on, no tile
+
+ $design->setDisposition(true, false, false);
+
+ $result = $design->update($original);
+
+ if ($result === false) {
+ common_log_db_error($design, 'UPDATE', __FILE__);
+ $this->showForm(_('Couldn\'t update your design.'));
+ return;
+ }
+ }
+ }
+
+ /**
+ * Restore the user or group design to system defaults
+ *
+ * @return nothing
+ */
+
+ function restoreDefaults()
+ {
+ $design = $this->getWorkingDesign();
+ $default = $this->defaultDesign();
+ $original = clone($design);
+
+ $design->backgroundcolor = $default->backgroundcolor;
+ $design->contentcolor = $default->contentcolor;
+ $design->sidebarcolor = $default->sidebarcolor;
+ $design->textcolor = $default->textcolor;
+ $design->linkcolor = $default->linkcolor;
+
+ $design->setDisposition(false, true, false);
+
+ $result = $design->update($original);
+
+ if ($result === false) {
+ common_log_db_error($design, 'UPDATE', __FILE__);
+ $this->showForm(_('Couldn\'t update your design.'));
+ return;
+ }
+
+ $this->showForm(_('Design defaults restored.'), true);
+ }
+
+}
diff --git a/lib/error.php b/lib/error.php
index 282682133..bbf9987cf 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/event.php b/lib/event.php
index d815ae54b..4ccee17e6 100644
--- a/lib/event.php
+++ b/lib/event.php
@@ -110,4 +110,32 @@ class Event {
}
return ($result !== false);
}
+
+ /**
+ * Check to see if an event handler exists
+ *
+ * Look to see if there's any handler for a given event, or narrow
+ * by providing the name of a specific plugin class.
+ *
+ * @param string $name Name of the event to look for
+ * @param string $plugin Optional name of the plugin class to look for
+ *
+ * @return boolean flag saying whether such a handler exists
+ *
+ */
+
+ public static function hasHandler($name, $plugin=null) {
+ if (array_key_exists($name, Event::$_handlers)) {
+ if (isset($plugin)) {
+ foreach (Event::$_handlers[$name] as $handler) {
+ if (get_class($handler[0]) == $plugin) {
+ return true;
+ }
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/lib/facebookaction.php b/lib/facebookaction.php
index 043a078cd..1ae90d53b 100644
--- a/lib/facebookaction.php
+++ b/lib/facebookaction.php
@@ -38,14 +38,14 @@ require_once INSTALLDIR.'/lib/noticeform.php';
class FacebookAction extends Action
{
-
+
var $facebook = null;
var $fbuid = null;
var $flink = null;
var $action = null;
var $app_uri = null;
var $app_name = null;
-
+
/**
* Constructor
*
@@ -60,71 +60,71 @@ class FacebookAction extends Action
function __construct($output='php://output', $indent=true, $facebook=null, $flink=null)
{
parent::__construct($output, $indent);
-
+
$this->facebook = $facebook;
$this->flink = $flink;
-
+
if ($this->flink) {
- $this->fbuid = $flink->foreign_id;
+ $this->fbuid = $flink->foreign_id;
$this->user = $flink->getUser();
}
-
+
$this->args = array();
}
-
+
function prepare($argarray)
- {
+ {
parent::prepare($argarray);
-
+
$this->facebook = getFacebook();
$this->fbuid = $this->facebook->require_login();
-
+
$this->action = $this->trimmed('action');
-
+
$app_props = $this->facebook->api_client->Admin_getAppProperties(
array('canvas_name', 'application_name'));
-
+
$this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name'];
$this->app_name = $app_props['application_name'];
$this->flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
-
+
return true;
-
+
}
-
+
function showStylesheets()
{
// Add a timestamp to the file so Facebook cache wont ignore our changes
$ts = filemtime(INSTALLDIR.'/theme/base/css/display.css');
-
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', 'base') . '?ts=' . $ts));
-
+
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', 'base') . '?ts=' . $ts));
+
$theme = common_config('site', 'theme');
-
+
$ts = filemtime(INSTALLDIR. '/theme/' . $theme .'/css/display.css');
-
+
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/display.css', null) . '?ts=' . $ts));
-
+
$ts = filemtime(INSTALLDIR.'/theme/base/css/facebookapp.css');
-
+
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/facebookapp.css', 'base') . '?ts=' . $ts));
}
-
+
function showScripts()
{
// Add a timestamp to the file so Facebook cache wont ignore our changes
$ts = filemtime(INSTALLDIR.'/js/facebookapp.js');
-
+
$this->element('script', array('src' => common_path('js/facebookapp.js') . '?ts=' . $ts));
}
-
+
/**
* Start an Facebook ready HTML document
*
@@ -138,11 +138,11 @@ class FacebookAction extends Action
* @return void
*/
- function startHTML($type=null)
- {
+ function startHTML($type=null)
+ {
$this->showStylesheets();
$this->showScripts();
-
+
$this->elementStart('div', array('class' => 'facebook-page'));
}
@@ -177,18 +177,18 @@ class FacebookAction extends Action
$this->showFooter();
$this->elementEnd('div');
}
-
+
function showAside()
{
}
function showHead($error, $success)
{
-
+
if ($error) {
$this->element("h1", null, $error);
}
-
+
if ($success) {
$this->element("h1", null, $success);
}
@@ -198,10 +198,10 @@ class FacebookAction extends Action
$this->element('fb:add-section-button', array('section' => 'profile'));
$this->elementEnd('span');
$this->elementEnd('fb:if-section-not-added');
-
+
}
-
+
// Make this into a widget later
function showLocalNav()
{
@@ -213,12 +213,14 @@ class FacebookAction extends Action
array('href' => 'index.php', 'title' => _('Home')), _('Home'));
$this->elementEnd('li');
- $this->elementStart('li',
- array('class' =>
- ($this->action == 'facebookinvite') ? 'current' : 'facebook_invite'));
- $this->element('a',
- array('href' => 'invite.php', 'title' => _('Invite')), _('Invite'));
- $this->elementEnd('li');
+ if (common_config('invite', 'enabled')) {
+ $this->elementStart('li',
+ array('class' =>
+ ($this->action == 'facebookinvite') ? 'current' : 'facebook_invite'));
+ $this->element('a',
+ array('href' => 'invite.php', 'title' => _('Invite')), _('Invite'));
+ $this->elementEnd('li');
+ }
$this->elementStart('li',
array('class' =>
@@ -229,8 +231,8 @@ class FacebookAction extends Action
$this->elementEnd('li');
$this->elementEnd('ul');
- }
-
+ }
+
/**
* Show header of the page.
*
@@ -245,7 +247,7 @@ class FacebookAction extends Action
$this->showNoticeForm();
$this->elementEnd('div');
}
-
+
/**
* Show page, a template method.
*
@@ -258,7 +260,7 @@ class FacebookAction extends Action
$this->showBody();
$this->endHTML();
}
-
+
function showInstructions()
{
@@ -278,7 +280,7 @@ class FacebookAction extends Action
$this->element('a',
array('href' => common_local_url('register')), _('Register'));
$this->text($loginmsg_part2);
- $this->elementEnd('p');
+ $this->elementEnd('p');
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -317,7 +319,7 @@ class FacebookAction extends Action
$this->elementEnd('ul');
$this->submit('submit', _('Login'));
- $this->elementEnd('fieldset');
+ $this->elementEnd('fieldset');
$this->elementEnd('form');
$this->elementStart('p');
@@ -329,73 +331,73 @@ class FacebookAction extends Action
$this->elementEnd('div');
}
-
-
+
+
function updateProfileBox($notice)
{
// Need to include inline CSS for styling the Profile box
- $app_props = $this->facebook->api_client->Admin_getAppProperties(array('icon_url'));
- $icon_url = $app_props['icon_url'];
+ $app_props = $this->facebook->api_client->Admin_getAppProperties(array('icon_url'));
+ $icon_url = $app_props['icon_url'];
$style = '<style>
- .entry-title *,
- .entry-content * {
- font-size:14px;
- font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif;
- }
- .entry-title a,
- .entry-content a {
- color:#002E6E;
- }
+ .entry-title *,
+ .entry-content * {
+ font-size:14px;
+ font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif;
+ }
+ .entry-title a,
+ .entry-content a {
+ color:#002E6E;
+ }
.entry-title .vcard .photo {
float:left;
display:inline;
- margin-right:11px;
- margin-bottom:11px
+ margin-right:11px;
+ margin-bottom:11px
}
- .entry-title {
- margin-bottom:11px;
- }
+ .entry-title {
+ margin-bottom:11px;
+ }
.entry-title p.entry-content {
display:inline;
- margin-left:5px;
+ margin-left:5px;
}
- div.entry-content {
- clear:both;
- }
+ div.entry-content {
+ clear:both;
+ }
div.entry-content dl,
div.entry-content dt,
div.entry-content dd {
display:inline;
- text-transform:lowercase;
+ text-transform:lowercase;
}
div.entry-content dd,
- div.entry-content .device dt {
- margin-left:0;
- margin-right:5px;
+ div.entry-content .device dt {
+ margin-left:0;
+ margin-right:5px;
}
div.entry-content dl.timestamp dt,
- div.entry-content dl.response dt {
+ div.entry-content dl.response dt {
display:none;
}
div.entry-content dd a {
display:inline-block;
}
- #facebook_laconica_app {
- text-indent:-9999px;
- height:16px;
- width:16px;
- display:block;
- background:url('.$icon_url.') no-repeat 0 0;
- float:right;
- }
- </style>';
+ #facebook_laconica_app {
+ text-indent:-9999px;
+ height:16px;
+ width:16px;
+ display:block;
+ background:url('.$icon_url.') no-repeat 0 0;
+ float:right;
+ }
+ </style>';
$this->xw->openMemory();
@@ -407,12 +409,12 @@ class FacebookAction extends Action
$fbml_main = "<fb:narrow>$style " . $this->xw->outputMemory(false) . "</fb:narrow>";
- $this->facebook->api_client->profile_setFBML(null, $this->fbuid, $fbml, null, null, $fbml_main);
+ $this->facebook->api_client->profile_setFBML(null, $this->fbuid, $fbml, null, null, $fbml_main);
$this->xw->openURI('php://output');
}
-
-
+
+
/**
* Generate pagination links
*
@@ -457,24 +459,24 @@ class FacebookAction extends Action
$this->elementEnd('div');
}
}
-
- function updateFacebookStatus($notice)
+
+ function updateFacebookStatus($notice)
{
$prefix = $this->facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $this->fbuid);
$content = "$prefix $notice->content";
-
+
if ($this->facebook->api_client->users_hasAppPermission('status_update', $this->fbuid)) {
$this->facebook->api_client->users_setStatus($content, $this->fbuid, false, true);
}
}
-
+
function saveNewNotice()
{
$user = $this->flink->getUser();
$content = $this->trimmed('status_textarea');
-
+
if (!$content) {
$this->showPage(_('No notice content!'));
return;
@@ -492,9 +494,9 @@ class FacebookAction extends Action
$cmd = $inter->handle_command($user, $content_shortened);
if ($cmd) {
-
+
// XXX fix this
-
+
$cmd->execute(new WebChannel());
return;
}
@@ -510,20 +512,20 @@ class FacebookAction extends Action
}
common_broadcast_notice($notice);
-
+
// Also update the user's Facebook status
$this->updateFacebookStatus($notice);
$this->updateProfileBox($notice);
-
+
}
}
-class FacebookNoticeForm extends NoticeForm
+class FacebookNoticeForm extends NoticeForm
{
-
+
var $post_action = null;
-
+
/**
* Constructor
*
@@ -532,13 +534,13 @@ class FacebookNoticeForm extends NoticeForm
* @param string $content content to pre-fill
*/
- function __construct($out=null, $action=null, $content=null,
+ function __construct($out=null, $action=null, $content=null,
$post_action=null, $user=null)
{
parent::__construct($out, $action, $content, $user);
$this->post_action = $post_action;
}
-
+
/**
* Action of the form
*
@@ -554,7 +556,7 @@ class FacebookNoticeForm extends NoticeForm
class FacebookNoticeList extends NoticeList
{
-
+
/**
* constructor
*
@@ -565,7 +567,7 @@ class FacebookNoticeList extends NoticeList
{
parent::__construct($notice, $out);
}
-
+
/**
* show the list of notices
*
@@ -619,7 +621,7 @@ class FacebookNoticeList extends NoticeList
}
class FacebookNoticeListItem extends NoticeListItem
-{
+{
/**
* constructor
@@ -646,51 +648,19 @@ class FacebookNoticeListItem extends NoticeListItem
function show()
{
$this->showStart();
+ $this->showNotice();
+ $this->showNoticeInfo();
- $this->out->elementStart('div', 'entry-title');
- $this->showAuthor();
- $this->showContent();
- $this->out->elementEnd('div');
-
- $this->out->elementStart('div', 'entry-content');
- $this->showNoticeLink();
- $this->showNoticeSource();
- $this->showReplyTo();
- $this->out->elementEnd('div');
+ // XXX: Need to update to show attachements and controls
$this->showEnd();
}
- function showNoticeLink()
- {
- $noticeurl = common_local_url('shownotice',
- array('notice' => $this->notice->id));
- // XXX: we need to figure this out better. Is this right?
- if (strcmp($this->notice->uri, $noticeurl) != 0 &&
- preg_match('/^http/', $this->notice->uri)) {
- $noticeurl = $this->notice->uri;
- }
-
- $this->out->elementStart('dl', 'timestamp');
- $this->out->element('dt', null, _('Published'));
- $this->out->elementStart('dd', null);
- $this->out->elementStart('a', array('rel' => 'bookmark',
- 'href' => $noticeurl));
- $dt = common_date_iso8601($this->notice->created);
- $this->out->element('abbr', array('class' => 'published',
- 'title' => $dt),
- common_date_string($this->notice->created));
- $this->out->elementEnd('a');
- $this->out->elementEnd('dd');
- $this->out->elementEnd('dl');
- }
-
}
-
class FacebookProfileBoxNotice extends FacebookNoticeListItem
-{
-
+{
+
/**
* constructor
*
@@ -703,36 +673,24 @@ class FacebookProfileBoxNotice extends FacebookNoticeListItem
{
parent::__construct($notice, $out);
}
-
+
/**
- * Recipe function for displaying a single notice in the
- * Facebook App's Profile
+ * Recipe function for displaying a single notice in the
+ * Facebook App profile notice box
*
* @return void
*/
function show()
{
-
- $this->out->elementStart('div', 'entry-title');
- $this->showAuthor();
- $this->showContent();
- $this->out->elementEnd('div');
-
- $this->out->elementStart('div', 'entry-content');
-
- $this->showNoticeLink();
- $this->showNoticeSource();
- $this->showReplyTo();
- $this->out->elementEnd('div');
-
+ $this->showNotice();
+ $this->showNoticeInfo();
$this->showAppLink();
-
}
- function showAppLink()
+ function showAppLink()
{
-
+
$this->facebook = getFacebook();
$app_props = $this->facebook->api_client->Admin_getAppProperties(
@@ -740,7 +698,7 @@ class FacebookProfileBoxNotice extends FacebookNoticeListItem
$this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name'];
$this->app_name = $app_props['application_name'];
-
+
$this->out->elementStart('a', array('id' => 'facebook_laconica_app',
'href' => $this->app_uri));
$this->out->text($this->app_name);
diff --git a/lib/facebookutil.php b/lib/facebookutil.php
index 242d2e06f..632ec4bad 100644
--- a/lib/facebookutil.php
+++ b/lib/facebookutil.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -51,6 +51,10 @@ function updateProfileBox($facebook, $flink, $notice) {
function isFacebookBound($notice, $flink) {
+ if (empty($flink)) {
+ return false;
+ }
+
// 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 " .
@@ -86,10 +90,10 @@ function isFacebookBound($notice, $flink) {
if ($result != 1) {
$user = $flink->getUser();
- $msg = "Can't send notice $notice->id to Facebook " .
+ $msg = "Not sending notice $notice->id to Facebook " .
"because user $user->nickname hasn't given the " .
'Facebook app \'status_update\' permission.';
- common_log(LOG_INFO, $msg);
+ common_debug($msg);
$success = false;
}
@@ -108,13 +112,16 @@ function facebookBroadcastNotice($notice)
{
$facebook = getFacebook();
$flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE);
- $fbuid = $flink->foreign_id;
if (isFacebookBound($notice, $flink)) {
$status = null;
+ $fbuid = $flink->foreign_id;
+
+ $user = $flink->getUser();
// Get the status 'verb' (prefix) the user has set
+
try {
$prefix = $facebook->api_client->
data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
@@ -122,23 +129,79 @@ function facebookBroadcastNotice($notice)
$status = "$prefix $notice->content";
} catch(FacebookRestClientException $e) {
- common_log(LOG_ERR, $e->getMessage());
- return false;
+ common_log(LOG_WARNING, $e->getMessage());
+ common_log(LOG_WARNING,
+ 'Unable to get the status verb setting from Facebook ' .
+ "for $user->nickname (user id: $user->id).");
}
- // Okay, we're good to go!
+ // Okay, we're good to go, update the FB status
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;
+ common_log(LOG_ERR,
+ 'Unable to update Facebook status for ' .
+ "$user->nickname (user id: $user->id)!");
+
+ $code = $e->getCode();
- // Should we remove flink if this fails?
+ if ($code >= 200) {
+
+ // 200 The application does not have permission to operate on the passed in uid parameter.
+ // 250 Updating status requires the extended permission status_update.
+ // see: http://wiki.developers.facebook.com/index.php/Users.setStatus#Example_Return_XML
+
+ remove_facebook_app($flink);
+ }
+
+ }
+
+ // Now try to update the profile box
+
+ try {
+ updateProfileBox($facebook, $flink, $notice);
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_WARNING, $e->getMessage());
+ common_log(LOG_WARNING,
+ 'Unable to update Facebook profile box for ' .
+ "$user->nickname (user id: $user->id).");
}
}
return true;
}
+
+function remove_facebook_app($flink)
+{
+
+ $user = $flink->getUser();
+
+ common_log(LOG_INFO, 'Removing Facebook App Foreign link for ' .
+ "user $user->nickname (user id: $user->id).");
+
+ $result = $flink->delete();
+
+ if (empty($result)) {
+ common_log(LOG_ERR, 'Could not remove Facebook App ' .
+ "Foreign_link for $user->nickname (user id: $user->id)!");
+ common_log_db_error($flink, 'DELETE', __FILE__);
+ }
+
+ // Notify the user that we are removing their FB app access
+
+ $result = mail_facebook_app_removed($user);
+
+ if (!$result) {
+
+ $msg = 'Unable to send email to notify ' .
+ "$user->nickname (user id: $user->id) " .
+ 'that their Facebook app link was ' .
+ 'removed!';
+
+ common_log(LOG_WARNING, $msg);
+ }
+
+}
diff --git a/lib/form.php b/lib/form.php
index 5317df471..f872aef0b 100644
--- a/lib/form.php
+++ b/lib/form.php
@@ -52,6 +52,8 @@ require_once INSTALLDIR.'/lib/widget.php';
class Form extends Widget
{
+ var $enctype = null;
+
/**
* Show the form
*
@@ -63,11 +65,15 @@ class Form extends Widget
function show()
{
- $this->out->elementStart('form',
- array('id' => $this->id(),
- 'class' => $this->formClass(),
- 'method' => 'post',
- 'action' => $this->action()));
+ $attributes = array('id' => $this->id(),
+ 'class' => $this->formClass(),
+ 'method' => 'post',
+ 'action' => $this->action());
+
+ if (!empty($this->enctype)) {
+ $attributes['enctype'] = $this->enctype;
+ }
+ $this->out->elementStart('form', $attributes);
$this->out->elementStart('fieldset');
$this->formLegend();
$this->sessionToken();
diff --git a/lib/galleryaction.php b/lib/galleryaction.php
index 8fa11a756..b389fc00f 100644
--- a/lib/galleryaction.php
+++ b/lib/galleryaction.php
@@ -1,7 +1,7 @@
<?php
/**
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -27,10 +27,9 @@ require_once INSTALLDIR.'/lib/profilelist.php';
define('AVATARS_PER_PAGE', 80);
-class GalleryAction extends Action
+class GalleryAction extends OwnerDesignAction
{
var $profile = null;
- var $user = null;
var $page = null;
var $tag = null;
diff --git a/lib/groupdesignaction.php b/lib/groupdesignaction.php
new file mode 100644
index 000000000..58777c283
--- /dev/null
+++ b/lib/groupdesignaction.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for actions that use the current user's design
+ *
+ * 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 Action
+ * @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);
+}
+
+/**
+ * Base class for actions that use a group's design
+ *
+ * Pages related to groups can be themed with a design.
+ * This superclass returns that design.
+ *
+ * @category Action
+ * @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 GroupDesignAction extends Action {
+
+ /** The group in question */
+ var $group = null;
+
+ /**
+ * Show the groups's design stylesheet
+ *
+ * @return nothing
+ */
+ function showStylesheets()
+ {
+ parent::showStylesheets();
+
+ $user = common_current_user();
+
+ if (empty($user) || $user->viewdesigns) {
+ $design = $this->getDesign();
+
+ if (!empty($design)) {
+ $design->showCSS($this);
+ }
+ }
+ }
+
+ /**
+ * A design for this action
+ *
+ * if the group attribute has been set, returns that group's
+ * design.
+ *
+ * @return Design a design object to use
+ */
+
+ function getDesign()
+ {
+ if (empty($this->group)) {
+ return null;
+ }
+
+ return $this->group->getDesign();
+ }
+}
diff --git a/lib/groupeditform.php b/lib/groupeditform.php
index ca674f3c8..fbb39129b 100644
--- a/lib/groupeditform.php
+++ b/lib/groupeditform.php
@@ -111,7 +111,6 @@ class GroupEditForm extends Form
}
}
-
/**
* Name of the form
*
@@ -131,32 +130,58 @@ class GroupEditForm extends Form
function formData()
{
+ if ($this->group) {
+ $id = $this->group->id;
+ $nickname = $this->group->nickname;
+ $fullname = $this->group->fullname;
+ $homepage = $this->group->homepage;
+ $description = $this->group->description;
+ $location = $this->group->location;
+ } else {
+ $id = '';
+ $nickname = '';
+ $fullname = '';
+ $homepage = '';
+ $description = '';
+ $location = '';
+ }
+
$this->out->elementStart('ul', 'form_data');
$this->out->elementStart('li');
- $this->out->hidden('groupid', $this->group->id);
+ $this->out->hidden('groupid', $id);
$this->out->input('nickname', _('Nickname'),
- ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $this->group->nickname,
+ ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname,
_('1-64 lowercase letters or numbers, no punctuation or spaces'));
$this->out->elementEnd('li');
$this->out->elementStart('li');
$this->out->input('fullname', _('Full name'),
- ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $this->group->fullname);
+ ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname);
$this->out->elementEnd('li');
$this->out->elementStart('li');
$this->out->input('homepage', _('Homepage'),
- ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $this->group->homepage,
+ ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
_('URL of the homepage or blog of the group or topic'));
$this->out->elementEnd('li');
$this->out->elementStart('li');
$this->out->textarea('description', _('Description'),
- ($this->out->arg('description')) ? $this->out->arg('description') : $this->group->description,
+ ($this->out->arg('description')) ? $this->out->arg('description') : $description,
_('Describe the group or topic in 140 chars'));
$this->out->elementEnd('li');
$this->out->elementStart('li');
$this->out->input('location', _('Location'),
- ($this->out->arg('location')) ? $this->out->arg('location') : $this->group->location,
+ ($this->out->arg('location')) ? $this->out->arg('location') : $location,
_('Location for the group, if any, like "City, State (or Region), Country"'));
$this->out->elementEnd('li');
+ if (common_config('group', 'maxaliases') > 0) {
+ $aliases = (empty($this->group)) ? array() : $this->group->getAliases();
+ $this->out->elementStart('li');
+ $this->out->input('aliases', _('Aliases'),
+ ($this->out->arg('aliases')) ? $this->out->arg('aliases') :
+ (!empty($aliases)) ? implode(' ', $aliases) : '',
+ sprintf(_('Extra nicknames for the group, comma- or space- separated, max %d'),
+ common_config('group', 'maxaliases')));;
+ $this->out->elementEnd('li');
+ }
$this->out->elementEnd('ul');
}
diff --git a/lib/grouplist.php b/lib/grouplist.php
index 1b8547499..1ded5160b 100644
--- a/lib/grouplist.php
+++ b/lib/grouplist.php
@@ -166,7 +166,7 @@ class GroupList extends Widget
if ($user->isMember($this->group)) {
$lf = new LeaveForm($this->out, $this->group);
$lf->show();
- } else {
+ } else if (!Group_block::isBlocked($this->group, $user->getProfile())) {
$jf = new JoinForm($this->out, $this->group);
$jf->show();
}
diff --git a/lib/groupnav.php b/lib/groupnav.php
index 90bdc1014..9e530c447 100644
--- a/lib/groupnav.php
+++ b/lib/groupnav.php
@@ -95,6 +95,12 @@ class GroupNav extends Widget
$cur = common_current_user();
if ($cur && $cur->isAdmin($this->group)) {
+ $this->out->menuItem(common_local_url('blockedfromgroup', array('nickname' =>
+ $nickname)),
+ _('Blocked'),
+ sprintf(_('%s blocked users'), $nickname),
+ $action_name == 'blockedfromgroup',
+ 'nav_group_blocked');
$this->out->menuItem(common_local_url('editgroup', array('nickname' =>
$nickname)),
_('Admin'),
@@ -107,6 +113,12 @@ class GroupNav extends Widget
sprintf(_('Add or edit %s logo'), $nickname),
$action_name == 'grouplogo',
'nav_group_logo');
+ $this->out->menuItem(common_local_url('groupdesignsettings', array('nickname' =>
+ $nickname)),
+ _('Design'),
+ sprintf(_('Add or edit %s design'), $nickname),
+ $action_name == 'groupdesignsettings',
+ 'nav_group_design');
}
$this->out->elementEnd('ul');
}
diff --git a/lib/grouptagcloudsection.php b/lib/grouptagcloudsection.php
index 5d68af28b..9b7a10f6b 100644
--- a/lib/grouptagcloudsection.php
+++ b/lib/grouptagcloudsection.php
@@ -32,7 +32,7 @@ if (!defined('LACONICA')) {
}
/**
- * Personal tag cloud section
+ * Group tag cloud section
*
* @category Widget
* @package Laconica
@@ -64,12 +64,27 @@ class GroupTagCloudSection extends TagCloudSection
$weightexpr='sum(exp(-(now() - notice_tag.created) / %s))';
}
+ $names = $this->group->getAliases();
+
+ $names = array_merge(array($this->group->nickname), $names);
+
+ // XXX This is dumb.
+
+ $quoted = array();
+
+ foreach ($names as $name) {
+ $quoted[] = "\"$name\"";
+ }
+
+ $namestring = implode(',', $quoted);
+
$qry = 'SELECT notice_tag.tag, '.
$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 ' .
'WHERE group_inbox.group_id = %d ' .
+ 'AND notice_tag.tag not in (%s) '.
'GROUP BY notice_tag.tag ' .
'ORDER BY weight DESC ';
@@ -85,9 +100,9 @@ class GroupTagCloudSection extends TagCloudSection
$tag = Memcached_DataObject::cachedQuery('Notice_tag',
sprintf($qry,
common_config('tag', 'dropoff'),
- $this->group->id),
+ $this->group->id,
+ $namestring),
3600);
return $tag;
}
-
}
diff --git a/lib/imagefile.php b/lib/imagefile.php
index 0c93b257e..52e4c4b22 100644
--- a/lib/imagefile.php
+++ b/lib/imagefile.php
@@ -72,7 +72,8 @@ class ImageFile
break;
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()));
+ throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'),
+ ImageFile::maxFileSize()));
return;
case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[$param]['tmp_name']);
diff --git a/lib/jabber.php b/lib/jabber.php
index 7d584ad01..e15076160 100644
--- a/lib/jabber.php
+++ b/lib/jabber.php
@@ -77,6 +77,14 @@ function jabber_daemon_address()
return common_config('xmpp', 'user') . '@' . common_config('xmpp', 'server');
}
+class Sharing_XMPP extends XMPPHP_XMPP
+{
+ function getSocket()
+ {
+ return $this->socket;
+ }
+}
+
/**
* connect the configured Jabber account to the configured server
*
@@ -89,7 +97,7 @@ function jabber_connect($resource=null)
{
static $conn = null;
if (!$conn) {
- $conn = new XMPPHP_XMPP(common_config('xmpp', 'host') ?
+ $conn = new Sharing_XMPP(common_config('xmpp', 'host') ?
common_config('xmpp', 'host') :
common_config('xmpp', 'server'),
common_config('xmpp', 'port'),
diff --git a/lib/jsonsearchresultslist.php b/lib/jsonsearchresultslist.php
index f786c20a8..7beea9328 100644
--- a/lib/jsonsearchresultslist.php
+++ b/lib/jsonsearchresultslist.php
@@ -89,6 +89,7 @@ class JSONSearchResultsList
function show()
{
$cnt = 0;
+ $this->max_id = 0;
$time_start = microtime(true);
diff --git a/lib/language.php b/lib/language.php
index cd6498d30..3ea3dd2aa 100644
--- a/lib/language.php
+++ b/lib/language.php
@@ -108,7 +108,7 @@ function get_all_languages() {
'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' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
- 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'),
+ 'en' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'),
'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'),
'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'),
'fr-fr' => array('q' => 1, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'),
diff --git a/lib/mail.php b/lib/mail.php
index 4e1f1dbb1..90ee3c992 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -625,3 +625,75 @@ function mail_notify_attn($user, $notice)
common_init_locale();
mail_to_user($user, $subject, $body);
}
+
+/**
+ * Send a mail message to notify a user that her Twitter bridge link
+ * has stopped working, and therefore has been removed. This can
+ * happen when the user changes her Twitter password, or otherwise
+ * revokes access.
+ *
+ * @param User $user user whose Twitter bridge link has been removed
+ *
+ * @return boolean success flag
+ */
+
+function mail_twitter_bridge_removed($user)
+{
+ common_init_locale($user->language);
+
+ $profile = $user->getProfile();
+
+ $subject = sprintf(_('Your Twitter bridge has been disabled.'));
+
+ $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that your " .
+ 'link to Twitter has been disabled. Your Twitter credentials ' .
+ 'have either changed (did you recently change your Twitter ' .
+ 'password?) or you have otherwise revoked our access to your ' .
+ "Twitter account.\n\n" .
+ 'You can re-enable your Twitter bridge by visiting your ' .
+ "Twitter settings page:\n\n\t%2\$s\n\n" .
+ "Regards,\n%3\$s\n"),
+ $profile->getBestName(),
+ common_local_url('twittersettings'),
+ common_config('site', 'name'));
+
+ common_init_locale();
+ return mail_to_user($user, $subject, $body);
+}
+
+/**
+ * Send a mail message to notify a user that her Facebook Application
+ * access has been removed.
+ *
+ * @param User $user user whose Facebook app link has been removed
+ *
+ * @return boolean success flag
+ */
+
+function mail_facebook_app_removed($user)
+{
+ common_init_locale($user->language);
+
+ $profile = $user->getProfile();
+
+ $site_name = common_config('site', 'name');
+
+ $subject = sprintf(
+ _('Your %s Facebook application access has been disabled.',
+ $site_name));
+
+ $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that we are " .
+ 'unable to update your Facebook status from %s, and have disabled ' .
+ 'the Facebook application for your account. This may be because ' .
+ 'you have removed the Facebook application\'s authorization, or ' .
+ 'have deleted your Facebook account. You can re-enable the ' .
+ 'Facebook application and automatic status updating by ' .
+ "re-installing the %1\$s Facebook application.\n\nRegards,\n\n%1\$s"),
+ $site_name);
+
+ common_init_locale();
+ return mail_to_user($user, $subject, $body);
+
+}
+
+
diff --git a/lib/mailbox.php b/lib/mailbox.php
index 01bbf5721..f1f6e98c1 100644
--- a/lib/mailbox.php
+++ b/lib/mailbox.php
@@ -31,8 +31,6 @@ if (!defined('LACONICA')) {
exit(1);
}
-require_once INSTALLDIR.'/lib/personal.php';
-
define('MESSAGES_PER_PAGE', 20);
/**
@@ -47,11 +45,11 @@ define('MESSAGES_PER_PAGE', 20);
* @see OutboxAction
*/
-class MailboxAction extends PersonalAction
+class MailboxAction extends CurrentUserDesignAction
{
var $page = null;
- function prepare($args)
+ function prepare($args)
{
parent::prepare($args);
@@ -265,12 +263,12 @@ class MailboxAction extends PersonalAction
* Returns 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 message
+ * @param string $source the source of the message
*
* @return void
*/
- function showSource($source)
+ function showSource($source)
{
$source_name = _($source);
switch ($source) {
@@ -297,4 +295,17 @@ class MailboxAction extends PersonalAction
return;
}
+ /**
+ * Mailbox actions are read only
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/lib/noticeform.php b/lib/noticeform.php
index 606b5d028..4e2a2edd6 100644
--- a/lib/noticeform.php
+++ b/lib/noticeform.php
@@ -89,7 +89,10 @@ class NoticeForm extends Form
} else {
$this->user = common_current_user();
}
-
+
+ if (common_config('attachments', 'uploads')) {
+ $this->enctype = 'multipart/form-data';
+ }
}
/**
@@ -142,13 +145,19 @@ class NoticeForm extends Form
'rows' => 4,
'name' => 'status_textarea'),
($this->content) ? $this->content : '');
-
$this->out->elementStart('dl', 'form_note');
$this->out->element('dt', null, _('Available characters'));
$this->out->element('dd', array('id' => 'notice_text-count'),
'140');
$this->out->elementEnd('dl');
-
+ if (common_config('attachments', 'uploads')) {
+ $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach'));
+ $this->out->element('input', array('id' => 'notice_data-attach',
+ 'type' => 'file',
+ 'name' => 'attach',
+ 'title' => _('Attach a file')));
+ $this->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota'));
+ }
if ($this->action) {
$this->out->hidden('notice_return-to', $this->action, 'returnto');
}
diff --git a/lib/noticelist.php b/lib/noticelist.php
index 4182d8808..44726a17b 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -34,6 +34,7 @@ if (!defined('LACONICA')) {
require_once INSTALLDIR.'/lib/favorform.php';
require_once INSTALLDIR.'/lib/disfavorform.php';
+require_once INSTALLDIR.'/lib/attachmentlist.php';
/**
* widget for displaying a list of notices
@@ -49,7 +50,6 @@ require_once INSTALLDIR.'/lib/disfavorform.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
* @see Notice
- * @see StreamAction
* @see NoticeListItem
* @see ProfileNoticeList
*/
@@ -85,7 +85,7 @@ class NoticeList extends Widget
{
$this->out->elementStart('div', array('id' =>'notices_primary'));
$this->out->element('h2', null, _('Notices'));
- $this->out->elementStart('ul', array('class' => 'notices'));
+ $this->out->elementStart('ol', array('class' => 'notices xoxo'));
$cnt = 0;
@@ -100,7 +100,7 @@ class NoticeList extends Widget
$item->show();
}
- $this->out->elementEnd('ul');
+ $this->out->elementEnd('ol');
$this->out->elementEnd('div');
return $cnt;
@@ -197,7 +197,7 @@ class NoticeListItem extends Widget
$this->out->elementStart('div', 'entry-content');
$this->showNoticeLink();
$this->showNoticeSource();
- $this->showReplyTo();
+ $this->showContext();
$this->out->elementEnd('div');
}
@@ -366,6 +366,7 @@ class NoticeListItem extends Widget
$this->out->element('abbr', array('class' => 'published',
'title' => $dt),
common_date_string($this->notice->created));
+
$this->out->elementEnd('a');
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
@@ -421,17 +422,18 @@ class NoticeListItem extends Widget
* @return void
*/
- function showReplyTo()
+ function showContext()
{
- if ($this->notice->reply_to) {
- $replyurl = common_local_url('shownotice',
- array('notice' => $this->notice->reply_to));
+ // XXX: also show context if there are replies to this notice
+ if (!empty($this->notice->conversation)
+ && $this->notice->conversation != $this->notice->id) {
+ $convurl = common_local_url('conversation',
+ array('id' => $this->notice->conversation));
$this->out->elementStart('dl', 'response');
$this->out->element('dt', null, _('To'));
$this->out->elementStart('dd');
- $this->out->element('a', array('href' => $replyurl,
- 'rel' => 'in-reply-to'),
- _('in reply to'));
+ $this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id),
+ _('in context'));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
}
diff --git a/lib/noticesection.php b/lib/noticesection.php
index 94c2738ef..ca1432686 100644
--- a/lib/noticesection.php
+++ b/lib/noticesection.php
@@ -51,17 +51,13 @@ class NoticeSection extends Section
function showContent()
{
$notices = $this->getNotices();
-
$cnt = 0;
-
- $this->out->elementStart('ul', 'notices');
-
+ $this->out->elementStart('ol', 'notices xoxo');
while ($notices->fetch() && ++$cnt <= NOTICES_PER_SECTION) {
$this->showNotice($notices);
}
- $this->out->elementEnd('ul');
-
+ $this->out->elementEnd('ol');
return ($cnt > NOTICES_PER_SECTION);
}
@@ -100,6 +96,37 @@ class NoticeSection extends Section
$this->out->elementStart('p', 'entry-content');
$this->out->raw($notice->rendered);
+
+ $notice_link_cfg = common_config('site', 'notice_link');
+ if ('direct' === $notice_link_cfg) {
+ $this->out->text(' (');
+ $this->out->element('a', array('href' => $notice->uri), 'see');
+ $this->out->text(')');
+ } elseif ('attachment' === $notice_link_cfg) {
+ if ($count = $notice->hasAttachments()) {
+ // link to attachment(s) pages
+ if (1 === $count) {
+ $f2p = File_to_post::staticGet('post_id', $notice->id);
+ $href = common_local_url('attachment', array('attachment' => $f2p->file_id));
+ $att_class = 'attachment';
+ } else {
+ $href = common_local_url('attachments', array('notice' => $notice->id));
+ $att_class = 'attachments';
+ }
+
+ $clip = theme_path('images/icons/clip.png', 'base');
+ $this->out->elementStart('a', array('class' => $att_class, 'style' => "font-style: italic;", 'href' => $href, 'title' => "# of attachments: $count"));
+ $this->out->raw(" ($count&nbsp");
+ $this->out->element('img', array('style' => 'display: inline', 'align' => 'top', 'width' => 20, 'height' => 20, 'src' => $clip, 'alt' => 'alt'));
+ $this->out->text(')');
+ $this->out->elementEnd('a');
+ } else {
+ $this->out->text(' (');
+ $this->out->element('a', array('href' => $notice->uri), 'see');
+ $this->out->text(')');
+ }
+ }
+
$this->out->elementEnd('p');
if (!empty($notice->value)) {
$this->out->elementStart('p');
diff --git a/lib/oauthstore.php b/lib/oauthstore.php
index 183164e17..f224c6c22 100644
--- a/lib/oauthstore.php
+++ b/lib/oauthstore.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/omb.php b/lib/omb.php
index 40cb847df..4f6a96095 100644
--- a/lib/omb.php
+++ b/lib/omb.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/openid.php b/lib/openid.php
index 3af7a39cf..0b7633284 100644
--- a/lib/openid.php
+++ b/lib/openid.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/ownerdesignaction.php b/lib/ownerdesignaction.php
new file mode 100644
index 000000000..785b8a93d
--- /dev/null
+++ b/lib/ownerdesignaction.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for actions that use the page owner's design
+ *
+ * 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 Action
+ * @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);
+}
+
+/**
+ * Base class for actions that use the page owner's design
+ *
+ * Some pages have a clear "owner" -- like the profile page, subscriptions
+ * pages, etc. This superclass uses that owner's chosen design for the page
+ * design.
+ *
+ * @category Action
+ * @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 OwnerDesignAction extends Action {
+
+ /** The user for this page. */
+
+ var $user = null;
+
+ /**
+ * Show the owner's design stylesheet
+ *
+ * @return nothing
+ */
+ function showStylesheets()
+ {
+ parent::showStylesheets();
+
+ $user = common_current_user();
+
+ if (empty($user) || $user->viewdesigns) {
+ $design = $this->getDesign();
+
+ if (!empty($design)) {
+ $design->showCSS($this);
+ }
+ }
+ }
+
+ /**
+ * A design for this action
+ *
+ * if the user attribute has been set, returns that user's
+ * design.
+ *
+ * @return Design a design object to use
+ */
+
+ function getDesign()
+ {
+ if (empty($this->user)) {
+ return null;
+ }
+
+ return $this->user->getDesign();
+ }
+}
diff --git a/lib/peoplesearchresults.php b/lib/peoplesearchresults.php
deleted file mode 100644
index d3f840852..000000000
--- a/lib/peoplesearchresults.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?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($args)
- {
- return true;
- }
-}
-
diff --git a/lib/ping.php b/lib/ping.php
index 3de541e9a..d26c73417 100644
--- a/lib/ping.php
+++ b/lib/ping.php
@@ -59,7 +59,7 @@ function ping_broadcast_notice($notice) {
$response = xmlrpc_decode($file);
- if (xmlrpc_is_fault($response)) {
+ if (is_array($response) && xmlrpc_is_fault($response)) {
common_log(LOG_WARNING,
"XML-RPC error for ping ($notify_url, $notice->id) ".
"$response[faultString] ($response[faultCode])");
diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php
index a8d47ef54..e47c9b385 100644
--- a/lib/popularnoticesection.php
+++ b/lib/popularnoticesection.php
@@ -51,7 +51,7 @@ class PopularNoticeSection extends NoticeSection
if (common_config('db', 'type') == 'pgsql') {
$weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))';
if (!empty($this->out->tag)) {
- $tag = pg_escape_string($this->tag);
+ $tag = pg_escape_string($this->out->tag);
}
} else {
$weightexpr='sum(exp(-(now() - fave.modified) / %s))';
@@ -68,7 +68,7 @@ class PopularNoticeSection extends NoticeSection
}
$qry .= ' 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 ' .
+ 'notice.reply_to,notice.is_local,notice.source,notice.conversation ' .
'ORDER BY weight DESC';
$offset = 0;
diff --git a/lib/profileaction.php b/lib/profileaction.php
index 1f2e30994..9e9c79c78 100644
--- a/lib/profileaction.php
+++ b/lib/profileaction.php
@@ -47,18 +47,18 @@ require_once INSTALLDIR.'/lib/groupminilist.php';
* @link http://laconi.ca/
*/
-class ProfileAction extends Action
+class ProfileAction extends OwnerDesignAction
{
- var $user = null;
- var $page = null;
+ var $page = null;
var $profile = null;
+ var $tag = null;
function prepare($args)
{
parent::prepare($args);
$nickname_arg = $this->arg('nickname');
- $nickname = common_canonical_nickname($nickname_arg);
+ $nickname = common_canonical_nickname($nickname_arg);
// Permanent redirect on non-canonical nickname
@@ -85,10 +85,9 @@ class ProfileAction extends Action
return false;
}
+ $this->tag = $this->trimmed('tag');
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
-
common_set_returnto($this->selfUrl());
-
return true;
}
@@ -109,8 +108,10 @@ class ProfileAction extends Action
$this->element('h2', null, _('Subscriptions'));
- if ($profile) {
- $pml = new ProfileMiniList($profile, $this->user, $this);
+ $cnt = 0;
+
+ if (!empty($profile)) {
+ $pml = new ProfileMiniList($profile, $this);
$cnt = $pml->show();
if ($cnt == 0) {
$this->element('p', null, _('(None)'));
@@ -138,8 +139,10 @@ class ProfileAction extends Action
$this->element('h2', null, _('Subscribers'));
- if ($profile) {
- $pml = new ProfileMiniList($profile, $this->user, $this);
+ $cnt = 0;
+
+ if (!empty($profile)) {
+ $pml = new ProfileMiniList($profile, $this);
$cnt = $pml->show();
if ($cnt == 0) {
$this->element('p', null, _('(None)'));
@@ -160,18 +163,9 @@ class ProfileAction extends Action
function showStatistics()
{
- // XXX: WORM cache this
- $subs = new Subscription();
- $subs->subscriber = $this->profile->id;
- $subs_count = (int) $subs->count() - 1;
-
- $subbed = new Subscription();
- $subbed->subscribed = $this->profile->id;
- $subbed_count = (int) $subbed->count() - 1;
-
- $notices = new Notice();
- $notices->profile_id = $this->profile->id;
- $notice_count = (int) $notices->count();
+ $subs_count = $this->profile->subscriptionCount();
+ $subbed_count = $this->profile->subscriberCount();
+ $notice_count = $this->profile->noticeCount();
$this->elementStart('div', array('id' => 'entity_statistics',
'class' => 'section'));
@@ -196,7 +190,7 @@ class ProfileAction extends Action
array('nickname' => $this->profile->nickname))),
_('Subscriptions'));
$this->elementEnd('dt');
- $this->element('dd', null, (is_int($subs_count)) ? $subs_count : '0');
+ $this->element('dd', null, $subs_count);
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_subscribers');
@@ -205,12 +199,12 @@ class ProfileAction extends Action
array('nickname' => $this->profile->nickname))),
_('Subscribers'));
$this->elementEnd('dt');
- $this->element('dd', 'subscribers', (is_int($subbed_count)) ? $subbed_count : '0');
+ $this->element('dd', 'subscribers', $subbed_count);
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_notices');
$this->element('dt', null, _('Notices'));
- $this->element('dd', null, (is_int($notice_count)) ? $notice_count : '0');
+ $this->element('dd', null, $notice_count);
$this->elementEnd('dl');
$this->elementEnd('div');
@@ -244,4 +238,5 @@ class ProfileAction extends Action
$this->elementEnd('div');
}
-} \ No newline at end of file
+}
+
diff --git a/lib/profilelist.php b/lib/profilelist.php
index a4cc23555..774538a4b 100644
--- a/lib/profilelist.php
+++ b/lib/profilelist.php
@@ -49,25 +49,37 @@ class ProfileList extends Widget
{
/** Current profile, profile query. */
var $profile = null;
- /** Owner of this list */
- var $owner = null;
/** Action object using us. */
var $action = null;
- function __construct($profile, $owner=null, $action=null)
+ function __construct($profile, $action=null)
{
parent::__construct($action);
$this->profile = $profile;
- $this->owner = $owner;
$this->action = $action;
}
function show()
{
+ $this->startList();
+ $cnt = $this->showProfiles();
+ $this->endList();
+ return $cnt;
+ }
+ function startList()
+ {
$this->out->elementStart('ul', 'profiles');
+ }
+
+ function endList()
+ {
+ $this->out->elementEnd('ul');
+ }
+ function showProfiles()
+ {
$cnt = 0;
while ($this->profile->fetch()) {
@@ -75,24 +87,66 @@ class ProfileList extends Widget
if($cnt > PROFILES_PER_PAGE) {
break;
}
- $this->showProfile();
+ $pli = $this->newListItem($this->profile);
+ $pli->show();
}
- $this->out->elementEnd('ul');
-
return $cnt;
}
- function showProfile()
+ function newListItem($profile)
+ {
+ return new ProfileListItem($this->profile, $this->action);
+ }
+}
+
+class ProfileListItem extends Widget
+{
+ /** Current profile. */
+ var $profile = null;
+ /** Action object using us. */
+ var $action = null;
+
+ function __construct($profile, $action)
+ {
+ parent::__construct($action);
+
+ $this->profile = $profile;
+ $this->action = $action;
+ }
+
+ function show()
+ {
+ $this->startItem();
+ $this->showProfile();
+ $this->showActions();
+ $this->endItem();
+ }
+
+ function startItem()
{
$this->out->elementStart('li', array('class' => 'profile',
'id' => 'profile-' . $this->profile->id));
+ }
- $user = common_current_user();
- $is_own = !is_null($user) && isset($this->owner) && ($user->id === $this->owner->id);
+ function showProfile()
+ {
+ $this->startProfile();
+ $this->showAvatar();
+ $this->showFullName();
+ $this->showLocation();
+ $this->showHomepage();
+ $this->showBio();
+ $this->endProfile();
+ }
+ function startProfile()
+ {
$this->out->elementStart('div', 'entity_profile vcard');
+ }
+ function showAvatar()
+ {
$avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE);
$this->out->elementStart('a', array('href' => $this->profile->profileurl,
'class' => 'url'));
@@ -108,7 +162,10 @@ class ProfileList extends Widget
$this->out->raw($this->highlight($this->profile->nickname));
$this->out->elementEnd('span');
$this->out->elementEnd('a');
+ }
+ function showFullName()
+ {
if (!empty($this->profile->fullname)) {
$this->out->elementStart('dl', 'entity_fn');
$this->out->element('dt', null, 'Full name');
@@ -119,6 +176,10 @@ class ProfileList extends Widget
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
}
+ }
+
+ function showLocation()
+ {
if (!empty($this->profile->location)) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
@@ -127,6 +188,10 @@ class ProfileList extends Widget
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
}
+ }
+
+ function showHomepage()
+ {
if (!empty($this->profile->homepage)) {
$this->out->elementStart('dl', 'entity_url');
$this->out->element('dt', null, _('URL'));
@@ -138,6 +203,10 @@ class ProfileList extends Widget
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
}
+ }
+
+ function showBio()
+ {
if (!empty($this->profile->bio)) {
$this->out->elementStart('dl', 'entity_note');
$this->out->element('dt', null, _('Note'));
@@ -146,94 +215,64 @@ class ProfileList extends Widget
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
}
+ }
- # If we're on a list with an owner (subscriptions or subscribers)...
-
- if ($this->owner) {
- # Get tags
- $tags = Profile_tag::getTags($this->owner->id, $this->profile->id);
-
- $this->out->elementStart('dl', 'entity_tags');
- $this->out->elementStart('dt');
- if ($is_own) {
- $this->out->element('a', array('href' => common_local_url('tagother',
- array('id' => $this->profile->id))),
- _('Tags'));
- } else {
- $this->out->text(_('Tags'));
- }
- $this->out->elementEnd('dt');
- $this->out->elementStart('dd');
- if ($tags) {
- $this->out->elementStart('ul', 'tags xoxo');
- foreach ($tags as $tag) {
- $this->out->elementStart('li');
- $this->out->element('span', 'mark_hash', '#');
- $this->out->element('a', array('rel' => 'tag',
- 'href' => common_local_url($this->action->trimmed('action'),
- array('nickname' => $this->owner->nickname,
- 'tag' => $tag))),
- $tag);
- $this->out->elementEnd('li');
- }
- $this->out->elementEnd('ul');
- } else {
- $this->out->text(_('(none)'));
- }
- $this->out->elementEnd('dd');
- $this->out->elementEnd('dl');
- }
-
- if ($is_own) {
- $this->showOwnerControls($this->profile);
- }
-
+ function endProfile()
+ {
$this->out->elementEnd('div');
+ }
- $this->out->elementStart('div', 'entity_actions');
+ function showActions()
+ {
+ $this->startActions();
+ $this->showSubscribeButton();
+ $this->endActions();
+ }
+ function startActions()
+ {
+ $this->out->elementStart('div', 'entity_actions');
$this->out->elementStart('ul');
+ }
+ function showSubscribeButton()
+ {
// Is this a logged-in user, looking at someone else's
// profile?
+ $user = common_current_user();
+
if (!empty($user) && $this->profile->id != $user->id) {
$this->out->elementStart('li', 'entity_subscribe');
if ($user->isSubscribed($this->profile)) {
$usf = new UnsubscribeForm($this->out, $this->profile);
$usf->show();
} else {
- $sf = new SubscribeForm($this->out, $this->profile);
- $sf->show();
- }
- $this->out->elementEnd('li');
- $this->out->elementStart('li', 'entity_block');
- if ($user->id == $this->owner->id) {
- $this->showBlockForm();
+ // Is it a local user? can't remote sub from a list
+ // XXX: make that possible!
+ $other = User::staticGet('id', $this->profile->id);
+ if (!empty($other)) {
+ $sf = new SubscribeForm($this->out, $this->profile);
+ $sf->show();
+ }
}
$this->out->elementEnd('li');
}
+ }
+ function endActions()
+ {
$this->out->elementEnd('ul');
-
$this->out->elementEnd('div');
-
- $this->out->elementEnd('li');
}
- /* Override this in subclasses. */
-
- function showOwnerControls($profile)
+ function endItem()
{
- return;
+ $this->out->elementEnd('li');
}
function highlight($text)
{
return htmlspecialchars($text);
}
-
- function showBlockForm()
- {
- }
}
diff --git a/lib/profileminilist.php b/lib/profileminilist.php
index 57496d0e9..357b4a2db 100644
--- a/lib/profileminilist.php
+++ b/lib/profileminilist.php
@@ -47,26 +47,38 @@ define('PROFILES_PER_MINILIST', 27);
class ProfileMiniList extends ProfileList
{
- function show()
+
+ function startList()
{
$this->out->elementStart('ul', 'entities users xoxo');
+ }
+
+ function newListItem($profile)
+ {
+ return new ProfileMiniListItem($profile, $this->action);
+ }
+ function showProfiles()
+ {
$cnt = 0;
while ($this->profile->fetch()) {
$cnt++;
- if($cnt > PROFILES_PER_MINILIST) {
+ if ($cnt > PROFILES_PER_MINILIST) {
break;
}
- $this->showProfile();
+ $pli = $this->newListItem($this->profile);
+ $pli->show();
}
- $this->out->elementEnd('ul');
-
return $cnt;
}
- function showProfile()
+}
+
+class ProfileMiniListItem extends ProfileListItem
+{
+ function show()
{
$this->out->elementStart('li', 'vcard');
$this->out->elementStart('a', array('title' => $this->profile->getBestName(),
diff --git a/lib/profilesection.php b/lib/profilesection.php
index 8ed290e03..9ff243fb5 100644
--- a/lib/profilesection.php
+++ b/lib/profilesection.php
@@ -94,8 +94,8 @@ class ProfileSection extends Section
$profile->fullname :
$profile->nickname));
$this->out->element('span', 'fn nickname', $profile->nickname);
- $this->out->elementEnd('span');
$this->out->elementEnd('a');
+ $this->out->elementEnd('span');
$this->out->elementEnd('td');
if ($profile->value) {
$this->out->element('td', 'value', $profile->value);
diff --git a/lib/queuehandler.php b/lib/queuehandler.php
index fde650d9e..c2ff10f32 100644
--- a/lib/queuehandler.php
+++ b/lib/queuehandler.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,26 +17,34 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-define('CLAIM_TIMEOUT', 1200);
-
if (!defined('LACONICA')) { exit(1); }
require_once(INSTALLDIR.'/lib/daemon.php');
require_once(INSTALLDIR.'/classes/Queue_item.php');
require_once(INSTALLDIR.'/classes/Notice.php');
+define('CLAIM_TIMEOUT', 1200);
+define('QUEUE_HANDLER_MISS_IDLE', 10);
+define('QUEUE_HANDLER_HIT_IDLE', 0);
+
class QueueHandler extends Daemon
{
-
var $_id = 'generic';
- function QueueHandler($id=null)
+ function __construct($id=null, $daemonize=true)
{
+ parent::__construct($daemonize);
+
if ($id) {
$this->set_id($id);
}
}
+ function timeout()
+ {
+ return 60;
+ }
+
function class_name()
{
return ucfirst($this->transport()) . 'Handler';
@@ -80,41 +88,16 @@ class QueueHandler extends Daemon
if (!$this->start()) {
return false;
}
- $transport = $this->transport();
- $this->log(LOG_INFO, 'checking for queued notices for "' . $transport . '"');
- do {
- $qi = Queue_item::top($transport);
- if ($qi) {
- $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($qi->created));
- $notice = Notice::staticGet($qi->notice_id);
- if ($notice) {
- $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id);
- # XXX: what to do if broadcast fails?
- $result = $this->handle_notice($notice);
- if (!$result) {
- $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id);
- $orig = $qi;
- $qi->claimed = null;
- $qi->update($orig);
- $this->log(LOG_WARNING, 'Abandoned claim for notice ID = ' . $notice->id);
- continue;
- }
- $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id);
- $notice->free();
- unset($notice);
- $notice = null;
- } else {
- $this->log(LOG_WARNING, 'queue item for notice that does not exist');
- }
- $qi->delete();
- $qi->free();
- unset($qi);
- $this->idle(0);
- } else {
- $this->clear_old_claims();
- $this->idle(5);
- }
- } while (true);
+
+ $this->log(LOG_INFO, 'checking for queued notices');
+
+ $queue = $this->transport();
+ $timeout = $this->timeout();
+
+ $qm = QueueManager::get();
+
+ $qm->service($queue, $this);
+
if (!$this->finish()) {
return false;
}
@@ -123,23 +106,19 @@ class QueueHandler extends Daemon
function idle($timeout=0)
{
- if ($timeout>0) {
+ if ($timeout > 0) {
sleep($timeout);
}
}
- function clear_old_claims()
+ function log($level, $msg)
{
- $qi = new Queue_item();
- $qi->transport = $this->transport();
- $qi->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
- $qi->update(DB_DATAOBJECT_WHEREADD_ONLY);
- $qi->free();
- unset($qi);
+ common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg);
}
- function log($level, $msg)
+ function getSockets()
{
- common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg);
+ return array();
}
}
+
diff --git a/lib/queuemanager.php b/lib/queuemanager.php
new file mode 100644
index 000000000..582c24790
--- /dev/null
+++ b/lib/queuemanager.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Abstract class for queue managers
+ *
+ * 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 QueueManager
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @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/
+ */
+
+class QueueManager
+{
+ static $qm = null;
+
+ static function get()
+ {
+ if (empty(self::$qm)) {
+
+ if (Event::handle('StartNewQueueManager', array(&self::$qm))) {
+
+ $enabled = common_config('queue', 'enabled');
+ $type = common_config('queue', 'subsystem');
+
+ if (!$enabled) {
+ // does everything immediately
+ self::$qm = new UnQueueManager();
+ } else {
+ switch ($type) {
+ case 'db':
+ self::$qm = new DBQueueManager();
+ break;
+ case 'stomp':
+ self::$qm = new StompQueueManager();
+ break;
+ default:
+ throw new ServerException("No queue manager class for type '$type'");
+ }
+ }
+ }
+ }
+
+ return self::$qm;
+ }
+
+ function enqueue($object, $queue)
+ {
+ throw ServerException("Unimplemented function 'enqueue' called");
+ }
+
+ function service($queue, $handler)
+ {
+ throw ServerException("Unimplemented function 'service' called");
+ }
+}
diff --git a/lib/router.php b/lib/router.php
index 748966567..75e72f932 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -101,7 +101,8 @@ class Router
$main = array('login', 'logout', 'register', 'subscribe',
'unsubscribe', 'confirmaddress', 'recoverpassword',
'invite', 'favor', 'disfavor', 'sup',
- 'block', 'unblock', 'subedit');
+ 'block', 'unblock', 'subedit',
+ 'groupblock', 'groupunblock');
foreach ($main as $a) {
$m->connect('main/'.$a, array('action' => $a));
@@ -131,7 +132,7 @@ class Router
// settings
foreach (array('profile', 'avatar', 'password', 'openid', 'im',
- 'email', 'sms', 'twitter', 'other') as $s) {
+ 'email', 'sms', 'twitter', 'userdesign', 'other') as $s) {
$m->connect('settings/'.$s, array('action' => $s.'settings'));
}
@@ -151,12 +152,27 @@ class Router
$m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
array('q' => '.+'));
- // notice
+ $m->connect('attachment/:attachment',
+ array('action' => 'attachment'),
+ array('attachment' => '[0-9]+'));
+
+ $m->connect('attachment/:attachment/ajax',
+ array('action' => 'attachment_ajax'),
+ array('attachment' => '[0-9]+'));
+
+ $m->connect('attachment/:attachment/thumbnail',
+ array('action' => 'attachment_thumbnail'),
+ array('attachment' => '[0-9]+'));
$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/file',
+ array('action' => 'file'),
+ array('notice' => '[0-9]+'));
+
$m->connect('notice/:notice',
array('action' => 'shownotice'),
array('notice' => '[0-9]+'));
@@ -165,6 +181,12 @@ class Router
array('action' => 'deletenotice'),
array('notice' => '[0-9]+'));
+ // conversation
+
+ $m->connect('conversation/:id',
+ array('action' => 'conversation'),
+ array('id' => '[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',
@@ -205,12 +227,20 @@ class Router
array('nickname' => '[a-zA-Z0-9]+'));
}
- foreach (array('members', 'logo', 'rss') as $n) {
+ foreach (array('members', 'logo', 'rss', 'designsettings') as $n) {
$m->connect('group/:nickname/'.$n,
array('action' => 'group'.$n),
array('nickname' => '[a-zA-Z0-9]+'));
}
+ $m->connect('group/:nickname/blocked',
+ array('action' => 'blockedfromgroup'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+
+ $m->connect('group/:nickname/makeadmin',
+ array('action' => 'makeadmin'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+
$m->connect('group/:id/id',
array('action' => 'groupbyid'),
array('id' => '[0-9]+'));
@@ -231,7 +261,7 @@ class Router
$m->connect('api/statuses/:method',
array('action' => 'api',
'apiaction' => 'statuses'),
- array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+ array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|show|friends|followers|featured)(\.(atom|rss|xml|json))?'));
$m->connect('api/statuses/:method/:argument',
array('action' => 'api',
@@ -287,7 +317,7 @@ class Router
$m->connect('api/friendships/:method',
array('action' => 'api',
'apiaction' => 'friendships'),
- array('method' => 'exists(\.(xml|json))'));
+ array('method' => '(show|exists)(\.(xml|json))'));
// Social graph
@@ -325,7 +355,8 @@ class Router
$m->connect('api/favorites/:method/:argument',
array('action' => 'api',
- 'apiaction' => 'favorites'));
+ 'apiaction' => 'favorites',
+ array('method' => '(create|destroy)')));
$m->connect('api/favorites/:argument',
array('action' => 'api',
@@ -406,6 +437,16 @@ class Router
array('size' => '(original|96|48|24)',
'nickname' => '[a-zA-Z0-9]{1,64}'));
+ $m->connect(':nickname/tag/:tag/rss',
+ array('action' => 'userrss'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect(':nickname/tag/:tag',
+ array('action' => 'showstream'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
$m->connect(':nickname',
array('action' => 'showstream'),
array('nickname' => '[a-zA-Z0-9]{1,64}'));
diff --git a/lib/rssaction.php b/lib/rssaction.php
index ddba862dc..fe3fd6f4a 100644
--- a/lib/rssaction.php
+++ b/lib/rssaction.php
@@ -97,7 +97,11 @@ class Rss10Action extends Action
// Parent handling, including cache check
parent::handle($args);
// Get the list of notices
- $this->notices = $this->getNotices($this->limit);
+ if (empty($this->tag)) {
+ $this->notices = $this->getNotices($this->limit);
+ } else {
+ $this->notices = $this->getTaggedNotices($this->tag, $this->limit);
+ }
$this->showRss();
}
@@ -166,7 +170,7 @@ class Rss10Action extends Action
$this->elementStart('rdf:Seq');
foreach ($this->notices as $notice) {
- $this->element('sioct:MicroblogPost', array('rdf:resource' => $notice->uri));
+ $this->element('rdf:li', array('rdf:resource' => $notice->uri));
}
$this->elementEnd('rdf:Seq');
@@ -193,16 +197,32 @@ class Rss10Action extends Action
$profile = Profile::staticGet($notice->profile_id);
$nurl = common_local_url('shownotice', array('notice' => $notice->id));
$creator_uri = common_profile_uri($profile);
- $this->elementStart('item', array('rdf:about' => $notice->uri));
+ $this->elementStart('item', array('rdf:about' => $notice->uri,
+ 'rdf:type' => 'http://rdfs.org/sioc/types#MicroblogPost'));
$title = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
$this->element('title', null, $title);
$this->element('link', null, $nurl);
$this->element('description', null, $profile->nickname."'s status on ".common_exact_date($notice->created));
+ if ($notice->rendered) {
+ $this->element('content:encoded', null, common_xml_safe_str($notice->rendered));
+ }
$this->element('dc:date', null, common_date_w3dtf($notice->created));
$this->element('dc:creator', null, ($profile->fullname) ? $profile->fullname : $profile->nickname);
- $this->element('sioc:has_creator', array('rdf:resource' => $creator_uri));
+ $this->element('foaf:maker', array('rdf:resource' => $creator_uri));
+ $this->element('sioc:has_creator', array('rdf:resource' => $creator_uri.'#acct'));
$this->element('laconica:postIcon', array('rdf:resource' => $profile->avatarUrl()));
$this->element('cc:licence', array('rdf:resource' => common_config('license', 'url')));
+ if ($notice->reply_to) {
+ $replyurl = common_local_url('shownotice', array('notice' => $notice->reply_to));
+ $this->element('sioc:reply_of', array('rdf:resource' => $replyurl));
+ }
+ $attachments = $notice->attachments();
+ if($attachments){
+ foreach($attachments as $attachment){
+ $this->element('enc:enclosure', array('rdf:resource'=>$attachment->url,'enc:type'=>$attachment->mimetype,'enc:length'=>$attachment->size), null);
+ }
+ }
+
$this->elementEnd('item');
$this->creators[$creator_uri] = $profile;
}
@@ -212,15 +232,15 @@ class Rss10Action extends Action
foreach ($this->creators as $uri => $profile) {
$id = $profile->id;
$nickname = $profile->nickname;
- $this->elementStart('sioc:User', array('rdf:about' => $uri));
+ $this->elementStart('foaf:Agent', array('rdf:about' => $uri));
$this->element('foaf:nick', null, $nickname);
if ($profile->fullname) {
$this->element('foaf:name', null, $profile->fullname);
}
- $this->element('sioc:id', null, $id);
+ $this->element('foaf:holdsAccount', array('rdf:resource' => $uri.'#acct'));
$avatar = $profile->avatarUrl();
- $this->element('sioc:avatar', array('rdf:resource' => $avatar));
- $this->elementEnd('sioc:User');
+ $this->element('foaf:depiction', array('rdf:resource' => $avatar));
+ $this->elementEnd('foaf:Agent');
}
}
@@ -235,9 +255,11 @@ class Rss10Action extends Action
'xmlns:dc' =>
'http://purl.org/dc/elements/1.1/',
'xmlns:cc' =>
- 'http://web.resource.org/cc/',
+ 'http://creativecommons.org/ns#',
'xmlns:content' =>
'http://purl.org/rss/1.0/modules/content/',
+ 'xmlns:enc' =>
+ 'http://purl.oclc.org/net/rss_2.0/enc#',
'xmlns:foaf' =>
'http://xmlns.com/foaf/0.1/',
'xmlns:sioc' =>
@@ -249,10 +271,10 @@ class Rss10Action extends Action
'xmlns' => 'http://purl.org/rss/1.0/'));
$this->elementStart('sioc:Site', array('rdf:about' => common_root_url()));
$this->element('sioc:name', null, common_config('site', 'name'));
- $this->elementStart('sioc:container_of');
+ $this->elementStart('sioc:space_of');
$this->element('sioc:Container', array('rdf:about' =>
$channel['url']));
- $this->elementEnd('sioc:container_of');
+ $this->elementEnd('sioc:space_of');
$this->elementEnd('sioc:Site');
}
diff --git a/lib/search_engines.php b/lib/search_engines.php
index 7b9dbb618..772f41883 100644
--- a/lib/search_engines.php
+++ b/lib/search_engines.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -118,12 +118,20 @@ class MySQLSearch extends SearchEngine
}
return true;
} else if ('identica_notices' === $this->table) {
- $this->target->whereAdd('MATCH(content) ' .
- 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)');
+
+ // Don't show imported notices
+ $this->target->whereAdd('notice.is_local != ' . NOTICE_GATEWAY);
+
if (strtolower($q) != $q) {
+ $this->target->whereAdd("( MATCH(content) AGAINST ('" . addslashes($q) .
+ "' IN BOOLEAN MODE)) OR ( MATCH(content) " .
+ "AGAINST ('" . addslashes(strtolower($q)) .
+ "' IN BOOLEAN MODE))");
+ } else {
$this->target->whereAdd('MATCH(content) ' .
- 'AGAINST (\''.addslashes(strtolower($q)).'\' IN BOOLEAN MODE)', 'OR');
+ 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)');
}
+
return true;
} else {
throw new ServerException('Unknown table: ' . $this->table);
@@ -131,6 +139,28 @@ class MySQLSearch extends SearchEngine
}
}
+class MySQLLikeSearch extends SearchEngine
+{
+ function query($q)
+ {
+ if ('identica_people' === $this->table) {
+ $qry = sprintf('(nickname LIKE "%%%1$s%%" OR '.
+ ' fullname LIKE "%%%1$s%%" OR '.
+ ' location LIKE "%%%1$s%%" OR '.
+ ' bio LIKE "%%%1$s%%" OR '.
+ ' homepage LIKE "%%%1$s%%")', addslashes($q));
+ } else if ('identica_notices' === $this->table) {
+ $qry = sprintf('content LIKE "%%%1$s%%"', addslashes($q));
+ } else {
+ throw new ServerException('Unknown table: ' . $this->table);
+ }
+
+ $this->target->whereAdd($qry);
+
+ return true;
+ }
+}
+
class PGSearch extends SearchEngine
{
function query($q)
@@ -138,6 +168,9 @@ class PGSearch extends SearchEngine
if ('identica_people' === $this->table) {
return $this->target->whereAdd('textsearch @@ plainto_tsquery(\''.addslashes($q).'\')');
} else if ('identica_notices' === $this->table) {
+
+ // XXX: We need to filter out gateway notices (notice.is_local = -2) --Zach
+
return $this->target->whereAdd('to_tsvector(\'english\', content) @@ plainto_tsquery(\''.addslashes($q).'\')');
} else {
throw new ServerException('Unknown table: ' . $this->table);
diff --git a/lib/searchaction.php b/lib/searchaction.php
index e74450e11..34fe9373f 100644
--- a/lib/searchaction.php
+++ b/lib/searchaction.php
@@ -12,7 +12,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -148,10 +148,10 @@ You can also try your search on other engines:
* [Tweet scan](http://www.tweetscan.com/indexi.php?s=%s)
* [Google](http://www.google.com/search?q=site%%3A%%%%site.server%%%%+%s)
* [Yahoo](http://search.yahoo.com/search?p=site%%3A%%%%site.server%%%%+%s)
-
+* [Collecta](http://collecta.com/#q=%s)
E_O_T
-), $qe, $qe, $qe, $qe);
+), $qe, $qe, $qe, $qe, $qe);
$this->elementStart('dl', array('id' => 'help_search', 'class' => 'help'));
$this->element('dt', null, _('Search help'));
$this->elementStart('dd', 'instructions');
diff --git a/lib/servererroraction.php b/lib/servererroraction.php
index 595dcf147..db7352166 100644
--- a/lib/servererroraction.php
+++ b/lib/servererroraction.php
@@ -13,7 +13,7 @@
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/lib/settingsaction.php b/lib/settingsaction.php
index db20c5804..17d3a2f64 100644
--- a/lib/settingsaction.php
+++ b/lib/settingsaction.php
@@ -43,7 +43,7 @@ if (!defined('LACONICA')) {
* @see Widget
*/
-class SettingsAction extends Action
+class SettingsAction extends CurrentUserDesignAction
{
/**
* A message for the user.
diff --git a/lib/snapshot.php b/lib/snapshot.php
new file mode 100644
index 000000000..4b05b502d
--- /dev/null
+++ b/lib/snapshot.php
@@ -0,0 +1,227 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * A snapshot of site stats that can report itself to headquarters
+ *
+ * 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 Stats
+ * @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);
+}
+
+/**
+ * A snapshot of site stats that can report itself to headquarters
+ *
+ * This class will collect statistics on the site and report them to
+ * a statistics server of the admin's choice. (Default is the big one
+ * at laconi.ca.)
+ *
+ * It can either be called from a cron job, or run occasionally by the
+ * Web site.
+ *
+ * @category Stats
+ * @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 Snapshot
+{
+ var $stats = null;
+
+ /**
+ * Constructor for a snapshot
+ */
+
+ function __construct()
+ {
+ }
+
+ /**
+ * Static function for reporting statistics
+ *
+ * This function checks whether it should report statistics, based on
+ * the current configuation settings. If it should, it creates a new
+ * Snapshot object, takes a snapshot, and reports it to headquarters.
+ *
+ * @return void
+ */
+
+ static function check()
+ {
+ switch (common_config('snapshot', 'run')) {
+ case 'web':
+ // skip if we're not running on the Web.
+ if (!isset($_SERVER) || !array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ break;
+ }
+ // Run once every frequency hits
+ // XXX: do frequency by time (once a week, etc.) rather than
+ // hits
+ if (rand() % common_config('snapshot', 'frequency') == 0) {
+ $snapshot = new Snapshot();
+ $snapshot->take();
+ $snapshot->report();
+ }
+ break;
+ case 'cron':
+ // skip if we're running on the Web
+ if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ break;
+ }
+ common_log(LOG_INFO, 'Running snapshot from cron job');
+ // We're running from the command line; assume
+
+ $snapshot = new Snapshot();
+ $snapshot->take();
+ common_log(LOG_INFO, count($snapshot->stats) . " statistics being uploaded.");
+ $snapshot->report();
+
+ break;
+ case 'never':
+ break;
+ default:
+ common_log(LOG_WARNING, "Unrecognized value for snapshot run config.");
+ }
+ }
+
+ /**
+ * Take a snapshot of the server
+ *
+ * Builds an array of statistical and configuration data based
+ * on the local database and config files. We avoid grabbing any
+ * information that could be personal or private.
+ *
+ * @return void
+ */
+
+ function take()
+ {
+ $this->stats = array();
+
+ // Some basic identification stuff
+
+ $this->stats['version'] = LACONICA_VERSION;
+ $this->stats['phpversion'] = phpversion();
+ $this->stats['name'] = common_config('site', 'name');
+ $this->stats['root'] = common_root_url();
+
+ // non-identifying stats on various tables. Primary
+ // interest is size and rate of activity of service.
+
+ $tables = array('user',
+ 'notice',
+ 'subscription',
+ 'remote_profile',
+ 'user_group');
+
+ foreach ($tables as $table) {
+ $this->tableStats($table);
+ }
+
+ // stats on some important config options
+
+ $this->stats['theme'] = common_config('site', 'theme');
+ $this->stats['dbtype'] = common_config('db', 'type');
+ $this->stats['xmpp'] = common_config('xmpp', 'enabled');
+ $this->stats['inboxes'] = common_config('inboxes', 'enabled');
+ $this->stats['queue'] = common_config('queue', 'enabled');
+ $this->stats['license'] = common_config('license', 'url');
+ $this->stats['fancy'] = common_config('site', 'fancy');
+ $this->stats['private'] = common_config('site', 'private');
+ $this->stats['closed'] = common_config('site', 'closed');
+ $this->stats['memcached'] = common_config('memcached', 'enabled');
+ $this->stats['language'] = common_config('site', 'language');
+ $this->stats['timezone'] = common_config('site', 'timezone');
+
+ }
+
+ /**
+ * Reports statistics to headquarters
+ *
+ * Posts statistics to a reporting server.
+ *
+ * @return void
+ */
+
+ function report()
+ {
+ // XXX: Use OICU2 and OAuth to make authorized requests
+
+ $postdata = http_build_query($this->stats);
+
+ $opts =
+ array('http' =>
+ array(
+ 'method' => 'POST',
+ 'header' => 'Content-type: '.
+ 'application/x-www-form-urlencoded',
+ 'content' => $postdata,
+ 'user_agent' => 'Laconica/'.LACONICA_VERSION
+ )
+ );
+
+ $context = stream_context_create($opts);
+
+ $reporturl = common_config('snapshot', 'reporturl');
+
+ $result = @file_get_contents($reporturl, false, $context);
+
+ return $result;
+ }
+
+ /**
+ * Updates statistics for a single table
+ *
+ * Determines the size of a table and its oldest and newest rows.
+ * Goal here is to see how active a site is. Note that it
+ * fills up the instance stats variable.
+ *
+ * @param string $table name of table to check
+ *
+ * @return void
+ */
+
+ function tableStats($table)
+ {
+ $inst = DB_DataObject::factory($table);
+
+ $inst->selectAdd();
+ $inst->selectAdd('count(*) as cnt, '.
+ 'min(created) as first, '.
+ 'max(created) as last');
+
+ if ($inst->find(true)) {
+ $this->stats[$table.'count'] = $inst->cnt;
+ $this->stats[$table.'first'] = $inst->first;
+ $this->stats[$table.'last'] = $inst->last;
+ }
+
+ $inst->free();
+ unset($inst);
+ }
+}
diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php
new file mode 100644
index 000000000..46baeb5c7
--- /dev/null
+++ b/lib/stompqueuemanager.php
@@ -0,0 +1,169 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Abstract class for queue managers
+ *
+ * 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 QueueManager
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @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/
+ */
+
+require_once 'Stomp.php';
+
+class LiberalStomp extends Stomp
+{
+ function getSocket()
+ {
+ return $this->_socket;
+ }
+}
+
+class StompQueueManager
+{
+ var $server = null;
+ var $username = null;
+ var $password = null;
+ var $base = null;
+ var $con = null;
+
+ function __construct()
+ {
+ $this->server = common_config('queue', 'stomp_server');
+ $this->username = common_config('queue', 'stomp_username');
+ $this->password = common_config('queue', 'stomp_password');
+ $this->base = common_config('queue', 'queue_basename');
+ }
+
+ function _connect()
+ {
+ if (empty($this->con)) {
+ $this->_log(LOG_INFO, "Connecting to '$this->server' as '$this->username'...");
+ $this->con = new LiberalStomp($this->server);
+
+ if ($this->con->connect($this->username, $this->password)) {
+ $this->_log(LOG_INFO, "Connected.");
+ } else {
+ $this->_log(LOG_ERR, 'Failed to connect to queue server');
+ throw new ServerException('Failed to connect to queue server');
+ }
+ }
+ }
+
+ function enqueue($object, $queue)
+ {
+ $notice = $object;
+
+ $this->_connect();
+
+ // XXX: serialize and send entire notice
+
+ $result = $this->con->send($this->_queueName($queue),
+ $notice->id, // BODY of the message
+ array ('created' => $notice->created));
+
+ if (!$result) {
+ common_log(LOG_ERR, 'Error sending to '.$queue.' queue');
+ return false;
+ }
+
+ common_log(LOG_DEBUG, 'complete remote queueing notice ID = '
+ . $notice->id . ' for ' . $queue);
+ }
+
+ function service($queue, $handler)
+ {
+ $result = null;
+
+ $this->_connect();
+
+ $this->con->setReadTimeout($handler->timeout());
+
+ $this->con->subscribe($this->_queueName($queue));
+
+ while (true) {
+
+ // Wait for something on one of our sockets
+
+ $stompsock = $this->con->getSocket();
+
+ $handsocks = $handler->getSockets();
+
+ $socks = array_merge(array($stompsock), $handsocks);
+
+ $read = $socks;
+ $write = array();
+ $except = array();
+
+ $ready = stream_select($read, $write, $except, $handler->timeout(), 0);
+
+ if ($ready === false) {
+ $this->_log(LOG_ERR, "Error selecting on sockets");
+ } else if ($ready > 0) {
+ if (in_array($stompsock, $read)) {
+ $this->_handleNotice($queue, $handler);
+ }
+ $handler->idle(QUEUE_HANDLER_HIT_IDLE);
+ }
+ }
+
+ $this->con->unsubscribe($this->_queueName($queue));
+ }
+
+ function _handleNotice($queue, $handler)
+ {
+ $frame = $this->con->readFrame();
+
+ if (!empty($frame)) {
+ $notice = Notice::staticGet('id', $frame->body);
+
+ if (empty($notice)) {
+ $this->_log(LOG_WARNING, 'Got ID '. $frame->body .' for non-existent notice in queue '. $queue);
+ $this->con->ack($frame);
+ } else {
+ if ($handler->handle_notice($notice)) {
+ $this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue);
+ $this->con->ack($frame);
+ } else {
+ $this->_log(LOG_WARNING, 'Failed handling notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue);
+ // FIXME we probably shouldn't have to do
+ // this kind of queue management ourselves
+ $this->con->ack($frame);
+ $this->enqueue($notice, $queue);
+ }
+ unset($notice);
+ }
+
+ unset($frame);
+ }
+ }
+
+ function _queueName($queue)
+ {
+ return common_config('queue', 'queue_basename') . $queue;
+ }
+
+ function _log($level, $msg)
+ {
+ common_log($level, 'StompQueueManager: '.$msg);
+ }
+}
diff --git a/lib/subgroupnav.php b/lib/subgroupnav.php
index 4a9b36ae8..520991923 100644
--- a/lib/subgroupnav.php
+++ b/lib/subgroupnav.php
@@ -100,7 +100,7 @@ class SubGroupNav extends Widget
$this->user->nickname),
$action == 'usergroups',
'nav_usergroups');
- if (!is_null($cur) && $this->user->id === $cur->id) {
+ if (common_config('invite', 'enabled') && !is_null($cur) && $this->user->id === $cur->id) {
$this->out->menuItem(common_local_url('invite'),
_('Invite'),
sprintf(_('Invite friends and colleagues to join you on %s'),
diff --git a/lib/subs.php b/lib/subs.php
index 0e7b9ded5..e76023752 100644
--- a/lib/subs.php
+++ b/lib/subs.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -44,7 +44,6 @@ function subs_subscribe_user($user, $other_nickname)
function subs_subscribe_to($user, $other)
{
-
if ($user->isSubscribed($other)) {
return _('Already subscribed!.');
}
@@ -60,12 +59,16 @@ function subs_subscribe_to($user, $other)
subs_notify($other, $user);
- $cache = common_memcache();
+ $cache = common_memcache();
if ($cache) {
$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
}
+ $profile = $user->getProfile();
+
+ $profile->blowSubscriptionsCount();
+ $other->blowSubscribersCount();
if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) {
if (!$other->subscribeTo($user)) {
@@ -117,7 +120,6 @@ function subs_unsubscribe_user($user, $other_nickname)
function subs_unsubscribe_to($user, $other)
{
-
if (!$user->isSubscribed($other))
return _('Not subscribed!.');
@@ -139,6 +141,11 @@ function subs_unsubscribe_to($user, $other)
$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
}
+ $profile = $user->getProfile();
+
+ $profile->blowSubscriptionsCount();
+ $other->blowSubscribersCount();
+
return true;
}
diff --git a/lib/subscriptionlist.php b/lib/subscriptionlist.php
new file mode 100644
index 000000000..23da64cca
--- /dev/null
+++ b/lib/subscriptionlist.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Widget to show a list of profiles
+ *
+ * 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 Public
+ * @package Laconica
+ * @author Evan Prodromou <evan@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/profilelist.php';
+
+/**
+ * Widget to show a list of subscriptions
+ *
+ * @category Public
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @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 SubscriptionList extends ProfileList
+{
+ /** Owner of this list */
+ var $owner = null;
+
+ function __construct($profile, $owner=null, $action=null)
+ {
+ parent::__construct($profile, $action);
+
+ $this->owner = $owner;
+ }
+
+ function newListItem($profile)
+ {
+ return new SubscriptionListItem($profile, $this->owner, $this->action);
+ }
+}
+
+class SubscriptionListItem extends ProfileListItem
+{
+ /** Owner of this list */
+ var $owner = null;
+
+ function __construct($profile, $owner, $action)
+ {
+ parent::__construct($profile, $action);
+
+ $this->owner = $owner;
+ }
+
+ function showProfile()
+ {
+ $this->startProfile();
+ $this->showAvatar();
+ $this->showFullName();
+ $this->showLocation();
+ $this->showHomepage();
+ $this->showBio();
+ // Relevant portion!
+ $this->showTags();
+ $this->endProfile();
+ }
+
+ function isOwn()
+ {
+ $user = common_current_user();
+ return (!empty($user) && ($this->owner->id == $user->id));
+ }
+
+ function showTags()
+ {
+ $tags = Profile_tag::getTags($this->owner->id, $this->profile->id);
+
+ $this->out->elementStart('dl', 'entity_tags');
+ $this->out->elementStart('dt');
+ if ($this->isOwn()) {
+ $this->out->element('a', array('href' => common_local_url('tagother',
+ array('id' => $this->profile->id))),
+ _('Tags'));
+ } else {
+ $this->out->text(_('Tags'));
+ }
+ $this->out->elementEnd('dt');
+ $this->out->elementStart('dd');
+ if ($tags) {
+ $this->out->elementStart('ul', 'tags xoxo');
+ foreach ($tags as $tag) {
+ $this->out->elementStart('li');
+ $this->out->element('span', 'mark_hash', '#');
+ $this->out->element('a', array('rel' => 'tag',
+ 'href' => common_local_url($this->action->trimmed('action'),
+ array('nickname' => $this->owner->nickname,
+ 'tag' => $tag))),
+ $tag);
+ $this->out->elementEnd('li');
+ }
+ $this->out->elementEnd('ul');
+ } else {
+ $this->out->text(_('(none)'));
+ }
+ $this->out->elementEnd('dd');
+ $this->out->elementEnd('dl');
+ }
+}
diff --git a/lib/tagcloudsection.php b/lib/tagcloudsection.php
index ff2aca6d6..62f7d8961 100644
--- a/lib/tagcloudsection.php
+++ b/lib/tagcloudsection.php
@@ -114,7 +114,11 @@ class TagCloudSection extends Section
function tagUrl($tag)
{
- return common_local_url('tag', array('tag' => $tag));
+ if ('showstream' === $this->out->trimmed('action')) {
+ return common_local_url('showstream', array('nickname' => $this->out->profile->nickname, 'tag' => $tag));
+ } else {
+ return common_local_url('tag', array('tag' => $tag));
+ }
}
function divId()
diff --git a/lib/theme.php b/lib/theme.php
index 95030affe..2fe6ab69b 100644
--- a/lib/theme.php
+++ b/lib/theme.php
@@ -43,10 +43,14 @@ if (!defined('LACONICA')) {
function theme_file($relative, $theme=null)
{
- if (!$theme) {
+ if (empty($theme)) {
$theme = common_config('site', 'theme');
}
- return INSTALLDIR.'/theme/'.$theme.'/'.$relative;
+ $dir = common_config('theme', 'dir');
+ if (empty($dir)) {
+ $dir = INSTALLDIR.'/theme';
+ }
+ return $dir.'/'.$theme.'/'.$relative;
}
/**
@@ -60,13 +64,31 @@ function theme_file($relative, $theme=null)
function theme_path($relative, $theme=null)
{
- if (!$theme) {
+ if (empty($theme)) {
$theme = common_config('site', 'theme');
}
+
+ $path = common_config('theme', 'path');
+
+ if (empty($path)) {
+ $path = common_config('site', 'path') . '/theme/';
+ }
+
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
+
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
+
$server = common_config('theme', 'server');
- if ($server) {
- return 'http://'.$server.'/'.$theme.'/'.$relative;
- } else {
- return common_path('theme/'.$theme.'/'.$relative);
+
+ if (empty($server)) {
+ $server = common_config('site', 'server');
}
-} \ No newline at end of file
+
+ // XXX: protocol
+
+ return 'http://'.$server.$path.$theme.'/'.$relative;
+}
diff --git a/lib/twitter.php b/lib/twitter.php
index ccc6c93ca..47af32e61 100644
--- a/lib/twitter.php
+++ b/lib/twitter.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -346,7 +346,7 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password)
function is_twitter_bound($notice, $flink) {
// Check to see if notice should go to Twitter
- if ($flink->noticesync & FOREIGN_NOTICE_SEND) {
+ if (!empty($flink) && ($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) ||
@@ -360,14 +360,11 @@ function is_twitter_bound($notice, $flink) {
function broadcast_twitter($notice)
{
- $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_null($flink) && is_twitter_bound($notice, $flink)) {
+ if (is_twitter_bound($notice, $flink)) {
$fuser = $flink->getForeignUser();
$twitter_user = $fuser->nickname;
@@ -401,33 +398,99 @@ function broadcast_twitter($notice)
curl_setopt_array($ch, $options);
$data = curl_exec($ch);
$errmsg = curl_error($ch);
+ $errno = curl_errno($ch);
- if ($errmsg) {
- common_debug("cURL error: $errmsg - " .
+ if (!empty($errmsg)) {
+ common_debug("cURL error ($errno): $errmsg - " .
"trying to send notice for $twitter_user.",
__FILE__);
- $success = false;
+
+ $user = $flink->getUser();
+
+ if ($errmsg == 'The requested URL returned error: 401') {
+ common_debug(sprintf('User %s (user id: %s) ' .
+ 'has bad Twitter credentials!',
+ $user->nickname, $user->id));
+
+ // Bad credentials we need to delete the foreign_link
+ // to Twitter and inform the user.
+
+ remove_twitter_link($flink);
+
+ return true;
+
+ } else {
+
+ // Some other error happened, so we should try to
+ // send again later
+
+ return false;
+ }
+
}
curl_close($ch);
- if (!$data) {
+ if (empty($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);
+ // XXX: Not sure this represents a failure to send, but it
+ // probably does
- if (!$status->id) {
- common_debug("Unexpected data returned by Twitter " .
- " API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
+ return false;
+
+ } else {
+
+ // Twitter should return a status
+ $status = json_decode($data);
+
+ if (empty($status)) {
+ common_debug("Unexpected data returned by Twitter " .
+ " API trying to send update for $twitter_user",
+ __FILE__);
+
+ // XXX: Again, this could represent a failure posting
+ // or the Twitter API might just be behaving flakey.
+ // We're treating it as a failure to post.
+
+ return false;
+ }
}
}
- return $success;
+ return true;
+}
+
+function remove_twitter_link($flink)
+{
+ $user = $flink->getUser();
+
+ common_log(LOG_INFO, 'Removing Twitter bridge Foreign link for ' .
+ "user $user->nickname (user id: $user->id).");
+
+ $result = $flink->delete();
+
+ if (empty($result)) {
+ common_log(LOG_ERR, 'Could not remove Twitter bridge ' .
+ "Foreign_link for $user->nickname (user id: $user->id)!");
+ common_log_db_error($flink, 'DELETE', __FILE__);
+ }
+
+ // Notify the user that her Twitter bridge is down
+
+ $result = mail_twitter_bridge_removed($user);
+
+ if (!$result) {
+
+ $msg = 'Unable to send email to notify ' .
+ "$user->nickname (user id: $user->id) " .
+ 'that their Twitter bridge link was ' .
+ 'removed!';
+
+ common_log(LOG_WARNING, $msg);
+ }
+
}
+
diff --git a/lib/twitterapi.php b/lib/twitterapi.php
index ca8b03cdc..f48513e67 100644
--- a/lib/twitterapi.php
+++ b/lib/twitterapi.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,7 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('LACONICA')) {
+ exit(1);
+}
class TwitterapiAction extends Action
{
@@ -87,7 +89,7 @@ class TwitterapiAction extends Action
$twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
$twitter_user['protected'] = false; # not supported by Laconica yet
- $twitter_user['followers_count'] = $this->count_subscriptions($profile);
+ $twitter_user['followers_count'] = $profile->subscriberCount();
// To be supported soon...
$twitter_user['profile_background_color'] = '';
@@ -96,17 +98,11 @@ class TwitterapiAction extends Action
$twitter_user['profile_sidebar_fill_color'] = '';
$twitter_user['profile_sidebar_border_color'] = '';
- $subbed = DB_DataObject::factory('subscription');
- $subbed->subscriber = $profile->id;
- $subbed_count = (int) $subbed->count() - 1;
- $twitter_user['friends_count'] = (is_int($subbed_count)) ? $subbed_count : 0;
+ $twitter_user['friends_count'] = $profile->subscriptionCount();
$twitter_user['created_at'] = $this->date_twitter($profile->created);
- $faves = DB_DataObject::factory('fave');
- $faves->user_id = $user->id;
- $faves_count = (int) $faves->count();
- $twitter_user['favourites_count'] = $faves_count; // British spelling!
+ $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
// Need to pull up the user for some of this
$user = User::staticGet($profile->id);
@@ -127,11 +123,7 @@ class TwitterapiAction extends Action
$twitter_user['profile_background_image_url'] = '';
$twitter_user['profile_background_tile'] = false;
- $notices = DB_DataObject::factory('notice');
- $notices->profile_id = $profile->id;
- $notice_count = (int) $notices->count();
-
- $twitter_user['statuses_count'] = (is_int($notice_count)) ? $notice_count : 0;
+ $twitter_user['statuses_count'] = $profile->noticeCount();
// Is the requesting user following this user?
$twitter_user['following'] = false;
@@ -205,7 +197,6 @@ class TwitterapiAction extends Action
function twitter_rss_entry_array($notice)
{
-
$profile = $notice->getProfile();
$entry = array();
@@ -222,6 +213,19 @@ class TwitterapiAction extends Action
$entry['updated'] = $entry['published'];
$entry['author'] = $profile->getBestName();
+ # Enclosure
+ $attachments = $notice->attachments();
+ if($attachments){
+ $entry['enclosures']=array();
+ foreach($attachments as $attachment){
+ $enclosure=array();
+ $enclosure['url']=$attachment->url;
+ $enclosure['mimetype']=$attachment->mimetype;
+ $enclosure['size']=$attachment->size;
+ $entry['enclosures'][]=$enclosure;
+ }
+ }
+
# RSS Item specific
$entry['description'] = $entry['content'];
$entry['pubDate'] = common_date_rfc2822($notice->created);
@@ -276,6 +280,67 @@ class TwitterapiAction extends Action
return $twitter_dm;
}
+ function twitter_relationship_array($source, $target)
+ {
+ $relationship = array();
+
+ $relationship['source'] =
+ $this->relationship_details_array($source, $target);
+ $relationship['target'] =
+ $this->relationship_details_array($target, $source);
+
+ return array('relationship' => $relationship);
+ }
+
+ function relationship_details_array($source, $target)
+ {
+ $details = array();
+
+ $details['screen_name'] = $source->nickname;
+ $details['followed_by'] = $target->isSubscribed($source);
+ $details['following'] = $source->isSubscribed($target);
+
+ $notifications = false;
+
+ if ($source->isSubscribed($target)) {
+
+ $sub = Subscription::pkeyGet(array('subscriber' =>
+ $source->id, 'subscribed' => $target->id));
+
+ if (!empty($sub)) {
+ $notifications = ($sub->jabber || $sub->sms);
+ }
+ }
+
+ $details['notifications_enabled'] = $notifications;
+ $details['blocking'] = $source->hasBlocked($target);
+ $details['id'] = $source->id;
+
+ return $details;
+ }
+
+ function show_twitter_xml_relationship($relationship)
+ {
+ $this->elementStart('relationship');
+
+ foreach($relationship as $element => $value) {
+ if ($element == 'source' || $element == 'target') {
+ $this->elementStart($element);
+ $this->show_xml_relationship_details($value);
+ $this->elementEnd($element);
+ }
+ }
+
+ $this->elementEnd('relationship');
+ }
+
+ function show_xml_relationship_details($details)
+ {
+ foreach($details as $element => $value) {
+ $this->element($element, null, $value);
+ }
+ }
+
function show_twitter_xml_status($twitter_status)
{
$this->elementStart('status');
@@ -315,6 +380,13 @@ class TwitterapiAction extends Action
$this->element('pubDate', null, $entry['pubDate']);
$this->element('guid', null, $entry['guid']);
$this->element('link', null, $entry['link']);
+
+ # RSS only supports 1 enclosure per item
+ if($entry['enclosures']){
+ $enclosure = $entry['enclosures'][0];
+ $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
+ }
+
$this->elementEnd('item');
}
@@ -521,11 +593,11 @@ class TwitterapiAction extends Action
function init_document($type='xml')
{
switch ($type) {
- case 'xml':
+ case 'xml':
header('Content-Type: application/xml; charset=utf-8');
$this->startXML();
break;
- case 'json':
+ case 'json':
header('Content-Type: application/json; charset=utf-8');
// Check for JSONP callback
@@ -534,16 +606,16 @@ class TwitterapiAction extends Action
print $callback . '(';
}
break;
- case 'rss':
+ case 'rss':
header("Content-Type: application/rss+xml; charset=utf-8");
$this->init_twitter_rss();
break;
- case 'atom':
+ case 'atom':
header('Content-Type: application/atom+xml; charset=utf-8');
$this->init_twitter_atom();
break;
- default:
- $this->client_error(_('Not a supported data format.'));
+ default:
+ $this->clientError(_('Not a supported data format.'));
break;
}
@@ -553,10 +625,10 @@ class TwitterapiAction extends Action
function end_document($type='xml')
{
switch ($type) {
- case 'xml':
+ case 'xml':
$this->endXML();
break;
- case 'json':
+ case 'json':
// Check for JSONP callback
$callback = $this->arg('callback');
@@ -564,20 +636,20 @@ class TwitterapiAction extends Action
print ')';
}
break;
- case 'rss':
+ case 'rss':
$this->end_twitter_rss();
break;
- case 'atom':
+ case 'atom':
$this->end_twitter_rss();
break;
- default:
- $this->client_error(_('Not a supported data format.'));
+ default:
+ $this->clientError(_('Not a supported data format.'));
break;
}
return;
}
- function client_error($msg, $code = 400, $content_type = 'json')
+ function clientError($msg, $code = 400, $content_type = 'json')
{
static $status = array(400 => 'Bad Request',
@@ -657,14 +729,14 @@ class TwitterapiAction extends Action
{
$profile_array = $this->twitter_user_array($profile, true);
switch ($content_type) {
- case 'xml':
+ case 'xml':
$this->show_twitter_xml_user($profile_array);
break;
- case 'json':
+ case 'json':
$this->show_json_objects($profile_array);
break;
- default:
- $this->client_error(_('Not a supported data format.'));
+ default:
+ $this->clientError(_('Not a supported data format.'));
return;
}
return;
@@ -672,8 +744,8 @@ class TwitterapiAction extends Action
function get_user($id, $apidata=null)
{
- if (!$id) {
-
+ if (empty($id)) {
+
// Twitter supports these other ways of passing the user ID
if (is_numeric($this->arg('id'))) {
return User::staticGet($this->arg('id'));
@@ -681,7 +753,7 @@ class TwitterapiAction extends Action
$nickname = common_canonical_nickname($this->arg('id'));
return User::staticGet('nickname', $nickname);
} else if ($this->arg('user_id')) {
- // This is to ensure that a non-numeric user_id still
+ // This is to ensure that a non-numeric user_id still
// overrides screen_name even if it doesn't get used
if (is_numeric($this->arg('user_id'))) {
return User::staticGet('id', $this->arg('user_id'));
@@ -693,7 +765,7 @@ class TwitterapiAction extends Action
// Fall back to trying the currently authenticated user
return $apidata['user'];
}
-
+
} else if (is_numeric($id)) {
return User::staticGet($id);
} else {
@@ -720,13 +792,13 @@ class TwitterapiAction extends Action
{
$source_name = _($source);
switch ($source) {
- case 'web':
- case 'xmpp':
- case 'mail':
- case 'omb':
- case 'api':
+ case 'web':
+ case 'xmpp':
+ case 'mail':
+ case 'omb':
+ case 'api':
break;
- default:
+ default:
$ns = Notice_source::staticGet($source);
if ($ns) {
$source_name = '<a href="' . $ns->url . '">' . $ns->name . '</a>';
@@ -736,4 +808,49 @@ class TwitterapiAction extends Action
return $source_name;
}
+ /**
+ * Returns query argument or default value if not found. Certain
+ * parameters used throughout the API are lightly scrubbed and
+ * bounds checked. This overrides Action::arg().
+ *
+ * @param string $key requested argument
+ * @param string $def default value to return if $key is not provided
+ *
+ * @return var $var
+ */
+ function arg($key, $def=null)
+ {
+
+ // XXX: Do even more input validation/scrubbing?
+
+ if (array_key_exists($key, $this->args)) {
+ switch($key) {
+ case 'page':
+ $page = (int)$this->args['page'];
+ return ($page < 1) ? 1 : $page;
+ case 'count':
+ $count = (int)$this->args['count'];
+ if ($count < 1) {
+ return 20;
+ } elseif ($count > 200) {
+ return 200;
+ } else {
+ return $count;
+ }
+ case 'since_id':
+ $since_id = (int)$this->args['since_id'];
+ return ($since_id < 1) ? 0 : $since_id;
+ case 'max_id':
+ $max_id = (int)$this->args['max_id'];
+ return ($max_id < 1) ? 0 : $max_id;
+ case 'since':
+ return strtotime($this->args['since']);
+ default:
+ return parent::arg($key, $def);
+ }
+ } else {
+ return $def;
+ }
+ }
+
}
diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php
new file mode 100644
index 000000000..515461072
--- /dev/null
+++ b/lib/unqueuemanager.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * A queue manager interface for just doing things immediately
+ *
+ * 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 QueueManager
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @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/
+ */
+
+class UnQueueManager
+{
+ function enqueue($object, $queue)
+ {
+ $notice = $object;
+
+ switch ($queue)
+ {
+ case 'omb':
+ if ($this->_isLocal($notice)) {
+ require_once(INSTALLDIR.'/lib/omb.php');
+ omb_broadcast_remote_subscribers($notice);
+ }
+ break;
+ case 'public':
+ if ($this->_isLocal($notice)) {
+ require_once(INSTALLDIR.'/lib/jabber.php');
+ jabber_public_notice($notice);
+ }
+ break;
+ case 'twitter':
+ if ($this->_isLocal($notice)) {
+ broadcast_twitter($notice);
+ }
+ break;
+ case 'facebook':
+ if ($this->_isLocal($notice)) {
+ require_once INSTALLDIR . '/lib/facebookutil.php';
+ return facebookBroadcastNotice($notice);
+ }
+ break;
+ case 'ping':
+ if ($this->_isLocal($notice)) {
+ require_once INSTALLDIR . '/lib/ping.php';
+ return ping_broadcast_notice($notice);
+ }
+ case 'sms':
+ require_once(INSTALLDIR.'/lib/mail.php');
+ mail_broadcast_notice_sms($notice);
+ break;
+ case 'jabber':
+ require_once(INSTALLDIR.'/lib/jabber.php');
+ jabber_broadcast_notice($notice);
+ break;
+ default:
+ throw ServerException("UnQueueManager: Unknown queue: $type");
+ }
+ }
+
+ function _isLocal($notice)
+ {
+ return ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
+ $notice->is_local == NOTICE_LOCAL_NONPUBLIC);
+ }
+} \ No newline at end of file
diff --git a/lib/util.php b/lib/util.php
index 22308f432..9e8ec41d2 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -114,7 +114,7 @@ function common_check_user($nickname, $password)
return false;
}
$user = User::staticGet('nickname', $nickname);
- if (is_null($user)) {
+ if (is_null($user) || $user === false) {
return false;
} else {
if (0 == strcmp(common_munge_password($password, $user->id),
@@ -139,8 +139,22 @@ function common_have_session()
function common_ensure_session()
{
+ $c = null;
+ if (array_key_exists(session_name, $_COOKIE)) {
+ $c = $_COOKIE[session_name()];
+ }
if (!common_have_session()) {
+ if (common_config('sessions', 'handle')) {
+ Session::setSaveHandler();
+ }
@session_start();
+ if (!isset($_SESSION['started'])) {
+ $_SESSION['started'] = time();
+ if (!empty($c)) {
+ common_log(LOG_WARNING, 'Session cookie "' . $_COOKIE[session_name()] . '" ' .
+ ' is set but started value is null');
+ }
+ }
}
}
@@ -395,7 +409,7 @@ function common_render_text($text)
return $r;
}
-function common_replace_urls_callback($text, $callback) {
+function common_replace_urls_callback($text, $callback, $notice_id = null) {
// Start off with a regex
$regex = '#'.
'(?:'.
@@ -466,7 +480,11 @@ function common_replace_urls_callback($text, $callback) {
$url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
// Call user specified func
- $modified_url = call_user_func($callback, $url);
+ if (empty($notice_id)) {
+ $modified_url = call_user_func($callback, $url);
+ } else {
+ $modified_url = call_user_func($callback, array($url, $notice_id));
+ }
// Replace it!
$start = mb_strpos($text, $url, $offset);
@@ -481,107 +499,79 @@ function common_linkify($url) {
// It comes in special'd, so we unspecial it before passing to the stringifying
// functions
$url = htmlspecialchars_decode($url);
- $display = $url;
- $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url;
- $attrs = array('href' => $url, 'rel' => 'external');
+ $canon = File_redirection::_canonUrl($url);
- if ($longurl = common_longurl($url)) {
- $attrs['title'] = $longurl;
+ $longurl_data = File_redirection::where($url);
+ if (is_array($longurl_data)) {
+ $longurl = $longurl_data['url'];
+ } elseif (is_string($longurl_data)) {
+ $longurl = $longurl_data;
+ } else {
+ throw new ServerException("Can't linkify url '$url'");
}
- return XMLStringer::estring('a', $attrs, $display);
-}
+ $attrs = array('href' => $canon, 'rel' => 'external');
-function common_longurl($short_url)
-{
- $long_url = common_shorten_link($short_url, true);
- if ($long_url === $short_url) return false;
- return $long_url;
-}
+ $is_attachment = false;
+ $attachment_id = null;
+ $has_thumb = false;
-function common_longurl2($uri)
-{
- $uri_e = urlencode($uri);
- $longurl = unserialize(file_get_contents("http://api.longurl.org/v1/expand?format=php&url=$uri_e"));
- if (empty($longurl['long_url']) || $uri === $longurl['long_url']) return false;
- return stripslashes($longurl['long_url']);
-}
-
-function common_shorten_links($text)
-{
- if (mb_strlen($text) <= 140) return $text;
- static $cache = array();
- if (isset($cache[$text])) return $cache[$text];
- // \s = not a horizontal whitespace character (since PHP 5.2.4)
- return $cache[$text] = common_replace_urls_callback($text, 'common_shorten_link');;
-}
-
-function common_shorten_link($url, $reverse = false)
-{
+ // Check to see whether there's a filename associated with this URL.
+ // If there is, it's an upload and qualifies as an attachment
- static $url_cache = array();
- if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url;
+ $localfile = File::staticGet('url', $longurl);
- $user = common_current_user();
- if (!isset($user)) {
- // common current user does not find a user when called from the XMPP daemon
- // therefore we'll set one here fix, so that XMPP given URLs may be shortened
- $user->urlshorteningservice = 'ur1.ca';
+ if (!empty($localfile)) {
+ if (isset($localfile->filename)) {
+ $is_attachment = true;
+ $attachment_id = $localfile->id;
+ }
}
- $curlh = curl_init();
- curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
- curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
- curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
- switch($user->urlshorteningservice) {
- case 'ur1.ca':
- $short_url_service = new LilUrl;
- $short_url = $short_url_service->shorten($url);
- break;
+ // if this URL is an attachment, then we set class='attachment' and id='attahcment-ID'
+ // where ID is the id of the attachment for the given URL.
+ //
+ // we need a better test telling what can be shown as an attachment
+ // we're currently picking up oembeds only.
+ // I think the best option is another file_view table in the db
+ // and associated dbobject.
- case '2tu.us':
- $short_url_service = new TightUrl;
- $short_url = $short_url_service->shorten($url);
- break;
+ $query = "select file_oembed.file_id as file_id from file join file_oembed on file.id = file_oembed.file_id where file.url='$longurl'";
+ $file = new File;
+ $file->query($query);
+ $file->fetch();
- case 'ptiturl.com':
- $short_url_service = new PtitUrl;
- $short_url = $short_url_service->shorten($url);
- break;
+ if (!empty($file->file_id)) {
+ $is_attachment = true;
+ $attachment_id = $file->file_id;
- case 'bit.ly':
- curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($url));
- $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
- break;
+ $query = "select file_thumbnail.file_id as file_id from file join file_thumbnail on file.id = file_thumbnail.file_id where file.url='$longurl'";
+ $file2 = new File;
+ $file2->query($query);
+ $file2->fetch();
- case 'is.gd':
- curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($url));
- $short_url = curl_exec($curlh);
- break;
- case 'snipr.com':
- curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($url));
- $short_url = curl_exec($curlh);
- break;
- case 'metamark.net':
- curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($url));
- $short_url = curl_exec($curlh);
- break;
- case 'tinyurl.com':
- curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($url));
- $short_url = curl_exec($curlh);
- break;
- default:
- $short_url = false;
+ if (!empty($file2)) {
+ $has_thumb = true;
+ }
}
- curl_close($curlh);
-
- if ($short_url) {
- $url_cache[(string)$short_url] = $url;
- return (string)$short_url;
+ // Add clippy
+ if ($is_attachment) {
+ $attrs['class'] = 'attachment';
+ if ($has_thumb) {
+ $attrs['class'] = 'attachment thumbnail';
+ }
+ $attrs['id'] = "attachment-{$attachment_id}";
}
- return $url;
+
+ return XMLStringer::estring('a', $attrs, $url);
+}
+
+function common_shorten_links($text)
+{
+ if (mb_strlen($text) <= 140) return $text;
+ return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
}
function common_xml_safe_str($str)
@@ -644,7 +634,7 @@ function common_at_link($sender_id, $nickname)
function common_group_link($sender_id, $nickname)
{
$sender = Profile::staticGet($sender_id);
- $group = User_group::staticGet('nickname', common_canonical_nickname($nickname));
+ $group = User_group::getForNickname($nickname);
if ($group && $sender->isMember($group)) {
$attrs = array('href' => $group->permalink(),
'class' => 'url');
@@ -843,7 +833,12 @@ function common_date_iso8601($dt)
function common_sql_now()
{
- return strftime('%Y-%m-%d %H:%M:%S', time());
+ return common_sql_date(time());
+}
+
+function common_sql_date($datetime)
+{
+ return strftime('%Y-%m-%d %H:%M:%S', $datetime);
}
function common_redirect($url, $code=307)
@@ -867,101 +862,43 @@ function common_redirect($url, $code=307)
function common_broadcast_notice($notice, $remote=false)
{
- if (common_config('queue', 'enabled')) {
- // Do it later!
- return common_enqueue_notice($notice);
- } else {
- return common_real_broadcast($notice, $remote);
- }
+ return common_enqueue_notice($notice);
}
// Stick the notice on the queue
function common_enqueue_notice($notice)
{
- $transports = array('twitter', 'facebook', 'ping');
+ static $localTransports = array('omb',
+ 'twitter',
+ 'facebook',
+ 'ping');
+ static $allTransports = array('sms');
- // If inboxes are enabled, wait till inboxes are filled
- // before doing inbox-dependent broadcasts
+ $transports = $allTransports;
- $transports = array_merge($transports, common_post_inbox_transports());
+ $xmpp = common_config('xmpp', 'enabled');
- foreach ($transports as $transport) {
- common_enqueue_notice_transport($notice, $transport);
+ if ($xmpp) {
+ $transports[] = 'jabber';
}
- return $result;
-}
-
-function common_post_inbox_transports()
-{
- $transports = array('omb', 'sms');
-
- if (common_config('xmpp', 'enabled')) {
- $transports = array_merge($transports, array('jabber', 'public'));
+ if ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
+ $notice->is_local == NOTICE_LOCAL_NONPUBLIC) {
+ $transports = array_merge($transports, $localTransports);
+ if ($xmpp) {
+ $transports[] = 'public';
+ }
}
- return $transports;
-}
+ $qm = QueueManager::get();
-function common_enqueue_notice_transport($notice, $transport)
-{
- $qi = new Queue_item();
- $qi->notice_id = $notice->id;
- $qi->transport = $transport;
- $qi->created = $notice->created;
- $result = $qi->insert();
- if (!$result) {
- $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
- common_log(LOG_ERR, 'DB error inserting queue item: ' . $last_error->message);
- throw new ServerException('DB error inserting queue item: ' . $last_error->message);
+ foreach ($transports as $transport)
+ {
+ $qm->enqueue($notice, $transport);
}
- common_log(LOG_DEBUG, 'complete queueing notice ID = ' . $notice->id . ' for ' . $transport);
- return true;
-}
-function common_real_broadcast($notice, $remote=false)
-{
- $success = true;
- if (!$remote) {
- // Make sure we have the OMB stuff
- require_once(INSTALLDIR.'/lib/omb.php');
- $success = omb_broadcast_remote_subscribers($notice);
- if (!$success) {
- common_log(LOG_ERR, 'Error in OMB broadcast for notice ' . $notice->id);
- }
- }
- if ($success) {
- require_once(INSTALLDIR.'/lib/jabber.php');
- $success = jabber_broadcast_notice($notice);
- if (!$success) {
- common_log(LOG_ERR, 'Error in jabber broadcast for notice ' . $notice->id);
- }
- }
- if ($success) {
- require_once(INSTALLDIR.'/lib/mail.php');
- $success = mail_broadcast_notice_sms($notice);
- if (!$success) {
- common_log(LOG_ERR, 'Error in sms broadcast for notice ' . $notice->id);
- }
- }
- if ($success) {
- $success = jabber_public_notice($notice);
- if (!$success) {
- 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;
+ return true;
}
function common_broadcast_profile($profile)
@@ -1041,20 +978,26 @@ function common_ensure_syslog()
{
static $initialized = false;
if (!$initialized) {
- openlog(common_config('syslog', 'appname'), 0, LOG_USER);
+ openlog(common_config('syslog', 'appname'), 0,
+ common_config('syslog', 'facility'));
$initialized = true;
}
}
+function common_log_line($priority, $msg)
+{
+ static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR',
+ 'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG');
+ return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n";
+}
+
function common_log($priority, $msg, $filename=null)
{
$logfile = common_config('site', 'logfile');
if ($logfile) {
$log = fopen($logfile, "a");
if ($log) {
- static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR',
- 'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG');
- $output = date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n";
+ $output = common_log_line($priority, $msg);
fwrite($log, $output);
fclose($log);
}
@@ -1085,6 +1028,9 @@ function common_log_objstring(&$object)
if (is_null($object)) {
return "null";
}
+ if (!($object instanceof DB_DataObject)) {
+ return "(unknown)";
+ }
$arr = $object->toArray();
$fields = array();
foreach ($arr as $k => $v) {
@@ -1124,7 +1070,7 @@ function common_accept_to_prefs($accept, $def = '*/*')
foreach($parts as $part) {
// FIXME: doesn't deal with params like 'text/html; level=1'
- @list($value, $qpart) = explode(';', $part);
+ @list($value, $qpart) = explode(';', trim($part));
$match = array();
if(!isset($qpart)) {
$prefs[$value] = 1;
@@ -1285,18 +1231,39 @@ function common_canonical_sms($sms)
function common_error_handler($errno, $errstr, $errfile, $errline, $errcontext)
{
switch ($errno) {
+
+ case E_ERROR:
+ case E_COMPILE_ERROR:
+ case E_CORE_ERROR:
case E_USER_ERROR:
- common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline)");
- exit(1);
+ case E_PARSE:
+ case E_RECOVERABLE_ERROR:
+ common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline) [ABORT]");
+ die();
break;
+ case E_WARNING:
+ case E_COMPILE_WARNING:
+ case E_CORE_WARNING:
case E_USER_WARNING:
common_log(LOG_WARNING, "[$errno] $errstr ($errfile:$errline)");
break;
+ case E_NOTICE:
case E_USER_NOTICE:
common_log(LOG_NOTICE, "[$errno] $errstr ($errfile:$errline)");
break;
+
+ case E_STRICT:
+ case E_DEPRECATED:
+ case E_USER_DEPRECATED:
+ // XXX: config variable to log this stuff, too
+ break;
+
+ default:
+ common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline) [UNKNOWN LEVEL, die()'ing]");
+ die();
+ break;
}
// FIXME: show error page if we're on the Web
@@ -1315,7 +1282,13 @@ function common_session_token()
function common_cache_key($extra)
{
- return 'laconica:' . common_keyize(common_config('site', 'name')) . ':' . $extra;
+ $base_key = common_config('memcached', 'base');
+
+ if (empty($base_key)) {
+ $base_key = common_keyize(common_config('site', 'name'));
+ }
+
+ return 'laconica:' . $base_key . ':' . $extra;
}
function common_keyize($str)
@@ -1364,3 +1337,92 @@ function common_database_tablename($tablename)
//table prefixes could be added here later
return $tablename;
}
+
+function common_shorten_url($long_url)
+{
+ $user = common_current_user();
+ if (empty($user)) {
+ // common current user does not find a user when called from the XMPP daemon
+ // therefore we'll set one here fix, so that XMPP given URLs may be shortened
+ $svc = 'ur1.ca';
+ } else {
+ $svc = $user->urlshorteningservice;
+ }
+
+ $curlh = curl_init();
+ curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
+ curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
+ curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
+
+ switch($svc) {
+ case 'ur1.ca':
+ require_once INSTALLDIR.'/lib/Shorturl_api.php';
+ $short_url_service = new LilUrl;
+ $short_url = $short_url_service->shorten($long_url);
+ break;
+
+ case '2tu.us':
+ $short_url_service = new TightUrl;
+ require_once INSTALLDIR.'/lib/Shorturl_api.php';
+ $short_url = $short_url_service->shorten($long_url);
+ break;
+
+ case 'ptiturl.com':
+ require_once INSTALLDIR.'/lib/Shorturl_api.php';
+ $short_url_service = new PtitUrl;
+ $short_url = $short_url_service->shorten($long_url);
+ break;
+
+ case 'bit.ly':
+ curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url));
+ $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
+ break;
+
+ case 'is.gd':
+ curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url));
+ $short_url = curl_exec($curlh);
+ break;
+ case 'snipr.com':
+ curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url));
+ $short_url = curl_exec($curlh);
+ break;
+ case 'metamark.net':
+ curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url));
+ $short_url = curl_exec($curlh);
+ break;
+ case 'tinyurl.com':
+ curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url));
+ $short_url = curl_exec($curlh);
+ break;
+ default:
+ $short_url = false;
+ }
+
+ curl_close($curlh);
+
+ return $short_url;
+}
+
+function common_client_ip()
+{
+ if (!isset($_SERVER) || !array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ return null;
+ }
+
+ if ($_SERVER['HTTP_X_FORWARDED_FOR']) {
+ if ($_SERVER['HTTP_CLIENT_IP']) {
+ $proxy = $_SERVER['HTTP_CLIENT_IP'];
+ } else {
+ $proxy = $_SERVER['REMOTE_ADDR'];
+ }
+ $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+ } else {
+ if ($_SERVER['HTTP_CLIENT_IP']) {
+ $ip = $_SERVER['HTTP_CLIENT_IP'];
+ } else {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ }
+ }
+
+ return array($ip, $proxy);
+}
diff --git a/lib/webcolor.php b/lib/webcolor.php
new file mode 100644
index 000000000..f3ca6e94a
--- /dev/null
+++ b/lib/webcolor.php
@@ -0,0 +1,192 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for deleting things
+ *
+ * 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 Personal
+ * @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);
+}
+
+class WebColor {
+
+ // XXX: Maybe make getters and setters for r,g,b values and tuples,
+ // e.g.: to support this kinda CSS representation: rgb(255,0,0)
+ // http://www.w3.org/TR/CSS21/syndata.html#color-units
+
+ var $red = 0;
+ var $green = 0;
+ var $blue = 0;
+
+ /**
+ * Constructor
+ *
+ * @return nothing
+ */
+
+ function __construct($color = null)
+ {
+ if (isset($color)) {
+ $this->parseColor($color);
+ }
+ }
+
+ /**
+ * Parses input to and tries to determine whether the color
+ * is being specified via an integer or hex tuple and sets
+ * the RGB instance variables accordingly.
+ *
+ * XXX: Maybe support (r,g,b) style, and array?
+ *
+ * @param mixed $color
+ *
+ * @return nothing
+ */
+
+ function parseColor($color) {
+
+ if (is_numeric($color)) {
+ $this->setIntColor($color);
+ } else {
+
+ // XXX named colors
+
+ // XXX: probably should do even more validation
+
+ if (preg_match('/(#([0-9A-Fa-f]{3,6})\b)/u', $color) > 0) {
+ $this->setHexColor($color);
+ } else {
+ $errmsg = _('%s is not a valid color!');
+ throw new WebColorException(sprintf($errmsg, $color));
+ }
+ }
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return nothing
+ */
+
+ function setNamedColor($name)
+ {
+ // XXX Implement this
+ }
+
+
+ /**
+ * Sets the RGB color values from a a hex tuple
+ *
+ * @param string $hexcolor
+ *
+ * @return nothing
+ */
+
+ function setHexColor($hexcolor) {
+
+ if ($hexcolor[0] == '#') {
+ $hexcolor = substr($hexcolor, 1);
+ }
+
+ if (strlen($hexcolor) == 6) {
+ list($r, $g, $b) = array($hexcolor[0].$hexcolor[1],
+ $hexcolor[2].$hexcolor[3],
+ $hexcolor[4].$hexcolor[5]);
+ } elseif (strlen($hexcolor) == 3) {
+ list($r, $g, $b) = array($hexcolor[0].$hexcolor[0],
+ $hexcolor[1].$hexcolor[1],
+ $hexcolor[2].$hexcolor[2]);
+ } else {
+ $errmsg = _('%s is not a valid color! Use 3 or 6 hex chars.');
+ throw new WebColorException(sprintf($errmsg, $hexcolor));
+ }
+
+ $this->red = hexdec($r);
+ $this->green = hexdec($g);
+ $this->blue = hexdec($b);
+
+ }
+
+ /**
+ * Sets the RGB color values from a 24-bit integer
+ *
+ * @param int $intcolor
+ *
+ * @return nothing
+ */
+
+ function setIntColor($intcolor)
+ {
+ // We could do 32 bit and have an alpha channel because
+ // Sarven wants one real bad, but nah.
+
+ $this->red = $intcolor >> 16;
+ $this->green = $intcolor >> 8 & 0xFF;
+ $this->blue = $intcolor & 0xFF;
+
+ }
+
+ /**
+ * Returns a hex tuple of the RGB color useful for output in HTML
+ *
+ * @return string
+ */
+
+ function hexValue() {
+
+ $hexcolor = (strlen(dechex($this->red)) < 2 ? '0' : '' ) .
+ dechex($this->red);
+ $hexcolor .= (strlen(dechex($this->green)) < 2 ? '0' : '') .
+ dechex($this->green);
+ $hexcolor .= (strlen(dechex($this->blue)) < 2 ? '0' : '') .
+ dechex($this->blue);
+
+ return strtoupper($hexcolor);
+
+ }
+
+ /**
+ * Returns a 24-bit packed integer representation of the RGB color
+ * for convenient storage in the DB
+ *
+ * XXX: probably could just use hexdec() instead
+ *
+ * @return int
+ */
+
+ function intValue()
+ {
+ $intcolor = 256 * 256 * $this->red + 256 * $this->green + $this->blue;
+ return $intcolor;
+ }
+
+}
+
+class WebColorException extends Exception
+{
+}
+
+?> \ No newline at end of file
diff --git a/lib/xmppqueuehandler.php b/lib/xmppqueuehandler.php
index 91015fd45..77d476c30 100644
--- a/lib/xmppqueuehandler.php
+++ b/lib/xmppqueuehandler.php
@@ -1,7 +1,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -21,8 +21,10 @@ if (!defined('LACONICA')) { exit(1); }
require_once(INSTALLDIR.'/lib/queuehandler.php');
+define('PING_INTERVAL', 120);
+
/**
- * Common superclass for all XMPP-using queue handlers. They all need to
+ * Common superclass for all XMPP-using queue handlers. They all need to
* service their message queues on idle, and forward any incoming messages
* to the XMPP listener connection. So, we abstract out common code to a
* superclass.
@@ -30,12 +32,14 @@ require_once(INSTALLDIR.'/lib/queuehandler.php');
class XmppQueueHandler extends QueueHandler
{
-
+ var $pingid = 0;
+ var $lastping = null;
+
function start()
{
# Low priority; we don't want to receive messages
$this->log(LOG_INFO, "INITIALIZE");
- $this->conn = jabber_connect($this->_id);
+ $this->conn = jabber_connect($this->_id.$this->transport());
if ($this->conn) {
$this->conn->addEventHandler('message', 'forward_message', $this);
$this->conn->addEventHandler('reconnect', 'handle_reconnect', $this);
@@ -44,7 +48,12 @@ class XmppQueueHandler extends QueueHandler
}
return !is_null($this->conn);
}
-
+
+ function timeout()
+ {
+ return 10;
+ }
+
function handle_reconnect(&$pl)
{
$this->conn->processUntil('session_start');
@@ -56,14 +65,36 @@ class XmppQueueHandler extends QueueHandler
# Process the queue for as long as needed
try {
if ($this->conn) {
+ $this->log(LOG_DEBUG, "Servicing the XMPP queue.");
$this->conn->processTime($timeout);
+ $now = time();
+ if (empty($this->lastping) || $now - $this->lastping > PING_INTERVAL) {
+ $this->sendPing();
+ $this->lastping = $now;
+ }
}
} catch (XMPPHP_Exception $e) {
$this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
die($e->getMessage());
}
}
-
+
+ function sendPing()
+ {
+ $jid = jabber_daemon_address().'/'.$this->_id.$this->transport();
+ $server = common_config('xmpp', 'server');
+
+ if (!isset($this->pingid)) {
+ $this->pingid = 0;
+ } else {
+ $this->pingid++;
+ }
+
+ $this->log(LOG_DEBUG, "Sending ping #{$this->pingid}");
+
+ $this->conn->send("<iq from='{$jid}' to='{$server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
+ }
+
function forward_message(&$pl)
{
if ($pl['type'] != 'chat') {
@@ -92,7 +123,12 @@ class XmppQueueHandler extends QueueHandler
if (common_config('xmpp', 'listener')) {
return common_config('xmpp', 'listener');
} else {
- return jabber_daemon_address() . '/' . common_config('xmpp','resource') . '-listener';
+ return jabber_daemon_address() . '/' . common_config('xmpp','resource') . 'daemon';
}
}
+
+ function getSockets()
+ {
+ return array($this->conn->getSocket());
+ }
}
diff --git a/plugins/FBConnect/FBCLoginGroupNav.php b/plugins/FBConnect/FBCLoginGroupNav.php
new file mode 100644
index 000000000..6eb09c3c0
--- /dev/null
+++ b/plugins/FBConnect/FBCLoginGroupNav.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Menu for login group of actions
+ *
+ * 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 Menu
+ * @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);
+}
+
+require_once INSTALLDIR . '/lib/widget.php';
+
+/**
+ * Menu for login group of actions
+ *
+ * @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 Widget
+ */
+
+class FBCLoginGroupNav extends Widget
+{
+ var $action = null;
+
+ /**
+ * Construction
+ *
+ * @param Action $action current action, used for output
+ */
+
+ function __construct($action=null)
+ {
+ parent::__construct($action);
+ $this->action = $action;
+ }
+
+ /**
+ * Show the menu
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $this->action->elementStart('dl', array('id' => 'site_nav_local_views'));
+ $this->action->element('dt', null, _('Local views'));
+ $this->action->elementStart('dd');
+
+ // action => array('prompt', 'title')
+ $menu = array();
+
+ $menu['login'] = array(_('Login'),
+ _('Login with a username and password'));
+
+ if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
+ $menu['register'] = array(_('Register'),
+ _('Sign up for a new account'));
+ }
+
+ $menu['openidlogin'] = array(_('OpenID'),
+ _('Login or register with OpenID'));
+
+ $menu['FBConnectLogin'] = array(_('Facebook'),
+ _('Login or register using Facebook'));
+
+ $action_name = $this->action->trimmed('action');
+ $this->action->elementStart('ul', array('class' => 'nav'));
+
+ foreach ($menu as $menuaction => $menudesc) {
+ $this->action->menuItem(common_local_url($menuaction),
+ $menudesc[0],
+ $menudesc[1],
+ $action_name === $menuaction);
+ }
+
+ $this->action->elementEnd('ul');
+
+ $this->action->elementEnd('dd');
+ $this->action->elementEnd('dl');
+ }
+}
diff --git a/plugins/FBConnect/FBCSettingsNav.php b/plugins/FBConnect/FBCSettingsNav.php
new file mode 100644
index 000000000..8b8411853
--- /dev/null
+++ b/plugins/FBConnect/FBCSettingsNav.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Menu for login group of actions
+ *
+ * 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 Menu
+ * @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);
+}
+
+require_once INSTALLDIR . '/lib/widget.php';
+
+/**
+ * A widget for showing the connect group local nav menu
+ *
+ * @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 Widget
+ */
+
+class FBCSettingsNav extends Widget
+{
+ var $action = null;
+
+ /**
+ * Construction
+ *
+ * @param Action $action current action, used for output
+ */
+
+ function __construct($action=null)
+ {
+ parent::__construct($action);
+ $this->action = $action;
+ }
+
+ /**
+ * Show the menu
+ *
+ * @return void
+ */
+
+ function show()
+ {
+
+ $this->action->elementStart('dl', array('id' => 'site_nav_local_views'));
+ $this->action->element('dt', null, _('Local views'));
+ $this->action->elementStart('dd');
+
+ # action => array('prompt', 'title')
+ $menu =
+ array('imsettings' =>
+ array(_('IM'),
+ _('Updates by instant messenger (IM)')),
+ 'smssettings' =>
+ array(_('SMS'),
+ _('Updates by SMS')),
+ 'twittersettings' =>
+ array(_('Twitter'),
+ _('Twitter integration options')),
+ 'FBConnectSettings' =>
+ array(_('Facebook'),
+ _('Facebook Connect settings')));
+
+ $action_name = $this->action->trimmed('action');
+ $this->action->elementStart('ul', array('class' => 'nav'));
+
+ foreach ($menu as $menuaction => $menudesc) {
+ if ($menuaction == 'imsettings' &&
+ !common_config('xmpp', 'enabled')) {
+ continue;
+ }
+ $this->action->menuItem(common_local_url($menuaction),
+ $menudesc[0],
+ $menudesc[1],
+ $action_name === $menuaction);
+ }
+
+ $this->action->elementEnd('ul');
+
+ $this->action->elementEnd('dd');
+ $this->action->elementEnd('dl');
+ }
+}
diff --git a/plugins/FBConnect/FBC_XDReceiver.php b/plugins/FBConnect/FBC_XDReceiver.php
new file mode 100644
index 000000000..57c98b4f1
--- /dev/null
+++ b/plugins/FBConnect/FBC_XDReceiver.php
@@ -0,0 +1,73 @@
+<?php
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/*
+ * Generates the cross domain communication channel file
+ * (xd_receiver.html). By generating it we can add some caching
+ * instructions.
+ *
+ * See: http://wiki.developers.facebook.com/index.php/Cross_Domain_Communication_Channel
+ */
+class FBC_XDReceiverAction extends Action
+{
+
+ /**
+ * Do we need to write to the database?
+ *
+ * @return boolean true
+ */
+
+ function isReadonly()
+ {
+ return true;
+ }
+
+ /**
+ * Handle a request
+ *
+ * @param array $args Arguments from $_REQUEST
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ // Parent handling, including cache check
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ function showPage()
+ {
+ // cache the xd_receiver
+ header('Cache-Control: max-age=225065900');
+ header('Expires:');
+ header('Pragma:');
+
+ $this->startXML('html',
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+
+ $language = $this->getLanguage();
+
+ $this->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
+ 'xml:lang' => $language,
+ 'lang' => $language));
+ $this->elementStart('head');
+ $this->element('title', null, 'cross domain receiver page');
+ $this->element('script',
+ array('src' =>
+ 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js',
+ 'type' => 'text/javascript'), '');
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->elementEnd('body');
+
+ $this->elementEnd('html');
+ }
+
+}
+
diff --git a/plugins/FBConnect/FBConnectAuth.php b/plugins/FBConnect/FBConnectAuth.php
new file mode 100644
index 000000000..3cf9fefc1
--- /dev/null
+++ b/plugins/FBConnect/FBConnectAuth.php
@@ -0,0 +1,454 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to enable Facebook Connect
+ *
+ * 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 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/
+ */
+
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
+
+class FBConnectauthAction extends Action
+{
+
+ var $fbuid = null;
+ var $fb_fields = null;
+
+ function prepare($args) {
+ parent::prepare($args);
+
+ try {
+
+ $this->fbuid = getFacebook()->get_loggedin_user();
+
+ if ($this->fbuid > 0) {
+ $this->fb_fields = $this->getFacebookFields($this->fbuid,
+ array('first_name', 'last_name', 'name'));
+ } else {
+ common_debug("No Facebook User found.");
+ }
+
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, 'Problem getting Facebook uid: ' .
+ $e->getMessage());
+ }
+
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if (common_is_real_login()) {
+
+ // User is already logged in. Does she already have a linked Facebook acct?
+ $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_CONNECT_SERVICE);
+
+ if (!empty($flink)) {
+
+ // User already has a linked Facebook account and shouldn't be here
+ common_debug('There is already a local user (' . $flink->user_id .
+ ') linked with this Facebook (' . $this->fbuid . ').');
+
+ // We don't want these cookies
+ getFacebook()->clear_cookie_state();
+
+ $this->clientError(_('There is already a local user linked with this Facebook.'));
+
+ } else {
+
+ // User came from the Facebook connect settings tab, and
+ // probably just wants to link/relink their Facebook account
+ $this->connectUser();
+ }
+
+ } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. Try again, please.'));
+ return;
+ }
+ if ($this->arg('create')) {
+ if (!$this->boolean('license')) {
+ $this->showForm(_('You can\'t register if you don\'t agree to the license.'),
+ $this->trimmed('newname'));
+ return;
+ }
+ $this->createNewUser();
+ } else if ($this->arg('connect')) {
+ $this->connectNewUser();
+ } else {
+ common_debug(print_r($this->args, true), __FILE__);
+ $this->showForm(_('Something weird happened.'),
+ $this->trimmed('newname'));
+ }
+ } else {
+ $this->tryLogin();
+ }
+ }
+
+ function showPageNotice()
+ {
+ if ($this->error) {
+ $this->element('div', array('class' => 'error'), $this->error);
+ } else {
+ $this->element('div', 'instructions',
+ sprintf(_('This is the first time you\'ve logged into %s so we must connect your Facebook to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name')));
+ }
+ }
+
+ function title()
+ {
+ return _('Facebook Account Setup');
+ }
+
+ function showForm($error=null, $username=null)
+ {
+ $this->error = $error;
+ $this->username = $username;
+
+ $this->showPage();
+ }
+
+ function showPage()
+ {
+ parent::showPage();
+ }
+
+ function showContent()
+ {
+ if (!empty($this->message_text)) {
+ $this->element('p', null, $this->message);
+ return;
+ }
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_facebook_connect',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('FBConnectAuth')));
+ $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
+ $this->element('legend', null, _('Connection options'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->element('input', array('type' => 'checkbox',
+ 'id' => 'license',
+ 'class' => 'checkbox',
+ 'name' => 'license',
+ 'value' => 'true'));
+ $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
+ $this->text(_('My text and files are available under '));
+ $this->element('a', array('href' => common_config('license', 'url')),
+ common_config('license', 'title'));
+ $this->text(_(' except this private data: password, email address, IM address, phone number.'));
+ $this->elementEnd('label');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+ $this->element('legend', null,
+ _('Create new account'));
+ $this->element('p', null,
+ _('Create a new user with this nickname.'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('newname', _('New nickname'),
+ ($this->username) ? $this->username : '',
+ _('1-64 lowercase letters or numbers, no punctuation or spaces'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('create', _('Create'));
+ $this->elementEnd('fieldset');
+
+ $this->elementStart('fieldset');
+ $this->element('legend', null,
+ _('Connect existing account'));
+ $this->element('p', null,
+ _('If you already have an account, login with your username and password to connect it to your Facebook.'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('nickname', _('Existing nickname'));
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->password('password', _('Password'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('connect', _('Connect'));
+ $this->elementEnd('fieldset');
+
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ function message($msg)
+ {
+ $this->message_text = $msg;
+ $this->showPage();
+ }
+
+ function createNewUser()
+ {
+
+ if (common_config('site', 'closed')) {
+ $this->clientError(_('Registration not allowed.'));
+ return;
+ }
+
+ $invite = null;
+
+ if (common_config('site', 'inviteonly')) {
+ $code = $_SESSION['invitecode'];
+ if (empty($code)) {
+ $this->clientError(_('Registration not allowed.'));
+ return;
+ }
+
+ $invite = Invitation::staticGet($code);
+
+ if (empty($invite)) {
+ $this->clientError(_('Not a valid invitation code.'));
+ return;
+ }
+ }
+
+ $nickname = $this->trimmed('newname');
+
+ if (!Validate::string($nickname, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+ $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
+ return;
+ }
+
+ if (!User::allowed_nickname($nickname)) {
+ $this->showForm(_('Nickname not allowed.'));
+ return;
+ }
+
+ if (User::staticGet('nickname', $nickname)) {
+ $this->showForm(_('Nickname already in use. Try another one.'));
+ return;
+ }
+
+ $fullname = trim($this->fb_fields['firstname'] .
+ ' ' . $this->fb_fields['lastname']);
+
+ $args = array('nickname' => $nickname, 'fullname' => $fullname);
+
+ if (!empty($invite)) {
+ $args['code'] = $invite->code;
+ }
+
+ $user = User::register($args);
+
+ $result = $this->flinkUser($user->id, $this->fbuid);
+
+ if (!$result) {
+ $this->serverError(_('Error connecting user to Facebook.'));
+ return;
+ }
+
+ common_set_user($user);
+ common_real_login(true);
+
+ common_debug("Registered new user $user->id from Facebook user $this->fbuid");
+
+ common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)),
+ 303);
+ }
+
+ function connectNewUser()
+ {
+ $nickname = $this->trimmed('nickname');
+ $password = $this->trimmed('password');
+
+ if (!common_check_user($nickname, $password)) {
+ $this->showForm(_('Invalid username or password.'));
+ return;
+ }
+
+ $user = User::staticGet('nickname', $nickname);
+
+ if ($user) {
+ common_debug("Legit user to connect to Facebook: $nickname");
+ }
+
+ $result = $this->flinkUser($user->id, $this->fbuid);
+
+ if (!$result) {
+ $this->serverError(_('Error connecting user to Facebook.'));
+ return;
+ }
+
+ common_debug("Connected Facebook user $this->fbuid to local user $user->id");
+
+ common_set_user($user);
+ common_real_login(true);
+
+ $this->goHome($user->nickname);
+ }
+
+ function connectUser()
+ {
+ $user = common_current_user();
+
+ $result = $this->flinkUser($user->id, $this->fbuid);
+
+ if (!$result) {
+ $this->serverError(_('Error connecting user to Facebook.'));
+ return;
+ }
+
+ common_debug("Connected Facebook user $this->fbuid to local user $user->id");
+
+ // Return to Facebook connection settings tab
+ common_redirect(common_local_url('FBConnectSettings'), 303);
+ }
+
+ function tryLogin()
+ {
+ common_debug("Trying Facebook Login...");
+
+ $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_CONNECT_SERVICE);
+
+ if ($flink) {
+ $user = $flink->getUser();
+
+ if (!empty($user)) {
+
+ common_debug("Logged in Facebook user $flink->foreign_id as user $user->id ($user->nickname)");
+
+ common_set_user($user);
+ common_real_login(true);
+ $this->goHome($user->nickname);
+ }
+
+ } else {
+
+ common_debug("No flink found for fbuid: $this->fbuid");
+
+ $this->showForm(null, $this->bestNewNickname());
+ }
+ }
+
+ function goHome($nickname)
+ {
+ $url = common_get_returnto();
+ if ($url) {
+ // We don't have to return to it again
+ common_set_returnto(null);
+ } else {
+ $url = common_local_url('all',
+ array('nickname' =>
+ $nickname));
+ }
+
+ common_redirect($url, 303);
+ }
+
+ function flinkUser($user_id, $fbuid)
+ {
+ $flink = new Foreign_link();
+ $flink->user_id = $user_id;
+ $flink->foreign_id = $fbuid;
+ $flink->service = FACEBOOK_CONNECT_SERVICE;
+ $flink->created = common_sql_now();
+
+ $flink_id = $flink->insert();
+
+ return $flink_id;
+ }
+
+ function bestNewNickname()
+ {
+ if (!empty($this->fb_fields['name'])) {
+ $nickname = $this->nicknamize($this->fb_fields['name']);
+ if ($this->isNewNickname($nickname)) {
+ return $nickname;
+ }
+ }
+
+ // Try the full name
+
+ $fullname = trim($this->fb_fields['firstname'] .
+ ' ' . $this->fb_fields['lastname']);
+
+ if (!empty($fullname)) {
+ $fullname = $this->nicknamize($fullname);
+ if ($this->isNewNickname($fullname)) {
+ return $fullname;
+ }
+ }
+
+ return null;
+ }
+
+ // Given a string, try to make it work as a nickname
+
+ function nicknamize($str)
+ {
+ $str = preg_replace('/\W/', '', $str);
+ return strtolower($str);
+ }
+
+ function isNewNickname($str)
+ {
+ if (!Validate::string($str, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+ return false;
+ }
+ if (!User::allowed_nickname($str)) {
+ return false;
+ }
+ if (User::staticGet('nickname', $str)) {
+ return false;
+ }
+ return true;
+ }
+
+ // XXX: Consider moving this to lib/facebookutil.php
+ function getFacebookFields($fb_uid, $fields) {
+ try {
+
+ $facebook = getFacebook();
+
+ $infos = $facebook->api_client->users_getInfo($fb_uid, $fields);
+
+ if (empty($infos)) {
+ return null;
+ }
+ return reset($infos);
+
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, "Facebook client failure when requesting " .
+ join(",", $fields) . " on uid " . $fb_uid .
+ " : ". $e->getMessage());
+ return null;
+ }
+ }
+
+}
diff --git a/plugins/FBConnect/FBConnectLogin.php b/plugins/FBConnect/FBConnectLogin.php
new file mode 100644
index 000000000..205086cd8
--- /dev/null
+++ b/plugins/FBConnect/FBConnectLogin.php
@@ -0,0 +1,67 @@
+<?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);
+}
+
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
+
+class FBConnectLoginAction extends Action
+{
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if (common_is_real_login()) {
+ $this->clientError(_('Already logged in.'));
+ }
+
+ $this->showPage();
+ }
+
+ function getInstructions()
+ {
+ return _('Login with your Facebook Account');
+ }
+
+ function showPageNotice()
+ {
+ $instr = $this->getInstructions();
+ $output = common_markup_to_html($instr);
+ $this->elementStart('div', 'instructions');
+ $this->raw($output);
+ $this->elementEnd('div');
+ }
+
+ function title()
+ {
+ return _('Facebook Login');
+ }
+
+ function showContent() {
+
+ $this->elementStart('fieldset');
+ $this->element('fb:login-button', array('onlogin' => 'goto_login()',
+ 'length' => 'long'));
+
+ $this->elementEnd('fieldset');
+ }
+
+}
diff --git a/plugins/FBConnect/FBConnectPlugin.css b/plugins/FBConnect/FBConnectPlugin.css
new file mode 100644
index 000000000..e52675459
--- /dev/null
+++ b/plugins/FBConnect/FBConnectPlugin.css
@@ -0,0 +1,36 @@
+/** Styles for Facebook logo and Facebook user profile avatar.
+ *
+ * @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/
+ */
+
+#site_nav_global_primary #nav_fb {
+position:relative;
+margin-left:18px;
+}
+
+#nav_fb #fbc_profile-pic {
+position:absolute;
+top:-3px;
+left:-18px;
+display:inline;
+border:1px solid #3B5998;
+padding:1px;
+}
+
+#nav_fb #fb_favicon {
+position:absolute;
+top:-13px;
+left:-25px;
+display:inline;
+}
+
+#settings_facebook_connect_options legend {
+display:none;
+}
+#form_settings_facebook_connect fieldset fieldset legend {
+display:block;
+}
diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php
new file mode 100644
index 000000000..65870a187
--- /dev/null
+++ b/plugins/FBConnect/FBConnectPlugin.php
@@ -0,0 +1,395 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to enable Facebook Connect
+ *
+ * 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 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);
+}
+
+define("FACEBOOK_CONNECT_SERVICE", 3);
+
+require_once INSTALLDIR . '/lib/facebookutil.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectAuth.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectLogin.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectSettings.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBCLoginGroupNav.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBCSettingsNav.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBC_XDReceiver.php';
+
+/**
+ * Plugin to enable Facebook Connect
+ *
+ * @category Plugin
+ * @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 FBConnectPlugin extends Plugin
+{
+ function __construct()
+ {
+ parent::__construct();
+ }
+
+ // Hook in new actions
+ function onRouterInitialized(&$m) {
+ $m->connect('main/facebookconnect', array('action' => 'FBConnectAuth'));
+ $m->connect('main/facebooklogin', array('action' => 'FBConnectLogin'));
+ $m->connect('settings/facebook', array('action' => 'FBConnectSettings'));
+ $m->connect('xd_receiver.html', array('action' => 'FBC_XDReceiver'));
+ }
+
+ // Add in xmlns:fb
+ function onStartShowHTML($action)
+ {
+
+ if ($this->reqFbScripts($action)) {
+
+ // XXX: Horrible hack to make Safari, FF2, and Chrome work with
+ // Facebook Connect. These browser cannot use Facebook's
+ // DOM parsing routines unless the mime type of the page is
+ // text/html even though Facebook Connect uses XHTML. This is
+ // A bug in Facebook Connect, and this is a temporary solution
+ // until they fix their JavaScript libs.
+ header('Content-Type: text/html');
+
+ $action->extraHeaders();
+
+ $action->startXML('html',
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+
+ $language = $action->getLanguage();
+
+ $action->elementStart('html',
+ array('xmlns' => 'http://www.w3.org/1999/xhtml',
+ 'xmlns:fb' => 'http://www.facebook.com/2008/fbml',
+ 'xml:lang' => $language,
+ 'lang' => $language));
+
+ return false;
+
+ } else {
+
+ return true;
+ }
+ }
+
+ // Note: this script needs to appear in the <body>
+
+ function onStartShowHeader($action)
+ {
+ if ($this->reqFbScripts($action)) {
+
+ $apikey = common_config('facebook', 'apikey');
+ $plugin_path = common_path('plugins/FBConnect');
+
+ $login_url = common_local_url('FBConnectAuth');
+ $logout_url = common_local_url('logout');
+
+ // XXX: Facebook says we don't need this FB_RequireFeatures(),
+ // but we actually do, for IE and Safari. Gar.
+
+ $html = sprintf('<script type="text/javascript">
+ window.onload = function () {
+ FB_RequireFeatures(
+ ["XFBML"],
+ function() {
+ FB.Facebook.init("%s", "../xd_receiver.html");
+ }
+ ); }
+
+ function goto_login() {
+ window.location = "%s";
+ }
+
+ function goto_logout() {
+ window.location = "%s";
+ }
+ </script>', $apikey,
+ $login_url, $logout_url);
+
+ $action->raw($html);
+ }
+
+ }
+
+ // Note: this script needs to appear as close as possible to </body>
+
+ function onEndShowFooter($action)
+ {
+ if ($this->reqFbScripts($action)) {
+
+ $action->element('script',
+ array('type' => 'text/javascript',
+ 'src' => 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php'),
+ '');
+ }
+ }
+
+ function onEndShowLaconicaStyles($action)
+ {
+
+ if ($this->reqFbScripts($action)) {
+
+ $action->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => common_path('plugins/FBConnect/FBConnectPlugin.css')));
+ }
+ }
+
+ /**
+ * Does the Action we're plugged into require the FB Scripts? We only
+ * want to output FB namespace, scripts, CSS, etc. on the pages that
+ * really need them.
+ *
+ * @param Action the action in question
+ *
+ * @return boolean true
+ */
+
+ function reqFbScripts($action) {
+
+ // If you're logged in w/FB Connect, you always need the FB stuff
+
+ $fbuid = $this->loggedIn();
+
+ if (!empty($fbuid)) {
+ return true;
+ }
+
+ // List of actions that require FB stuff
+
+ $needy = array('FBConnectLoginAction',
+ 'FBConnectauthAction',
+ 'FBConnectSettingsAction');
+
+ if (in_array(get_class($action), $needy)) {
+ return true;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Is the user currently logged in with FB Connect?
+ *
+ * @return mixed $fbuid the Facebook ID of the logged in user, or null
+ */
+
+ function loggedIn()
+ {
+ $user = common_current_user();
+
+ if (!empty($user)) {
+
+ $flink = Foreign_link::getByUserId($user->id,
+ FACEBOOK_CONNECT_SERVICE);
+ $fbuid = 0;
+
+ if (!empty($flink)) {
+
+ try {
+
+ $facebook = getFacebook();
+ $fbuid = getFacebook()->get_loggedin_user();
+
+ } catch (Exception $e) {
+ common_log(LOG_WARNING,
+ 'Problem getting Facebook client: ' .
+ $e->getMessage());
+ }
+
+ if ($fbuid > 0) {
+ return $fbuid;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function onStartPrimaryNav($action)
+ {
+
+ $user = common_current_user();
+
+ if (!empty($user)) {
+
+ $fbuid = $this->loggedIn();
+
+ if (!empty($fbuid)) {
+
+ /* Default FB silhouette pic for FB users who haven't
+ uploaded a profile pic yet. */
+
+ $silhouetteUrl =
+ 'http://static.ak.fbcdn.net/pics/q_silhouette.gif';
+
+ $url = $this->getProfilePicURL($fbuid);
+
+ $action->elementStart('li', array('id' => 'nav_fb'));
+
+ $action->element('img', array('id' => 'fbc_profile-pic',
+ 'src' => (!empty($url)) ? $url : $silhouetteUrl,
+ 'alt' => 'Facebook Connect User',
+ 'width' => '16'), '');
+
+ $iconurl = common_path('plugins/FBConnect/fbfavicon.ico');
+ $action->element('img', array('id' => 'fb_favicon',
+ 'src' => $iconurl));
+
+ $action->elementEnd('li');
+
+ }
+
+ $action->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
+ _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
+ $action->menuItem(common_local_url('profilesettings'),
+ _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
+ if (common_config('xmpp', 'enabled')) {
+ $action->menuItem(common_local_url('imsettings'),
+ _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
+ } else {
+ $action->menuItem(common_local_url('smssettings'),
+ _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+ }
+ if (common_config('invite', 'enabled')) {
+ $action->menuItem(common_local_url('invite'),
+ _('Invite'),
+ sprintf(_('Invite friends and colleagues to join you on %s'),
+ common_config('site', 'name')),
+ false, 'nav_invitecontact');
+ }
+
+ // Need to override the Logout link to make it do FB stuff
+ if (!empty($fbuid)) {
+
+ $logout_url = common_local_url('logout');
+ $title = _('Logout from the site');
+ $text = _('Logout');
+
+ $html = sprintf('<li id="nav_logout"><a href="%s" title="%s" ' .
+ 'onclick="FB.Connect.logout(function() { goto_logout() })">%s</a></li>',
+ $logout_url, $title, $text);
+
+ $action->raw($html);
+
+ } else {
+ $action->menuItem(common_local_url('logout'),
+ _('Logout'), _('Logout from the site'), false, 'nav_logout');
+ }
+ }
+ else {
+ if (!common_config('site', 'closed')) {
+ $action->menuItem(common_local_url('register'),
+ _('Register'), _('Create an account'), false, 'nav_register');
+ }
+ $action->menuItem(common_local_url('login'),
+ _('Login'), _('Login to the site'), false, 'nav_login');
+ }
+
+ $action->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'), _('Help me!'), false, 'nav_help');
+ $action->menuItem(common_local_url('peoplesearch'),
+ _('Search'), _('Search for people or text'), false, 'nav_search');
+
+ return false;
+ }
+
+ function onStartShowLocalNavBlock($action)
+ {
+ $action_name = get_class($action);
+
+ $login_actions = array('LoginAction', 'RegisterAction',
+ 'OpenidloginAction', 'FBConnectLoginAction');
+
+ if (in_array($action_name, $login_actions)) {
+ $nav = new FBCLoginGroupNav($action);
+ $nav->show();
+ return false;
+ }
+
+ $connect_actions = array('SmssettingsAction', 'ImsettingsAction',
+ 'TwittersettingsAction', 'FBConnectSettingsAction');
+
+ if (in_array($action_name, $connect_actions)) {
+ $nav = new FBCSettingsNav($action);
+ $nav->show();
+ return false;
+ }
+
+ return true;
+ }
+
+ function onStartLogout($action)
+ {
+ $action->logout();
+ $fbuid = $this->loggedIn();
+
+ if (!empty($fbuid)) {
+ try {
+ $facebook = getFacebook();
+ $facebook->expire_session();
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, 'Could\'t logout of Facebook: ' .
+ $e->getMessage());
+ }
+ }
+
+ return true;
+ }
+
+ function getProfilePicURL($fbuid)
+ {
+
+ $facebook = getFacebook();
+ $url = null;
+
+ try {
+
+ $fqry = 'SELECT pic_square FROM user WHERE uid = %s';
+
+ $result = $facebook->api_client->fql_query(sprintf($fqry, $fbuid));
+
+ if (!empty($result)) {
+ $url = $result[0]['pic_square'];
+ }
+
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, "Facebook client failure requesting profile pic!");
+ }
+
+ return $url;
+
+ }
+
+}
diff --git a/plugins/FBConnect/FBConnectSettings.php b/plugins/FBConnect/FBConnectSettings.php
new file mode 100644
index 000000000..034ecebae
--- /dev/null
+++ b/plugins/FBConnect/FBConnectSettings.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Facebook Connect settings
+ *
+ * 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 Settings
+ * @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);
+}
+
+require_once INSTALLDIR.'/lib/connectsettingsaction.php';
+
+/**
+ * Facebook Connect settings action
+ *
+ * @category Settings
+ * @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 FBConnectSettingsAction extends ConnectSettingsAction
+{
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('Facebook Connect Settings');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('Manage how your account connects to Facebook');
+ }
+
+ /**
+ * Content area of the page
+ *
+ * Shows a form for uploading an avatar.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $user = common_current_user();
+ $flink = Foreign_link::getByUserID($user->id, FACEBOOK_CONNECT_SERVICE);
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_facebook',
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('FBConnectSettings')));
+
+ if (!$flink) {
+
+ $this->element('p', 'instructions',
+ _('There is no Facebook user connected to this account.'));
+
+ $this->element('fb:login-button', array('onlogin' => 'goto_login()',
+ 'length' => 'long'));
+
+ } else {
+
+ $this->element('p', 'form_note',
+ _('Connected Facebook user'));
+
+ $this->elementStart('p', array('class' => 'facebook-user-display'));
+ $this->elementStart('fb:profile-pic',
+ array('uid' => $flink->foreign_id,
+ 'size' => 'small',
+ 'linked' => 'true',
+ 'facebook-logo' => 'true'));
+ $this->elementEnd('fb:profile-pic');
+
+ $this->elementStart('fb:name', array('uid' => $flink->foreign_id,
+ 'useyou' => 'false'));
+ $this->elementEnd('fb:name');
+ $this->elementEnd('p');
+
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset');
+
+ $this->element('legend', null, _('Disconnect my account from Facebook'));
+
+ if (!$user->password) {
+
+ $this->elementStart('p', array('class' => 'form_guide'));
+ $this->text(_('Disconnecting your Faceboook ' .
+ 'would make it impossible to log in! Please '));
+ $this->element('a',
+ array('href' => common_local_url('passwordsettings')),
+ _('set a password'));
+
+ $this->text(_(' first.'));
+ $this->elementEnd('p');
+ } else {
+
+ $note = 'Keep your %s account but disconnect from Facebook. ' .
+ 'You\'ll use your %s password to log in.';
+
+ $site = common_config('site', 'name');
+
+ $this->element('p', 'instructions',
+ sprintf($note, $site, $site));
+
+ $this->submit('disconnect', _('Disconnect'));
+ }
+
+ $this->elementEnd('fieldset');
+ }
+
+ $this->elementEnd('form');
+ }
+
+ /**
+ * Handle post
+ *
+ * Disconnects the current Facebook user from the current user's account
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('disconnect')) {
+
+ $user = common_current_user();
+
+ $flink = Foreign_link::getByUserID($user->id, FACEBOOK_CONNECT_SERVICE);
+ $result = $flink->delete();
+
+ if ($result === false) {
+ common_log_db_error($user, 'DELETE', __FILE__);
+ $this->serverError(_('Couldn\'t delete link to Facebook.'));
+ return;
+ }
+
+ try {
+
+ // Clear FB Connect cookies out
+ $facebook = getFacebook();
+ $facebook->clear_cookie_state();
+
+ } catch (Exception $e) {
+ common_log(LOG_WARNING,
+ 'Couldn\'t clear Facebook cookies: ' .
+ $e->getMessage());
+ }
+
+ $this->showForm(_('You have disconnected from Facebook.'), true);
+
+ } else {
+ $this->showForm(_('Not sure what you\'re trying to do.'));
+ return;
+ }
+
+ }
+
+}
diff --git a/plugins/FBConnect/fbfavicon.ico b/plugins/FBConnect/fbfavicon.ico
new file mode 100644
index 000000000..c57c0342f
--- /dev/null
+++ b/plugins/FBConnect/fbfavicon.ico
Binary files differ
diff --git a/scripts/allsites.php b/scripts/allsites.php
new file mode 100755
index 000000000..d6768c278
--- /dev/null
+++ b/scripts/allsites.php
@@ -0,0 +1,40 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2009, Control Yourself, 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
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+$helptext = <<<ENDOFHELP
+allsites.php - list all sites configured for multi-site use
+
+returns the nickname of each site configured for multi-site use
+
+ENDOFHELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+$sn = new Status_network();
+
+if ($sn->find()) {
+ while ($sn->fetch()) {
+ print "$sn->nickname\n";
+ }
+} \ No newline at end of file
diff --git a/scripts/commandline.inc b/scripts/commandline.inc
new file mode 100644
index 000000000..3b6ef6098
--- /dev/null
+++ b/scripts/commandline.inc
@@ -0,0 +1,174 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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/>.
+ */
+
+// -*- mode: php -*-
+
+# 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('LACONICA', true);
+
+// Set various flags so we don't time out on long-running processes
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+// Add extlib to our path so we can get Console_Getopt
+
+$_extra_path = array(INSTALLDIR.'/extlib/');
+
+set_include_path(implode(PATH_SEPARATOR, $_extra_path) . PATH_SEPARATOR . get_include_path());
+
+require_once 'Console/Getopt.php';
+
+// Note: $shortoptions and $longoptions should be pre-defined!
+
+$_default_shortoptions = 'qvhc:s:p:';
+
+$_default_longoptions = array('quiet', 'verbose', 'help', 'conf=', 'server=', 'path=');
+
+if (isset($shortoptions)) {
+ $shortoptions .= $_default_shortoptions;
+} else {
+ $shortoptions = $_default_shortoptions;
+}
+
+if (isset($longoptions)) {
+ $longoptions = array_merge($longoptions, $_default_longoptions);
+} else {
+ $longoptions = $_default_longoptions;
+}
+
+$parser = new Console_Getopt();
+
+$result = $parser->getopt($argv, $shortoptions, $longoptions);
+
+if (PEAR::isError($result)) {
+ print $result->getMessage()."\n";
+ exit(1);
+} else {
+ list($options, $args) = $result;
+}
+
+function show_help()
+{
+ global $helptext;
+
+ $_default_help_text = <<<END_OF_DEFAULT
+ General options:
+
+ -q --quiet Quiet (little output)
+ -v --verbose Verbose (lots of output)
+ -c --conf=<filename> Use <filename> as config file
+ -s --server=<name> Use <name> as server name
+ -p --path=<path> Use <path> as path name
+ -h --help Show this message and quit.
+
+END_OF_DEFAULT;
+ if (isset($helptext)) {
+ print $helptext;
+ }
+ print $_default_help_text;
+ exit(0);
+}
+
+foreach ($options as $option) {
+
+ switch ($option[0]) {
+ case '--server':
+ case 's':
+ $server = $option[1];
+ break;
+
+ case '--path':
+ case 'p':
+ $path = $option[1];
+ break;
+
+ case '--conf':
+ case 'c':
+ $conffile = $option[1];
+ break;
+
+ case '--help':
+ case 'h':
+ show_help();
+ }
+}
+
+require_once INSTALLDIR . '/lib/common.php';
+
+set_error_handler('common_error_handler');
+
+function _make_matches($opt, $alt)
+{
+ $matches = array();
+
+ if (strlen($opt) > 1 && 0 != strncmp($opt, '--', 2)) {
+ $matches[] = '--'.$opt;
+ } else {
+ $matches[] = $opt;
+ }
+
+ if (!empty($alt)) {
+ if (strlen($alt) > 1 && 0 != strncmp($alt, '--', 2)) {
+ $matches[] = '--'.$alt;
+ } else {
+ $matches[] = $alt;
+ }
+ }
+
+ return $matches;
+}
+
+function have_option($opt, $alt=null)
+{
+ global $options;
+
+ $matches = _make_matches($opt, $alt);
+
+ foreach ($options as $option) {
+ if (in_array($option[0], $matches)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function get_option_value($opt, $alt=null)
+{
+ global $options;
+
+ $matches = _make_matches($opt, $alt);
+
+ foreach ($options as $option) {
+ if (in_array($option[0], $matches)) {
+ return $option[1];
+ }
+ }
+
+ return null;
+}
diff --git a/scripts/decache.php b/scripts/decache.php
index b18eaa2cd..90e1ec63c 100644
--- a/scripts/decache.php
+++ b/scripts/decache.php
@@ -18,35 +18,26 @@
* 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(1);
-}
-
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
-
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
-require_once(INSTALLDIR . '/lib/common.php');
+$helptext = <<<ENDOFHELP
+USAGE: decache.php <table> <id> [<column>]
+Clears the cache for the object in table <table> with id <id>
+If <column> is specified, use that instead of 'id'
+ENDOFHELP;
-if ($argc < 3 || $argc > 4) {
- print "USAGE: decache.php <table> <id> [<column>]\n";
- print "Clears the cache for the object in table <table> with id <id>.\n\n";
- print "If <column> is specified, use that instead of 'id'\n";
- exit(1);
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (count($args) < 2 || count($args) > 3) {
+ show_help();
}
-$table = $argv[1];
-$id = $argv[2];
-if ($argc > 3) {
- $column = $argv[3];
+$table = $args[0];
+$id = $args[1];
+if (count($args) > 2) {
+ $column = $args[2];
} else {
- $colum = 'id';
+ $column = 'id';
}
$object = Memcached_DataObject::staticGet($table, $column, $id);
diff --git a/scripts/delete_status_network.sh b/scripts/delete_status_network.sh
new file mode 100755
index 000000000..32187382c
--- /dev/null
+++ b/scripts/delete_status_network.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+source /etc/laconica/setup.cfg
+
+export nickname=$1
+
+export database=$nickname$DBBASE
+
+# Create the db
+
+mysqladmin -h $DBHOST -u $ADMIN --password=$ADMINPASS -f drop $database
+
+mysql -h $DBHOST -u $ADMIN --password=$ADMINPASS $SITEDB << ENDOFCOMMANDS
+
+delete from status_network where nickname = '$nickname';
+
+ENDOFCOMMANDS
+
+for top in $AVATARBASE $FILEBASE $BACKGROUNDBASE; do
+ rm -Rf $top/$nickname
+done
diff --git a/scripts/enjitqueuehandler.php b/scripts/enjitqueuehandler.php
index 40f60da5d..05e1d9366 100755
--- a/scripts/enjitqueuehandler.php
+++ b/scripts/enjitqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,24 +18,27 @@
* 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/mail.php');
-require_once(INSTALLDIR . '/lib/queuehandler.php');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_ENJIT_HELP
+Daemon script for watching new notices and posting to enjit.
+
+ -i --id Identity (default none)
+
+END_OF_ENJIT_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/mail.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
set_error_handler('common_error_handler');
class EnjitQueueHandler extends QueueHandler
{
-
function transport()
{
return 'enjit';
@@ -60,7 +63,6 @@ class EnjitQueueHandler extends QueueHandler
return "skipped";
}
-
#
# Build an Atom message from the notice
#
@@ -93,8 +95,8 @@ class EnjitQueueHandler extends QueueHandler
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
-
- curl_setopt($ch, CURLOPT_HEADER, 1);
+
+ curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1) ;
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
@@ -103,7 +105,7 @@ class EnjitQueueHandler extends QueueHandler
#
# curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
# curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
- # curl_setopt($ch, CURLOPT_VERBOSE, 1);
+ # curl_setopt($ch, CURLOPT_VERBOSE, 1);
$result = curl_exec($ch);
@@ -115,13 +117,18 @@ class EnjitQueueHandler extends QueueHandler
return $code;
}
-
}
-mb_internal_encoding('UTF-8');
-
-$id = ($argc > 1) ? $argv[1] : null;
+if (have_option('-i')) {
+ $id = get_option_value('-i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
$handler = new EnjitQueueHandler($id);
diff --git a/scripts/facebookqueuehandler.php b/scripts/facebookqueuehandler.php
index c6859cb21..05a35577f 100755
--- a/scripts/facebookqueuehandler.php
+++ b/scripts/facebookqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,29 +18,30 @@
* 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');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_FACEBOOK_HELP
+Daemon script for pushing new notices to Facebook.
+
+ -i --id Identity (default none)
+
+END_OF_FACEBOOK_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
-set_error_handler('common_error_handler');
+require_once INSTALLDIR . '/lib/facebookutil.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
class FacebookQueueHandler extends QueueHandler
{
-
function transport()
{
return 'facebook';
}
-
+
function start()
{
$this->log(LOG_INFO, "INITIALIZE");
@@ -51,20 +52,22 @@ class FacebookQueueHandler extends QueueHandler
{
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;
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
$handler = new FacebookQueueHandler($id);
diff --git a/scripts/fixup_conversations.php b/scripts/fixup_conversations.php
new file mode 100755
index 000000000..0be0b4bac
--- /dev/null
+++ b/scripts/fixup_conversations.php
@@ -0,0 +1,70 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+common_log(LOG_INFO, 'Fixing up conversations.');
+
+$nid = new Notice();
+$nid->query('select id, reply_to from notice where conversation is null');
+
+while ($nid->fetch()) {
+
+ $cid = null;
+
+ $notice = new Notice();
+
+ if (empty($nid->reply_to)) {
+ $cid = $nid->id;
+ } else {
+ $reply = Notice::staticGet('id', $notice->reply_to);
+
+ if (empty($reply)) {
+ common_log(LOG_WARNING, "Replied-to notice $notice->reply_to not found.");
+ $notice->conversation = $notice->id;
+ } else if (empty($reply->conversation)) {
+ common_log(LOG_WARNING, "Replied-to notice $reply->id has no conversation ID.");
+ $notice->conversation = $notice->id;
+ } else {
+ $notice->conversation = $reply->conversation;
+ }
+
+ unset($reply);
+ $reply = null;
+ }
+
+ print "$notice->conversation";
+
+ $result = $notice->update($orig);
+
+ if (!$result) {
+ common_log_db_error($notice, 'UPDATE', __FILE__);
+ continue;
+ }
+
+ $notice = null;
+ $orig = null;
+ unset($notice);
+ unset($orig);
+
+ print ".\n";
+}
diff --git a/scripts/fixup_hashtags.php b/scripts/fixup_hashtags.php
index 6f65c78a1..bd38e3105 100755
--- a/scripts/fixup_hashtags.php
+++ b/scripts/fixup_hashtags.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/scripts/fixup_inboxes.php b/scripts/fixup_inboxes.php
index a5c8a0a5a..3e55edef1 100755
--- a/scripts/fixup_inboxes.php
+++ b/scripts/fixup_inboxes.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/scripts/fixup_notices_rendered.php b/scripts/fixup_notices_rendered.php
index c27185546..3e7eb7acb 100755
--- a/scripts/fixup_notices_rendered.php
+++ b/scripts/fixup_notices_rendered.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/scripts/fixup_replies.php b/scripts/fixup_replies.php
index 6010e21d1..9d8cfda08 100755
--- a/scripts/fixup_replies.php
+++ b/scripts/fixup_replies.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/scripts/fixup_utf8.php b/scripts/fixup_utf8.php
index 169376091..8c9a9127f 100644
--- a/scripts/fixup_utf8.php
+++ b/scripts/fixup_utf8.php
@@ -19,21 +19,18 @@
*/
# 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(1);
-}
-
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
-require_once(INSTALLDIR . '/lib/common.php');
-require_once('DB.php');
+$helptext = <<<ENDOFHELP
+fixup_utf8.php <maxdate> <maxid> <minid>
+
+Fixup records in a database that stored the data incorrectly (pre-0.7.4 for Laconica).
+
+ENDOFHELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+require_once 'DB.php';
class UTF8FixerUpper
{
@@ -356,9 +353,9 @@ class UTF8FixerUpper
}
}
-$max_date = ($argc > 1) ? $argv[1] : null;
-$max_id = ($argc > 2) ? $argv[2] : null;
-$min_id = ($argc > 3) ? $argv[3] : null;
+$max_date = (count($args) > 0) ? $args[0] : null;
+$max_id = (count($args) > 1) ? $args[1] : null;
+$min_id = (count($args) > 2) ? $args[2] : null;
$fixer = new UTF8FixerUpper(array('max_date' => $max_date,
'max_notice' => $max_id,
diff --git a/scripts/getpiddir.php b/scripts/getpiddir.php
index 4f5704249..9927cc6d9 100755
--- a/scripts/getpiddir.php
+++ b/scripts/getpiddir.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,15 +18,13 @@
* 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');
+$helptext = <<<ENDOFHELP
+getpiddir.php - print out the currently configured PID directory
+
+ENDOFHELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
-echo common_config('daemon','piddir');
+echo common_config('daemon', 'piddir');
diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php
index 0996ba9f4..97c230784 100755
--- a/scripts/getvaliddaemons.php
+++ b/scripts/getvaliddaemons.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -25,21 +25,22 @@
* daemon names.
*/
-# 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');
+$helptext = <<<ENDOFHELP
+getvaliddaemons.php - print out the currently configured PID directory
+
+ENDOFHELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
if(common_config('xmpp','enabled')) {
echo "xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php ";
echo "xmppconfirmhandler.php ";
}
+if(common_config('twitterbridge','enabled')) {
+ echo "twitterstatusfetcher.php ";
+}
echo "ombqueuehandler.php ";
echo "twitterqueuehandler.php ";
echo "facebookqueuehandler.php ";
diff --git a/scripts/inbox_users.php b/scripts/inbox_users.php
index 7d14f0efe..4883fea20 100755
--- a/scripts/inbox_users.php
+++ b/scripts/inbox_users.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -20,59 +20,55 @@
# 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__) . '/..'));
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
+$helptext = <<<ENDOFHELP
+inbox_users.php <idfile>
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
+Update users to use inbox table. Listed in an ID file, default 'ids.txt'.
-require_once(INSTALLDIR . '/lib/common.php');
+ENDOFHELP;
-$id_file = ($argc > 1) ? $argv[1] : 'ids.txt';
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+$id_file = (count($args) > 1) ? $args[0] : 'ids.txt';
common_log(LOG_INFO, 'Updating user inboxes.');
$ids = file($id_file);
foreach ($ids as $id) {
-
+
$user = User::staticGet('id', $id);
if (!$user) {
common_log(LOG_WARNING, 'No such user: ' . $id);
continue;
}
-
+
if ($user->inboxed) {
common_log(LOG_WARNING, 'Already inboxed: ' . $id);
continue;
}
-
+
common_log(LOG_INFO, 'Updating inbox for user ' . $user->id);
-
+
$user->query('BEGIN');
-
+
$old_inbox = new Notice_inbox();
$old_inbox->user_id = $user->id;
-
+
$result = $old_inbox->delete();
-
+
if (is_null($result) || $result === false) {
common_log_db_error($old_inbox, 'DELETE', __FILE__);
continue;
}
$old_inbox->free();
-
+
$inbox = new Notice_inbox();
-
+
$result = $inbox->query('INSERT INTO notice_inbox (user_id, notice_id, created) ' .
'SELECT ' . $user->id . ', notice.id, notice.created ' .
'FROM subscription JOIN notice ON subscription.subscribed = notice.profile_id ' .
@@ -80,30 +76,30 @@ foreach ($ids as $id) {
'AND notice.created >= subscription.created ' .
'AND NOT EXISTS (SELECT user_id, notice_id ' .
'FROM notice_inbox ' .
- 'WHERE user_id = ' . $user->id . ' ' .
+ 'WHERE user_id = ' . $user->id . ' ' .
'AND notice_id = notice.id) ' .
'ORDER BY notice.created DESC ' .
'LIMIT 0, 1000');
-
+
if (is_null($result) || $result === false) {
common_log_db_error($inbox, 'INSERT', __FILE__);
continue;
}
-
+
$orig = clone($user);
$user->inboxed = 1;
$result = $user->update($orig);
-
+
if (!$result) {
common_log_db_error($user, 'UPDATE', __FILE__);
continue;
}
-
+
$user->query('COMMIT');
-
+
$inbox->free();
unset($inbox);
-
+
if ($cache) {
$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last'));
diff --git a/scripts/jabberqueuehandler.php b/scripts/jabberqueuehandler.php
index 8b6e974c0..5b581629d 100755
--- a/scripts/jabberqueuehandler.php
+++ b/scripts/jabberqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,24 +18,26 @@
* 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/jabber.php');
-require_once(INSTALLDIR . '/lib/xmppqueuehandler.php');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_JABBER_HELP
+Daemon script for pushing new notices to Jabber users.
+
+ -i --id Identity (default none)
-set_error_handler('common_error_handler');
+END_OF_JABBER_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/lib/jabber.php';
+require_once INSTALLDIR . '/lib/xmppqueuehandler.php';
class JabberQueueHandler extends XmppQueueHandler
{
-
var $conn = null;
function transport()
@@ -61,13 +63,16 @@ if (common_config('xmpp','enabled')==false) {
exit();
}
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
-
-$resource = ($argc > 1) ? $argv[1] : (common_config('xmpp','resource') . '-queuehandler');
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
-$handler = new JabberQueueHandler($resource);
+$handler = new JabberQueueHandler($id);
$handler->runOnce();
diff --git a/scripts/maildaemon.php b/scripts/maildaemon.php
index 9dd647bf4..11ddf06b7 100755
--- a/scripts/maildaemon.php
+++ b/scripts/maildaemon.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,16 +18,16 @@
* 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');
+$helptext = <<<END_OF_HELP
+Script for converting mail messages into notices. Takes message body
+as STDIN.
+
+END_OF_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
require_once(INSTALLDIR . '/lib/mail.php');
require_once('Mail/mimeDecode.php');
@@ -36,18 +36,17 @@ require_once('Mail/mimeDecode.php');
class MailerDaemon
{
-
function __construct()
{
}
function handle_message($fname='php://stdin')
{
- list($from, $to, $msg) = $this->parse_message($fname);
+ list($from, $to, $msg, $attachments) = $this->parse_message($fname);
if (!$from || !$to || !$msg) {
$this->error(null, _('Could not parse message.'));
}
- common_log(LOG_INFO, "Mail from $from to $to: " .substr($msg, 0, 20));
+ common_log(LOG_INFO, "Mail from $from to $to with ".count($attachments) .' attachment(s): ' .substr($msg, 0, 20));
$user = $this->user_from($from);
if (!$user) {
$this->error($from, _('Not a registered user.'));
@@ -66,7 +65,47 @@ class MailerDaemon
return true;
}
$msg = $this->cleanup_msg($msg);
- $err = $this->add_notice($user, $msg);
+ $msg = common_shorten_links($msg);
+ if (mb_strlen($msg) > 140) {
+ $this->error($from,_('That\'s too long. '.
+ 'Max notice size is 140 chars.'));
+ }
+ $fileRecords = array();
+ foreach($attachments as $attachment){
+ $mimetype = $this->getUploadedFileType($attachment);
+ $stream = stream_get_meta_data($attachment);
+ if (!$this->isRespectsQuota($user,filesize($stream['uri']))) {
+ die('error() should trigger an exception before reaching here.');
+ }
+ $filename = $this->saveFile($user, $attachment,$mimetype);
+
+ fclose($attachment);
+
+ if (empty($filename)) {
+ $this->error($from,_('Couldn\'t save file.'));
+ }
+
+ $fileRecord = $this->storeFile($filename, $mimetype);
+ $fileRecords[] = $fileRecord;
+ $fileurl = common_local_url('attachment',
+ array('attachment' => $fileRecord->id));
+
+ // not sure this is necessary -- Zach
+ $this->maybeAddRedir($fileRecord->id, $fileurl);
+
+ $short_fileurl = common_shorten_url($fileurl);
+ $msg .= ' ' . $short_fileurl;
+
+ if (mb_strlen($msg) > 140) {
+ $this->deleteFile($filename);
+ $this->error($from,_('Max notice size is 140 chars, including attachment URL.'));
+ }
+
+ // Also, not sure this is necessary -- Zach
+ $this->maybeAddRedir($fileRecord->id, $short_fileurl);
+ }
+
+ $err = $this->add_notice($user, $msg, $fileRecords);
if (is_string($err)) {
$this->error($from, $err);
return false;
@@ -75,6 +114,89 @@ class MailerDaemon
}
}
+ function saveFile($user, $attachment, $mimetype) {
+
+ $filename = File::filename($user->getProfile(), "email", $mimetype);
+
+ $filepath = File::path($filename);
+
+ $stream = stream_get_meta_data($attachment);
+ if (copy($stream['uri'], $filepath) && chmod($filepath,0664)) {
+ return $filename;
+ } else {
+ $this->error(null,_('File could not be moved to destination directory.' . $stream['uri'] . ' ' . $filepath));
+ }
+ }
+
+ function storeFile($filename, $mimetype) {
+
+ $file = new File;
+ $file->filename = $filename;
+
+ $file->url = File::url($filename);
+
+ $filepath = File::path($filename);
+
+ $file->size = filesize($filepath);
+ $file->date = time();
+ $file->mimetype = $mimetype;
+
+ $file_id = $file->insert();
+
+ if (!$file_id) {
+ common_log_db_error($file, "INSERT", __FILE__);
+ $this->error(null,_('There was a database error while saving your file. Please try again.'));
+ }
+
+ return $file;
+ }
+
+ function maybeAddRedir($file_id, $url)
+ {
+ $file_redir = File_redirection::staticGet('url', $url);
+
+ if (empty($file_redir)) {
+ $file_redir = new File_redirection;
+ $file_redir->url = $url;
+ $file_redir->file_id = $file_id;
+
+ $result = $file_redir->insert();
+
+ if (!$result) {
+ common_log_db_error($file_redir, "INSERT", __FILE__);
+ $this->error(null,_('There was a database error while saving your file. Please try again.'));
+ }
+ }
+ }
+
+ function getUploadedFileType($fileHandle) {
+ require_once 'MIME/Type.php';
+
+ $cmd = &PEAR::getStaticProperty('MIME_Type', 'fileCmd');
+ $cmd = common_config('attachments', 'filecommand');
+
+ $stream = stream_get_meta_data($fileHandle);
+ $filetype = MIME_Type::autoDetect($stream['uri']);
+ if (in_array($filetype, common_config('attachments', 'supported'))) {
+ return $filetype;
+ }
+ $media = MIME_Type::getMedia($filetype);
+ if ('application' !== $media) {
+ $hint = sprintf(_(' Try using another %s format.'), $media);
+ } else {
+ $hint = '';
+ }
+ $this->error(null,sprintf(
+ _('%s is not a supported filetype on this server.'), $filetype) . $hint);
+ }
+
+ function isRespectsQuota($user,$fileSize) {
+ $file = new File;
+ $ret = $file->isRespectsQuota($user,$fileSize);
+ if (true === $ret) return true;
+ $this->error(null,$ret);
+ }
+
function error($from, $msg)
{
file_put_contents("php://stderr", $msg . "\n");
@@ -134,19 +256,30 @@ class MailerDaemon
common_log($level, 'MailDaemon: '.$msg);
}
- function add_notice($user, $msg)
+ function add_notice($user, $msg, $fileRecords)
{
$notice = Notice::saveNew($user->id, $msg, 'mail');
if (is_string($notice)) {
$this->log(LOG_ERR, $notice);
return $notice;
}
+ foreach($fileRecords as $fileRecord){
+ $this->attachFile($notice, $fileRecord);
+ }
common_broadcast_notice($notice);
$this->log(LOG_INFO,
'Added notice ' . $notice->id . ' from user ' . $user->nickname);
return true;
}
+ function attachFile($notice, $filerec)
+ {
+ File_to_post::processNew($filerec->id, $notice->id);
+
+ $this->maybeAddRedir($filerec->id,
+ common_local_url('file', array('notice' => $notice->id)));
+ }
+
function parse_message($fname)
{
$contents = file_get_contents($fname);
@@ -164,12 +297,19 @@ class MailerDaemon
$type = $parsed->ctype_primary . '/' . $parsed->ctype_secondary;
+ $attachments = array();
+
if ($parsed->ctype_primary == 'multipart') {
foreach ($parsed->parts as $part) {
if ($part->ctype_primary == 'text' &&
$part->ctype_secondary == 'plain') {
$msg = $part->body;
- break;
+ }else{
+ if ($part->body) {
+ $attachment = tmpfile();
+ fwrite($attachment, $part->body);
+ $attachments[] = $attachment;
+ }
}
}
} else if ($type == 'text/plain') {
@@ -177,8 +317,7 @@ class MailerDaemon
} else {
$this->unsupported_type($type);
}
-
- return array($from, $to, $msg);
+ return array($from, $to, $msg, $attachments);
}
function unsupported_type($type)
diff --git a/scripts/ombqueuehandler.php b/scripts/ombqueuehandler.php
index cdcea51dc..1587192b6 100755
--- a/scripts/ombqueuehandler.php
+++ b/scripts/ombqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,29 +18,33 @@
* 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/omb.php');
-require_once(INSTALLDIR . '/lib/queuehandler.php');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_OMB_HELP
+Daemon script for pushing new notices to OpenMicroBlogging subscribers.
+
+ -i --id Identity (default none)
+
+END_OF_OMB_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/omb.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
set_error_handler('common_error_handler');
class OmbQueueHandler extends QueueHandler
{
-
+
function transport()
{
return 'omb';
}
-
+
function start()
{
$this->log(LOG_INFO, "INITIALIZE");
@@ -56,7 +60,7 @@ class OmbQueueHandler extends QueueHandler
return omb_broadcast_remote_subscribers($notice);
}
}
-
+
function finish()
{
}
@@ -68,12 +72,15 @@ class OmbQueueHandler extends QueueHandler
}
}
-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;
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
$handler = new OmbQueueHandler($id);
diff --git a/scripts/pingqueuehandler.php b/scripts/pingqueuehandler.php
index ada6ecdba..23678ea4b 100644
--- a/scripts/pingqueuehandler.php
+++ b/scripts/pingqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,20 +18,22 @@
* 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');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_PING_HELP
+Daemon script for pushing new notices to ping servers.
+
+ -i --id Identity (default none)
-set_error_handler('common_error_handler');
+END_OF_PING_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/ping.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
class PingQueueHandler extends QueueHandler {
@@ -52,12 +54,15 @@ class PingQueueHandler extends QueueHandler {
}
}
-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;
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
$handler = new PingQueueHandler($id);
diff --git a/scripts/publicqueuehandler.php b/scripts/publicqueuehandler.php
index b0fa22d43..701d50e01 100755
--- a/scripts/publicqueuehandler.php
+++ b/scripts/publicqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,29 +18,31 @@
* 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/jabber.php');
-require_once(INSTALLDIR . '/lib/xmppqueuehandler.php');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_PUBLIC_HELP
+Daemon script for pushing new notices to public XMPP subscribers.
+
+ -i --id Identity (default none)
+
+END_OF_PUBLIC_HELP;
-set_error_handler('common_error_handler');
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/jabber.php';
+require_once INSTALLDIR . '/lib/xmppqueuehandler.php';
class PublicQueueHandler extends XmppQueueHandler
{
-
+
function transport()
{
return 'public';
}
-
+
function handle_notice($notice)
{
try {
@@ -59,13 +61,16 @@ if (common_config('xmpp','enabled')==false) {
exit();
}
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
-
-$resource = ($argc > 1) ? $argv[1] : (common_config('xmpp','resource') . '-public');
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
-$handler = new PublicQueueHandler($resource);
+$handler = new PublicQueueHandler($id);
$handler->runOnce();
diff --git a/lib/stream.php b/scripts/reportsnapshot.php
index 0cb9e0bf4..c644b557f 100644
--- a/lib/stream.php
+++ b/scripts/reportsnapshot.php
@@ -1,7 +1,8 @@
+#!/usr/bin/env php
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2009, Control Yourself, 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
@@ -10,23 +11,20 @@
*
* 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
+ * 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/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('LACONICA')) { exit(1); }
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-require_once(INSTALLDIR.'/lib/personal.php');
-require_once(INSTALLDIR.'/lib/noticelist.php');
+$helptext = <<<END_OF_SNAPSHOT_HELP
+Batch script for sending snapshot information about this installation to devs.
-class StreamAction extends PersonalAction
-{
- function show_notice_list($notice)
- {
- $nl = new NoticeList($notice);
- return $nl->show();
- }
-}
+END_OF_SNAPSHOT_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+Snapshot::check();
diff --git a/scripts/setpassword.php b/scripts/setpassword.php
index d694eed09..b70689f03 100755
--- a/scripts/setpassword.php
+++ b/scripts/setpassword.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,30 +18,23 @@
* 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(1);
-}
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
+$helptext = <<<END_OF_PASSWORD_HELP
+setpassword.php <username> <password>
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
+Sets the password of user with name <username> to <password>
-require_once(INSTALLDIR . '/lib/common.php');
+END_OF_PASSWORD_HELP;
-if ($argc != 3) {
- print "USAGE: setpassword.php <username> <password>\n";
- print "Sets the password of user with name <username> to <password>\n";
- exit(1);
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (count($args) < 2) {
+ show_help();
}
-$nickname = $argv[1];
-$password = $argv[2];
+$nickname = $args[0];
+$password = $args[1];
if (mb_strlen($password) < 6) {
print "Password must be 6 characters or more.\n";
diff --git a/scripts/setup.cfg.sample b/scripts/setup.cfg.sample
new file mode 100644
index 000000000..8d03b06f5
--- /dev/null
+++ b/scripts/setup.cfg.sample
@@ -0,0 +1,14 @@
+# CONFIGURATION FILE for setup_status_network.sh
+
+export DBHOST=localhost
+export DBHOSTNAME=masterdb.example.net
+export DBBASE=_example_net
+export USERBASE=_example_net
+export ADMIN=root
+export ADMINPASS=yourpassword
+export SITEDB=example_net_site
+export AVATARBASE=/var/www/avatar.example.net
+export BACKGROUNDBASE=/var/www/background.example.net
+export FILEBASE=/var/www/file.example.net
+export PWDGEN="pwgen 20"
+
diff --git a/scripts/setup_status_network.sh b/scripts/setup_status_network.sh
new file mode 100755
index 000000000..17440640e
--- /dev/null
+++ b/scripts/setup_status_network.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+source /etc/laconica/setup.cfg
+
+export nickname=$1
+export sitename=$2
+
+export password=`$PWDGEN`
+export database=$nickname$DBBASE
+export username=$nickname$USERBASE
+
+# Create the db
+
+mysqladmin -h $DBHOST -u $ADMIN --password=$ADMINPASS create $database
+
+for f in laconica.sql innodb.sql sms_carrier.sql foreign_services.sql notice_source.sql; do
+ mysql -h $DBHOST -u $ADMIN --password=$ADMINPASS $database < ../db/$f;
+done
+
+mysql -h $DBHOST -u $ADMIN --password=$ADMINPASS $SITEDB << ENDOFCOMMANDS
+
+GRANT INSERT,SELECT,UPDATE,DELETE ON $database.* TO '$username'@'localhost' IDENTIFIED BY '$password';
+GRANT INSERT,SELECT,UPDATE,DELETE ON $database.* TO '$username'@'%' IDENTIFIED BY '$password';
+INSERT INTO status_network (nickname, dbhost, dbuser, dbpass, dbname, sitename, created)
+VALUES ('$nickname', '$DBHOSTNAME', '$username', '$password', '$database', '$sitename', now());
+
+ENDOFCOMMANDS
+
+for top in $AVATARBASE $FILEBASE $BACKGROUNDBASE; do
+ mkdir $top/$nickname
+ chmod a+w $top/$nickname
+done
diff --git a/scripts/showcache.php b/scripts/showcache.php
new file mode 100644
index 000000000..7a88fdbbb
--- /dev/null
+++ b/scripts/showcache.php
@@ -0,0 +1,71 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2009, Control Yourself, 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/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+$shortoptions = "t:c:v:k:";
+
+$helptext = <<<ENDOFHELP
+USAGE: showcache.php <args>
+shows the cached object based on the args
+
+ -t table Table to look up
+ -c column Column to look up, default "id"
+ -v value Value to look up
+ -k key Key to look up; other args are ignored
+
+ENDOFHELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+$karg = get_option_value('k');
+
+if (!empty($karg)) {
+ $k = common_cache_key($karg);
+} else {
+ $table = get_option_value('t');
+ if (empty($table)) {
+ die("No table or key specified\n");
+ }
+ $column = get_option_value('c');
+ if (empty($column)) {
+ $column = 'id';
+ }
+ $value = get_option_value('v');
+
+ $k = Memcached_DataObject::cacheKey($table, $column, $value);
+}
+
+print "Checking key '$k'...\n";
+
+$c = common_memcache();
+
+if (empty($c)) {
+ die("Can't initialize cache object!\n");
+}
+
+$obj = $c->get($k);
+
+if (empty($obj)) {
+ print "Empty.\n";
+} else {
+ var_dump($obj);
+ print "\n";
+}
diff --git a/scripts/sitemap.php b/scripts/sitemap.php
index 39eb859bb..88ca2ba7a 100755
--- a/scripts/sitemap.php
+++ b/scripts/sitemap.php
@@ -1,10 +1,37 @@
+#!/usr/bin/env php
<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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/>.
+ */
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
-require_once(INSTALLDIR . '/lib/common.php');
-require_once(INSTALLDIR . '/lib/util.php');
+$shortoptions = 'f:d:u:';
+
+$helptext = <<<END_OF_SITEMAP_HELP
+Script for creating sitemaps files per http://sitemaps.org/
+
+ -f <indexfile> Use <indexfile> as output file
+ -d <outputdir> Use <outputdir> for new sitemaps
+ -u <outputurl> Use <outputurl> as root for URLs
+
+END_OF_SITEMAP_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
$output_paths = parse_args();
@@ -13,11 +40,11 @@ notices_map();
user_map();
index_map();
-# ------------------------------------------------------------------------------
-# Main functions: get data out and turn them into sitemaps
-# ------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------
+// Main functions: get data out and turn them into sitemaps
+// ------------------------------------------------------------------------------
-# Generate index sitemap of all other sitemaps.
+// Generate index sitemap of all other sitemaps.
function index_map()
{
global $output_paths;
@@ -26,7 +53,7 @@ function index_map()
foreach (glob("$output_dir*.xml") as $file_name) {
- # Just the file name please.
+ // Just the file name please.
$file_name = preg_replace("|$output_dir|", '', $file_name);
$index_urls .= sitemap(
@@ -40,7 +67,7 @@ function index_map()
write_file($output_paths['index_file'], sitemapindex($index_urls));
}
-# Generate sitemap of standard site elements.
+// Generate sitemap of standard site elements.
function standard_map()
{
global $output_paths;
@@ -61,7 +88,7 @@ function standard_map()
)
);
- $docs = array('about', 'faq', 'contact', 'im', 'openid', 'openmublog',
+ $docs = array('about', 'faq', 'contact', 'im', 'openid', 'openmublog',
'privacy', 'source', 'badge');
foreach($docs as $title) {
@@ -79,7 +106,7 @@ function standard_map()
write_file($urlset_path, urlset($standard_map_urls));
}
-# Generate sitemaps of all notices.
+// Generate sitemaps of all notices.
function notices_map()
{
global $output_paths;
@@ -93,14 +120,14 @@ function notices_map()
while ($notices->fetch()) {
- # Maximum 50,000 URLs per sitemap file.
+ // Maximum 50,000 URLs per sitemap file.
if ($notice_count == 50000) {
$notice_count = 0;
$map_count++;
}
- # remote notices have an URL
-
+ // remote notices have an URL
+
if (!$notices->url && $notices->uri) {
$notice = array(
'url' => ($notices->uri) ? $notices->uri : common_local_url('shownotice', array('notice' => $notices->id)),
@@ -114,11 +141,11 @@ function notices_map()
}
}
- # Make full sitemaps from the lists and save them.
+ // Make full sitemaps from the lists and save them.
array_to_map($notice_list, 'notice');
}
-# Generate sitemaps of all users.
+// Generate sitemaps of all users.
function user_map()
{
global $output_paths;
@@ -132,7 +159,7 @@ function user_map()
while ($users->fetch()) {
- # Maximum 50,000 URLs per sitemap file.
+ // Maximum 50,000 URLs per sitemap file.
if ($user_count == 50000) {
$user_count = 0;
$map_count++;
@@ -140,7 +167,7 @@ function user_map()
$user_args = array('nickname' => $users->nickname);
- # Define parameters for generating <url></url> elements.
+ // Define parameters for generating <url></url> elements.
$user = array(
'url' => common_local_url('showstream', $user_args),
'changefreq' => 'daily',
@@ -183,8 +210,8 @@ function user_map()
'priority' => '0.5',
);
- # Construct a <url></url> element for each user facet and add it
- # to our existing list of those.
+ // Construct a <url></url> element for each user facet and add it
+ // to our existing list of those.
$user_list[$map_count] .= url($user);
$user_rss_list[$map_count] .= url($user_rss);
$all_list[$map_count] .= url($all);
@@ -196,9 +223,9 @@ function user_map()
$user_count++;
}
- # Make full sitemaps from the lists and save them.
- # Possible factoring: put all the lists into a master array, thus allowing
- # calling with single argument (i.e., array_to_map('user')).
+ // Make full sitemaps from the lists and save them.
+ // Possible factoring: put all the lists into a master array, thus allowing
+ // calling with single argument (i.e., array_to_map('user')).
array_to_map($user_list, 'user');
array_to_map($user_rss_list, 'user_rss');
array_to_map($all_list, 'all');
@@ -208,14 +235,14 @@ function user_map()
array_to_map($foaf_list, 'foaf');
}
-# ------------------------------------------------------------------------------
-# XML generation functions
-# ------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------
+// XML generation functions
+// ------------------------------------------------------------------------------
-# Generate a <url></url> element.
+// Generate a <url></url> element.
function url($url_args)
{
- $url = preg_replace('/&/', '&amp;', $url_args['url']); # escape ampersands for XML
+ $url = preg_replace('/&/', '&amp;', $url_args['url']); // escape ampersands for XML
$lastmod = $url_args['lastmod'];
$changefreq = $url_args['changefreq'];
$priority = $url_args['priority'];
@@ -246,7 +273,7 @@ function url($url_args)
function sitemap($sitemap_args)
{
- $url = preg_replace('/&/', '&amp;', $sitemap_args['url']); # escape ampersands for XML
+ $url = preg_replace('/&/', '&amp;', $sitemap_args['url']); // escape ampersands for XML
$lastmod = $sitemap_args['lastmod'];
if (is_null($url)) {
@@ -265,7 +292,7 @@ function sitemap($sitemap_args)
return $sitemap_out;
}
-# Generate a <urlset></urlset> element.
+// Generate a <urlset></urlset> element.
function urlset($urlset_text)
{
$urlset = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" .
@@ -276,7 +303,7 @@ function urlset($urlset_text)
return $urlset;
}
-# Generate a <urlset></urlset> element.
+// Generate a <urlset></urlset> element.
function sitemapindex($sitemapindex_text)
{
$sitemapindex = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" .
@@ -287,49 +314,31 @@ function sitemapindex($sitemapindex_text)
return $sitemapindex;
}
-# Generate a sitemap from an array containing <url></url> elements and write it to a file.
+// Generate a sitemap from an array containing <url></url> elements and write it to a file.
function array_to_map($url_list, $filename_prefix)
{
global $output_paths;
if ($url_list) {
- # $map_urls is a long string containing concatenated <url></url> elements.
+ // $map_urls is a long string containing concatenated <url></url> elements.
while (list($map_idx, $map_urls) = each($url_list)) {
$urlset_path = $output_paths['output_dir'] . "$filename_prefix-$map_idx.xml";
-
+
write_file($urlset_path, urlset($map_urls));
}
}
}
-# ------------------------------------------------------------------------------
-# Internal functions
-# ------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------
+// Internal functions
+// ------------------------------------------------------------------------------
-# Parse command line arguments.
+// Parse command line arguments.
function parse_args()
{
- $args = getopt('f:d:u:');
-
- if (is_null($args[f]) && is_null($args[d]) && is_null($args[u])) {
- error('Mandatory arguments: -f <index file path> -d <output directory path> -u <URL of sitemaps directory>');
- }
-
- if (is_null($args[f])) {
- error('You must specify an index file name with the -f option.');
- }
-
- if (is_null($args[d])) {
- error('You must specify a directory for the output file with the -d option.');
- }
-
- if (is_null($args[u])) {
- error('You must specify a URL for the directory where the sitemaps will be kept with the -u option.');
- }
-
- $index_file = $args[f];
- $output_dir = $args[d];
- $output_url = $args[u];
+ $index_file = get_option_value('f');
+ $output_dir = get_option_value('d');
+ $output_url = get_option_value('u');
if (file_exists($output_dir)) {
if (is_writable($output_dir) === false) {
@@ -348,7 +357,7 @@ function parse_args()
return $paths;
}
-# Ensure paths end with a "/".
+// Ensure paths end with a "/".
function trailing_slash($path)
{
if (preg_match('/\/$/', $path) == 0) {
@@ -358,7 +367,7 @@ function trailing_slash($path)
return $path;
}
-# Write data to disk.
+// Write data to disk.
function write_file($path, $data)
{
if (is_null($path)) {
@@ -376,7 +385,7 @@ function write_file($path, $data)
}
}
-# Display an error message and exit.
+// Display an error message and exit.
function error ($error_msg)
{
if (is_null($error_msg)) {
diff --git a/scripts/smsqueuehandler.php b/scripts/smsqueuehandler.php
index 38f2f11fe..94b846d98 100755
--- a/scripts/smsqueuehandler.php
+++ b/scripts/smsqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,24 +18,25 @@
* 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/mail.php');
-require_once(INSTALLDIR . '/lib/queuehandler.php');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_SMS_HELP
+Daemon script for pushing new notices to local subscribers using SMS.
+
+ -i --id Identity (default none)
+
+END_OF_SMS_HELP;
-set_error_handler('common_error_handler');
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/mail.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
class SmsQueueHandler extends QueueHandler
{
-
function transport()
{
return 'sms';
@@ -51,18 +52,21 @@ class SmsQueueHandler extends QueueHandler
{
return mail_broadcast_notice_sms($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;
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
$handler = new SmsQueueHandler($id);
diff --git a/scripts/sphinx-cron.sh b/scripts/sphinx-cron.sh
index 9759adbd0..c16af3c4b 100755
--- a/scripts/sphinx-cron.sh
+++ b/scripts/sphinx-cron.sh
@@ -2,7 +2,7 @@
# Laconica - a distributed open-source microblogging tool
-# Copyright (C) 2008, Controlez-Vous, Inc.
+# Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/scripts/sphinx-indexer.sh b/scripts/sphinx-indexer.sh
index 3311b2ed1..fe7c16bea 100755
--- a/scripts/sphinx-indexer.sh
+++ b/scripts/sphinx-indexer.sh
@@ -2,7 +2,7 @@
# Laconica - a distributed open-source microblogging tool
-# Copyright (C) 2008, Controlez-Vous, Inc.
+# Copyright (C) 2008, 2009, Control Yourself, 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
diff --git a/scripts/startdaemons.sh b/scripts/startdaemons.sh
index 3869e95c4..9ead20acd 100755
--- a/scripts/startdaemons.sh
+++ b/scripts/startdaemons.sh
@@ -2,7 +2,7 @@
# Laconica - a distributed open-source microblogging tool
-# Copyright (C) 2008, Controlez-Vous, Inc.
+# Copyright (C) 2008, 2009, Control Yourself, 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
@@ -20,12 +20,27 @@
# This program tries to start the daemons for Laconica.
# Note that the 'maildaemon' needs to run as a mail filter.
+ARGSG=
+ARGSD=
+
+if [ $# -gt 0 ]; then
+ ARGSG="$ARGSG -s$1"
+ ID=`echo $1 | sed s/\\\\./_/g`
+ ARGSD="$ARGSD -s$1 -i$ID"
+fi
+
+if [ $# -gt 1 ]; then
+ ARGSD="$ARGSD -p$2"
+ ARGSG="$ARGSG -p$2"
+fi
+
DIR=`dirname $0`
-DAEMONS=`php $DIR/getvaliddaemons.php`
+DAEMONS=`php $DIR/getvaliddaemons.php $ARGSG`
for f in $DAEMONS; do
- echo -n "Starting $f...";
- php $DIR/$f
- echo "DONE."
+ printf "Starting $f...";
+ php $DIR/$f $ARGSD
+ printf "DONE.\n"
+
done
diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh
index 2134b4ab0..60ffd83ad 100755
--- a/scripts/stopdaemons.sh
+++ b/scripts/stopdaemons.sh
@@ -2,7 +2,7 @@
# Laconica - a distributed open-source microblogging tool
-# Copyright (C) 2008, Controlez-Vous, Inc.
+# Copyright (C) 2008, 2009, Control Yourself, 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
@@ -24,7 +24,8 @@ SDIR=`dirname $0`
DIR=`php $SDIR/getpiddir.php`
for f in jabberhandler ombhandler publichandler smshandler pinghandler \
- xmppconfirmhandler xmppdaemon twitterhandler facebookhandler; do
+ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \
+ twitterstatusfetcher; do
FILES="$DIR/$f.*.pid"
for ff in "$FILES" ; do
diff --git a/scripts/synctwitterfriends.php b/scripts/synctwitterfriends.php
index bd08ba58d..fe53ff44d 100755
--- a/scripts/synctwitterfriends.php
+++ b/scripts/synctwitterfriends.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,19 +18,16 @@
* 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);
// Uncomment this to get useful console output
-//define('SCRIPT_DEBUG', true);
-require_once(INSTALLDIR . '/lib/common.php');
+$helptext = <<<END_OF_TWITTER_HELP
+Batch script for synching local friends with Twitter friends.
+
+END_OF_TWITTER_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
// Make a lockfile
$lockfilename = lockFilename();
diff --git a/scripts/triminboxes.php b/scripts/triminboxes.php
index 0d2eaeaf0..b2135d682 100644
--- a/scripts/triminboxes.php
+++ b/scripts/triminboxes.php
@@ -18,26 +18,37 @@
* 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(1);
-}
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
+$shortoptions = 'u::';
+$longoptions = array('start-user-id::');
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for trimming notice inboxes to a reasonable size.
+
+ -u <id>
+ --start-user-id=<id> User ID to start after. Default is all.
-require_once(INSTALLDIR . '/lib/common.php');
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+$id = null;
+
+if (have_option('u')) {
+ $id = get_option_value('u');
+} else if (have_option('--start-user-id')) {
+ $id = get_option_value('--start-user-id');
+} else {
+ $id = null;
+}
$user = new User();
-if ($argc > 1) {
- $user->whereAdd('id > ' . $argv[1]);
+
+if (!empty($id)) {
+ $user->whereAdd('id > ' . $id);
}
+
$cnt = $user->find();
while ($user->fetch()) {
@@ -74,10 +85,10 @@ while ($user->fetch()) {
$delay = 3.0 * ($finish - $start);
print "Delaying $delay seconds...";
-
+
// Wait to let slaves catch up
usleep($delay * 1000000);
-
+
print "DONE.\n";
}
diff --git a/scripts/twitterqueuehandler.php b/scripts/twitterqueuehandler.php
index 7da4f1e20..00e735d98 100755
--- a/scripts/twitterqueuehandler.php
+++ b/scripts/twitterqueuehandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,29 +18,30 @@
* 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');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_ENJIT_HELP
+Daemon script for pushing new notices to Twitter.
+
+ -i --id Identity (default none)
+
+END_OF_ENJIT_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
-set_error_handler('common_error_handler');
+require_once INSTALLDIR . '/lib/twitter.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
class TwitterQueueHandler extends QueueHandler
{
-
function transport()
{
return 'twitter';
}
-
+
function start()
{
$this->log(LOG_INFO, "INITIALIZE");
@@ -51,20 +52,22 @@ class TwitterQueueHandler extends QueueHandler
{
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;
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
$handler = new TwitterQueueHandler($id);
diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php
new file mode 100755
index 000000000..8b10bfbad
--- /dev/null
+++ b/scripts/twitterstatusfetcher.php
@@ -0,0 +1,645 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+// Tune number of processes and how often to poll Twitter
+// XXX: Should these things be in config.php?
+define('MAXCHILDREN', 2);
+define('POLL_INTERVAL', 60); // in seconds
+
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for retrieving Twitter messages from foreign service.
+
+ -i --id Identity (default 'generic')
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/lib/daemon.php';
+
+/**
+ * Fetcher for statuses from Twitter
+ *
+ * Fetches statuses from Twitter and inserts them as notices in local
+ * system.
+ *
+ * @category Twitter
+ * @package Laconica
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @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/
+ */
+
+// NOTE: an Avatar path MUST be set in config.php for this
+// script to work: e.g.: $config['avatar']['path'] = '/laconica/avatar';
+
+class TwitterStatusFetcher extends Daemon
+{
+ private $_children = array();
+
+ /**
+ * Name of this daemon
+ *
+ * @return string Name of the daemon.
+ */
+
+ function name()
+ {
+ return ('twitterstatusfetcher.'.$this->_id);
+ }
+
+ /**
+ * Run the daemon
+ *
+ * @return void
+ */
+
+ function run()
+ {
+ do {
+
+ $flinks = $this->refreshFlinks();
+
+ foreach ($flinks as $f) {
+
+ // We have to disconnect from the DB before forking so
+ // each sub-process will open its own connection and
+ // avoid stomping on the others
+
+ $conn = &$f->getDatabaseConnection();
+ $conn->disconnect();
+
+ $pid = pcntl_fork();
+
+ if ($pid == -1) {
+ die ("Couldn't fork!");
+ }
+
+ if ($pid) {
+
+ // Parent
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Parent: forked new status ".
+ " fetcher process " . $pid);
+ }
+
+ $this->_children[] = $pid;
+
+ } else {
+
+ // Child
+ $this->getTimeline($f);
+ exit();
+ }
+
+ // Remove child from ps list as it finishes
+ while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Child $c finished.");
+ }
+
+ $this->removePs($this->_children, $c);
+ }
+
+ // Wait! We have too many damn kids.
+ if (sizeof($this->_children) > MAXCHILDREN) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Too many children. Waiting...');
+ }
+
+ if (($c = pcntl_wait($status, WUNTRACED)) > 0) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Finished waiting for $c");
+ }
+
+ $this->removePs($this->_children, $c);
+ }
+ }
+ }
+
+ // Remove all children from the process list before restarting
+ while (($c = pcntl_wait($status, WUNTRACED)) > 0) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Child $c finished.");
+ }
+
+ $this->removePs($this->_children, $c);
+ }
+
+ // Rest for a bit before we fetch more statuses
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Waiting ' . POLL_INTERVAL .
+ ' secs before hitting Twitter again.');
+ }
+
+ if (POLL_INTERVAL > 0) {
+ sleep(POLL_INTERVAL);
+ }
+
+ } while (true);
+ }
+
+ /**
+ * Refresh the foreign links for this user
+ *
+ * @return void
+ */
+
+ function refreshFlinks()
+ {
+ $flink = new Foreign_link();
+
+ $flink->service = 1; // Twitter
+
+ $flink->orderBy('last_noticesync');
+
+ $cnt = $flink->find();
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Updating Twitter friends subscriptions' .
+ " for $cnt users.");
+ }
+
+ $flinks = array();
+
+ while ($flink->fetch()) {
+
+ if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
+ FOREIGN_NOTICE_RECV) {
+ $flinks[] = clone($flink);
+ }
+ }
+
+ $flink->free();
+ unset($flink);
+
+ return $flinks;
+ }
+
+ /**
+ * Unknown
+ *
+ * @param array &$plist unknown.
+ * @param string $ps unknown.
+ *
+ * @return unknown
+ * @todo document
+ */
+
+ function removePs(&$plist, $ps)
+ {
+ for ($i = 0; $i < sizeof($plist); $i++) {
+ if ($plist[$i] == $ps) {
+ unset($plist[$i]);
+ $plist = array_values($plist);
+ break;
+ }
+ }
+ }
+
+ function getTimeline($flink)
+ {
+ if (empty($flink)) {
+ common_log(LOG_WARNING,
+ "Can't retrieve Foreign_link for foreign ID $fid");
+ return;
+ }
+
+ $fuser = $flink->getForeignUser();
+
+ if (empty($fuser)) {
+ common_log(LOG_WARNING, "Unmatched user for ID " .
+ $flink->user_id);
+ return;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Trying to get timeline for Twitter user ' .
+ "$fuser->nickname ($flink->foreign_id).");
+ }
+
+ // XXX: Biggest remaining issue - How do we know at which status
+ // to start importing? How many statuses? Right now I'm going
+ // with the default last 20.
+
+ $url = 'http://twitter.com/statuses/friends_timeline.json';
+
+ $timeline_json = get_twitter_data($url, $fuser->nickname,
+ $flink->credentials);
+
+ $timeline = json_decode($timeline_json);
+
+ if (empty($timeline)) {
+ common_log(LOG_WARNING, "Empty timeline.");
+ return;
+ }
+
+ // Reverse to preserve order
+ foreach (array_reverse($timeline) as $status) {
+
+ // Hacktastic: filter out stuff coming from this Laconica
+ $source = mb_strtolower(common_config('integration', 'source'));
+
+ if (preg_match("/$source/", mb_strtolower($status->source))) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Skipping import of status ' . $status->id .
+ ' with source ' . $source);
+ }
+ continue;
+ }
+
+ $this->saveStatus($status, $flink);
+ }
+
+ // Okay, record the time we synced with Twitter for posterity
+ $flink->last_noticesync = common_sql_now();
+ $flink->update();
+ }
+
+ function saveStatus($status, $flink)
+ {
+ $id = $this->ensureProfile($status->user);
+ $profile = Profile::staticGet($id);
+
+ if (!$profile) {
+ common_log(LOG_ERR,
+ 'Problem saving notice. No associated Profile.');
+ return null;
+ }
+
+ // XXX: change of screen name?
+
+ $uri = 'http://twitter.com/' . $status->user->screen_name .
+ '/status/' . $status->id;
+
+ $notice = Notice::staticGet('uri', $uri);
+
+ // check to see if we've already imported the status
+
+ if (!$notice) {
+
+ $notice = new Notice();
+
+ $notice->profile_id = $id;
+ $notice->uri = $uri;
+ $notice->created = strftime('%Y-%m-%d %H:%M:%S',
+ strtotime($status->created_at));
+ $notice->content = common_shorten_links($status->text); // XXX
+ $notice->rendered = common_render_content($notice->content, $notice);
+ $notice->source = 'twitter';
+ $notice->reply_to = null; // XXX lookup reply
+ $notice->is_local = NOTICE_GATEWAY;
+
+ if (Event::handle('StartNoticeSave', array(&$notice))) {
+ $id = $notice->insert();
+ Event::handle('EndNoticeSave', array($notice));
+ }
+ }
+
+ if (!Notice_inbox::pkeyGet(array('notice_id' => $notice->id,
+ 'user_id' => $flink->user_id))) {
+ // Add to inbox
+ $inbox = new Notice_inbox();
+
+ $inbox->user_id = $flink->user_id;
+ $inbox->notice_id = $notice->id;
+ $inbox->created = $notice->created;
+ $inbox->source = NOTICE_INBOX_SOURCE_GATEWAY; // From a private source
+
+ $inbox->insert();
+ }
+ }
+
+ function ensureProfile($user)
+ {
+ // check to see if there's already a profile for this user
+ $profileurl = 'http://twitter.com/' . $user->screen_name;
+ $profile = Profile::staticGet('profileurl', $profileurl);
+
+ if ($profile) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Profile for $profile->nickname found.");
+ }
+
+ // Check to see if the user's Avatar has changed
+ $this->checkAvatar($user, $profile);
+
+ return $profile->id;
+
+ } else {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Adding profile and remote profile ' .
+ "for Twitter user: $profileurl");
+ }
+
+ $profile = new Profile();
+ $profile->query("BEGIN");
+
+ $profile->nickname = $user->screen_name;
+ $profile->fullname = $user->name;
+ $profile->homepage = $user->url;
+ $profile->bio = $user->description;
+ $profile->location = $user->location;
+ $profile->profileurl = $profileurl;
+ $profile->created = common_sql_now();
+
+ $id = $profile->insert();
+
+ if (empty($id)) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+
+ // check for remote profile
+ $remote_pro = Remote_profile::staticGet('uri', $profileurl);
+
+ if (!$remote_pro) {
+
+ $remote_pro = new Remote_profile();
+
+ $remote_pro->id = $id;
+ $remote_pro->uri = $profileurl;
+ $remote_pro->created = common_sql_now();
+
+ $rid = $remote_pro->insert();
+
+ if (empty($rid)) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+ }
+
+ $profile->query("COMMIT");
+
+ $this->saveAvatars($user, $id);
+
+ return $id;
+ }
+ }
+
+ function checkAvatar($twitter_user, $profile)
+ {
+ global $config;
+
+ $path_parts = pathinfo($twitter_user->profile_image_url);
+
+ $newname = 'Twitter_' . $twitter_user->id . '_' .
+ $path_parts['basename'];
+
+ $oldname = $profile->getAvatar(48)->filename;
+
+ if ($newname != $oldname) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Avatar for Twitter user ' .
+ "$profile->nickname has changed.");
+ common_debug("old: $oldname new: $newname");
+ }
+
+ $this->updateAvatars($twitter_user, $profile);
+ }
+
+ if ($this->missingAvatarFile($profile)) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Twitter user ' . $profile->nickname .
+ ' is missing one or more local avatars.');
+ common_debug("old: $oldname new: $newname");
+ }
+
+ $this->updateAvatars($twitter_user, $profile);
+ }
+
+ }
+
+ function updateAvatars($twitter_user, $profile) {
+
+ global $config;
+
+ $path_parts = pathinfo($twitter_user->profile_image_url);
+
+ $img_root = substr($path_parts['basename'], 0, -11);
+ $ext = $path_parts['extension'];
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $twitter_user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ $this->updateAvatar($profile->id, $size, $mediatype, $filename);
+ $this->fetchAvatar($url, $filename);
+ }
+ }
+
+ function missingAvatarFile($profile) {
+
+ foreach (array(24, 48, 73) as $size) {
+
+ $filename = $profile->getAvatar($size)->filename;
+ $avatarpath = Avatar::path($filename);
+
+ if (file_exists($avatarpath) == FALSE) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function getMediatype($ext)
+ {
+ $mediatype = null;
+
+ switch (strtolower($ext)) {
+ case 'jpg':
+ $mediatype = 'image/jpg';
+ break;
+ case 'gif':
+ $mediatype = 'image/gif';
+ break;
+ default:
+ $mediatype = 'image/png';
+ }
+
+ return $mediatype;
+ }
+
+ function saveAvatars($user, $id)
+ {
+ global $config;
+
+ $path_parts = pathinfo($user->profile_image_url);
+ $ext = $path_parts['extension'];
+ $end = strlen('_normal' . $ext);
+ $img_root = substr($path_parts['basename'], 0, -($end+1));
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ if ($this->fetchAvatar($url, $filename)) {
+ $this->newAvatar($id, $size, $mediatype, $filename);
+ } else {
+ common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
+ }
+ }
+ }
+
+ function updateAvatar($profile_id, $size, $mediatype, $filename) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Updating avatar: $size");
+ }
+
+ $profile = Profile::staticGet($profile_id);
+
+ if (empty($profile)) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Couldn't get profile: $profile_id!");
+ }
+ return;
+ }
+
+ $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
+ $avatar = $profile->getAvatar($sizes[$size]);
+
+ // Delete the avatar, if present
+ if ($avatar) {
+ $avatar->delete();
+ }
+
+ $this->newAvatar($profile->id, $size, $mediatype, $filename);
+ }
+
+ function newAvatar($profile_id, $size, $mediatype, $filename)
+ {
+ global $config;
+
+ $avatar = new Avatar();
+ $avatar->profile_id = $profile_id;
+
+ switch($size) {
+ case 'mini':
+ $avatar->width = 24;
+ $avatar->height = 24;
+ break;
+ case 'normal':
+ $avatar->width = 48;
+ $avatar->height = 48;
+ break;
+ default:
+
+ // Note: Twitter's big avatars are a different size than
+ // Laconica's (Laconica's = 96)
+
+ $avatar->width = 73;
+ $avatar->height = 73;
+ }
+
+ $avatar->original = 0; // we don't have the original
+ $avatar->mediatype = $mediatype;
+ $avatar->filename = $filename;
+ $avatar->url = Avatar::url($filename);
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("new filename: $avatar->url");
+ }
+
+ $avatar->created = common_sql_now();
+
+ $id = $avatar->insert();
+
+ if (empty($id)) {
+ common_log_db_error($avatar, 'INSERT', __FILE__);
+ return null;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Saved new $size avatar for $profile_id.");
+ }
+
+ return $id;
+ }
+
+ function fetchAvatar($url, $filename)
+ {
+ $avatar_dir = INSTALLDIR . '/avatar/';
+
+ $avatarfile = $avatar_dir . $filename;
+
+ $out = fopen($avatarfile, 'wb');
+ if (!$out) {
+ common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
+ return false;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Fetching avatar: $url");
+ }
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_FILE, $out);
+ curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
+ $result = curl_exec($ch);
+ curl_close($ch);
+
+ fclose($out);
+
+ return $result;
+ }
+}
+
+declare(ticks = 1);
+
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
+
+$fetcher = new TwitterStatusFetcher($id);
+$fetcher->runOnce();
+
diff --git a/scripts/uncache_users.php b/scripts/uncache_users.php
index fa0fb64cd..b0b576eb4 100644
--- a/scripts/uncache_users.php
+++ b/scripts/uncache_users.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -17,32 +17,27 @@
* 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/>.
*/
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-# 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();
-}
+$helptext = <<<ENDOFHELP
+uncache_users.php <idfile>
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
+Uncache users listed in an ID file, default 'ids.txt'.
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
+ENDOFHELP;
-require_once(INSTALLDIR . '/lib/common.php');
+require_once INSTALLDIR.'/scripts/commandline.inc';
-$id_file = ($argc > 1) ? $argv[1] : 'ids.txt';
+$id_file = (count($args) > 1) ? $args[0] : 'ids.txt';
common_log(LOG_INFO, 'Updating user inboxes.');
$ids = file($id_file);
+$memc = common_memcache();
+
foreach ($ids as $id) {
-
+
$user = User::staticGet('id', $id);
if (!$user) {
@@ -51,9 +46,7 @@ foreach ($ids as $id) {
}
$user->decache();
-
- $memc = common_memcache();
-
+
$memc->delete(common_cache_key('user:notices_with_friends:'. $user->id));
$memc->delete(common_cache_key('user:notices_with_friends:'. $user->id . ';last'));
}
diff --git a/scripts/update_translations.php b/scripts/update_translations.php
index 4d7adafea..2f4ca8720 100755
--- a/scripts/update_translations.php
+++ b/scripts/update_translations.php
@@ -1,66 +1,97 @@
+#!/usr/bin/env php
<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, Control Yourself, 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');
+
+// Master Laconica .pot file location (created by update_pot.sh)
+$laconica_pot = INSTALLDIR . '/locale/laconica.po';
set_time_limit(60);
-chdir(dirname(__FILE__) . '/..');
/* Languages to pull */
-$languages = array(
- 'da_DK' => 'http://laconi.ca/translate/download.php?file_id=93',
- 'nl_NL' => 'http://laconi.ca/translate/download.php?file_id=97',
- 'en_NZ' => 'http://laconi.ca/translate/download.php?file_id=87',
- 'eo' => 'http://laconi.ca/translate/download.php?file_id=88',
- 'fr_FR' => 'http://laconi.ca/translate/download.php?file_id=99',
- 'de_DE' => 'http://laconi.ca/translate/download.php?file_id=100',
- 'it_IT' => 'http://laconi.ca/translate/download.php?file_id=101',
- 'ko' => 'http://laconi.ca/translate/download.php?file_id=102',
- 'no_NB' => 'http://laconi.ca/translate/download.php?file_id=104',
- 'pt' => 'http://laconi.ca/translate/download.php?file_id=106',
- 'pt_BR' => 'http://laconi.ca/translate/download.php?file_id=107',
- 'ru_RU' => 'http://laconi.ca/translate/download.php?file_id=109',
- 'es' => 'http://laconi.ca/translate/download.php?file_id=110',
- 'tr_TR' => 'http://laconi.ca/translate/download.php?file_id=114',
- 'uk_UA' => 'http://laconi.ca/translate/download.php?file_id=115',
- 'he_IL' => 'http://laconi.ca/translate/download.php?file_id=116',
- 'mk_MK' => 'http://laconi.ca/translate/download.php?file_id=103',
- 'ja_JP' => 'http://laconi.ca/translate/download.php?file_id=117',
- 'cs_CZ' => 'http://laconi.ca/translate/download.php?file_id=96',
- 'ca_ES' => 'http://laconi.ca/translate/download.php?file_id=95',
- 'pl_PL' => 'http://laconi.ca/translate/download.php?file_id=105',
- 'sv_SE' => 'http://laconi.ca/translate/download.php?file_id=128'
-);
+$languages = get_all_languages();
/* Update the languages */
-foreach ($languages as $code => $file) {
-
- $lcdir='locale/'.$code;
- $msgdir=$lcdir.'/LC_MESSAGES';
- $pofile=$msgdir.'/laconica.po';
- $mofile=$msgdir.'/laconica.mo';
-
- /* Check for an existing */
- if (!is_dir($msgdir)) {
- mkdir($lcdir);
- mkdir($msgdir);
- $existingSHA1 = '';
- } else {
- $existingSHA1 = file_exists($pofile) ? sha1_file($pofile) : '';
- }
-
- /* Get the remote one */
- $newFile = file_get_contents($file);
-
- // Update if the local .po file is different to the one downloaded, or
- // if the .mo file is not present.
- if(sha1($newFile)!=$existingSHA1 || !file_exists($mofile)) {
- echo "Updating ".$code."\n";
- file_put_contents($pofile, $newFile);
- $prevdir = getcwd();
- chdir($msgdir);
- system('msgmerge -U laconica.po ../../laconica.pot');
- system('msgfmt -f -o laconica.mo laconica.po');
- chdir($prevdir);
- } else {
- echo "Unchanged - ".$code."\n";
- }
+
+foreach ($languages as $language) {
+
+ $code = $language['lang'];
+ $file_url = 'http://laconi.ca/pootle/' . $code .
+ '/laconica/LC_MESSAGES/laconica.po';
+ $lcdir = INSTALLDIR . '/locale/' . $code;
+ $msgdir = "$lcdir/LC_MESSAGES";
+ $pofile = "$msgdir/laconica.po";
+ $mofile = "$msgdir/laconica.mo";
+
+ /* Check for an existing */
+ if (!is_dir($msgdir)) {
+ mkdir($lcdir);
+ mkdir($msgdir);
+ $existingSHA1 = '';
+ } else {
+ $existingSHA1 = file_exists($pofile) ? sha1_file($pofile) : '';
+ }
+
+ /* Get the remote one */
+ $new_file = curl_get_file($file_url);
+
+ if ($new_file === FALSE) {
+ echo "Couldn't retrieve .po file for $code: $file_url\n";
+ continue;
+ }
+
+ // Update if the local .po file is different to the one downloaded, or
+ // if the .mo file is not present.
+ if (sha1($new_file) != $existingSHA1 || !file_exists($mofile)) {
+ echo "Updating ".$code."\n";
+ file_put_contents($pofile, $new_file);
+ system(sprintf('msgmerge -U %s %s', $pofile, $laconica_pot));
+ system(sprintf('msgfmt -f -o %s %s', $mofile, $pofile));
+ } else {
+ echo "Unchanged - ".$code."\n";
+ }
}
+
echo "Finished\n";
+
+
+function curl_get_file($url)
+{
+ $c = curl_init();
+ curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($c, CURLOPT_URL, $url);
+ $contents = curl_exec($c);
+ curl_close($c);
+
+ if (!empty($contents)) {
+ return $contents;
+ }
+
+ return FALSE;
+}
diff --git a/scripts/xmppconfirmhandler.php b/scripts/xmppconfirmhandler.php
index 7f39235fe..d6821ddef 100755
--- a/scripts/xmppconfirmhandler.php
+++ b/scripts/xmppconfirmhandler.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,33 +18,33 @@
* 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/jabber.php');
-require_once(INSTALLDIR . '/lib/xmppqueuehandler.php');
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_JABBER_HELP
+Daemon script for pushing new confirmations to Jabber users.
+
+ -i --id Identity (default none)
+
+END_OF_JABBER_HELP;
-set_error_handler('common_error_handler');
+require_once INSTALLDIR.'/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/jabber.php';
+require_once INSTALLDIR . '/lib/xmppqueuehandler.php';
define('CLAIM_TIMEOUT', 1200);
class XmppConfirmHandler extends XmppQueueHandler
{
-
var $_id = 'confirm';
-
+
function class_name()
{
return 'XmppConfirmHandler';
}
-
+
function run()
{
if (!$this->start()) {
@@ -147,14 +147,17 @@ if (common_config('xmpp','enabled')==false) {
exit();
}
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
-
-$resource = ($argc > 1) ? $argv[1] : (common_config('xmpp', 'resource').'-confirm');
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
-$handler = new XmppConfirmHandler($resource);
+$handler = new XmppConfirmHandler($id);
$handler->runOnce();
diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php
index b79fa1b3b..488b4b514 100755
--- a/scripts/xmppdaemon.php
+++ b/scripts/xmppdaemon.php
@@ -2,7 +2,7 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -18,20 +18,24 @@
* 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/jabber.php');
-require_once(INSTALLDIR . '/lib/daemon.php');
+$shortoptions = 'fi::';
+$longoptions = array('id::', 'foreground');
+
+$helptext = <<<END_OF_XMPP_HELP
+Daemon script for receiving new notices from Jabber users.
+
+ -i --id Identity (default none)
+ -f --foreground Stay in the foreground (default background)
+
+END_OF_XMPP_HELP;
-set_error_handler('common_error_handler');
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/lib/jabber.php';
+require_once INSTALLDIR . '/lib/daemon.php';
# This is kind of clunky; we create a class to call the global functions
# in jabber.php, which create a new XMPP class. A more elegant (?) solution
@@ -39,9 +43,10 @@ set_error_handler('common_error_handler');
class XMPPDaemon extends Daemon
{
-
- function XMPPDaemon($resource=null)
+ function __construct($resource=null, $daemonize=true)
{
+ parent::__construct($daemonize);
+
static $attrs = array('server', 'port', 'user', 'password', 'host');
foreach ($attrs as $attr)
@@ -50,17 +55,18 @@ class XMPPDaemon extends Daemon
}
if ($resource) {
- $this->resource = $resource;
+ $this->resource = $resource . 'daemon';
} else {
$this->resource = common_config('xmpp', 'resource') . 'daemon';
}
- $this->log(LOG_INFO, "INITIALIZE XMPPDaemon {$this->user}@{$this->server}/{$this->resource}");
+ $this->jid = $this->user.'@'.$this->server.'/'.$this->resource;
+
+ $this->log(LOG_INFO, "INITIALIZE XMPPDaemon {$this->jid}");
}
function connect()
{
-
$connect_to = ($this->host) ? $this->host : $this->server;
$this->log(LOG_INFO, "Connecting to $connect_to on port $this->port");
@@ -71,10 +77,17 @@ class XMPPDaemon extends Daemon
return false;
}
+ $this->log(LOG_INFO, "Connected");
+
$this->conn->setReconnectTimeout(600);
+ $this->log(LOG_INFO, "Sending initial presence.");
+
jabber_send_presence("Send me a message to post a notice", 'available',
null, 'available', 100);
+
+ $this->log(LOG_INFO, "Done connecting.");
+
return !$this->conn->isDisconnected();
}
@@ -87,18 +100,44 @@ class XMPPDaemon extends Daemon
{
if ($this->connect()) {
+ $this->log(LOG_DEBUG, "Initializing stanza handlers.");
+
$this->conn->addEventHandler('message', 'handle_message', $this);
$this->conn->addEventHandler('presence', 'handle_presence', $this);
$this->conn->addEventHandler('reconnect', 'handle_reconnect', $this);
- $this->conn->process();
+ $this->log(LOG_DEBUG, "Beginning processing loop.");
+
+ while ($this->conn->processTime(60)) {
+ $this->sendPing();
+ }
+ }
+ }
+
+ function sendPing()
+ {
+ if (!isset($this->pingid)) {
+ $this->pingid = 0;
+ } else {
+ $this->pingid++;
}
+
+ $this->log(LOG_DEBUG, "Sending ping #{$this->pingid}");
+
+ $this->conn->send("<iq from='{$this->jid}' to='{$this->server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
}
function handle_reconnect(&$pl)
{
+ $this->log(LOG_DEBUG, "Got reconnection callback.");
$this->conn->processUntil('session_start');
+ $this->log(LOG_DEBUG, "Sending reconnection presence.");
$this->conn->presence('Send me a message to post a notice', 'available', null, 'available', 100);
+ unset($pl['xml']);
+ $pl['xml'] = null;
+
+ $pl = null;
+ unset($pl);
}
function get_user($from)
@@ -109,21 +148,27 @@ class XMPPDaemon extends Daemon
function handle_message(&$pl)
{
+ $from = jabber_normalize_jid($pl['from']);
+
if ($pl['type'] != 'chat') {
+ $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from.");
return;
}
+
if (mb_strlen($pl['body']) == 0) {
+ $this->log(LOG_WARNING, "Ignoring message with empty body from $from.");
return;
}
- $from = jabber_normalize_jid($pl['from']);
-
# Forwarded from another daemon (probably a broadcaster) for
# us to handle
if ($this->is_self($from)) {
+ $this->log(LOG_INFO, "Got forwarded notice from self ($from).");
$from = $this->get_ofrom($pl);
+ $this->log(LOG_INFO, "Originally sent by $from.");
if (is_null($from) || $this->is_self($from)) {
+ $this->log(LOG_INFO, "Ignoring notice originally sent by $from.");
return;
}
}
@@ -138,6 +183,7 @@ class XMPPDaemon extends Daemon
return;
}
if ($this->handle_command($user, $pl['body'])) {
+ $this->log(LOG_INFO, "Command messag by $from handled.");
return;
} else if ($this->is_autoreply($pl['body'])) {
$this->log(LOG_INFO, 'Ignoring auto reply from ' . $from);
@@ -146,17 +192,31 @@ class XMPPDaemon extends Daemon
$this->log(LOG_INFO, 'Ignoring OTR from ' . $from);
return;
} else if ($this->is_direct($pl['body'])) {
+ $this->log(LOG_INFO, 'Got a direct message ' . $from);
+
preg_match_all('/d[\ ]*([a-z0-9]{1,64})/', $pl['body'], $to);
$to = preg_replace('/^d([\ ])*/', '', $to[0][0]);
$body = preg_replace('/d[\ ]*('. $to .')[\ ]*/', '', $pl['body']);
+
+ $this->log(LOG_INFO, 'Direct message from '. $user->nickname . ' to ' . $to);
+
$this->add_direct($user, $body, $to, $from);
} else {
+
+ $this->log(LOG_INFO, 'Posting a notice from ' . $user->nickname);
+
$this->add_notice($user, $pl);
}
$user->free();
unset($user);
+
+ unset($pl['xml']);
+ $pl['xml'] = null;
+
+ $pl = null;
+ unset($pl);
}
function is_self($from)
@@ -259,6 +319,7 @@ class XMPPDaemon extends Daemon
$notice = Notice::saveNew($user->id, $content_shortened, 'xmpp');
if (is_string($notice)) {
$this->log(LOG_ERR, $notice);
+ $this->from_site($user->jabber, $notice);
return;
}
common_broadcast_notice($notice);
@@ -301,11 +362,23 @@ class XMPPDaemon extends Daemon
}
break;
}
+ unset($pl['xml']);
+ $pl['xml'] = null;
+
+ $pl = null;
+ unset($pl);
}
function log($level, $msg)
{
- common_log($level, 'XMPPDaemon('.$this->resource.'): '.$msg);
+ $text = 'XMPPDaemon('.$this->resource.'): '.$msg;
+ common_log($level, $text);
+ if (!$this->daemonize)
+ {
+ $line = common_log_line($level, $text);
+ echo $line;
+ echo "\n";
+ }
}
function subscribed($to)
@@ -321,13 +394,16 @@ if (common_config('xmpp','enabled')==false) {
exit();
}
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
+if (have_option('i', 'id')) {
+ $id = get_option_value('i', 'id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
-$resource = ($argc > 1) ? $argv[1] : (common_config('xmpp','resource') . '-listen');
+$foreground = have_option('f', 'foreground');
-$daemon = new XMPPDaemon($resource);
+$daemon = new XMPPDaemon($id, !$foreground);
$daemon->runOnce();
diff --git a/sphinx.conf.sample b/sphinx.conf.sample
index b79adf15c..8204b9db6 100644
--- a/sphinx.conf.sample
+++ b/sphinx.conf.sample
@@ -4,68 +4,68 @@
source src1
{
- type = mysql
- sql_host = localhost
- sql_user = USERNAME
- sql_pass = PASSWORD
- sql_db = identi_ca
- sql_port = 3306
- sql_query = SELECT id, UNIX_TIMESTAMP(created) as created_ts, nickname, fullname, location, bio, homepage FROM profile
- sql_query_info = SELECT * FROM profile where id = $id
- sql_attr_timestamp = created_ts
+ type = mysql
+ sql_host = localhost
+ sql_user = USERNAME
+ sql_pass = PASSWORD
+ sql_db = identi_ca
+ sql_port = 3306
+ sql_query = SELECT id, UNIX_TIMESTAMP(created) as created_ts, nickname, fullname, location, bio, homepage FROM profile
+ sql_query_info = SELECT * FROM profile where id = $id
+ sql_attr_timestamp = created_ts
}
source src2
{
- type = mysql
- sql_host = localhost
- sql_user = USERNAME
- sql_pass = PASSWORD
- sql_db = identi_ca
- sql_port = 3306
- sql_query = SELECT id, UNIX_TIMESTAMP(created) as created_ts, content FROM notice
- sql_query_info = SELECT * FROM notice where id = $id
- sql_attr_timestamp = created_ts
+ type = mysql
+ sql_host = localhost
+ sql_user = USERNAME
+ sql_pass = PASSWORD
+ sql_db = identi_ca
+ sql_port = 3306
+ sql_query = SELECT id, UNIX_TIMESTAMP(created) as created_ts, content FROM notice
+ sql_query_info = SELECT * FROM notice where notice.id = $id AND notice.is_local != -2
+ sql_attr_timestamp = created_ts
}
index identica_notices
{
- source = src2
- path = DIRECTORY/data/identica_notices
- docinfo = extern
- charset_type = utf-8
- min_word_len = 3
- stopwords = DIRECTORY/data/stopwords-en.txt
+ source = src2
+ path = DIRECTORY/data/identica_notices
+ docinfo = extern
+ charset_type = utf-8
+ min_word_len = 3
+ stopwords = DIRECTORY/data/stopwords-en.txt
}
index identica_people
{
- source = src1
- path = DIRECTORY/data/identica_people
- docinfo = extern
- charset_type = utf-8
- min_word_len = 3
- stopwords = DIRECTORY/data/stopwords-en.txt
+ source = src1
+ path = DIRECTORY/data/identica_people
+ docinfo = extern
+ charset_type = utf-8
+ min_word_len = 3
+ stopwords = DIRECTORY/data/stopwords-en.txt
}
indexer
{
- mem_limit = 32M
+ mem_limit = 32M
}
searchd
{
- port = 3312
- log = DIRECTORY/log/searchd.log
- query_log = DIRECTORY/log/query.log
- read_timeout = 5
- max_children = 30
- pid_file = DIRECTORY/log/searchd.pid
- max_matches = 1000
- seamless_rotate = 1
- preopen_indexes = 0
- unlink_old = 1
+ port = 3312
+ log = DIRECTORY/log/searchd.log
+ query_log = DIRECTORY/log/query.log
+ read_timeout = 5
+ max_children = 30
+ pid_file = DIRECTORY/log/searchd.pid
+ max_matches = 1000
+ seamless_rotate = 1
+ preopen_indexes = 0
+ unlink_old = 1
}
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index 8029a5eee..3426e71c0 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -12,9 +12,9 @@ img { display:block; border:0; }
a abbr { cursor: pointer; border-bottom:0; }
table { border-collapse:collapse; }
ol { list-style-position:inside; }
-html { font-size: 87.5%; background-color:#fff; height:100%; }
+html { font-size: 87.5%; }
body {
-background-color:#fff;
+background-color:#FFFFFF;
color:#000;
font-family:sans-serif;
font-size:1em;
@@ -77,7 +77,8 @@ margin:0 0 18px 0;
form label {
font-weight:bold;
}
-input.checkbox {
+input.checkbox,
+input.radio {
position:relative;
top:2px;
left:0;
@@ -154,7 +155,8 @@ font-weight:bold;
#form_invite legend,
#form_notice_delete legend,
#form_password_recover legend,
-#form_password_change legend {
+#form_password_change legend,
+.form_entity_block legend {
display:none;
}
@@ -168,7 +170,8 @@ margin-bottom:0;
margin-bottom:11px;
}
-.form_settings input.checkbox {
+.form_settings input.checkbox,
+.form_settings input.radio {
margin-top:3px;
margin-left:0;
}
@@ -180,13 +183,19 @@ margin-left:11px;
float:left;
width:90%;
}
-
+.form_settings label.radio {
+margin-top:0;
+margin-right:47px;
+margin-left:11px;
+width:auto;
+}
#form_login p.form_guide,
#form_register #settings_rememberme p.form_guide,
#form_openid_login #settings_rememberme p.form_guide,
#settings_twitter_remove p.form_guide,
-#form_search ul.form_data #q {
+#form_search ul.form_data #q,
+#design_background-image_onoff p.form_guide {
margin-left:0;
}
@@ -197,10 +206,15 @@ border-radius:4px;
padding:0 7px;
}
-
+.form_settings input.form_action-default {
+margin-right:11px;
+}
+.form_settings input.form_action-default,
+.form_settings input.form_action-primary {
+padding:0;
+}
.form_settings input.form_action-secondary {
margin-left:29px;
-padding:0;
}
#form_search .submit {
@@ -248,10 +262,10 @@ display:none;
}
#site_notice {
-position:absolute;
-top:65px;
-right:18px;
-width:250px;
+float:right;
+clear:right;
+margin-top:7px;
+margin-right:18px;
width:24%;
}
#page_notice {
@@ -259,10 +273,9 @@ clear:both;
margin-bottom:18px;
}
-
#anon_notice {
float:left;
-width:43.2%;
+width:42.4%;
padding:1.1%;
border-radius:7px;
-moz-border-radius:7px;
@@ -274,7 +287,6 @@ font-size:1.1em;
font-weight:bold;
}
-
#footer {
float:left;
width:64%;
@@ -304,7 +316,6 @@ padding:4px 11px;
border-width:1px;
border-style:solid;
border-bottom:0;
-text-shadow: 2px 2px 2px #ddd;
font-weight:bold;
}
#site_nav_local_views .nav {
@@ -385,7 +396,7 @@ margin-bottom:1em;
}
#content {
-width:64.009%;
+width:63.311%;
min-height:259px;
padding:1.795%;
float:left;
@@ -394,8 +405,11 @@ border-radius:7px;
-moz-border-radius-topleft:0;
-webkit-border-radius:7px;
-webkit-border-top-left-radius:0;
-border-style:solid;
border-width:1px;
+border-style:solid;
+}
+#shownotice #content {
+min-height:0;
}
#content_inner {
@@ -408,7 +422,7 @@ float:left;
width:27.917%;
min-height:259px;
float:left;
-margin-left:0.385%;
+margin-left:0.699%;
padding:1.795%;
border-radius:7px;
-moz-border-radius:7px;
@@ -418,7 +432,7 @@ border-style:solid;
}
#form_notice {
-width:45.664%;
+width:45%;
float:left;
position:relative;
line-height:1;
@@ -440,6 +454,8 @@ width:80.789%;
height:67px;
line-height:1.5;
padding:7px 7px 16px 7px;
+position:relative;
+z-index:2;
}
#form_notice label {
display:block;
@@ -447,8 +463,22 @@ float:left;
font-size:1.3em;
margin-bottom:7px;
}
-#form_notice #notice_submit label {
-display:none;
+#form_notice label[for=notice_data-attach],
+#form_notice #notice_data-attach {
+position:absolute;
+top:25px;
+cursor:pointer;
+}
+#form_notice label[for=notice_data-attach] {
+text-indent:-9999px;
+left:86%;
+width:16px;
+height:16px;
+}
+#form_notice #notice_data-attach {
+left:40.6%;
+padding:0;
+height:16px;
}
#form_notice .form_note {
position:absolute;
@@ -481,13 +511,26 @@ margin-bottom:7px;
margin-left:18px;
float:left;
}
-#form_notice .error {
+#form_notice .error,
+#form_notice .success {
float:left;
clear:both;
-width:96.9%;
+width:81.5%;
margin-bottom:0;
line-height:1.618;
}
+#form_notice #notice_data-attach_selected code {
+float:left;
+width:90%;
+display:block;
+font-size:1.1em;
+line-height:1.8;
+overflow:auto;
+}
+#form_notice #notice_data-attach_selected button {
+float:right;
+font-size:0.8em;
+}
/* entity_profile */
.entity_profile {
@@ -519,7 +562,8 @@ margin-bottom:18px;
.entity_profile .entity_location,
.entity_profile .entity_url,
.entity_profile .entity_note,
-.entity_profile .entity_tags {
+.entity_profile .entity_tags,
+.entity_profile .entity_aliases {
margin-left:113px;
margin-bottom:4px;
}
@@ -545,16 +589,16 @@ font-weight:normal;
content: ")";
font-weight:normal;
}
-
-.entity_profile dt {
-display:none;
-}
+.entity_profile dt,
.entity_profile h2 {
display:none;
}
+.entity_profile .role {
+margin-left:11px;
+font-style:italic;
+}
/* entity_profile */
-
/*entity_actions*/
.entity_actions {
float:right;
@@ -596,10 +640,13 @@ display:block;
.form_user_block input.submit,
.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit,
.entity_send-a-message a,
.entity_edit a,
.form_user_nudge input.submit,
-.entity_nudge p {
+.entity_nudge p,
+.form_make_admin input.submit {
border:0;
padding-left:20px;
}
@@ -636,6 +683,7 @@ margin-bottom:29px;
clear:both;
float:left;
width:100%;
+list-style-position:inside;
}
.aside .section h2 {
text-transform:uppercase;
@@ -659,6 +707,7 @@ list-style-type:none;
float:left;
margin-right:7px;
margin-bottom:7px;
+display:inline;
}
.section .entities li .photo {
margin-right:0;
@@ -678,7 +727,6 @@ margin-bottom:0;
min-height:60px;
}
-
.profile .form_group_join legend,
.profile .form_group_leave legend,
.profile .form_user_subscribe legend,
@@ -713,13 +761,11 @@ display:inline;
margin-right:11px;
}
-
.profile .entity_profile .form_subscription_edit label {
font-weight:normal;
margin-right:11px;
}
-
/* NOTICE */
.notice,
.profile {
@@ -730,15 +776,16 @@ clear:both;
float:left;
width:100%;
border-top-width:1px;
-border-top-style:dashed;
+border-top-style:dotted;
}
.notices li {
list-style-type:none;
}
-.notices li.hover {
-border-radius:4px;
--moz-border-radius:4px;
--webkit-border-radius:4px;
+.notices .notices {
+margin-top:7px;
+margin-left:2%;
+width:98%;
+float:left;
}
/* NOTICES */
@@ -769,16 +816,14 @@ overflow:hidden;
font-weight:bold;
}
-.notice .author .photo {
-margin-bottom:0;
-}
-
.vcard .photo {
display:inline;
margin-right:11px;
-margin-bottom:11px;
float:left;
}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
.vcard .url {
text-decoration:none;
}
@@ -791,6 +836,9 @@ float:left;
width:100%;
overflow:hidden;
}
+.notice .entry-title.ov {
+overflow:visible;
+}
#shownotice .notice .entry-title {
font-size:2.2em;
}
@@ -815,9 +863,10 @@ clear:left;
float:left;
font-size:0.95em;
margin-left:59px;
-width:70%;
+width:60%;
}
-#showstream .notice div.entry-content {
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
margin-left:0;
}
@@ -844,15 +893,12 @@ display:inline-block;
text-transform:lowercase;
}
-
.notice-options {
-padding-left:2%;
-float:left;
-width:50%;
position:relative;
font-size:0.95em;
-width:12.5%;
+width:90px;
float:right;
+margin-right:11px;
}
.notice-options a {
@@ -913,6 +959,97 @@ border:0;
padding:0;
}
+.notice .attachment {
+position:relative;
+padding-left:16px;
+}
+#attachments .attachment {
+padding-left:0;
+}
+.notice .attachment img {
+position:absolute;
+top:18px;
+left:0;
+z-index:99;
+}
+#shownotice .notice .attachment img {
+position:static;
+}
+
+#attachments {
+clear:both;
+float:left;
+width:100%;
+margin-top:18px;
+}
+#attachments dt {
+font-weight:bold;
+font-size:1.3em;
+margin-bottom:4px;
+}
+
+#attachments ol li {
+margin-bottom:18px;
+list-style-type:decimal;
+float:left;
+clear:both;
+}
+
+#jOverlayContent,
+#jOverlayContent #content,
+#jOverlayContent #content_inner {
+width: auto !important;
+margin-bottom:0;
+}
+#jOverlayContent #content {
+padding:11px;
+min-height:auto;
+}
+#jOverlayContent .external span {
+display:block;
+margin-bottom:11px;
+}
+#jOverlayContent button {
+position:absolute;
+top:0;
+right:0;
+width:29px;
+height:29px;
+text-align:center;
+font-weight:bold;
+padding:0;
+}
+#jOverlayContent h1 {
+max-width:425px;
+}
+#jOverlayContent #content {
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#jOverlayLoading {
+top:22.5%;
+left:40%;
+}
+#attachment_view img {
+max-width:480px;
+max-height:480px;
+}
+#attachment_view #oembed_info {
+margin-top:11px;
+}
+#attachment_view #oembed_info dt,
+#attachment_view #oembed_info dd {
+float:left;
+}
+#attachment_view #oembed_info dt {
+clear:left;
+margin-right:11px;
+font-weight:bold;
+}
+#attachment_view #oembed_info dt:after {
+content: ":";
+}
#usergroups #new_group {
float: left;
@@ -925,7 +1062,6 @@ margin-bottom:18px;
padding-left:20px;
}
-
#filter_tags {
margin-bottom:11px;
float:left;
@@ -971,8 +1107,6 @@ top:3px;
left:3px;
}
-
-
.pagination {
float:left;
clear:both;
@@ -1018,7 +1152,6 @@ padding-right:30px;
}
/* END: NOTICE */
-
.hentry .entry-content p {
margin-bottom:18px;
}
@@ -1035,12 +1168,9 @@ margin-bottom:18px;
margin-left:18px;
}
-
-
-
/* TOP_POSTERS */
.section tbody td {
-padding-right:11px;
+padding-right:18px;
padding-bottom:11px;
}
.section .vcard .photo {
@@ -1065,7 +1195,6 @@ margin-right:0;
display:none;
}
-
/* tagcloud */
.tag-cloud {
list-style-type:none;
@@ -1148,6 +1277,35 @@ clear:both;
margin-bottom:0;
}
+#form_settings_design #settings_design_background-image img {
+max-width:480px;
+max-height:480px;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+#settings_design_color .form_data li {
+width:33%;
+}
+#settings_design_color .form_data label {
+float:none;
+display:block;
+}
+#settings_design_color .form_data .swatch {
+padding:11px;
+margin-left:0;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
.instructions p,
.instructions ul {
margin-bottom:18px;
diff --git a/theme/base/css/facebookapp.css b/theme/base/css/facebookapp.css
index 163b41fb4..e6b1c9ee5 100644
--- a/theme/base/css/facebookapp.css
+++ b/theme/base/css/facebookapp.css
@@ -1,6 +1,3 @@
-@import url("display.css");
-@import url("../../identica/css/display.css");
-
* {
font-size:14px;
font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif;
diff --git a/theme/base/css/farbtastic.css b/theme/base/css/farbtastic.css
new file mode 100644
index 000000000..7efcc73c3
--- /dev/null
+++ b/theme/base/css/farbtastic.css
@@ -0,0 +1,32 @@
+.farbtastic {
+ position: relative;
+}
+.farbtastic * {
+ position: absolute;
+ cursor: crosshair;
+}
+.farbtastic, .farbtastic .wheel {
+ width: 195px;
+ height: 195px;
+}
+.farbtastic .color, .farbtastic .overlay {
+ top: 47px;
+ left: 47px;
+ width: 101px;
+ height: 101px;
+}
+.farbtastic .wheel {
+ background: url(../../../js/farbtastic/wheel.png) no-repeat;
+ width: 195px;
+ height: 195px;
+}
+.farbtastic .overlay {
+ background: url(../../../js/farbtastic/mask.png) no-repeat;
+}
+.farbtastic .marker {
+ width: 17px;
+ height: 17px;
+ margin: -8px 0 0 -8px;
+ overflow: hidden;
+ background: url(../../../js/farbtastic/marker.png) no-repeat;
+}
diff --git a/theme/base/css/ie.css b/theme/base/css/ie.css
index 5d8bea8ae..3e128b84e 100644
--- a/theme/base/css/ie.css
+++ b/theme/base/css/ie.css
@@ -1,21 +1,30 @@
/* IE specific styles */
-legend {
-margin-left:-7px;
-}
-input.checkbox {
+input.checkbox,
+input.radio {
top:0;
}
#form_notice textarea {
width:78%;
}
+#form_notice .form_note + label {
+position:absolute;
+top:25px;
+left:83%;
+text-indent:-9999px;
+height:16px;
+width:16px;
+display:block;
+}
#form_notice #notice_action-submit {
width:17%;
max-width:17%;
}
-#anon_notice {
-max-width:39%;
+#form_notice #notice_data-attach_selected {
+width:78.5%;
+}
+#form_notice #notice_data-attach_selected button {
+padding:0 4px;
}
-
.notice-options input.submit {
font-size:0;
margin-top:3px;
@@ -30,3 +39,12 @@ margin-right:4px;
.entity_profile {
width:64%;
}
+.notice {
+z-index:1;
+}
+.notice:hover {
+z-index:9999;
+}
+.notice .thumbnail img {
+z-index:9999;
+}
diff --git a/theme/base/css/ie6.css b/theme/base/css/ie6.css
index 76a82c004..dde4d6fc7 100644
--- a/theme/base/css/ie6.css
+++ b/theme/base/css/ie6.css
@@ -5,6 +5,12 @@ margin-left:7px;
address .fn {
display:none;
}
+
+#wrap {
+width:1003px;
+margin:0 auto;
+}
+
#content {
width:70%;
}
@@ -26,5 +32,6 @@ margin-bottom:123px;
width:20%;
}
.notice div.entry-content {
-width:63%;
+width:50%;
+margin-left:30px;
}
diff --git a/theme/base/default-avatar-mini.png b/theme/base/default-avatar-mini.png
new file mode 100644
index 000000000..38b8692b4
--- /dev/null
+++ b/theme/base/default-avatar-mini.png
Binary files differ
diff --git a/theme/base/default-avatar-profile.png b/theme/base/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/base/default-avatar-profile.png
Binary files differ
diff --git a/theme/base/default-avatar-stream.png b/theme/base/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/base/default-avatar-stream.png
Binary files differ
diff --git a/theme/base/images/icons/twotone/green/admin.gif b/theme/base/images/icons/twotone/green/admin.gif
new file mode 100644
index 000000000..10fa431ce
--- /dev/null
+++ b/theme/base/images/icons/twotone/green/admin.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-left.gif b/theme/base/images/icons/twotone/green/arrow-left.gif
index afed19084..afed19084 100644
--- a/theme/default/images/icons/twotone/green/arrow-left.gif
+++ b/theme/base/images/icons/twotone/green/arrow-left.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-right.gif b/theme/base/images/icons/twotone/green/arrow-right.gif
index ee1707ed9..ee1707ed9 100644
--- a/theme/default/images/icons/twotone/green/arrow-right.gif
+++ b/theme/base/images/icons/twotone/green/arrow-right.gif
Binary files differ
diff --git a/theme/base/images/icons/twotone/green/clip-01.gif b/theme/base/images/icons/twotone/green/clip-01.gif
new file mode 100644
index 000000000..f2dee7e5e
--- /dev/null
+++ b/theme/base/images/icons/twotone/green/clip-01.gif
Binary files differ
diff --git a/theme/base/images/icons/twotone/green/clip-02.gif b/theme/base/images/icons/twotone/green/clip-02.gif
new file mode 100644
index 000000000..77a729799
--- /dev/null
+++ b/theme/base/images/icons/twotone/green/clip-02.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/disfavourite.gif b/theme/base/images/icons/twotone/green/disfavourite.gif
index 3946869ae..3946869ae 100644
--- a/theme/default/images/icons/twotone/green/disfavourite.gif
+++ b/theme/base/images/icons/twotone/green/disfavourite.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/edit.gif b/theme/base/images/icons/twotone/green/edit.gif
index c746aca60..c746aca60 100644
--- a/theme/default/images/icons/twotone/green/edit.gif
+++ b/theme/base/images/icons/twotone/green/edit.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/favourite.gif b/theme/base/images/icons/twotone/green/favourite.gif
index d93515e37..d93515e37 100644
--- a/theme/default/images/icons/twotone/green/favourite.gif
+++ b/theme/base/images/icons/twotone/green/favourite.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/mail.gif b/theme/base/images/icons/twotone/green/mail.gif
index 1084c862f..1084c862f 100644
--- a/theme/default/images/icons/twotone/green/mail.gif
+++ b/theme/base/images/icons/twotone/green/mail.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/news.gif b/theme/base/images/icons/twotone/green/news.gif
index 712c685dc..712c685dc 100644
--- a/theme/default/images/icons/twotone/green/news.gif
+++ b/theme/base/images/icons/twotone/green/news.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/quote.gif b/theme/base/images/icons/twotone/green/quote.gif
index 4ba1f0c03..4ba1f0c03 100644
--- a/theme/default/images/icons/twotone/green/quote.gif
+++ b/theme/base/images/icons/twotone/green/quote.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/reply.gif b/theme/base/images/icons/twotone/green/reply.gif
index 6ff01bb35..6ff01bb35 100644
--- a/theme/default/images/icons/twotone/green/reply.gif
+++ b/theme/base/images/icons/twotone/green/reply.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/shield.gif b/theme/base/images/icons/twotone/green/shield.gif
index 419d5ee4b..419d5ee4b 100644
--- a/theme/default/images/icons/twotone/green/shield.gif
+++ b/theme/base/images/icons/twotone/green/shield.gif
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/trash.gif b/theme/base/images/icons/twotone/green/trash.gif
index 78dd64a3d..78dd64a3d 100644
--- a/theme/default/images/icons/twotone/green/trash.gif
+++ b/theme/base/images/icons/twotone/green/trash.gif
Binary files differ
diff --git a/theme/base/logo.png b/theme/base/logo.png
new file mode 100644
index 000000000..7c68b34f6
--- /dev/null
+++ b/theme/base/logo.png
Binary files differ
diff --git a/theme/biz/css/base.css b/theme/biz/css/base.css
new file mode 100644
index 000000000..696fd0645
--- /dev/null
+++ b/theme/biz/css/base.css
@@ -0,0 +1,1194 @@
+/** theme: biz 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/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 87.5%; background-color:#fff; height:100%; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 1.55%;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:113px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:124px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:18px;
+}
+
+#site_nav_global_primary {
+float:left;
+margin-right:18px;
+margin-bottom:11px;
+width:50%;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-right:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+float:right;
+clear:right;
+margin-top:7px;
+margin-right:18px;
+width:24%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:left;
+width:45.4%;
+/*
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:2px;
+border-style:solid;
+*/
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+width:14.5%;
+float:left;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+list-style-type:none;
+}
+#site_nav_local_views a {
+display:block;
+text-decoration:none;
+padding:4px 11px;
+-moz-border-radius-topleft:4px;
+-moz-border-radius-bottomleft:4px;
+-webkit-border-top-left-radius:4px;
+-webkit-border-bottom-left-radius:4px;
+border-width:1px;
+border-style:solid;
+border-right:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:51.009%;
+min-height:259px;
+padding:1.795%;
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-moz-border-radius-topleft:0;
+-webkit-border-radius:7px;
+-webkit-border-top-left-radius:0;
+border-style:solid;
+border-width:1px;
+}
+#shownotice #content {
+min-height:0;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:29.917%;
+min-height:259px;
+float:left;
+margin-left:0.385%;
+}
+
+#form_notice {
+width:45.664%;
+float:left;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+position:relative;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+width:80.789%;
+height:67px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice label[for=notice_data-attach] {
+text-indent:-9999px;
+}
+#form_notice label[for=notice_data-attach],
+#form_notice #notice_data-attach {
+position:absolute;
+top:25px;
+right:49px;
+width:16px;
+height:16px;
+cursor:pointer;
+}
+#form_notice #notice_data-attach {
+text-indent:-279px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:99px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:18px;
+clear:both;
+float:left;
+width:87.985%;
+padding:6%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:1px;
+border-style:solid;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:100%;
+border-top-width:1px;
+border-top-style:dotted;
+}
+.notices li {
+list-style-type:none;
+}
+.notices li.hover {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+float:left;
+}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:left;
+width:100%;
+overflow:hidden;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content a:visited {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+clear:left;
+float:left;
+font-size:0.95em;
+margin-left:59px;
+width:65%;
+}
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+.notice-options {
+padding-left:2%;
+float:left;
+width:50%;
+position:relative;
+font-size:0.95em;
+width:12.5%;
+float:right;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+top:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+left:0;
+}
+.notice-options .notice_reply {
+left:29px;
+}
+.notice-options .notice_delete {
+right:0;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.system_notice ul,
+.instructions ul,
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/biz/css/display.css b/theme/biz/css/display.css
new file mode 100644
index 000000000..3af4c06b9
--- /dev/null
+++ b/theme/biz/css/display.css
@@ -0,0 +1,259 @@
+/** theme: biz
+ *
+ * @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/
+ */
+
+@import url(base.css);
+
+html {
+background-color:#144A6E;
+}
+a:active {
+background-color:#F4F7E7;
+}
+body {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+font-size:1em;
+background:#144A6E url(../images/illustrations/illu_pattern-01.png) repeat-x;
+}
+
+address {
+margin-right:7.18%;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#9BB43E;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+.entity_remote_subscribe,
+#site_nav_local_views a {
+color:#fff;
+}
+
+a,
+#site_nav_local_views .current a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#002E6E;
+}
+
+#header a,
+#footer a {
+color:#87B4C8;
+}
+
+.notice,
+.profile {
+border-top-color:#CEE1E9;
+}
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#content .notice p.entry-content a:visited {
+background-color:#fcfcfc;
+}
+#content .notice p.entry-content .vcard a {
+background-color:#fcfffc;
+}
+
+.aside .section {
+background-color:#F1F5F8;
+background-position:100% 0;
+background-image:url(../images/illustrations/illu_pattern-02.png);
+background-repeat:no-repeat;
+}
+
+#notice_text-count {
+color:#333;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice label[for=notice_data-attach] {
+background:transparent url(../../base/images/icons/twotone/green/clip-01.gif) no-repeat 0 45%;
+}
+#form_notice #notice_data-attach {
+opacity:0;
+}
+
+#form_notice.processing #notice_action-submit {
+background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a,
+.aside .section {
+border-color:#fff;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:rgba(135, 180, 200, 0.3);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.7);
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+color:#fff;
+}
+
+#showstream #anon_notice {
+}
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#fff;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+.notices li.hover {
+background-color:#fcfcfc;
+}
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#CEE1E9;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/biz/css/ie.css b/theme/biz/css/ie.css
new file mode 100644
index 000000000..2f463bb44
--- /dev/null
+++ b/theme/biz/css/ie.css
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/biz/default-avatar-mini.png b/theme/biz/default-avatar-mini.png
new file mode 100644
index 000000000..38b8692b4
--- /dev/null
+++ b/theme/biz/default-avatar-mini.png
Binary files differ
diff --git a/theme/biz/default-avatar-profile.png b/theme/biz/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/biz/default-avatar-profile.png
Binary files differ
diff --git a/theme/biz/default-avatar-stream.png b/theme/biz/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/biz/default-avatar-stream.png
Binary files differ
diff --git a/theme/biz/images/illustrations/illu_pattern-01.png b/theme/biz/images/illustrations/illu_pattern-01.png
new file mode 100644
index 000000000..79bb46b60
--- /dev/null
+++ b/theme/biz/images/illustrations/illu_pattern-01.png
Binary files differ
diff --git a/theme/biz/images/illustrations/illu_pattern-02.png b/theme/biz/images/illustrations/illu_pattern-02.png
new file mode 100644
index 000000000..4438b751a
--- /dev/null
+++ b/theme/biz/images/illustrations/illu_pattern-02.png
Binary files differ
diff --git a/theme/biz/logo.png b/theme/biz/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/biz/logo.png
Binary files differ
diff --git a/theme/cloudy/css/display.css b/theme/cloudy/css/display.css
new file mode 100644
index 000000000..12f186a56
--- /dev/null
+++ b/theme/cloudy/css/display.css
@@ -0,0 +1,1552 @@
+/** theme: cloudy
+ *
+ * @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/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 100%; background-color:#fff; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:0.75em;
+line-height:normal;
+position:relative;
+height:100%;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 7px;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:143px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:155px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:0;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:11px;
+z-index:1;
+}
+
+#site_nav_global_primary {
+float:right;
+margin-right:0;
+margin-bottom:11px;
+margin-left:18px;
+padding-top:7px;
+padding-bottom:7px;
+padding-right:11px;
+-moz-border-radius:4px;
+border-radius:4px;
+-webkit-border-radius:4px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-left:11px;
+}
+#site_nav_global_primary a {
+text-decoration:none;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+position:absolute;
+top:65px;
+right:18px;
+width:250px;
+width:24%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+clear:both;
+width:99.8%;
+padding-top:36px;
+line-height:1.5;
+font-size:1.3em;
+font-weight:bold;
+}
+#anon_notice p {
+border-style:solid;
+border-width:1px;
+width:96%;
+padding:2%;
+}
+
+#footer {
+float:left;
+margin-bottom:1em;
+padding:7px;
+-moz-border-radius:4px;
+border-radius:4px;
+-webkit-border-radius:4px;
+}
+#footer a {
+text-decoration:none;
+}
+
+#site_nav_local_views {
+width:203px;
+float:right;
+margin-right:0;
+-moz-border-radius-topright:4px;
+border-radius-topright:4px;
+-webkit-border-top-right-radius:4px;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+list-style-type:none;
+padding:0;
+border-width:1px;
+border-style:solid;
+border-top:0;
+border-right:0;
+}
+#site_nav_local_views a {
+text-decoration:none;
+padding:13px;
+border-width:1px;
+border-style:solid;
+border-bottom:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+font-size:1em;
+display:block;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin:0 18px 7px 0;
+float:left;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width: 763px;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+padding-top:10px;
+}
+
+#content {
+width:518px;
+min-height:322px;
+padding:20px;
+float:left;
+border-radius-topleft:4px;
+-moz-border-radius-topleft:4px;
+-webkit-border-top-left-radius:4px;
+border-style:solid;
+border-width:1px;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:182px;
+min-height:259px;
+float:left;
+margin-left:0;
+padding:10px;
+border-width:1px;
+border-style:solid;
+border-right:0;
+border-top:0;
+}
+
+#form_notice {
+width:505px;
+line-height:1;
+position:absolute;
+top:200px;
+left:20px;
+z-index:9;
+}
+#form_notice fieldset {
+border:0;
+padding:0 0 50px 0;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+width:505px;
+height:45px;
+line-height:1.5;
+padding:5px;
+border-width:1px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:-10px;
+right:-10px;
+z-index:9;
+font-family:Georgia, serif;
+font-size:1.7em;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:35px;
+padding-top:0;
+padding-bottom:0;
+position:absolute;
+bottom:10px;
+right:-10px;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:18px;
+clear:both;
+float:left;
+width:100%;
+}
+.aside .section h2 {
+font-size:110%;
+text-transform:none;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:100%;
+border-top-width:1px;
+border-top-style:dotted;
+font-size:1.2em;
+}
+.notices li {
+list-style-type:none;
+line-height:1.1;
+width:94%;
+padding-right:5%;
+padding-left:1%;
+min-height:47px;
+}
+
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.notice .author .photo {
+margin-bottom:0;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+margin-bottom:11px;
+float:left;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:none;
+display:inline;
+width:100%;
+overflow:hidden;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+font-size:0.95em;
+margin-left:59px;
+margin-top:3px;
+width:70%;
+font-family:Georgia, serif;
+font-style:italic;
+font-size:0.8em;
+display:block;
+}
+.notice div.entry-content a {
+text-decoration:none;
+}
+.notice div.entry-content a:hover {
+text-decoration:underline;
+}
+#showstream .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+
+.notice-data {
+position:absolute;
+top:18px;
+right:0;
+min-height:50px;
+margin-bottom:4px;
+}
+.notice .entry-content .notice-data dt {
+display:none;
+}
+
+.notice-data a {
+display:block;
+outline:none;
+}
+
+.notice-options {
+padding-left:2%;
+float:left;
+width:50%;
+font-size:0.95em;
+width:12.5%;
+float:right;
+display:none;
+}
+.notices li.hover div.notice-options {
+display:block;
+}
+
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+top:7px;
+right:7px;
+}
+.notice-options .notice_reply {
+top:30px;
+right:7px;
+}
+.notice-options .notice_delete {
+bottom:7px;
+right:7px;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
+
+#public.user_in #content,
+#groups.user_in #content,
+#publictagcloud.user_in #content,
+#featured.user_in #content,
+#favorited.user_in #content,
+#all.user_in #content,
+#replies.user_in #content,
+#showstream.user_in #content,
+#showfavorites.user_in #content,
+#inbox.user_in #content,
+#outbox.user_in #content,
+#subscriptions.user_in #content,
+#subscribers.user_in #content,
+#showgroup.user_in #content {
+padding-top:160px;
+}
+
+#profilesettings #form_notice,
+#avatarsettings #form_notice,
+#passwordsettings #form_notice,
+#emailsettings #form_notice,
+#openidsettings #form_notice,
+#othersettings #form_notice,
+#smssettings #form_notice,
+#twittersettings #form_notice,
+#imsettings #form_notice,
+#doc #form_notice,
+#usergroups #form_notice,
+#invite #form_notice,
+#deletenotice #form_notice,
+#newgroup #form_notice,
+#register #form_notice,
+#shownotice #form_notice,
+#confirmaddress #form_notice,
+#tag #form_notice {
+display:none;
+}
+
+
+html,
+body,
+a:active {
+background-color:#9AE4E8;
+}
+body {
+font-family:'Lucida Grande',sans-serif;
+background:#9AE4E8 url(../images/illustrations/illu_clouds-01.gif) 0 0 no-repeat;
+color:#333333;
+}
+#core {
+background:url(../images/illustrations/illu_arrow-up-01.gif) no-repeat 25px 0;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+#nav_register a,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#9BB43E;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+#nav_register a,
+.entity_remote_subscribe {
+color:#fff;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#0084B4;
+}
+
+.notice,
+.profile {
+border-top-color:#DDFFCC;
+}
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+
+#content .notice p.entry-content a:visited {
+background-color:#fcfcfc;
+}
+#content .notice p.entry-content .vcard a {
+background-color:#fcfffc;
+}
+
+#aside_primary {
+background-color:#DDFFCC;
+}
+
+
+#notice_text-count {
+color:#333;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a,
+#aside_primary {
+border-color:#fff;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:rgba(135, 180, 200, 0.3);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.7);
+}
+
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+
+#anon_notice {
+background-color:#FEFFDF;
+color:#333;
+border-color:#fff;
+}
+
+#showstream #anon_notice {
+background-color:#FEFFDF;
+}
+
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#fff;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../images/icons/twotone/green/shield.gif);
+}
+
+
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../images/icons/icon_reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../images/icons/icon_favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../images/icons/icon_disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../images/icons/icon_trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+.notices li.hover {
+background-color:#fcfcfc;
+}
+/*END: NOTICES */
+
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#DDFFCC;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
+
+
+/*--------------------------------------*/
+
+#anon_notice {
+background:url(../images/illustrations/illu_unicorn-01.png) no-repeat 0 0;
+}
+#showstream #anon_notice,
+#content .notice p.entry-content a:visited,
+content .notice p.entry-content .vcard a {
+background-color:transparent;
+}
+
+#anon_notice p {
+background-color:#FEFFDF;
+border-color:#FFFF00;
+}
+
+
+#form_notice .form_note {
+color:#CCC;
+}
+input.submit {
+background-color:#eee;
+color:#666;
+}
+
+.notices li.hover {
+background-color:#F7F7F7;
+}
+
+
+.notice div.entry-content,
+.notice div.entry-content a {
+color:#999;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:1;
+}
+
+#site_nav_local_views {
+background-color:#DDFFCC;
+}
+#site_nav_local_views li,
+#aside_primary {
+border-color:#BDDCAD;
+}
+#site_nav_local_views a,
+.aside .section h2 {
+background-color:transparent;
+border-color:transparent;
+color:#4C4C4C;
+}
+#site_nav_local_views .current {
+border-left-color:#fff;
+}
+
+#site_nav_local_views .current a,
+#site_nav_global_primary,
+#footer {
+background-color:#fff;
+}
+
diff --git a/theme/cloudy/css/ie.css b/theme/cloudy/css/ie.css
new file mode 100644
index 000000000..095122100
--- /dev/null
+++ b/theme/cloudy/css/ie.css
@@ -0,0 +1,34 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#ddffcc;
+}
+
+#aside_primary {
+width:181px;
+}
+
+#form_notice,
+#anon_notice {
+top:158px;
+}
+
+#public #content,
+#groups #content,
+#publictagcloud #content,
+#featured #content,
+#favorited #content,
+#all #content,
+#replies #content,
+#showstream #content,
+#showfavorites #content,
+#inbox #content,
+#outbox #content,
+#subscriptions #content,
+#subscribers #content {
+padding-top:138px;
+}
diff --git a/theme/cloudy/default-avatar-mini.png b/theme/cloudy/default-avatar-mini.png
new file mode 100644
index 000000000..4fd8bd9e1
--- /dev/null
+++ b/theme/cloudy/default-avatar-mini.png
Binary files differ
diff --git a/theme/cloudy/default-avatar-profile.png b/theme/cloudy/default-avatar-profile.png
new file mode 100644
index 000000000..eb08571d9
--- /dev/null
+++ b/theme/cloudy/default-avatar-profile.png
Binary files differ
diff --git a/theme/cloudy/default-avatar-stream.png b/theme/cloudy/default-avatar-stream.png
new file mode 100644
index 000000000..926b8a9ca
--- /dev/null
+++ b/theme/cloudy/default-avatar-stream.png
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_atom.png b/theme/cloudy/images/icons/icon_atom.png
new file mode 100644
index 000000000..6a001f11a
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_atom.png
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_disfavourite.gif b/theme/cloudy/images/icons/icon_disfavourite.gif
new file mode 100644
index 000000000..2b02ac8a6
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_disfavourite.gif
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_favourite.gif b/theme/cloudy/images/icons/icon_favourite.gif
new file mode 100644
index 000000000..716ce3549
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_favourite.gif
Binary files differ
diff --git a/theme/default/images/icons/icon_foaf.gif b/theme/cloudy/images/icons/icon_foaf.gif
index f8f784423..f8f784423 100644
--- a/theme/default/images/icons/icon_foaf.gif
+++ b/theme/cloudy/images/icons/icon_foaf.gif
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_processing.gif b/theme/cloudy/images/icons/icon_processing.gif
new file mode 100644
index 000000000..d0bce1542
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_processing.gif
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_reply.gif b/theme/cloudy/images/icons/icon_reply.gif
new file mode 100644
index 000000000..a4379a70b
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_reply.gif
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_rss.png b/theme/cloudy/images/icons/icon_rss.png
new file mode 100644
index 000000000..0ccd1ce25
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_rss.png
Binary files differ
diff --git a/theme/cloudy/images/icons/icon_trash.gif b/theme/cloudy/images/icons/icon_trash.gif
new file mode 100644
index 000000000..916a332a3
--- /dev/null
+++ b/theme/cloudy/images/icons/icon_trash.gif
Binary files differ
diff --git a/theme/default/images/icons/icon_vcard.gif b/theme/cloudy/images/icons/icon_vcard.gif
index 6d52947f3..6d52947f3 100644
--- a/theme/default/images/icons/icon_vcard.gif
+++ b/theme/cloudy/images/icons/icon_vcard.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-left.gif b/theme/cloudy/images/icons/twotone/green/arrow-left.gif
index afed19084..afed19084 100644
--- a/theme/identica/images/icons/twotone/green/arrow-left.gif
+++ b/theme/cloudy/images/icons/twotone/green/arrow-left.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-right.gif b/theme/cloudy/images/icons/twotone/green/arrow-right.gif
index ee1707ed9..ee1707ed9 100644
--- a/theme/identica/images/icons/twotone/green/arrow-right.gif
+++ b/theme/cloudy/images/icons/twotone/green/arrow-right.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/edit.gif b/theme/cloudy/images/icons/twotone/green/edit.gif
index c746aca60..c746aca60 100644
--- a/theme/identica/images/icons/twotone/green/edit.gif
+++ b/theme/cloudy/images/icons/twotone/green/edit.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/mail.gif b/theme/cloudy/images/icons/twotone/green/mail.gif
index 1084c862f..1084c862f 100644
--- a/theme/identica/images/icons/twotone/green/mail.gif
+++ b/theme/cloudy/images/icons/twotone/green/mail.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/news.gif b/theme/cloudy/images/icons/twotone/green/news.gif
index 712c685dc..712c685dc 100644
--- a/theme/identica/images/icons/twotone/green/news.gif
+++ b/theme/cloudy/images/icons/twotone/green/news.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/quote.gif b/theme/cloudy/images/icons/twotone/green/quote.gif
index 4ba1f0c03..4ba1f0c03 100644
--- a/theme/identica/images/icons/twotone/green/quote.gif
+++ b/theme/cloudy/images/icons/twotone/green/quote.gif
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/shield.gif b/theme/cloudy/images/icons/twotone/green/shield.gif
index 419d5ee4b..419d5ee4b 100644
--- a/theme/identica/images/icons/twotone/green/shield.gif
+++ b/theme/cloudy/images/icons/twotone/green/shield.gif
Binary files differ
diff --git a/theme/cloudy/images/illustrations/illu_arrow-up-01.gif b/theme/cloudy/images/illustrations/illu_arrow-up-01.gif
new file mode 100644
index 000000000..577be1871
--- /dev/null
+++ b/theme/cloudy/images/illustrations/illu_arrow-up-01.gif
Binary files differ
diff --git a/theme/cloudy/images/illustrations/illu_clouds-01.gif b/theme/cloudy/images/illustrations/illu_clouds-01.gif
new file mode 100644
index 000000000..41cd622cf
--- /dev/null
+++ b/theme/cloudy/images/illustrations/illu_clouds-01.gif
Binary files differ
diff --git a/theme/cloudy/images/illustrations/illu_jcrop.gif b/theme/cloudy/images/illustrations/illu_jcrop.gif
new file mode 100644
index 000000000..72ea7ccb5
--- /dev/null
+++ b/theme/cloudy/images/illustrations/illu_jcrop.gif
Binary files differ
diff --git a/theme/cloudy/images/illustrations/illu_progress_loading-01.gif b/theme/cloudy/images/illustrations/illu_progress_loading-01.gif
new file mode 100644
index 000000000..82290f483
--- /dev/null
+++ b/theme/cloudy/images/illustrations/illu_progress_loading-01.gif
Binary files differ
diff --git a/theme/cloudy/images/illustrations/illu_unicorn-01.png b/theme/cloudy/images/illustrations/illu_unicorn-01.png
new file mode 100644
index 000000000..6cb51b298
--- /dev/null
+++ b/theme/cloudy/images/illustrations/illu_unicorn-01.png
Binary files differ
diff --git a/theme/cloudy/logo.png b/theme/cloudy/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/cloudy/logo.png
Binary files differ
diff --git a/theme/default/css/display.css b/theme/default/css/display.css
index 69a600cc2..251d6706b 100644
--- a/theme/default/css/display.css
+++ b/theme/default/css/display.css
@@ -9,17 +9,16 @@
@import url(../../base/css/display.css);
-html,
body,
a:active {
-background-color:#97BFD1;
+background-color:#CEE1E9;
}
body {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-size:1em;
}
address {
-margin-right:7.18%;
+margin-right:7.2%;
}
input, textarea, select, option {
@@ -27,13 +26,13 @@ font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
input, textarea, select,
.entity_remote_subscribe {
-border-color:#aaa;
+border-color:#AAAAAA;
}
#filter_tags ul li {
-border-color:#97BFD1;
+border-color:#DDDDDD;
}
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
background:none;
}
@@ -41,76 +40,95 @@ input.submit,
#form_notice.warning #notice_text-count,
.form_settings .form_note,
.entity_remote_subscribe {
-background-color:#A9BF4F;
+background-color:#9BB43E;
}
input:focus, textarea:focus, select:focus,
#form_notice.warning #notice_data-text {
-border-color:#A9BF4F;
+border-color:#9BB43E;
+box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+-moz-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+-webkit-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
}
input.submit,
.entity_remote_subscribe {
-color:#fff;
+color:#FFFFFF;
}
a,
div.notice-options input,
.form_user_block input.submit,
.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit,
.entity_send-a-message a,
.form_user_nudge input.submit,
.entity_nudge p,
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary,
+.form_make_admin input.submit {
color:#002E6E;
}
.notice,
.profile {
-border-top-color:#D1D9E4;
+border-top-color:#C8D1D5;
}
.section .profile {
-border-top-color:#97BFD1;
-}
-
-#content .notice p.entry-content a:visited {
-background-color:#fcfcfc;
-}
-#content .notice p.entry-content .vcard a {
-background-color:#fcfffc;
+border-top-color:#87B4C8;
}
#aside_primary {
-background-color:#CEE1E9;
+background-color:#C8D1D5;
}
#notice_text-count {
-color:#333;
+color:#333333;
}
#form_notice.warning #notice_text-count {
-color:#000;
+color:#000000;
+}
+#form_notice label[for=notice_data-attach] {
+background:transparent url(../../base/images/icons/twotone/green/clip-01.gif) no-repeat 0 45%;
+}
+#form_notice #notice_data-attach {
+opacity:0;
}
+
#form_notice.processing #notice_action-submit {
-background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+background:#FFFFFF url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
cursor:wait;
text-indent:-9999px;
}
+#content {
+box-shadow:5px 7px 7px rgba(194, 194, 194, 0.3);
+-moz-box-shadow:5px 7px 7px rgba(194, 194, 194, 0.3);
+-webkit-box-shadow:5px 7px 7px rgba(194, 194, 194, 0.3);
+}
#content,
#site_nav_local_views a,
#aside_primary {
-border-color:#fff;
+border-color:transparent;
}
#content,
#site_nav_local_views .current a {
-background-color:#fff;
+background-color:#FFFFFF;
}
+#site_nav_local_views li {
+box-shadow:3px 7px 5px rgba(194, 194, 194, 0.5);
+-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.5);
+-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.5);
+}
#site_nav_local_views a {
-background-color:rgba(255, 255, 255, 0.2);
+background-color:rgba(194, 194, 194, 0.5);
}
#site_nav_local_views a:hover {
background-color:rgba(255, 255, 255, 0.7);
}
+#site_nav_local_views .current a {
+text-shadow: rgba(194,194,194,0.5) 1px 1px 1px;
+}
.error {
background-color:#F7E8E8;
@@ -120,13 +138,13 @@ background-color:#EFF3DC;
}
#anon_notice {
-background-color:#97BFD1;
-color:#fff;
-border-color:#fff;
+background-color:#87B4C8;
+color:#FFFFFF;
+border-color:#FFFFFF;
}
#showstream #anon_notice {
-background-color:#A9BF4F;
+background-color:#9BB43E;
}
#export_data li a {
@@ -148,7 +166,10 @@ background-image:url(../../base/images/icons/icon_foaf.gif);
.form_user_nudge input.submit,
.form_user_block input.submit,
.form_user_unblock input.submit,
-.entity_nudge p {
+.form_group_block input.submit,
+.form_group_unblock input.submit,
+.entity_nudge p,
+.form_make_admin input.submit {
background-position: 0 40%;
background-repeat: no-repeat;
background-color:transparent;
@@ -157,86 +178,109 @@ background-color:transparent;
.form_group_leave input.submit
.form_user_subscribe input.submit,
.form_user_unsubscribe input.submit {
-background-color:#A9BF4F;
-color:#fff;
+background-color:#9BB43E;
+color:#FFFFFF;
}
.form_user_unsubscribe input.submit,
.form_group_leave input.submit,
.form_user_authorization input.reject {
-background-color:#97BFD1;
+background-color:#87B4C8;
}
.entity_edit a {
-background-image:url(../images/icons/twotone/green/edit.gif);
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
}
.entity_send-a-message a {
-background-image:url(../images/icons/twotone/green/quote.gif);
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
}
.entity_nudge p,
.form_user_nudge input.submit {
-background-image:url(../images/icons/twotone/green/mail.gif);
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
}
.form_user_block input.submit,
-.form_user_unblock input.submit {
-background-image:url(../images/icons/twotone/green/shield.gif);
+.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+.form_make_admin input.submit {
+background-image:url(../../base/images/icons/twotone/green/admin.gif);
}
/* NOTICES */
-.notices li.over {
-background-color:#fcfcfc;
+.notice .attachment {
+background:transparent url(../../base/images/icons/twotone/green/clip-02.gif) no-repeat 0 45%;
+}
+#attachments .attachment {
+background:none;
}
-
.notice-options .notice_reply a,
.notice-options form input.submit {
background-color:transparent;
}
.notice-options .notice_reply a {
-background:transparent url(../images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
}
.notice-options form.form_favor input.submit {
-background:transparent url(../images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
}
.notice-options form.form_disfavor input.submit {
-background:transparent url(../images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
}
.notice-options .notice_delete a {
-background:transparent url(../images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
}
.notices div.entry-content,
.notices div.notice-options {
opacity:0.4;
}
-.notices li.hover div.entry-content,
-.notices li.hover div.notice-options {
+.notices li:hover div.entry-content,
+.notices li:hover div.notice-options {
opacity:1;
}
div.entry-content {
-color:#333;
+color:#333333;
}
div.notice-options a,
div.notice-options input {
font-family:sans-serif;
}
-.notices li.hover {
-background-color:#fcfcfc;
+#content .notices li:hover {
+background-color:rgba(240, 240, 240, 0.2);
+}
+#conversation .notices li:hover {
+background-color:transparent;
+}
+
+.notices .notices {
+background-color:rgba(200, 200, 200, 0.050);
+}
+.notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.100);
+}
+.notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.150);
+}
+.notices .notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.300);
}
/*END: NOTICES */
#new_group a {
-background:transparent url(../images/icons/twotone/green/news.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
}
.pagination .nav_prev a,
.pagination .nav_next a {
background-repeat:no-repeat;
-border-color:#D1D9E4;
+border-color:#C8D1D5;
}
.pagination .nav_prev a {
-background-image:url(../images/icons/twotone/green/arrow-left.gif);
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
background-position:10% 45%;
}
.pagination .nav_next a {
-background-image:url(../images/icons/twotone/green/arrow-right.gif);
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
background-position:90% 45%;
}
diff --git a/theme/default/css/ie.css b/theme/default/css/ie.css
index 2b06768ea..cbbd49ce6 100644
--- a/theme/default/css/ie.css
+++ b/theme/default/css/ie.css
@@ -1,9 +1,14 @@
/* IE specific styles */
.notice-options input.submit {
-color:#fff;
+color:#FFFFFF;
}
-
#site_nav_local_views a {
-background-color:#ACCCDA;
+background-color:#C8D1D5;
+}
+#form_notice .form_note + label {
+background:transparent url(../../base/images/icons/twotone/green/clip-01.gif) no-repeat 0 45%;
+}
+#form_notice #notice_data-attach {
+filter: alpha(opacity=0);
}
diff --git a/theme/default/images/icons/icon_atom.jpg b/theme/default/images/icons/icon_atom.jpg
deleted file mode 100644
index 22853edc4..000000000
--- a/theme/default/images/icons/icon_atom.jpg
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/icon_rss.jpg b/theme/default/images/icons/icon_rss.jpg
deleted file mode 100644
index da23422d0..000000000
--- a/theme/default/images/icons/icon_rss.jpg
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/against.gif b/theme/default/images/icons/twotone/green/against.gif
deleted file mode 100644
index ca796c8a3..000000000
--- a/theme/default/images/icons/twotone/green/against.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-down.gif b/theme/default/images/icons/twotone/green/arrow-down.gif
deleted file mode 100644
index c709e5877..000000000
--- a/theme/default/images/icons/twotone/green/arrow-down.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-downleft.gif b/theme/default/images/icons/twotone/green/arrow-downleft.gif
deleted file mode 100644
index a4a98035d..000000000
--- a/theme/default/images/icons/twotone/green/arrow-downleft.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-downright.gif b/theme/default/images/icons/twotone/green/arrow-downright.gif
deleted file mode 100644
index 3e6001a61..000000000
--- a/theme/default/images/icons/twotone/green/arrow-downright.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-up.gif b/theme/default/images/icons/twotone/green/arrow-up.gif
deleted file mode 100644
index d0f5fbeaa..000000000
--- a/theme/default/images/icons/twotone/green/arrow-up.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-upleft.gif b/theme/default/images/icons/twotone/green/arrow-upleft.gif
deleted file mode 100644
index 1e9e6935b..000000000
--- a/theme/default/images/icons/twotone/green/arrow-upleft.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/arrow-upright.gif b/theme/default/images/icons/twotone/green/arrow-upright.gif
deleted file mode 100644
index c7fecc8a0..000000000
--- a/theme/default/images/icons/twotone/green/arrow-upright.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/back-forth.gif b/theme/default/images/icons/twotone/green/back-forth.gif
deleted file mode 100644
index 33a9540c8..000000000
--- a/theme/default/images/icons/twotone/green/back-forth.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/bookmark.gif b/theme/default/images/icons/twotone/green/bookmark.gif
deleted file mode 100644
index 23f318ecc..000000000
--- a/theme/default/images/icons/twotone/green/bookmark.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/bulb.gif b/theme/default/images/icons/twotone/green/bulb.gif
deleted file mode 100644
index f70652c03..000000000
--- a/theme/default/images/icons/twotone/green/bulb.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/calendar.gif b/theme/default/images/icons/twotone/green/calendar.gif
deleted file mode 100644
index a09b65aca..000000000
--- a/theme/default/images/icons/twotone/green/calendar.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/calendar2.gif b/theme/default/images/icons/twotone/green/calendar2.gif
deleted file mode 100644
index 7884b02dd..000000000
--- a/theme/default/images/icons/twotone/green/calendar2.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/camera.gif b/theme/default/images/icons/twotone/green/camera.gif
deleted file mode 100644
index 1a85fbad0..000000000
--- a/theme/default/images/icons/twotone/green/camera.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/cart.gif b/theme/default/images/icons/twotone/green/cart.gif
deleted file mode 100644
index 47eaa0a2e..000000000
--- a/theme/default/images/icons/twotone/green/cart.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/caution.gif b/theme/default/images/icons/twotone/green/caution.gif
deleted file mode 100644
index 3ad2c322b..000000000
--- a/theme/default/images/icons/twotone/green/caution.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/chart.gif b/theme/default/images/icons/twotone/green/chart.gif
deleted file mode 100644
index 136d74517..000000000
--- a/theme/default/images/icons/twotone/green/chart.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/checkmark.gif b/theme/default/images/icons/twotone/green/checkmark.gif
deleted file mode 100644
index 892429d48..000000000
--- a/theme/default/images/icons/twotone/green/checkmark.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/clipboard.gif b/theme/default/images/icons/twotone/green/clipboard.gif
deleted file mode 100644
index 9317bdcd0..000000000
--- a/theme/default/images/icons/twotone/green/clipboard.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/clock.gif b/theme/default/images/icons/twotone/green/clock.gif
deleted file mode 100644
index d1410f925..000000000
--- a/theme/default/images/icons/twotone/green/clock.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/closed-folder.gif b/theme/default/images/icons/twotone/green/closed-folder.gif
deleted file mode 100644
index 0410fc6e8..000000000
--- a/theme/default/images/icons/twotone/green/closed-folder.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/database.gif b/theme/default/images/icons/twotone/green/database.gif
deleted file mode 100644
index 29ce02492..000000000
--- a/theme/default/images/icons/twotone/green/database.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/diskette.gif b/theme/default/images/icons/twotone/green/diskette.gif
deleted file mode 100644
index e970b0a30..000000000
--- a/theme/default/images/icons/twotone/green/diskette.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/document.gif b/theme/default/images/icons/twotone/green/document.gif
deleted file mode 100644
index 9c08f4a3a..000000000
--- a/theme/default/images/icons/twotone/green/document.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/double-arrow.gif b/theme/default/images/icons/twotone/green/double-arrow.gif
deleted file mode 100644
index 2e8648264..000000000
--- a/theme/default/images/icons/twotone/green/double-arrow.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/eject.gif b/theme/default/images/icons/twotone/green/eject.gif
deleted file mode 100644
index 7e0906cfe..000000000
--- a/theme/default/images/icons/twotone/green/eject.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/exclaim.gif b/theme/default/images/icons/twotone/green/exclaim.gif
deleted file mode 100644
index 588e28c26..000000000
--- a/theme/default/images/icons/twotone/green/exclaim.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/fastforward.gif b/theme/default/images/icons/twotone/green/fastforward.gif
deleted file mode 100644
index 28e495103..000000000
--- a/theme/default/images/icons/twotone/green/fastforward.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/flag.gif b/theme/default/images/icons/twotone/green/flag.gif
deleted file mode 100644
index 68c8aee25..000000000
--- a/theme/default/images/icons/twotone/green/flag.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/graph.gif b/theme/default/images/icons/twotone/green/graph.gif
deleted file mode 100644
index 0c1794b4e..000000000
--- a/theme/default/images/icons/twotone/green/graph.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/grow.gif b/theme/default/images/icons/twotone/green/grow.gif
deleted file mode 100644
index c4118d53b..000000000
--- a/theme/default/images/icons/twotone/green/grow.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/headphones.gif b/theme/default/images/icons/twotone/green/headphones.gif
deleted file mode 100644
index 5be6c67dd..000000000
--- a/theme/default/images/icons/twotone/green/headphones.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/home.gif b/theme/default/images/icons/twotone/green/home.gif
deleted file mode 100644
index d2a3421ef..000000000
--- a/theme/default/images/icons/twotone/green/home.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/hourglass.gif b/theme/default/images/icons/twotone/green/hourglass.gif
deleted file mode 100644
index b62b9480c..000000000
--- a/theme/default/images/icons/twotone/green/hourglass.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/info.gif b/theme/default/images/icons/twotone/green/info.gif
deleted file mode 100644
index 86ef1f8b4..000000000
--- a/theme/default/images/icons/twotone/green/info.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/key.gif b/theme/default/images/icons/twotone/green/key.gif
deleted file mode 100644
index ccf357ab2..000000000
--- a/theme/default/images/icons/twotone/green/key.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/lock.gif b/theme/default/images/icons/twotone/green/lock.gif
deleted file mode 100644
index db00706b5..000000000
--- a/theme/default/images/icons/twotone/green/lock.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/move.gif b/theme/default/images/icons/twotone/green/move.gif
deleted file mode 100644
index d2c30b1d2..000000000
--- a/theme/default/images/icons/twotone/green/move.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/music.gif b/theme/default/images/icons/twotone/green/music.gif
deleted file mode 100644
index 64b51d4e1..000000000
--- a/theme/default/images/icons/twotone/green/music.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/note.gif b/theme/default/images/icons/twotone/green/note.gif
deleted file mode 100644
index bcc0b149b..000000000
--- a/theme/default/images/icons/twotone/green/note.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/open-folder.gif b/theme/default/images/icons/twotone/green/open-folder.gif
deleted file mode 100644
index d41300a08..000000000
--- a/theme/default/images/icons/twotone/green/open-folder.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/paper-clip.gif b/theme/default/images/icons/twotone/green/paper-clip.gif
deleted file mode 100644
index 1d45f1d1e..000000000
--- a/theme/default/images/icons/twotone/green/paper-clip.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/paper-clip2.gif b/theme/default/images/icons/twotone/green/paper-clip2.gif
deleted file mode 100644
index a8c7805be..000000000
--- a/theme/default/images/icons/twotone/green/paper-clip2.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/pause.gif b/theme/default/images/icons/twotone/green/pause.gif
deleted file mode 100644
index ced0b6440..000000000
--- a/theme/default/images/icons/twotone/green/pause.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/phone.gif b/theme/default/images/icons/twotone/green/phone.gif
deleted file mode 100644
index 69359f764..000000000
--- a/theme/default/images/icons/twotone/green/phone.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/play.gif b/theme/default/images/icons/twotone/green/play.gif
deleted file mode 100644
index 794ec85b6..000000000
--- a/theme/default/images/icons/twotone/green/play.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/plus.gif b/theme/default/images/icons/twotone/green/plus.gif
deleted file mode 100644
index 4407d0b2d..000000000
--- a/theme/default/images/icons/twotone/green/plus.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/print.gif b/theme/default/images/icons/twotone/green/print.gif
deleted file mode 100644
index 17727d5d7..000000000
--- a/theme/default/images/icons/twotone/green/print.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/question-mark.gif b/theme/default/images/icons/twotone/green/question-mark.gif
deleted file mode 100644
index 1689efcd0..000000000
--- a/theme/default/images/icons/twotone/green/question-mark.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/refresh.gif b/theme/default/images/icons/twotone/green/refresh.gif
deleted file mode 100644
index 8a8b8144f..000000000
--- a/theme/default/images/icons/twotone/green/refresh.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/rewind.gif b/theme/default/images/icons/twotone/green/rewind.gif
deleted file mode 100644
index aca3ee35b..000000000
--- a/theme/default/images/icons/twotone/green/rewind.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/search.gif b/theme/default/images/icons/twotone/green/search.gif
deleted file mode 100644
index c36463d0d..000000000
--- a/theme/default/images/icons/twotone/green/search.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/skip-back.gif b/theme/default/images/icons/twotone/green/skip-back.gif
deleted file mode 100644
index adca7aa3e..000000000
--- a/theme/default/images/icons/twotone/green/skip-back.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/skip.gif b/theme/default/images/icons/twotone/green/skip.gif
deleted file mode 100644
index ae5417f2f..000000000
--- a/theme/default/images/icons/twotone/green/skip.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/skull.gif b/theme/default/images/icons/twotone/green/skull.gif
deleted file mode 100644
index 033506732..000000000
--- a/theme/default/images/icons/twotone/green/skull.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/statusbar.gif b/theme/default/images/icons/twotone/green/statusbar.gif
deleted file mode 100644
index 47d61b106..000000000
--- a/theme/default/images/icons/twotone/green/statusbar.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/stop.gif b/theme/default/images/icons/twotone/green/stop.gif
deleted file mode 100644
index e0b108d35..000000000
--- a/theme/default/images/icons/twotone/green/stop.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/template.gif b/theme/default/images/icons/twotone/green/template.gif
deleted file mode 100644
index 65c0c4a0a..000000000
--- a/theme/default/images/icons/twotone/green/template.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/text-bigger.gif b/theme/default/images/icons/twotone/green/text-bigger.gif
deleted file mode 100644
index 45e143b7a..000000000
--- a/theme/default/images/icons/twotone/green/text-bigger.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/text-smaller.gif b/theme/default/images/icons/twotone/green/text-smaller.gif
deleted file mode 100644
index a54d0c1d3..000000000
--- a/theme/default/images/icons/twotone/green/text-smaller.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/two-docs.gif b/theme/default/images/icons/twotone/green/two-docs.gif
deleted file mode 100644
index 97e54b964..000000000
--- a/theme/default/images/icons/twotone/green/two-docs.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/twotone.gif b/theme/default/images/icons/twotone/green/twotone.gif
deleted file mode 100644
index 45aad25c4..000000000
--- a/theme/default/images/icons/twotone/green/twotone.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/undo.gif b/theme/default/images/icons/twotone/green/undo.gif
deleted file mode 100644
index 6869b3050..000000000
--- a/theme/default/images/icons/twotone/green/undo.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/user.gif b/theme/default/images/icons/twotone/green/user.gif
deleted file mode 100644
index c85460fcd..000000000
--- a/theme/default/images/icons/twotone/green/user.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/vegetable.gif b/theme/default/images/icons/twotone/green/vegetable.gif
deleted file mode 100644
index 4d421c1bb..000000000
--- a/theme/default/images/icons/twotone/green/vegetable.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/x.gif b/theme/default/images/icons/twotone/green/x.gif
deleted file mode 100644
index ffb2efea0..000000000
--- a/theme/default/images/icons/twotone/green/x.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/zoom-in.gif b/theme/default/images/icons/twotone/green/zoom-in.gif
deleted file mode 100644
index a59a5bb50..000000000
--- a/theme/default/images/icons/twotone/green/zoom-in.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/images/icons/twotone/green/zoom-out.gif b/theme/default/images/icons/twotone/green/zoom-out.gif
deleted file mode 100644
index c61f999fd..000000000
--- a/theme/default/images/icons/twotone/green/zoom-out.gif
+++ /dev/null
Binary files differ
diff --git a/theme/default/logo.png b/theme/default/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/default/logo.png
Binary files differ
diff --git a/theme/h4ck3r/css/base.css b/theme/h4ck3r/css/base.css
new file mode 100644
index 000000000..41b3a77e6
--- /dev/null
+++ b/theme/h4ck3r/css/base.css
@@ -0,0 +1,1139 @@
+/** theme: h4ck3r 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/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 100%; background-color:#fff; height:100%; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 1.55%;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:152px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:163px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:29px;
+}
+
+#site_nav_global_primary {
+float:right;
+margin-right:18px;
+margin-bottom:11px;
+margin-left:18px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-left:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+float:left;
+clear:right;
+margin-top:7px;
+margin-right:18px;
+width:31%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:right;
+clear:right;
+width:41.2%;
+padding:1.1%;
+border-width:2px;
+border-style:dashed;
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+-moz-transform:skewX(-30deg) scale(0.85);
+-webkit-transform:skewX(-30deg) scale(0.85);
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+width:100%;
+float:right;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+float:right;
+margin-left:11px;
+list-style-type:none;
+}
+#site_nav_local_views a {
+float:left;
+text-decoration:none;
+padding:4px 11px;
+border-width:1px;
+border-style:dashed;
+border-bottom:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:60.009%;
+min-height:259px;
+padding:1.795%;
+float:right;
+border-style:dashed;
+border-width:1px;
+}
+#shownotice #content {
+min-height:0;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:27.917%;
+min-height:259px;
+float:right;
+margin-right:4.385%;
+padding:1.795%;
+border-width:1px;
+border-style:dashed;
+}
+
+#form_notice {
+width:43.664%;
+float:right;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+position:relative;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+width:80.789%;
+height:67px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:99px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:29px;
+clear:both;
+float:left;
+width:100%;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:100%;
+border-top-width:1px;
+border-top-style:dashed;
+}
+.notices li {
+list-style-type:none;
+}
+
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+float:left;
+}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+display:inline;
+width:100%;
+overflow:hidden;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content a:visited {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+float:left;
+font-size:0.95em;
+width:65%;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+.notice-options {
+padding-left:2%;
+float:left;
+width:50%;
+position:relative;
+font-size:0.95em;
+width:12.5%;
+float:right;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+top:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+left:0;
+}
+.notice-options .notice_reply {
+left:29px;
+}
+.notice-options .notice_delete {
+right:0;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.system_notice ul,
+.instructions ul,
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/h4ck3r/css/display.css b/theme/h4ck3r/css/display.css
new file mode 100644
index 000000000..31d49a58e
--- /dev/null
+++ b/theme/h4ck3r/css/display.css
@@ -0,0 +1,236 @@
+/** theme: h4ck3r
+ *
+ * @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/
+ */
+
+@import url(base.css);
+
+html,
+body,
+a:active {
+background-color:#000;
+}
+
+body {
+background-image:url(../images/illustrations/illu_h4x0r1ng.gif);
+font-family: monospace;
+font-size:1em;
+color:#647819;
+}
+address {
+margin-right:7.18%;
+}
+
+input, textarea, select, option {
+font-family: monospace;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+background-color:#000;
+color:#ccc;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:rgba(0, 255, 0, 0.5);
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+.entity_remote_subscribe {
+color:#fff;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#0f0;
+}
+
+.notice,
+.profile {
+border-top-color:#333;
+}
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#aside_primary {
+background-color:rgba(0,128,0,0.3);
+}
+
+#notice_text-count {
+color:#0f0;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#ccc url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a,
+#aside_primary {
+border-color:#50964D;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:rgba(0, 0, 0, 0.698);
+}
+
+#site_nav_local_views a {
+background-color:rgba(0, 200, 0, 0.3);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.4);
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+color:#ccc;
+border-color:#50964D;
+}
+
+#showstream #anon_notice {
+}
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#ccc;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#ccc;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#000;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/h4ck3r/css/ie.css b/theme/h4ck3r/css/ie.css
new file mode 100644
index 000000000..2f463bb44
--- /dev/null
+++ b/theme/h4ck3r/css/ie.css
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/h4ck3r/default-avatar-mini.png b/theme/h4ck3r/default-avatar-mini.png
new file mode 100644
index 000000000..38b8692b4
--- /dev/null
+++ b/theme/h4ck3r/default-avatar-mini.png
Binary files differ
diff --git a/theme/h4ck3r/default-avatar-profile.png b/theme/h4ck3r/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/h4ck3r/default-avatar-profile.png
Binary files differ
diff --git a/theme/h4ck3r/default-avatar-stream.png b/theme/h4ck3r/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/h4ck3r/default-avatar-stream.png
Binary files differ
diff --git a/theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif b/theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif
new file mode 100644
index 000000000..c233af391
--- /dev/null
+++ b/theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif
Binary files differ
diff --git a/theme/h4ck3r/logo.png b/theme/h4ck3r/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/h4ck3r/logo.png
Binary files differ
diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css
index d05578d43..42a4573a7 100644
--- a/theme/identica/css/display.css
+++ b/theme/identica/css/display.css
@@ -9,7 +9,6 @@
@import url(../../base/css/display.css);
-html,
body,
a:active {
background-color:#F0F2F5;
@@ -19,7 +18,7 @@ font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-size:1em;
}
address {
-margin-right:7.18%;
+margin-right:7.2%;
}
input, textarea, select, option {
@@ -27,13 +26,13 @@ font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
input, textarea, select,
.entity_remote_subscribe {
-border-color:#aaa;
+border-color:#AAAAAA;
}
#filter_tags ul li {
-border-color:#ddd;
+border-color:#DDDDDD;
}
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
background:none;
}
@@ -47,20 +46,26 @@ background-color:#9BB43E;
input:focus, textarea:focus, select:focus,
#form_notice.warning #notice_data-text {
border-color:#9BB43E;
+box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+-moz-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+-webkit-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
}
input.submit,
.entity_remote_subscribe {
-color:#fff;
+color:#FFFFFF;
}
a,
div.notice-options input,
.form_user_block input.submit,
.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit,
.entity_send-a-message a,
.form_user_nudge input.submit,
.entity_nudge p,
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary,
+.form_make_admin input.submit {
color:#002E6E;
}
@@ -72,45 +77,58 @@ border-top-color:#CEE1E9;
border-top-color:#87B4C8;
}
-#content .notice p.entry-content a:visited {
-background-color:#fcfcfc;
-}
-#content .notice p.entry-content .vcard a {
-background-color:#fcfffc;
-}
-
#aside_primary {
background-color:#CEE1E9;
}
#notice_text-count {
-color:#333;
+color:#333333;
}
#form_notice.warning #notice_text-count {
-color:#000;
+color:#000000;
}
+#form_notice label[for=notice_data-attach] {
+background:transparent url(../../base/images/icons/twotone/green/clip-01.gif) no-repeat 0 45%;
+}
+#form_notice #notice_data-attach {
+opacity:0;
+}
+
#form_notice.processing #notice_action-submit {
-background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+background:#FFFFFF url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
cursor:wait;
text-indent:-9999px;
}
+#content {
+box-shadow:5px 7px 7px rgba(194, 194, 194, 0.3);
+-moz-box-shadow:5px 7px 7px rgba(194, 194, 194, 0.3);
+-webkit-box-shadow:5px 7px 7px rgba(194, 194, 194, 0.3);
+}
#content,
#site_nav_local_views a,
#aside_primary {
-border-color:#fff;
+border-color:transparent;
}
#content,
#site_nav_local_views .current a {
-background-color:#fff;
+background-color:#FFFFFF;
}
+#site_nav_local_views li {
+box-shadow:3px 7px 5px rgba(194, 194, 194, 0.5);
+-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.5);
+-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.5);
+}
#site_nav_local_views a {
-background-color:rgba(135, 180, 200, 0.3);
+background-color:rgba(194, 194, 194, 0.5);
}
#site_nav_local_views a:hover {
background-color:rgba(255, 255, 255, 0.7);
}
+#site_nav_local_views .current a {
+text-shadow: rgba(194,194,194,0.5) 1px 1px 1px;
+}
.error {
background-color:#F7E8E8;
@@ -121,8 +139,8 @@ background-color:#EFF3DC;
#anon_notice {
background-color:#87B4C8;
-color:#fff;
-border-color:#fff;
+color:#FFFFFF;
+border-color:#FFFFFF;
}
#showstream #anon_notice {
@@ -148,7 +166,10 @@ background-image:url(../../base/images/icons/icon_foaf.gif);
.form_user_nudge input.submit,
.form_user_block input.submit,
.form_user_unblock input.submit,
-.entity_nudge p {
+.form_group_block input.submit,
+.form_group_unblock input.submit,
+.entity_nudge p,
+.form_make_admin input.submit {
background-position: 0 40%;
background-repeat: no-repeat;
background-color:transparent;
@@ -158,7 +179,7 @@ background-color:transparent;
.form_user_subscribe input.submit,
.form_user_unsubscribe input.submit {
background-color:#9BB43E;
-color:#fff;
+color:#FFFFFF;
}
.form_user_unsubscribe input.submit,
.form_group_leave input.submit,
@@ -167,64 +188,87 @@ background-color:#87B4C8;
}
.entity_edit a {
-background-image:url(../images/icons/twotone/green/edit.gif);
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
}
.entity_send-a-message a {
-background-image:url(../images/icons/twotone/green/quote.gif);
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
}
.entity_nudge p,
.form_user_nudge input.submit {
-background-image:url(../images/icons/twotone/green/mail.gif);
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
}
.form_user_block input.submit,
-.form_user_unblock input.submit {
-background-image:url(../images/icons/twotone/green/shield.gif);
+.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+.form_make_admin input.submit {
+background-image:url(../../base/images/icons/twotone/green/admin.gif);
}
/* NOTICES */
-.notices li.over {
-background-color:#fcfcfc;
+.notice .attachment {
+background:transparent url(../../base/images/icons/twotone/green/clip-02.gif) no-repeat 0 45%;
+}
+#attachments .attachment {
+background:none;
}
-
.notice-options .notice_reply a,
.notice-options form input.submit {
background-color:transparent;
}
.notice-options .notice_reply a {
-background:transparent url(../images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
}
.notice-options form.form_favor input.submit {
-background:transparent url(../images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
}
.notice-options form.form_disfavor input.submit {
-background:transparent url(../images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
}
.notice-options .notice_delete a {
-background:transparent url(../images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
}
.notices div.entry-content,
.notices div.notice-options {
opacity:0.4;
}
-.notices li.hover div.entry-content,
-.notices li.hover div.notice-options {
+.notices li:hover div.entry-content,
+.notices li:hover div.notice-options {
opacity:1;
}
div.entry-content {
-color:#333;
+color:#333333;
}
div.notice-options a,
div.notice-options input {
font-family:sans-serif;
}
-.notices li.hover {
-background-color:#fcfcfc;
+#content .notices li:hover {
+background-color:rgba(240, 240, 240, 0.2);
+}
+#conversation .notices li:hover {
+background-color:transparent;
+}
+
+.notices .notices {
+background-color:rgba(200, 200, 200, 0.050);
+}
+.notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.100);
+}
+.notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.150);
+}
+.notices .notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.300);
}
/*END: NOTICES */
#new_group a {
-background:transparent url(../images/icons/twotone/green/news.gif) no-repeat 0 45%;
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
}
.pagination .nav_prev a,
@@ -233,10 +277,10 @@ background-repeat:no-repeat;
border-color:#CEE1E9;
}
.pagination .nav_prev a {
-background-image:url(../images/icons/twotone/green/arrow-left.gif);
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
background-position:10% 45%;
}
.pagination .nav_next a {
-background-image:url(../images/icons/twotone/green/arrow-right.gif);
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
background-position:90% 45%;
}
diff --git a/theme/identica/css/ie.css b/theme/identica/css/ie.css
index 2f463bb44..97cabc30a 100644
--- a/theme/identica/css/ie.css
+++ b/theme/identica/css/ie.css
@@ -1,9 +1,14 @@
/* IE specific styles */
.notice-options input.submit {
-color:#fff;
+color:#FFFFFF;
}
-
#site_nav_local_views a {
-background-color:#D0DFE7;
+background-color:#D9DADB;
+}
+#form_notice .form_note + label {
+background:transparent url(../../base/images/icons/twotone/green/clip-01.gif) no-repeat 0 45%;
+}
+#form_notice #notice_data-attach {
+filter: alpha(opacity=0);
}
diff --git a/theme/identica/images/icons/icon_atom.jpg b/theme/identica/images/icons/icon_atom.jpg
deleted file mode 100644
index 22853edc4..000000000
--- a/theme/identica/images/icons/icon_atom.jpg
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/icon_foaf.gif b/theme/identica/images/icons/icon_foaf.gif
deleted file mode 100644
index f8f784423..000000000
--- a/theme/identica/images/icons/icon_foaf.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/icon_rss.jpg b/theme/identica/images/icons/icon_rss.jpg
deleted file mode 100644
index da23422d0..000000000
--- a/theme/identica/images/icons/icon_rss.jpg
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/icon_vcard.gif b/theme/identica/images/icons/icon_vcard.gif
deleted file mode 100644
index 6d52947f3..000000000
--- a/theme/identica/images/icons/icon_vcard.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/against.gif b/theme/identica/images/icons/twotone/green/against.gif
deleted file mode 100644
index ca796c8a3..000000000
--- a/theme/identica/images/icons/twotone/green/against.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-down.gif b/theme/identica/images/icons/twotone/green/arrow-down.gif
deleted file mode 100644
index c709e5877..000000000
--- a/theme/identica/images/icons/twotone/green/arrow-down.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-downleft.gif b/theme/identica/images/icons/twotone/green/arrow-downleft.gif
deleted file mode 100644
index a4a98035d..000000000
--- a/theme/identica/images/icons/twotone/green/arrow-downleft.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-downright.gif b/theme/identica/images/icons/twotone/green/arrow-downright.gif
deleted file mode 100644
index 3e6001a61..000000000
--- a/theme/identica/images/icons/twotone/green/arrow-downright.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-up.gif b/theme/identica/images/icons/twotone/green/arrow-up.gif
deleted file mode 100644
index d0f5fbeaa..000000000
--- a/theme/identica/images/icons/twotone/green/arrow-up.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-upleft.gif b/theme/identica/images/icons/twotone/green/arrow-upleft.gif
deleted file mode 100644
index 1e9e6935b..000000000
--- a/theme/identica/images/icons/twotone/green/arrow-upleft.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/arrow-upright.gif b/theme/identica/images/icons/twotone/green/arrow-upright.gif
deleted file mode 100644
index c7fecc8a0..000000000
--- a/theme/identica/images/icons/twotone/green/arrow-upright.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/back-forth.gif b/theme/identica/images/icons/twotone/green/back-forth.gif
deleted file mode 100644
index 33a9540c8..000000000
--- a/theme/identica/images/icons/twotone/green/back-forth.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/bookmark.gif b/theme/identica/images/icons/twotone/green/bookmark.gif
deleted file mode 100644
index 23f318ecc..000000000
--- a/theme/identica/images/icons/twotone/green/bookmark.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/bulb.gif b/theme/identica/images/icons/twotone/green/bulb.gif
deleted file mode 100644
index f70652c03..000000000
--- a/theme/identica/images/icons/twotone/green/bulb.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/calendar.gif b/theme/identica/images/icons/twotone/green/calendar.gif
deleted file mode 100644
index a09b65aca..000000000
--- a/theme/identica/images/icons/twotone/green/calendar.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/calendar2.gif b/theme/identica/images/icons/twotone/green/calendar2.gif
deleted file mode 100644
index 7884b02dd..000000000
--- a/theme/identica/images/icons/twotone/green/calendar2.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/camera.gif b/theme/identica/images/icons/twotone/green/camera.gif
deleted file mode 100644
index 1a85fbad0..000000000
--- a/theme/identica/images/icons/twotone/green/camera.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/cart.gif b/theme/identica/images/icons/twotone/green/cart.gif
deleted file mode 100644
index 47eaa0a2e..000000000
--- a/theme/identica/images/icons/twotone/green/cart.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/caution.gif b/theme/identica/images/icons/twotone/green/caution.gif
deleted file mode 100644
index 3ad2c322b..000000000
--- a/theme/identica/images/icons/twotone/green/caution.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/chart.gif b/theme/identica/images/icons/twotone/green/chart.gif
deleted file mode 100644
index 136d74517..000000000
--- a/theme/identica/images/icons/twotone/green/chart.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/checkmark.gif b/theme/identica/images/icons/twotone/green/checkmark.gif
deleted file mode 100644
index 892429d48..000000000
--- a/theme/identica/images/icons/twotone/green/checkmark.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/clipboard.gif b/theme/identica/images/icons/twotone/green/clipboard.gif
deleted file mode 100644
index 9317bdcd0..000000000
--- a/theme/identica/images/icons/twotone/green/clipboard.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/clock.gif b/theme/identica/images/icons/twotone/green/clock.gif
deleted file mode 100644
index d1410f925..000000000
--- a/theme/identica/images/icons/twotone/green/clock.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/closed-folder.gif b/theme/identica/images/icons/twotone/green/closed-folder.gif
deleted file mode 100644
index 0410fc6e8..000000000
--- a/theme/identica/images/icons/twotone/green/closed-folder.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/database.gif b/theme/identica/images/icons/twotone/green/database.gif
deleted file mode 100644
index 29ce02492..000000000
--- a/theme/identica/images/icons/twotone/green/database.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/disfavourite.gif b/theme/identica/images/icons/twotone/green/disfavourite.gif
deleted file mode 100644
index 3946869ae..000000000
--- a/theme/identica/images/icons/twotone/green/disfavourite.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/diskette.gif b/theme/identica/images/icons/twotone/green/diskette.gif
deleted file mode 100644
index e970b0a30..000000000
--- a/theme/identica/images/icons/twotone/green/diskette.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/document.gif b/theme/identica/images/icons/twotone/green/document.gif
deleted file mode 100644
index 9c08f4a3a..000000000
--- a/theme/identica/images/icons/twotone/green/document.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/double-arrow.gif b/theme/identica/images/icons/twotone/green/double-arrow.gif
deleted file mode 100644
index 2e8648264..000000000
--- a/theme/identica/images/icons/twotone/green/double-arrow.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/eject.gif b/theme/identica/images/icons/twotone/green/eject.gif
deleted file mode 100644
index 7e0906cfe..000000000
--- a/theme/identica/images/icons/twotone/green/eject.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/exclaim.gif b/theme/identica/images/icons/twotone/green/exclaim.gif
deleted file mode 100644
index 588e28c26..000000000
--- a/theme/identica/images/icons/twotone/green/exclaim.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/fastforward.gif b/theme/identica/images/icons/twotone/green/fastforward.gif
deleted file mode 100644
index 28e495103..000000000
--- a/theme/identica/images/icons/twotone/green/fastforward.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/favourite.gif b/theme/identica/images/icons/twotone/green/favourite.gif
deleted file mode 100644
index d93515e37..000000000
--- a/theme/identica/images/icons/twotone/green/favourite.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/flag.gif b/theme/identica/images/icons/twotone/green/flag.gif
deleted file mode 100644
index 68c8aee25..000000000
--- a/theme/identica/images/icons/twotone/green/flag.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/graph.gif b/theme/identica/images/icons/twotone/green/graph.gif
deleted file mode 100644
index 0c1794b4e..000000000
--- a/theme/identica/images/icons/twotone/green/graph.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/grow.gif b/theme/identica/images/icons/twotone/green/grow.gif
deleted file mode 100644
index c4118d53b..000000000
--- a/theme/identica/images/icons/twotone/green/grow.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/headphones.gif b/theme/identica/images/icons/twotone/green/headphones.gif
deleted file mode 100644
index 5be6c67dd..000000000
--- a/theme/identica/images/icons/twotone/green/headphones.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/home.gif b/theme/identica/images/icons/twotone/green/home.gif
deleted file mode 100644
index d2a3421ef..000000000
--- a/theme/identica/images/icons/twotone/green/home.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/hourglass.gif b/theme/identica/images/icons/twotone/green/hourglass.gif
deleted file mode 100644
index b62b9480c..000000000
--- a/theme/identica/images/icons/twotone/green/hourglass.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/info.gif b/theme/identica/images/icons/twotone/green/info.gif
deleted file mode 100644
index 86ef1f8b4..000000000
--- a/theme/identica/images/icons/twotone/green/info.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/key.gif b/theme/identica/images/icons/twotone/green/key.gif
deleted file mode 100644
index ccf357ab2..000000000
--- a/theme/identica/images/icons/twotone/green/key.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/lock.gif b/theme/identica/images/icons/twotone/green/lock.gif
deleted file mode 100644
index db00706b5..000000000
--- a/theme/identica/images/icons/twotone/green/lock.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/move.gif b/theme/identica/images/icons/twotone/green/move.gif
deleted file mode 100644
index d2c30b1d2..000000000
--- a/theme/identica/images/icons/twotone/green/move.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/music.gif b/theme/identica/images/icons/twotone/green/music.gif
deleted file mode 100644
index 64b51d4e1..000000000
--- a/theme/identica/images/icons/twotone/green/music.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/note.gif b/theme/identica/images/icons/twotone/green/note.gif
deleted file mode 100644
index bcc0b149b..000000000
--- a/theme/identica/images/icons/twotone/green/note.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/open-folder.gif b/theme/identica/images/icons/twotone/green/open-folder.gif
deleted file mode 100644
index d41300a08..000000000
--- a/theme/identica/images/icons/twotone/green/open-folder.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/paper-clip.gif b/theme/identica/images/icons/twotone/green/paper-clip.gif
deleted file mode 100644
index 1d45f1d1e..000000000
--- a/theme/identica/images/icons/twotone/green/paper-clip.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/paper-clip2.gif b/theme/identica/images/icons/twotone/green/paper-clip2.gif
deleted file mode 100644
index a8c7805be..000000000
--- a/theme/identica/images/icons/twotone/green/paper-clip2.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/pause.gif b/theme/identica/images/icons/twotone/green/pause.gif
deleted file mode 100644
index ced0b6440..000000000
--- a/theme/identica/images/icons/twotone/green/pause.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/phone.gif b/theme/identica/images/icons/twotone/green/phone.gif
deleted file mode 100644
index 69359f764..000000000
--- a/theme/identica/images/icons/twotone/green/phone.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/play.gif b/theme/identica/images/icons/twotone/green/play.gif
deleted file mode 100644
index 794ec85b6..000000000
--- a/theme/identica/images/icons/twotone/green/play.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/plus.gif b/theme/identica/images/icons/twotone/green/plus.gif
deleted file mode 100644
index 4407d0b2d..000000000
--- a/theme/identica/images/icons/twotone/green/plus.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/print.gif b/theme/identica/images/icons/twotone/green/print.gif
deleted file mode 100644
index 17727d5d7..000000000
--- a/theme/identica/images/icons/twotone/green/print.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/question-mark.gif b/theme/identica/images/icons/twotone/green/question-mark.gif
deleted file mode 100644
index 1689efcd0..000000000
--- a/theme/identica/images/icons/twotone/green/question-mark.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/refresh.gif b/theme/identica/images/icons/twotone/green/refresh.gif
deleted file mode 100644
index 8a8b8144f..000000000
--- a/theme/identica/images/icons/twotone/green/refresh.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/reply.gif b/theme/identica/images/icons/twotone/green/reply.gif
deleted file mode 100644
index 6ff01bb35..000000000
--- a/theme/identica/images/icons/twotone/green/reply.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/rewind.gif b/theme/identica/images/icons/twotone/green/rewind.gif
deleted file mode 100644
index aca3ee35b..000000000
--- a/theme/identica/images/icons/twotone/green/rewind.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/search.gif b/theme/identica/images/icons/twotone/green/search.gif
deleted file mode 100644
index c36463d0d..000000000
--- a/theme/identica/images/icons/twotone/green/search.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/skip-back.gif b/theme/identica/images/icons/twotone/green/skip-back.gif
deleted file mode 100644
index adca7aa3e..000000000
--- a/theme/identica/images/icons/twotone/green/skip-back.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/skip.gif b/theme/identica/images/icons/twotone/green/skip.gif
deleted file mode 100644
index ae5417f2f..000000000
--- a/theme/identica/images/icons/twotone/green/skip.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/skull.gif b/theme/identica/images/icons/twotone/green/skull.gif
deleted file mode 100644
index 033506732..000000000
--- a/theme/identica/images/icons/twotone/green/skull.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/statusbar.gif b/theme/identica/images/icons/twotone/green/statusbar.gif
deleted file mode 100644
index 47d61b106..000000000
--- a/theme/identica/images/icons/twotone/green/statusbar.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/stop.gif b/theme/identica/images/icons/twotone/green/stop.gif
deleted file mode 100644
index e0b108d35..000000000
--- a/theme/identica/images/icons/twotone/green/stop.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/template.gif b/theme/identica/images/icons/twotone/green/template.gif
deleted file mode 100644
index 65c0c4a0a..000000000
--- a/theme/identica/images/icons/twotone/green/template.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/text-bigger.gif b/theme/identica/images/icons/twotone/green/text-bigger.gif
deleted file mode 100644
index 45e143b7a..000000000
--- a/theme/identica/images/icons/twotone/green/text-bigger.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/text-smaller.gif b/theme/identica/images/icons/twotone/green/text-smaller.gif
deleted file mode 100644
index a54d0c1d3..000000000
--- a/theme/identica/images/icons/twotone/green/text-smaller.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/trash.gif b/theme/identica/images/icons/twotone/green/trash.gif
deleted file mode 100644
index 78dd64a3d..000000000
--- a/theme/identica/images/icons/twotone/green/trash.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/two-docs.gif b/theme/identica/images/icons/twotone/green/two-docs.gif
deleted file mode 100644
index 97e54b964..000000000
--- a/theme/identica/images/icons/twotone/green/two-docs.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/twotone.gif b/theme/identica/images/icons/twotone/green/twotone.gif
deleted file mode 100644
index 45aad25c4..000000000
--- a/theme/identica/images/icons/twotone/green/twotone.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/undo.gif b/theme/identica/images/icons/twotone/green/undo.gif
deleted file mode 100644
index 6869b3050..000000000
--- a/theme/identica/images/icons/twotone/green/undo.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/user.gif b/theme/identica/images/icons/twotone/green/user.gif
deleted file mode 100644
index c85460fcd..000000000
--- a/theme/identica/images/icons/twotone/green/user.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/vegetable.gif b/theme/identica/images/icons/twotone/green/vegetable.gif
deleted file mode 100644
index 4d421c1bb..000000000
--- a/theme/identica/images/icons/twotone/green/vegetable.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/x.gif b/theme/identica/images/icons/twotone/green/x.gif
deleted file mode 100644
index ffb2efea0..000000000
--- a/theme/identica/images/icons/twotone/green/x.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/zoom-in.gif b/theme/identica/images/icons/twotone/green/zoom-in.gif
deleted file mode 100644
index a59a5bb50..000000000
--- a/theme/identica/images/icons/twotone/green/zoom-in.gif
+++ /dev/null
Binary files differ
diff --git a/theme/identica/images/icons/twotone/green/zoom-out.gif b/theme/identica/images/icons/twotone/green/zoom-out.gif
deleted file mode 100644
index c61f999fd..000000000
--- a/theme/identica/images/icons/twotone/green/zoom-out.gif
+++ /dev/null
Binary files differ
diff --git a/theme/otalk/css/base.css b/theme/otalk/css/base.css
new file mode 100644
index 000000000..b39992570
--- /dev/null
+++ b/theme/otalk/css/base.css
@@ -0,0 +1,1211 @@
+/** theme: otalk 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/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 87.5%; background-color:#fff; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 7px;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:152px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:163px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:29px;
+}
+
+#site_nav_global_primary {
+float:right;
+margin-right:18px;
+margin-bottom:11px;
+margin-left:18px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-left:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+position:absolute;
+top:65px;
+right:18px;
+width:250px;
+width:24%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:left;
+width:43.2%;
+padding:1.1%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:2px;
+border-style:solid;
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+float:left;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+float:left;
+margin-right:18px;
+list-style-type:none;
+}
+#site_nav_local_views a {
+float:left;
+text-decoration:none;
+padding:4px 11px;
+-moz-border-radius-topleft:4px;
+-moz-border-radius-topright:4px;
+-webkit-border-top-left-radius:4px;
+-webkit-border-top-right-radius:4px;
+border-width:0;
+border-style:solid;
+border-bottom:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+border-bottom-width:1px;
+border-bottom-style:solid;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:67.9%;
+min-height:259px;
+padding-top:1.795%;
+padding-bottom:1.795%;
+float:left;
+clear:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-moz-border-radius-topleft:0;
+-webkit-border-radius:7px;
+-webkit-border-top-left-radius:0;
+border-style:solid;
+border-width:0;
+margin-bottom:18px;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:27.917%;
+min-height:259px;
+float:left;
+padding:1.795%;
+margin-left:0.385%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:1px;
+border-style:solid;
+}
+
+#form_notice {
+width:45.664%;
+float:left;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+width:80.789%;
+height:67px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:99px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:521px;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:left;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:29px;
+clear:both;
+float:left;
+width:100%;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+clear:both;
+float:left;
+width:100%;
+border-width:0;
+border-style:solid;
+margin-bottom:29px;
+}
+.notices li {
+list-style-type:none;
+}
+
+#content .notice {
+width:37%;
+margin-left:17px;
+margin-bottom:47px;
+clear:none;
+overflow:hidden;
+padding: 0 0 0 65px;
+min-height:235px;
+}
+
+#aside_primary .notice {
+margin-bottom:18px;
+}
+
+#shownotice #content .notice {
+width:96%;
+}
+
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+#content .notice .author {
+/*overflow:hidden;*/
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.notice .author .photo {
+margin-bottom:0;
+}
+
+#content .notice .author .photo {
+margin-left:-83px;
+padding-right:17px;
+}
+
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+margin-bottom:11px;
+float:left;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:left;
+width:100%;
+overflow:hidden;
+}
+#content .notice .entry-title {
+overflow:visible;
+margin-bottom:11px;
+padding:18px;
+width:85%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+min-height:161px;
+}
+
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content
+overflow:hidden;
+}
+
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+clear:left;
+float:left;
+font-size:0.95em;
+}
+#showstream .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+
+.notice-data {
+position:absolute;
+top:18px;
+right:0;
+min-height:50px;
+margin-bottom:4px;
+}
+.notice .entry-content .notice-data dt {
+display:none;
+}
+
+.notice-data a {
+display:block;
+outline:none;
+}
+
+.notice-options {
+position:absolute;
+top:120px;
+left:30px;
+font-size:0.95em;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+left:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+top:0;
+}
+.notice-options .notice_reply {
+top:29px;
+}
+.notice-options .notice_delete {
+top:58px;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/otalk/css/display.css b/theme/otalk/css/display.css
new file mode 100644
index 000000000..d2a4719a8
--- /dev/null
+++ b/theme/otalk/css/display.css
@@ -0,0 +1,292 @@
+/** theme: otalk
+ *
+ * @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/
+ */
+
+@import url(base.css);
+
+html {
+}
+
+html,
+body,
+a:active {
+}
+body {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+font-size:1em;
+background:#ddd url(../images/illustrations/illu_pattern-01.png) repeat 0 0;
+background-color:rgba(127, 127, 127, 0.1);
+}
+address {
+margin-right:7.18%;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#9BB43E;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+.entity_remote_subscribe {
+color:#fff;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#8F0000;
+}
+
+.notice,
+.profile {
+border-color:#CEE1E9;
+}
+#content .notice .entry-title,
+input, textarea, select, option,
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-color:rgba(255,255,255,0.8);
+}
+
+#content .notices li.hover .entry-title {
+background-color:rgba(255,255,255,0.9);
+}
+
+#content .notice:nth-child(1) .entry-title {
+background-color:rgba(255,255,255,0.95);
+}
+#content .notice:nth-child(2) .entry-title {
+background-color:rgba(255,255,255,0.9);
+}
+#content .notice:nth-child(3) .entry-title {
+background-color:rgba(255,255,255,0.8);
+}
+#content .notice:nth-child(4) .entry-title {
+background-color:rgba(255,255,255,0.7);
+}
+#content .notice:nth-child(5) .entry-title {
+background-color:rgba(255,255,255,0.6);
+}
+#content .notice:nth-child(6) .entry-title {
+background-color:rgba(255,255,255,0.5);
+}
+#content .notice:nth-child(7) .entry-title {
+background-color:rgba(255,255,255,0.4);
+}
+#content .notice:nth-child(8) .entry-title {
+background-color:rgba(255,255,255,0.3);
+}
+#content .notice:nth-child(9) .entry-title {
+background-color:rgba(255,255,255,0.2);
+}
+#content .notice:nth-child(10) {
+background-color:rgba(255,255,255,0.1);
+}
+
+
+#content .notice .author .photo {
+background:url(../images/illustrations/illu_arrow-left-01.gif) no-repeat 100% 0;
+}
+
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#aside_primary {
+background-color:rgba(206, 225, 233,0.5);
+}
+
+#notice_text-count {
+color:#333;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views .nav,
+#site_nav_local_views a,
+#aside_primary {
+border-color:#fff;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:transparent;
+/*background-color:red;*/
+}
+
+#site_nav_local_views .current a {
+background-color:transparent;
+}
+
+#site_nav_local_views a {
+background-color:rgba(127, 127, 127, 0.2);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.8);
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+background-color:rgba(206, 225, 233, 0.7);
+color:#fff;
+border-color:#fff;
+}
+
+#showstream #anon_notice {
+background-color:rgba(155, 180, 62, 0.7);
+}
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#fff;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+.notices li.hover {
+/*background-color:#fcfcfc;*/
+}
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#CEE1E9;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/otalk/css/ie.css b/theme/otalk/css/ie.css
new file mode 100644
index 000000000..2f463bb44
--- /dev/null
+++ b/theme/otalk/css/ie.css
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/otalk/default-avatar-mini.png b/theme/otalk/default-avatar-mini.png
new file mode 100644
index 000000000..38b8692b4
--- /dev/null
+++ b/theme/otalk/default-avatar-mini.png
Binary files differ
diff --git a/theme/otalk/default-avatar-profile.png b/theme/otalk/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/otalk/default-avatar-profile.png
Binary files differ
diff --git a/theme/otalk/default-avatar-stream.png b/theme/otalk/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/otalk/default-avatar-stream.png
Binary files differ
diff --git a/theme/otalk/images/illustrations/illu_arrow-left-01.gif b/theme/otalk/images/illustrations/illu_arrow-left-01.gif
new file mode 100644
index 000000000..197775976
--- /dev/null
+++ b/theme/otalk/images/illustrations/illu_arrow-left-01.gif
Binary files differ
diff --git a/theme/otalk/images/illustrations/illu_pattern-01.png b/theme/otalk/images/illustrations/illu_pattern-01.png
new file mode 100644
index 000000000..5a72eafcb
--- /dev/null
+++ b/theme/otalk/images/illustrations/illu_pattern-01.png
Binary files differ
diff --git a/theme/otalk/logo.png b/theme/otalk/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/otalk/logo.png
Binary files differ
diff --git a/theme/pigeonthoughts/css/base.css b/theme/pigeonthoughts/css/base.css
new file mode 100644
index 000000000..9866e2d2c
--- /dev/null
+++ b/theme/pigeonthoughts/css/base.css
@@ -0,0 +1,1269 @@
+/** theme: pigeonthoughts 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/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 87.5%; }
+body {
+background-color:#FFFFFF;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+margin-left:183px;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox,
+input.radio {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 1.55%;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:152px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend,
+.form_entity_block legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:163px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+.form_settings label.radio {
+margin-top:0;
+margin-right:47px;
+margin-left:11px;
+width:auto;
+}
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q,
+#design_background-image_onoff p.form_guide {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:right;
+margin-bottom:18px;
+margin-right:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:98.5%;
+position:relative;
+float:left;
+padding-top:18px;
+padding-left:18px;
+margin-bottom:29px;
+}
+
+#site_nav_global_primary {
+float:left;
+margin-right:18px;
+margin-bottom:11px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-right:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+float:right;
+margin-top:7px;
+margin-right:18px;
+width:26%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:left;
+width:50.2%;
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+width:183px;
+float:left;
+margin-bottom:29px;
+position:fixed;
+top:179px;
+left:0;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+list-style-type:none;
+}
+#site_nav_local_views a {
+text-decoration:none;
+padding:4px 11px;
+text-shadow: 1px 1px 1px #ddd;
+font-weight:bold;
+display:block;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:49.009%;
+min-height:259px;
+float:left;
+padding:0 18px;
+}
+#shownotice #content {
+min-height:0;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:45.917%;
+min-height:259px;
+float:left;
+margin-left:1.385%;
+padding-bottom:47px;
+}
+
+#form_notice {
+width:45.664%;
+float:left;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+position:relative;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+width:80.789%;
+height:46px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+position:relative;
+z-index:2;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice label[for=notice_data-attach],
+#form_notice #notice_data-attach {
+position:absolute;
+top:25px;
+cursor:pointer;
+}
+#form_notice label[for=notice_data-attach] {
+text-indent:-9999px;
+left:394px;
+width:16px;
+height:16px;
+}
+#form_notice #notice_data-attach {
+left:183px;
+padding:0;
+height:16px;
+}
+#form_notice .form_note {
+position:absolute;
+top:76px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+.entity_profile .fn,
+.entity_profile .nickname {
+font-size:1.1em;
+font-weight:bold;
+}
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_make_admin input.submit {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:29px;
+float:right;
+width:44%;
+padding:1%;
+border-width:1px;
+border-style:solid;
+margin-left:2.5%;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+display:inline;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:96.41%;
+border-width:1px;
+border-style:solid;
+margin-bottom:11px;
+}
+.notices li {
+list-style-type:none;
+}
+.notices .notices {
+margin-top:7px;
+margin-left:5%;
+width:95%;
+float:left;
+}
+
+#aside_primary .notice,
+#aside_primary .profile {
+border:0;
+margin-bottom:11px;
+}
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+float:left;
+}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:left;
+width:100%;
+overflow:hidden;
+}
+.notice .entry-title.ov {
+overflow:visible;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content a:visited {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+clear:left;
+float:left;
+font-size:0.95em;
+margin-left:59px;
+width:60%;
+}
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+.notice-options {
+position:relative;
+font-size:0.95em;
+width:90px;
+float:right;
+margin-right:11px;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+top:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+left:0;
+}
+.notice-options .notice_reply {
+left:29px;
+}
+.notice-options .notice_delete {
+right:0;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+.notice .attachment {
+position:relative;
+padding-left:16px;
+}
+#attachments .attachment {
+padding-left:0;
+}
+.notice .attachment img {
+position:absolute;
+top:18px;
+left:0;
+z-index:99;
+}
+#shownotice .notice .attachment img {
+position:static;
+}
+
+#attachments {
+clear:both;
+float:left;
+width:100%;
+margin-top:18px;
+}
+#attachments dt {
+font-weight:bold;
+font-size:1.3em;
+margin-bottom:4px;
+}
+
+#attachments ol li {
+margin-bottom:18px;
+list-style-type:decimal;
+float:left;
+clear:both;
+}
+
+#jOverlayContent,
+#jOverlayContent #content,
+#jOverlayContent #content_inner {
+width: auto !important;
+margin-bottom:0;
+}
+#jOverlayContent #content {
+padding:11px;
+min-height:auto;
+}
+#jOverlayContent .external span {
+display:block;
+margin-bottom:11px;
+}
+#jOverlayContent button {
+position:absolute;
+top:0;
+right:0;
+width:29px;
+height:29px;
+text-align:center;
+font-weight:bold;
+padding:0;
+}
+#jOverlayContent h1 {
+max-width:475px;
+}
+#jOverlayContent #content {
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.system_notice ul,
+.instructions ul,
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+#settings_design_color .form_data li {
+width:33%;
+}
+#settings_design_color .form_data label {
+float:none;
+display:block;
+}
+#settings_design_color .form_data .swatch {
+padding:11px;
+margin-left:0;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/pigeonthoughts/css/display.css b/theme/pigeonthoughts/css/display.css
new file mode 100644
index 000000000..01af500bf
--- /dev/null
+++ b/theme/pigeonthoughts/css/display.css
@@ -0,0 +1,340 @@
+/** theme: pigeonthoughts
+ *
+ * @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/
+ */
+
+@import url(base.css);
+
+html {
+background:url(../images/illustrations/illu_pigeons-01.png) no-repeat 0 100%;
+}
+
+body,
+a:active {
+background-color:#AEA187;
+}
+body {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+font-size:1em;
+}
+address {
+margin-left:2%;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#AAAAAA;
+}
+#filter_tags ul li {
+border-color:#DDDDDD;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#8F0000;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#8F0000;
+box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+-moz-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+-webkit-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
+}
+input.submit,
+.entity_remote_subscribe {
+color:#FFFFFF;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary,
+.form_make_admin input.submit {
+color:#000000;
+}
+
+.notice,
+.profile {
+border-color:#000000;
+}
+.notice a,
+.profile a {
+color:#FFFFFF;
+}
+
+.notice:nth-child(3n-1),
+.profile:nth-child(3n-1) {
+border-color:#FFFFFF;
+}
+.notice:nth-child(3n-1) a,
+.profile:nth-child(3n-1) a {
+color:#7F1114;
+}
+.notice:nth-child(3n),
+.profile:nth-child(3n) {
+border-color:#7F1114;
+}
+.notice:nth-child(3n) a,
+.profile:nth-child(3n) a {
+color:#000000;
+}
+
+.aside .section .notice,
+.aside .section .profile,
+.aside .section .notice:nth-child(3n-1),
+.aside .section .profile:nth-child(3n-1),
+.aside .section .notice:nth-child(3n),
+.aside .section .profile:nth-child(3n) {
+background-color:transparent;
+color:#000000;
+}
+
+
+.aside .section {
+border-color:#FFFFFF;
+background-color:#FFFFFF;
+color:#000000;
+}
+
+.aside .section:nth-child(n) {
+border-color:#000000;
+background-color:#000000;
+color:#FFFFFF;
+}
+.aside .section:nth-child(3n-1) {
+border-color:#FFFFFF;
+background-color:#FFFFFF;
+color:#000000;
+}
+.aside .section:nth-child(3n) {
+background-color:#7F1114;
+border-color:#7F1114;
+color:#000000;
+}
+.aside .section a {
+color:#7F1114;
+}
+.aside .section:nth-child(3n-1) a {
+color:#7F1114;
+}
+.aside .section:nth-child(3n) a {
+color:#FFFFFF;
+}
+
+
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#aside_primary {
+background:url(../images/illustrations/illu_pigeons-02.png) no-repeat 10% 100%;
+}
+
+#notice_text-count {
+color:#333333;
+}
+#form_notice.warning #notice_text-count {
+color:#000000;
+}
+#form_notice label[for=notice_data-attach] {
+background:transparent url(../../base/images/icons/twotone/green/clip-01.gif) no-repeat 0 45%;
+}
+#form_notice #notice_data-attach {
+opacity:0;
+}
+
+#form_notice.processing #notice_action-submit {
+background:#FFFFFF url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a {
+border-color:#FFFFFF;
+}
+#site_nav_local_views .current a {
+background-color:rgba(143, 0, 0, 0.8);
+color:#FFFFFF;
+}
+
+#site_nav_local_views a {
+background-color:rgba(255, 255, 255, 0.5);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.9);
+color:#8F0000;
+}
+#site_nav_local_views .current a {
+text-shadow: rgba(194,194,194,0.5) 1px 1px 1px;
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+color:#000000;
+}
+
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit,
+.entity_nudge p,
+.form_make_admin input.submit {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#8F0000;
+color:#FFFFFF;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.form_group_block input.submit,
+.form_group_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+.form_make_admin input.submit {
+background-image:url(../../base/images/icons/twotone/green/admin.gif);
+}
+
+/* NOTICES */
+.notice .attachment {
+background:transparent url(../../base/images/icons/twotone/green/clip-02.gif) no-repeat 0 45%;
+}
+#attachments .attachment {
+background:none;
+}
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li:hover div.entry-content,
+.notices li:hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+#content .notices li:hover {
+background-color:transparent;
+}
+#conversation .notices li:hover {
+background-color:transparent;
+}
+
+.notices .notices {
+background-color:rgba(200, 200, 200, 0.050);
+}
+.notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.100);
+}
+.notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.150);
+}
+.notices .notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.300);
+}
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#000000;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/pigeonthoughts/css/ie.css b/theme/pigeonthoughts/css/ie.css
new file mode 100644
index 000000000..2f463bb44
--- /dev/null
+++ b/theme/pigeonthoughts/css/ie.css
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/pigeonthoughts/default-avatar-mini.png b/theme/pigeonthoughts/default-avatar-mini.png
new file mode 100644
index 000000000..38b8692b4
--- /dev/null
+++ b/theme/pigeonthoughts/default-avatar-mini.png
Binary files differ
diff --git a/theme/pigeonthoughts/default-avatar-profile.png b/theme/pigeonthoughts/default-avatar-profile.png
new file mode 100644
index 000000000..f8357d4fc
--- /dev/null
+++ b/theme/pigeonthoughts/default-avatar-profile.png
Binary files differ
diff --git a/theme/pigeonthoughts/default-avatar-stream.png b/theme/pigeonthoughts/default-avatar-stream.png
new file mode 100644
index 000000000..6b63baa70
--- /dev/null
+++ b/theme/pigeonthoughts/default-avatar-stream.png
Binary files differ
diff --git a/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png b/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png
new file mode 100644
index 000000000..4fdaaeb25
--- /dev/null
+++ b/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png
Binary files differ
diff --git a/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png b/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png
new file mode 100644
index 000000000..187c6c8a6
--- /dev/null
+++ b/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png
Binary files differ
diff --git a/theme/pigeonthoughts/logo.png b/theme/pigeonthoughts/logo.png
new file mode 100644
index 000000000..fdead6c4a
--- /dev/null
+++ b/theme/pigeonthoughts/logo.png
Binary files differ
diff --git a/theme/readme.txt b/theme/readme.txt
index 4998b3c98..83b5a61d0 100644
--- a/theme/readme.txt
+++ b/theme/readme.txt
@@ -23,14 +23,16 @@ Only alter this file if you want to change the layout of the site. Please note t
./default/css/display.css contains only the background images and colour rules:
This file is a good basis for creating your own theme.
+Let's create a theme:
-1. Copy over the default theme to start off (replace 'mytheme'):
-cp -r ./default ./mytheme
+1. To start off, copy over the default theme:
+cp -r default mytheme
2. Edit your mytheme stylesheet:
-nano ./mytheme/css/display.css
+nano mytheme/css/display.css
-3. Search and replace a colour or a path to the background image of your choice.
+a) Search and replace your colours and background images, or
+b) Create your own layout either importing a separate stylesheet (e.g., change to @import url(base.css);) or simply place it before the rest of the rules.
4. Set /config.php to load 'mytheme':
$config['site']['theme'] = 'mytheme';