diff --git a/README b/README index 0f1b5a43b4..2c92a75da8 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: +This is a major feature release since version 0.7.4, released May 31 +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. -- elements for pagination, supported by some browsers like Opera. -- Corrected date format in search API. -- Made the public XRDS file work correctly. +- 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 ============= @@ -198,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.) @@ -208,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 @@ -512,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 ----------------------- @@ -702,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. @@ -726,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 -p 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. @@ -791,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 -------------- @@ -817,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 @@ -855,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. @@ -892,8 +936,6 @@ notice: A plain string that will appear on every page. A good place to put introductory information about your service, or info about upgrades and outages, or other community info. Any HTML will be escaped. -dupelimit: Time in which it's not OK for the same person to post the - same notice; default = 60 seconds. logo: URL of an image file to use as the logo for the site. Overrides the logo in the theme, if any. ssl: Whether to use SSL and https:// URLs for some or all pages. @@ -909,6 +951,12 @@ 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. +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. db -- @@ -959,6 +1007,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 ----- @@ -968,7 +1020,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 ------- @@ -1038,6 +1102,8 @@ 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 ----- @@ -1100,6 +1166,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 ------ @@ -1153,6 +1228,7 @@ source: The name to use for the source of posts to Twitter. Defaults Twitter , 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 ------- @@ -1239,7 +1315,7 @@ 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 reckognize the types you want to + 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' @@ -1260,6 +1336,17 @@ user_quota: total size in bytes a user can store on this server. Each user 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 ----- @@ -1300,6 +1387,38 @@ handle: boolean. Whether we should register our own PHP session-handling 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 =============== @@ -1396,7 +1515,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 @@ -1405,6 +1524,11 @@ if anyone's been overlooked in error. * Tobias Diekershoff * Dan Moore * Fil +* Jeff Mitchell +* Brenda Wallace +* Jeffery To +* Federico Marani +* Craig Andrews 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/api.php b/actions/api.php index 08f5fadad9..4a00b77e8d 100644 --- a/actions/api.php +++ b/actions/api.php @@ -75,14 +75,14 @@ class ApiAction extends Action } } 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(); } @@ -117,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', @@ -127,7 +127,8 @@ class ApiAction extends Action 'help/downtime_schedule', 'laconica/version', 'laconica/config', - 'laconica/wadl'); + 'laconica/wadl', + 'groups/timeline'); static $bareauth = array('statuses/user_timeline', 'statuses/friends_timeline', @@ -135,28 +136,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 { - return true; } + + // 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/facebookhome.php b/actions/facebookhome.php index 34989c9786..6d8d0745d7 100644 --- a/actions/facebookhome.php +++ b/actions/facebookhome.php @@ -57,7 +57,7 @@ class FacebookhomeAction extends FacebookAction // If this is the first time the user has started the app // prompt for Facebook status update permission - if (!$this->facebook->api_client->users_hasAppPermission('status_update')) { + if (!$this->facebook->api_client->users_hasAppPermission('publish_stream')) { if ($this->facebook->api_client->data_getUserPreference( FACEBOOK_PROMPTED_UPDATE_PREF) != 'true') { @@ -203,7 +203,7 @@ class FacebookhomeAction extends FacebookAction $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=publish_stream&next=' . $next . '&next_cancel=' . $next . '&submit=skip'; $this->elementStart('span', array('class' => 'facebook-button')); diff --git a/actions/facebooklogin.php b/actions/facebooklogin.php index 22007da4fa..aa86cfbc0c 100644 --- a/actions/facebooklogin.php +++ b/actions/facebooklogin.php @@ -31,7 +31,7 @@ class FacebookinviteAction extends FacebookAction $this->error = $error; if ($this->flink) { - if (!$this->facebook->api_client->users_hasAppPermission('status_update') && + if (!$this->facebook->api_client->users_hasAppPermission('publish_stream') && $this->facebook->api_client->data_getUserPreference( FACEBOOK_PROMPTED_UPDATE_PREF) == 'true') { @@ -60,7 +60,7 @@ class FacebookinviteAction extends FacebookAction // If this is the first time the user has started the app // prompt for Facebook status update permission - if (!$this->facebook->api_client->users_hasAppPermission('status_update')) { + if (!$this->facebook->api_client->users_hasAppPermission('publish_stream')) { if ($this->facebook->api_client->data_getUserPreference( FACEBOOK_PROMPTED_UPDATE_PREF) != 'true') { diff --git a/actions/facebooksettings.php b/actions/facebooksettings.php index ee2c279ab5..c3b364743a 100644 --- a/actions/facebooksettings.php +++ b/actions/facebooksettings.php @@ -78,7 +78,7 @@ class FacebooksettingsAction extends FacebookAction } } - if ($this->facebook->api_client->users_hasAppPermission('status_update')) { + if ($this->facebook->api_client->users_hasAppPermission('publish_stream')) { $this->elementStart('form', array('method' => 'post', 'id' => 'facebook_settings')); @@ -131,7 +131,7 @@ class FacebooksettingsAction extends FacebookAction $this->elementStart('ul', array('id' => 'fb-permissions-list')); $this->elementStart('li', array('id' => 'fb-permissions-item')); - $this->elementStart('fb:prompt-permission', array('perms' => 'status_update', + $this->elementStart('fb:prompt-permission', array('perms' => 'publish_stream', 'next_fbjs' => 'document.setLocation(\'' . "$this->app_uri/settings.php" . '\')')); $this->element('span', array('class' => 'facebook-button'), sprintf(_('Allow %s to update my Facebook status'), common_config('site', 'name'))); diff --git a/actions/favorited.php b/actions/favorited.php index c902d80f53..156c7a7009 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/groupdesignsettings.php b/actions/groupdesignsettings.php index 6c1c052cba..bb01243c6e 100644 --- a/actions/groupdesignsettings.php +++ b/actions/groupdesignsettings.php @@ -312,36 +312,4 @@ class GroupDesignSettingsAction extends DesignSettingsAction $this->showForm(_('Design preferences saved.'), true); } - /** - * Handle input and output a page (overrided) - * - * @param array $args $_REQUEST arguments - * - * @return void - */ - - function handle($args) - { - parent::handle($args); - if (!common_logged_in()) { - $this->clientError(_('Not logged in.')); - return; - } else if (!common_is_real_login()) { - // Cookie theft means that automatic logins can't - // change important settings or see private info, and - // _all_ our settings are important - common_set_returnto($this->selfUrl()); - $user = common_current_user(); - if ($user->hasOpenID()) { - common_redirect(common_local_url('openidlogin'), 303); - } else { - common_redirect(common_local_url('login'), 303); - } - } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); - } else { - $this->showForm(); - } - } - } diff --git a/actions/newnotice.php b/actions/newnotice.php index 5f44a32a96..e254eac499 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -135,7 +135,7 @@ class NewnoticeAction extends Action function isRespectsQuota($user) { $file = new File; - $ret = $file->isRespectsQuota($user); + $ret = $file->isRespectsQuota($user,$_FILES['attach']['size']); if (true === $ret) return true; $this->clientError($ret); } diff --git a/actions/othersettings.php b/actions/othersettings.php index b542233ca7..1277f80527 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/showgroup.php b/actions/showgroup.php index ce11d574e9..32ec674a9b 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -317,8 +317,25 @@ class ShowgroupAction extends GroupDesignAction common_local_url('grouprss', array('nickname' => $this->group->nickname)); - return array(new Feed(Feed::RSS1, $url, sprintf(_('Notice feed for %s group'), - $this->group->nickname))); + return array(new Feed(Feed::RSS1, + common_local_url('grouprss', + array('nickname' => $this->group->nickname)), + sprintf(_('Notice feed for %s group (RSS 1.0)'), + $this->group->nickname)), + new Feed(Feed::RSS2, + common_local_url('api', + array('apiaction' => 'groups', + 'method' => 'timeline', + 'argument' => $this->group->nickname.'.rss')), + sprintf(_('Notice feed for %s group (RSS 2.0)'), + $this->group->nickname)), + new Feed(Feed::ATOM, + common_local_url('api', + array('apiaction' => 'groups', + 'method' => 'timeline', + 'argument' => $this->group->nickname.'.atom')), + sprintf(_('Notice feed for %s group (Atom)'), + $this->group->nickname))); } /** @@ -466,4 +483,4 @@ class GroupAdminSection extends ProfileSection { return null; } -} \ No newline at end of file +} diff --git a/actions/twitapigroups.php b/actions/twitapigroups.php new file mode 100644 index 0000000000..db15b2cd35 --- /dev/null +++ b/actions/twitapigroups.php @@ -0,0 +1,114 @@ +. + * + * @category Twitter + * @package Laconica + * @author Craig Andrews + * @author Zach Copley + * @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/twitterapi.php'; + +/** + * Group-specific API methods + * + * This class handles Laconica group API methods. + * + * @category Twitter + * @package Laconica + * @author Craig Andrews + * @author Zach Copley + * @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 TwitapigroupsAction extends TwitterapiAction + { + + function timeline($args, $apidata) + { + parent::handle($args); + + common_debug("in groups api action"); + + $this->auth_user = $apidata['user']; + $group = $this->get_group($apidata['api_arg'], $apidata); + + if (empty($group)) { + $this->clientError('Not Found', 404, $apidata['content-type']); + return; + } + + $sitename = common_config('site', 'name'); + $title = sprintf(_("%s timeline"), $group->nickname); + $taguribase = common_config('integration', 'taguri'); + $id = "tag:$taguribase:GroupTimeline:".$group->id; + $link = common_local_url('showgroup', + array('nickname' => $group->nickname)); + $subtitle = sprintf(_('Updates from %1$s on %2$s!'), + $group->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'); + + $notice = $group->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); + break; + case 'atom': + if (isset($apidata['api_arg'])) { + $selfuri = common_root_url() . + 'api/statuses/groups/timeline/' . + $apidata['api_arg'] . '.atom'; + } else { + $selfuri = common_root_url() . + 'api/statuses/groups/timeline.atom'; + } + $this->show_atom_timeline($notice, $title, $id, $link, + $subtitle, $suplink, $selfuri); + break; + case 'json': + $this->show_json_timeline($notice); + break; + default: + $this->clientError(_('API method not found!'), $code = 404); + } + } + +} \ No newline at end of file diff --git a/actions/twitapistatuses.php b/actions/twitapistatuses.php index 555c746cbc..c9943698dc 100644 --- a/actions/twitapistatuses.php +++ b/actions/twitapistatuses.php @@ -373,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') { @@ -389,7 +399,6 @@ class TwitapistatusesAction extends TwitterapiAction $this->clientError(_('No status with that ID found.'), 404, $apidata['content-type']); } - } function destroy($args, $apidata) diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php index 4057b63e74..fea41b3971 100644 --- a/actions/twitapiusers.php +++ b/actions/twitapiusers.php @@ -37,24 +37,24 @@ class TwitapiusersAction extends TwitterapiAction $user = null; $email = $this->arg('email'); - $user_id = $this->arg('user_id'); // XXX: email field deprecated in Twitter's API - // XXX: Also: need to add screen_name param - if ($email) { $user = User::staticGet('email', $email); - } elseif ($user_id) { - $user = $this->get_user($user_id); - } elseif (isset($apidata['api_arg'])) { - $user = $this->get_user($apidata['api_arg']); - } elseif (isset($apidata['user'])) { - $user = $apidata['user']; + } else { + $user = $this->get_user($apidata['api_arg'], $apidata); } if (empty($user)) { - $this->client_error(_('Not found.'), 404, $apidata['content-type']); + $this->clientError(_('Not found.'), 404, $apidata['content-type']); + return; + } + + $profile = $user->getProfile(); + + if (!$profile) { + common_server_error(_('User has no profile.')); return; } diff --git a/classes/File.php b/classes/File.php index 5dd7cd8651..533cc6e71d 100644 --- a/classes/File.php +++ b/classes/File.php @@ -122,17 +122,17 @@ class File extends Memcached_DataObject return $x; } - function isRespectsQuota($user) { - if ($_FILES['attach']['size'] > common_config('attachments', 'file_quota')) { + 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'), $_FILES['attach']['size']); + 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 + $_FILES['attach']['size']; + $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')); } @@ -140,7 +140,7 @@ class File extends Memcached_DataObject $query .= ' month(modified) = month(now()) and year(modified) = year(now())'; $this->query($query); $this->fetch(); - $total = $this->total + $_FILES['attach']['size']; + $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')); } diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 96f8d520ba..f7cbb9d5b6 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -33,41 +33,21 @@ class Memcached_DataObject extends DB_DataObject $k = $keys[0]; unset($i); } - $i = self::getcached($cls, $k, $v); + $i = Memcached_DataObject::getcached($cls, $k, $v); if ($i) { return $i; } else { $i = DB_DataObject::staticGet($cls, $k, $v); if ($i) { $i->encache(); - } else { - self::cachenull($cls, $k, $v); } return $i; } } - function cachenull($cls, $k, $v) - { - $c = self::memcache(); - if (empty($c)) { - return; - } - $c->set(self::cacheKey($cls, $k, $v), null); - } - - function multicachenull($cls, $kv) - { - $c = self::memcache(); - if (empty($c)) { - return; - } - $c->set(self::multicachekey($cls, $kv), null); - } - function &pkeyGet($cls, $kv) { - $i = self::multicache($cls, $kv); + $i = Memcached_DataObject::multicache($cls, $kv); if ($i) { return $i; } else { @@ -78,7 +58,6 @@ class Memcached_DataObject extends DB_DataObject if ($i->find(true)) { $i->encache(); } else { - self::multicachenull($cls, $kv); $i = null; } return $i; @@ -88,9 +67,6 @@ class Memcached_DataObject extends DB_DataObject function insert() { $result = parent::insert(); - if ($result) { - $this->encache(); - } return $result; } @@ -121,11 +97,11 @@ class Memcached_DataObject extends DB_DataObject } static function getcached($cls, $k, $v) { - $c = self::memcache(); + $c = Memcached_DataObject::memcache(); if (!$c) { return false; } else { - return $c->get(self::cacheKey($cls, $k, $v)); + return $c->get(Memcached_DataObject::cacheKey($cls, $k, $v)); } } @@ -192,23 +168,17 @@ class Memcached_DataObject extends DB_DataObject function multicache($cls, $kv) { - $c = self::memcache(); + ksort($kv); + $c = Memcached_DataObject::memcache(); if (!$c) { return false; } else { - return $c->get(self::multicachekey($cls, $kv)); + $pkeys = implode(',', array_keys($kv)); + $pvals = implode(',', array_values($kv)); + return $c->get(Memcached_DataObject::cacheKey($cls, $pkeys, $pvals)); } } - function multicachekey($cls, $kv) - { - ksort($kv); - $pkeys = implode(',', array_keys($kv)); - $pvals = implode(',', array_values($kv)); - - return self::cacheKey($cls, $pkeys, $pvals); - } - function getSearchEngine($table) { require_once INSTALLDIR.'/lib/search_engines.php'; @@ -241,7 +211,7 @@ class Memcached_DataObject extends DB_DataObject static function cachedQuery($cls, $qry, $expiry=3600) { - $c = self::memcache(); + $c = Memcached_DataObject::memcache(); if (!$c) { $inst = new $cls(); $inst->query($qry); diff --git a/classes/Notice.php b/classes/Notice.php index 8a018068ae..75044cf638 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -356,6 +356,8 @@ class Notice extends Memcached_DataObject $this->blowTagCache($blowLast); $this->blowGroupCache($blowLast); $this->blowConversationCache($blowLast); + $profile = Profile::staticGet($this->profile_id); + $profile->blowNoticeCount(); } function blowConversationCache($blowLast=false) @@ -1164,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(); @@ -1210,7 +1224,7 @@ class Notice extends Memcached_DataObject $window = explode(',', $laststr); $last_id = $window[0]; $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - $last_id, 0, null, $tag))); + $last_id, 0, null))); $new_window = array_merge($new_ids, $window); @@ -1225,7 +1239,7 @@ class Notice extends Memcached_DataObject } $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - 0, 0, null, $tag))); + 0, 0, null))); $windowstr = implode(',', $window); diff --git a/classes/Profile.php b/classes/Profile.php index a0ed6b3ca3..224b61bd2e 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -337,4 +337,132 @@ class Profile extends Memcached_DataObject 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/Queue_item.php b/classes/Queue_item.php index 9b909ec22b..295c321b57 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/User.php b/classes/User.php index 04b38a0d22..6c1f149e4d 100644 --- a/classes/User.php +++ b/classes/User.php @@ -494,6 +494,8 @@ class User extends Memcached_DataObject $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() diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql index b213bbd502..dae8b8fafe 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) ); @@ -436,8 +438,8 @@ create table file ( mimetype varchar(50), size integer, title varchar(255), - date integer(11), - protected integer(1) + date integer, + protected integer ); create sequence file_oembed_seq; @@ -454,7 +456,7 @@ create table file_oembed ( title varchar(255), author_name varchar(50), author_url varchar(255), - url varchar(255), + url varchar(255) ); create sequence file_redirection_seq; @@ -484,6 +486,18 @@ create table file_to_post ( 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 */ diff --git a/doc-src/tos b/doc-src/tos new file mode 100644 index 0000000000..bcfc319814 --- /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. + +
    + +
  1. Your %%site.name%% Account and Site. 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.
  2. + +
  3. Responsibility of Contributors. 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: + +
      + +
    • 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;
    • + +
    • 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;
    • + +
    • 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;
    • + +
    • the Content does not contain or install any viruses, worms, malware, +Trojan horses or other harmful or destructive content;
    • + +
    • 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);
    • + +
    • 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,
    • + +
    • 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;
    • + +
    • 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;
    • + +
    • 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
    • + +
    • 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.
    • + +
    + +

    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.

    + +

    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 %%license.title%%.

    + +

    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.

    + +

    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.

    +
  4. + +
  5. Responsibility of Website Visitors. 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.
  6. + +
  7. Content Posted on Other Websites. 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.
  8. + +
  9. Copyright Infringement and DMCA Policy. 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.
  10. + +
  11. Intellectual Property. 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.
  12. + +
  13. Changes. 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.
  14. + +
  15. Termination. 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.
  16. + +
  17. Disclaimer of Warranties. 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.
  18. + +
  19. Limitation of Liability. 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.
  20. + +
  21. General Representation and Warranty. 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.
  22. + +
  23. Indemnification. 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.
  24. + +
  25. Miscellaneous. 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.
+ +*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/js/util.js b/js/util.js index 9bb7c91288..bbcbc0bbb9 100644 --- a/js/util.js +++ b/js/util.js @@ -223,6 +223,7 @@ $(document).ready(function(){ } $("#notice_data-text").val(""); $("#notice_data-attach").val(""); + $("#notice_in-reply-to").val(""); $('#notice_data-attach_selected').remove(); counter(); } @@ -282,7 +283,7 @@ function NoticeAttachments() { }, timeout : 0, autoHide : true, - css : {'max-width':'502px', 'top':'22.5%', 'left':'32.5%'} + css : {'max-width':'542px', 'top':'22.5%', 'left':'32.5%'} }; $('#content .notice a.attachment').click(function() { diff --git a/lib/action.php b/lib/action.php index c89fe180ab..95ee10c642 100644 --- a/lib/action.php +++ b/lib/action.php @@ -439,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'); } @@ -708,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')), @@ -769,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', diff --git a/lib/command.php b/lib/command.php index 5646613829..4e2280bc80 100644 --- a/lib/command.php +++ b/lib/command.php @@ -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/common.php b/lib/common.php index 14be747bc7..c47702779d 100644 --- a/lib/common.php +++ b/lib/common.php @@ -19,7 +19,7 @@ if (!defined('LACONICA')) { exit(1); } -define('LACONICA_VERSION', '0.8.0dev'); +define('LACONICA_VERSION', '0.8.0'); define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); @@ -206,7 +206,7 @@ $config = 'inboxes' => array('enabled' => true), # on by default for new sites 'newuser' => - array('subscribe' => null, + array('default' => null, 'welcome' => null), 'snapshot' => array('run' => 'web', @@ -282,6 +282,39 @@ 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). diff --git a/lib/currentuserdesignaction.php b/lib/currentuserdesignaction.php index 7c2520cf67..4c7e15a8b7 100644 --- a/lib/currentuserdesignaction.php +++ b/lib/currentuserdesignaction.php @@ -53,14 +53,19 @@ class CurrentUserDesignAction extends Action * * @return nothing */ + function showStylesheets() { parent::showStylesheets(); - $design = $this->getDesign(); + $user = common_current_user(); - if (!empty($design)) { - $design->showCSS($this); + if (empty($user) || $user->viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } } } @@ -84,5 +89,4 @@ class CurrentUserDesignAction extends Action return $cur->getDesign(); } - } diff --git a/lib/dbqueuemanager.php b/lib/dbqueuemanager.php new file mode 100644 index 0000000000..6e7172de00 --- /dev/null +++ b/lib/dbqueuemanager.php @@ -0,0 +1,166 @@ +. + * + * @category QueueManager + * @package Laconica + * @author Evan Prodromou + * @author Sarven Capadisli + * @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/facebookaction.php b/lib/facebookaction.php index 1ae90d53bd..5be2f2fe66 100644 --- a/lib/facebookaction.php +++ b/lib/facebookaction.php @@ -460,16 +460,6 @@ class FacebookAction extends Action } } - 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() { @@ -504,7 +494,7 @@ class FacebookAction extends Action $replyto = $this->trimmed('inreplyto'); $notice = Notice::saveNew($user->id, $content, - 'Facebook', 1, ($replyto == 'false') ? null : $replyto); + 'web', 1, ($replyto == 'false') ? null : $replyto); if (is_string($notice)) { $this->showPage($notice); @@ -514,8 +504,7 @@ class FacebookAction extends Action common_broadcast_notice($notice); // Also update the user's Facebook status - $this->updateFacebookStatus($notice); - $this->updateProfileBox($notice); + facebookBroadcastNotice($notice); } diff --git a/lib/facebookutil.php b/lib/facebookutil.php index 4d0df797be..85077c254a 100644 --- a/lib/facebookutil.php +++ b/lib/facebookutil.php @@ -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 " . @@ -82,14 +86,18 @@ function isFacebookBound($notice, $flink) { // Check to see if the user has given the FB app status update perms $result = $facebook->api_client-> - users_hasAppPermission('status_update', $fbuid); + users_hasAppPermission('publish_stream', $fbuid); + if ($result != 1) { + $result = $facebook->api_client-> + users_hasAppPermission('status_update', $fbuid); + } 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); + 'Facebook app \'status_update\' or \'publish_stream\' permission.'; + common_debug($msg); $success = false; } @@ -108,13 +116,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 +133,128 @@ 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); + $result = $facebook->api_client-> + users_hasAppPermission('publish_stream', $fbuid); + if($result == 1){ + // authorized to use the stream api, so use it + $fbattachment = null; + $attachments = $notice->attachments(); + if($attachments){ + $fbattachment=array(); + $fbattachment['media']=array(); + //facebook only supports one attachment per item + $attachment = $attachments[0]; + $fbmedia=array(); + if(strncmp($attachment->mimetype,'image/',strlen('image/'))==0){ + $fbmedia['type']='image'; + $fbmedia['src']=$attachment->url; + $fbmedia['href']=$attachment->url; + $fbattachment['media'][]=$fbmedia; +/* Video doesn't seem to work. The notice never makes it to facebook, and no error is reported. + }else if(strncmp($attachment->mimetype,'video/',strlen('image/'))==0 || $attachment->mimetype="application/ogg"){ + $fbmedia['type']='video'; + $fbmedia['video_src']=$attachment->url; + // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29 + // says that preview_img is required... but we have no value to put in it + // $fbmedia['preview_img']=$attachment->url; + if($attachment->title){ + $fbmedia['video_title']=$attachment->title; + } + $fbmedia['video_type']=$attachment->mimetype; + $fbattachment['media'][]=$fbmedia; +*/ + }else if($attachment->mimetype=='audio/mpeg'){ + $fbmedia['type']='mp3'; + $fbmedia['src']=$attachment->url; + $fbattachment['media'][]=$fbmedia; + }else if($attachment->mimetype=='application/x-shockwave-flash'){ + $fbmedia['type']='flash'; + // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29 + // says that imgsrc is required... but we have no value to put in it + // $fbmedia['imgsrc']=''; + $fbmedia['swfsrc']=$attachment->url; + $fbattachment['media'][]=$fbmedia; + }else{ + $fbattachment['name']=($attachment->title?$attachment->title:$attachment->url); + $fbattachment['href']=$attachment->url; + } + } + $facebook->api_client->stream_publish($status, $fbattachment, null, null, $fbuid); + }else{ + $facebook->api_client->users_setStatus($status, $fbuid, false, true); + } } 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)!"); - // Should we remove flink if this fails? + $code = $e->getCode(); + + 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 or publish_stream. + // 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/groupdesignaction.php b/lib/groupdesignaction.php index bc95921f1b..58777c283a 100644 --- a/lib/groupdesignaction.php +++ b/lib/groupdesignaction.php @@ -34,7 +34,7 @@ if (!defined('LACONICA')) { /** * Base class for actions that use a group's design * - * Pages related to groups can be themed with a design. + * Pages related to groups can be themed with a design. * This superclass returns that design. * * @category Action @@ -48,7 +48,7 @@ class GroupDesignAction extends Action { /** The group in question */ var $group = null; - + /** * Show the groups's design stylesheet * @@ -58,10 +58,14 @@ class GroupDesignAction extends Action { { parent::showStylesheets(); - $design = $this->getDesign(); + $user = common_current_user(); - if (!empty($design)) { - $design->showCSS($this); + if (empty($user) || $user->viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } } } @@ -76,12 +80,10 @@ class GroupDesignAction extends Action { function getDesign() { - if (empty($this->group)) { return null; } return $this->group->getDesign(); } - } diff --git a/lib/jabber.php b/lib/jabber.php index 7d584ad016..e15076160f 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/mail.php b/lib/mail.php index 4e1f1dbb1d..90ee3c9928 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/ownerdesignaction.php b/lib/ownerdesignaction.php index 424474f429..785b8a93d3 100644 --- a/lib/ownerdesignaction.php +++ b/lib/ownerdesignaction.php @@ -61,11 +61,15 @@ class OwnerDesignAction extends Action { { parent::showStylesheets(); - $design = $this->getDesign(); + $user = common_current_user(); - if (!empty($design)) { - $design->showCSS($this); - } + if (empty($user) || $user->viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } + } } /** diff --git a/lib/ping.php b/lib/ping.php index 3de541e9aa..d26c734175 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 375d5538be..e47c9b3855 100644 --- a/lib/popularnoticesection.php +++ b/lib/popularnoticesection.php @@ -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 eeb5dbe48d..9e9c79c78a 100644 --- a/lib/profileaction.php +++ b/lib/profileaction.php @@ -163,18 +163,9 @@ class ProfileAction extends OwnerDesignAction 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')); @@ -199,7 +190,7 @@ class ProfileAction extends OwnerDesignAction 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'); @@ -208,12 +199,12 @@ class ProfileAction extends OwnerDesignAction 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'); diff --git a/lib/profilesection.php b/lib/profilesection.php index 8ed290e03a..9ff243fb53 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 c1c4f3309a..c2ff10f32f 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -17,14 +17,16 @@ * along with this program. If not, see . */ -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'; @@ -38,6 +40,11 @@ class QueueHandler extends Daemon } } + function timeout() + { + return 60; + } + function class_name() { return ucfirst($this->transport()) . 'Handler'; @@ -76,110 +83,21 @@ class QueueHandler extends Daemon return true; } - function db_dispatch() { - do { - $qi = Queue_item::top($this->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); - } - - function stomp_dispatch() { - - // use an external message queue system via STOMP - require_once("Stomp.php"); - - $server = common_config('queue','stomp_server'); - $username = common_config('queue', 'stomp_username'); - $password = common_config('queue', 'stomp_password'); - - $con = new Stomp($server); - - if (!$con->connect($username, $password)) { - $this->log(LOG_ERR, 'Failed to connect to queue server'); - return false; - } - - $queue_basename = common_config('queue','queue_basename'); - // subscribe to the relevant queue (format: basename-transport) - $con->subscribe('/queue/'.$queue_basename.'-'.$this->transport()); - - do { - $frame = $con->readFrame(); - if ($frame) { - $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($frame->headers['created'])); - - // XXX: Now the queue handler receives only the ID of the - // notice, and it has to get it from the DB - // A massive improvement would be avoid DB query by transmitting - // all the notice details via queue server... - $notice = Notice::staticGet($frame->body); - - if ($notice) { - $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id); - $result = $this->handle_notice($notice); - if ($result) { - // if the msg has been handled positively, ack it - // and the queue server will remove it from the queue - $con->ack($frame); - $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id); - } - else { - // no ack - $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id); - } - $notice->free(); - unset($notice); - $notice = null; - } else { - $this->log(LOG_WARNING, 'queue item for notice that does not exist'); - } - } - } while (true); - - $con->disconnect(); - } - function run() { if (!$this->start()) { return false; } + $this->log(LOG_INFO, 'checking for queued notices'); - if (common_config('queue','subsystem') == 'stomp') { - $this->stomp_dispatch(); - } - else { - $this->db_dispatch(); - } + + $queue = $this->transport(); + $timeout = $this->timeout(); + + $qm = QueueManager::get(); + + $qm->service($queue, $this); + if (!$this->finish()) { return false; } @@ -188,24 +106,19 @@ class QueueHandler extends Daemon function idle($timeout=0) { - if ($timeout>0) { + if ($timeout > 0) { sleep($timeout); } } - function clear_old_claims() - { - $qi = new Queue_item(); - $qi->transport = $this->transport(); - $qi->whereAdd('now() - claimed > '.CLAIM_TIMEOUT); - $qi->update(DB_DATAOBJECT_WHEREADD_ONLY); - $qi->free(); - unset($qi); - } - function log($level, $msg) { common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg); } + + function getSockets() + { + return array(); + } } diff --git a/lib/queuemanager.php b/lib/queuemanager.php new file mode 100644 index 0000000000..582c247901 --- /dev/null +++ b/lib/queuemanager.php @@ -0,0 +1,74 @@ +. + * + * @category QueueManager + * @package Laconica + * @author Evan Prodromou + * @author Sarven Capadisli + * @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 50b733453e..8104d7818c 100644 --- a/lib/router.php +++ b/lib/router.php @@ -261,12 +261,12 @@ 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', 'apiaction' => 'statuses'), - array('method' => '(user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)')); + array('method' => '(|user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)')); // users @@ -394,6 +394,15 @@ class Router array('action' => 'api', 'apiaction' => 'laconica')); + // Groups + $m->connect('api/laconica/groups/:method/:argument', + array('action' => 'api', + 'apiaction' => 'groups')); + + $m->connect('api/laconica/groups/:method', + array('action' => 'api', + 'apiaction' => 'groups')); + // search $m->connect('api/search.atom', array('action' => 'twitapisearchatom')); $m->connect('api/search.json', array('action' => 'twitapisearchjson')); diff --git a/lib/rssaction.php b/lib/rssaction.php index 0c8188e880..fe3fd6f4a2 100644 --- a/lib/rssaction.php +++ b/lib/rssaction.php @@ -216,6 +216,13 @@ class Rss10Action extends Action $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; } @@ -251,6 +258,8 @@ class Rss10Action extends Action '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' => diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php new file mode 100644 index 0000000000..46baeb5c73 --- /dev/null +++ b/lib/stompqueuemanager.php @@ -0,0 +1,169 @@ +. + * + * @category QueueManager + * @package Laconica + * @author Evan Prodromou + * @author Sarven Capadisli + * @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/subs.php b/lib/subs.php index 3bd67b39c7..e760237527 100644 --- a/lib/subs.php +++ b/lib/subs.php @@ -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/twitter.php b/lib/twitter.php index 3ec082686a..47af32e61f 100644 --- a/lib/twitter.php +++ b/lib/twitter.php @@ -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 40e5b50677..4f3a5c0b6f 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -89,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'] = ''; @@ -98,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); @@ -129,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; @@ -207,7 +197,6 @@ class TwitterapiAction extends Action function twitter_rss_entry_array($notice) { - $profile = $notice->getProfile(); $entry = array(); @@ -224,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); @@ -378,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'); } @@ -765,6 +774,34 @@ class TwitterapiAction extends Action } } + function get_group($id, $apidata=null) + { + if (empty($id)) { + + if (is_numeric($this->arg('id'))) { + return User_group::staticGet($this->arg('id')); + } else if ($this->arg('id')) { + $nickname = common_canonical_nickname($this->arg('id')); + return User_group::staticGet('nickname', $nickname); + } else if ($this->arg('group_id')) { + // 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('group_id'))) { + return User_group::staticGet('id', $this->arg('group_id')); + } + } else if ($this->arg('group_name')) { + $nickname = common_canonical_nickname($this->arg('group_name')); + return User_group::staticGet('nickname', $nickname); + } + + } else if (is_numeric($id)) { + return User_group::staticGet($id); + } else { + $nickname = common_canonical_nickname($id); + return User_group::staticGet('nickname', $nickname); + } + } + function get_profile($id) { if (is_numeric($id)) { diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php new file mode 100644 index 0000000000..5154610725 --- /dev/null +++ b/lib/unqueuemanager.php @@ -0,0 +1,85 @@ +. + * + * @category QueueManager + * @package Laconica + * @author Evan Prodromou + * @author Sarven Capadisli + * @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 d4d79afb30..9e8ec41d25 100644 --- a/lib/util.php +++ b/lib/util.php @@ -862,165 +862,45 @@ 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('omb', 'sms', 'public', 'twitter', 'facebook', 'ping'); + static $localTransports = array('omb', + 'twitter', + 'facebook', + 'ping'); + static $allTransports = array('sms'); - if (common_config('xmpp', 'enabled')) - { + $transports = $allTransports; + + $xmpp = common_config('xmpp', 'enabled'); + + if ($xmpp) { $transports[] = 'jabber'; } - if (common_config('queue','subsystem') == 'stomp') { - common_enqueue_notice_stomp($notice, $transports); - } - else { - common_enqueue_notice_db($notice, $transports); - } - return $result; -} - -function common_enqueue_notice_stomp($notice, $transports) -{ - // use an external message queue system via STOMP - require_once("Stomp.php"); - - $server = common_config('queue','stomp_server'); - $username = common_config('queue', 'stomp_username'); - $password = common_config('queue', 'stomp_password'); - - $con = new Stomp($server); - - if (!$con->connect($username, $password)) { - common_log(LOG_ERR, 'Failed to connect to queue server'); - return false; - } - - $queue_basename = common_config('queue','queue_basename'); - - foreach ($transports as $transport) { - $result = $con->send('/queue/'.$queue_basename.'-'.$transport, // QUEUE - $notice->id, // BODY of the message - array ('created' => $notice->created)); - if (!$result) { - common_log(LOG_ERR, 'Error sending to '.$transport.' queue'); - return false; - } - common_log(LOG_DEBUG, 'complete remote queueing notice ID = ' . $notice->id . ' for ' . $transport); - } - - //send tags as headers, so they can be used as JMS selectors - common_log(LOG_DEBUG, 'searching for tags ' . $notice->id); - $tags = array(); - $tag = new Notice_tag(); - $tag->notice_id = $notice->id; - if ($tag->find()) { - while ($tag->fetch()) { - common_log(LOG_DEBUG, 'tag found = ' . $tag->tag); - array_push($tags,$tag->tag); + if ($notice->is_local == NOTICE_LOCAL_PUBLIC || + $notice->is_local == NOTICE_LOCAL_NONPUBLIC) { + $transports = array_merge($transports, $localTransports); + if ($xmpp) { + $transports[] = 'public'; } } - $tag->free(); - $con->send('/topic/laconica.'.$notice->profile_id, - $notice->content, - array( - 'profile_id' => $notice->profile_id, - 'created' => $notice->created, - 'tags' => implode($tags,' - ') - ) - ); - common_log(LOG_DEBUG, 'sent to personal topic ' . $notice->id); - $con->send('/topic/laconica.allusers', - $notice->content, - array( - 'profile_id' => $notice->profile_id, - 'created' => $notice->created, - 'tags' => implode($tags,' - ') - ) - ); - common_log(LOG_DEBUG, 'sent to catch-all topic ' . $notice->id); - $result = true; -} + $qm = QueueManager::get(); -function common_enqueue_notice_db($notice, $transports) -{ - // in any other case, 'internal' - foreach ($transports as $transport) { - common_enqueue_notice_transport($notice, $transport); + foreach ($transports as $transport) + { + $qm->enqueue($notice, $transport); } -} -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); - } - 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; -} - function common_broadcast_profile($profile) { // XXX: optionally use a queue system like http://code.google.com/p/microapps/wiki/NQDQ @@ -1148,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) { diff --git a/lib/xmppqueuehandler.php b/lib/xmppqueuehandler.php index 986e09c25e..77d476c30e 100644 --- a/lib/xmppqueuehandler.php +++ b/lib/xmppqueuehandler.php @@ -21,6 +21,8 @@ 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 * service their message queues on idle, and forward any incoming messages @@ -30,6 +32,9 @@ 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 @@ -44,6 +49,11 @@ class XmppQueueHandler extends QueueHandler return !is_null($this->conn); } + function timeout() + { + return 10; + } + function handle_reconnect(&$pl) { $this->conn->processUntil('session_start'); @@ -55,7 +65,13 @@ 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()); @@ -63,6 +79,22 @@ class XmppQueueHandler extends QueueHandler } } + 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(""); + } + function forward_message(&$pl) { if ($pl['type'] != 'chat') { @@ -91,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/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php index d8af1a4e86..65870a187b 100644 --- a/plugins/FBConnect/FBConnectPlugin.php +++ b/plugins/FBConnect/FBConnectPlugin.php @@ -313,8 +313,6 @@ class FBConnectPlugin extends Plugin $action->menuItem(common_local_url('register'), _('Register'), _('Create an account'), false, 'nav_register'); } - $action->menuItem(common_local_url('openidlogin'), - _('OpenID'), _('Login with OpenID'), false, 'nav_openid'); $action->menuItem(common_local_url('login'), _('Login'), _('Login to the site'), false, 'nav_login'); } diff --git a/scripts/maildaemon.php b/scripts/maildaemon.php index cfb11a36fc..11ddf06b75 100755 --- a/scripts/maildaemon.php +++ b/scripts/maildaemon.php @@ -42,11 +42,11 @@ class MailerDaemon 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.')); @@ -65,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; @@ -74,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"); @@ -133,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); @@ -163,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') { @@ -176,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/update_translations.php b/scripts/update_translations.php index 4d7adafea0..2f4ca87208 100755 --- a/scripts/update_translations.php +++ b/scripts/update_translations.php @@ -1,66 +1,97 @@ +#!/usr/bin/env 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('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'; +foreach ($languages as $language) { - /* Check for an existing */ - if (!is_dir($msgdir)) { - mkdir($lcdir); - mkdir($msgdir); - $existingSHA1 = ''; - } else { - $existingSHA1 = file_exists($pofile) ? sha1_file($pofile) : ''; - } + $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"; - /* Get the remote one */ - $newFile = file_get_contents($file); + /* Check for an existing */ + if (!is_dir($msgdir)) { + mkdir($lcdir); + mkdir($msgdir); + $existingSHA1 = ''; + } else { + $existingSHA1 = file_exists($pofile) ? sha1_file($pofile) : ''; + } - // 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"; - } + /* 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/xmppdaemon.php b/scripts/xmppdaemon.php index bd1918ca98..488b4b514c 100755 --- a/scripts/xmppdaemon.php +++ b/scripts/xmppdaemon.php @@ -60,7 +60,9 @@ class XMPPDaemon extends Daemon $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() @@ -106,10 +108,25 @@ class XMPPDaemon extends Daemon $this->log(LOG_DEBUG, "Beginning processing loop."); - $this->conn->process(); + 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(""); + } + function handle_reconnect(&$pl) { $this->log(LOG_DEBUG, "Got reconnection callback."); diff --git a/theme/base/css/display.css b/theme/base/css/display.css index f2b200376d..3426e71c0b 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -275,7 +275,7 @@ margin-bottom:18px; #anon_notice { float:left; -width:43.2%; +width:42.4%; padding:1.1%; border-radius:7px; -moz-border-radius:7px; @@ -396,7 +396,7 @@ margin-bottom:1em; } #content { -width:64.009%; +width:63.311%; min-height:259px; padding:1.795%; float:left; @@ -422,7 +422,7 @@ float:left; width:27.917%; min-height:259px; float:left; -margin-left:0.5%; +margin-left:0.699%; padding:1.795%; border-radius:7px; -moz-border-radius:7px; @@ -432,7 +432,7 @@ border-style:solid; } #form_notice { -width:45.664%; +width:45%; float:left; position:relative; line-height:1; @@ -471,12 +471,12 @@ cursor:pointer; } #form_notice label[for=notice_data-attach] { text-indent:-9999px; -left:394px; +left:86%; width:16px; height:16px; } #form_notice #notice_data-attach { -left:183px; +left:40.6%; padding:0; height:16px; } @@ -783,8 +783,8 @@ list-style-type:none; } .notices .notices { margin-top:7px; -margin-left:5%; -width:95%; +margin-left:2%; +width:98%; float:left; } @@ -1020,20 +1020,21 @@ font-weight:bold; padding:0; } #jOverlayContent h1 { -max-width:475px; +max-width:425px; } #jOverlayContent #content { border-radius:7px; -moz-border-radius:7px; -webkit-border-radius:7px; } -#jOverlayContent #content img { -max-width:480px; -} #jOverlayLoading { top:22.5%; left:40%; } +#attachment_view img { +max-width:480px; +max-height:480px; +} #attachment_view #oembed_info { margin-top:11px; } @@ -1278,6 +1279,7 @@ 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, diff --git a/theme/base/css/ie.css b/theme/base/css/ie.css index 43fb01492a..3e128b84ed 100644 --- a/theme/base/css/ie.css +++ b/theme/base/css/ie.css @@ -9,7 +9,7 @@ width:78%; #form_notice .form_note + label { position:absolute; top:25px; -left:380px; +left:83%; text-indent:-9999px; height:16px; width:16px; @@ -25,10 +25,6 @@ width:78.5%; #form_notice #notice_data-attach_selected button { padding:0 4px; } -#anon_notice { -max-width:39%; -} - .notice-options input.submit { font-size:0; margin-top:3px; @@ -49,6 +45,6 @@ z-index:1; .notice:hover { z-index:9999; } -.notice .thumbnail img { +.notice .thumbnail img { z-index:9999; -} \ No newline at end of file +}