Merge branch 'testing'

This commit is contained in:
Evan Prodromou 2009-07-11 07:30:26 -07:00
commit 5541565238
54 changed files with 2181 additions and 623 deletions

208
README
View File

@ -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.
- <link> 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<rootuser> -p<rootpassword> <database> db/074to080.sql
Otherwise, go to your Laconica directory and AFTER YOU MAKE A
BACKUP run the rebuilddb.sh script like this:
./scripts/rebuilddb.sh rootuser rootpassword database db/laconica.sql
Here, rootuser and rootpassword are the username and password for a
user who can drop and create databases as well as tables; typically
that's _not_ the user Laconica runs as.
that's _not_ the user Laconica runs as. Note that rebuilddb.sh drops
your database and rebuilds it; if there is an error you have no
database. Make sure you have a backup.
For PostgreSQL databases there is an equivalent, rebuilddb_psql.sh,
which operates slightly differently. Read the documentation in that
script before running it.
@ -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 <http://twitter.com/help/request_source>, you can use
that here instead. Status updates on Twitter will then have
links to your site.
taguri: base for tag:// URIs. Defaults to site-server + ',2009'.
inboxes
-------
@ -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,

View File

@ -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;
}
}

View File

@ -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'));

View File

@ -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') {

View File

@ -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')));

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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)));
}
/**

114
actions/twitapigroups.php Normal file
View File

@ -0,0 +1,114 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Laconica extensions to the Twitter-like API for groups
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Twitter
* @package Laconica
* @author Craig Andrews
* @author Zach Copley <zach@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
require_once INSTALLDIR.'/lib/twitterapi.php';
/**
* Group-specific API methods
*
* This class handles Laconica group API methods.
*
* @category Twitter
* @package Laconica
* @author Craig Andrews
* @author Zach Copley <zach@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
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);
}
}
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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'));
}

View File

@ -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);

View File

@ -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);

View File

@ -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));
}
}
}

View File

@ -54,4 +54,9 @@ class Queue_item extends Memcached_DataObject
$qi = null;
return null;
}
function &pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('Queue_item', $kv);
}
}

View File

@ -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()

View File

@ -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 */

300
doc-src/tos Normal file
View File

@ -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 youre 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. Theyd 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
shouldnt 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, Operators [Privacy
Policy](%%doc.privacy%%))
and procedures that may be published from time to time on this Site by
Operator (collectively, the “Agreement”).
Please read this Agreement carefully before accessing or using the
Website. By accessing or using any part of the web site, you agree to
become bound by the terms and conditions of this agreement. If you do
not agree to all the terms and conditions of this agreement, then you
may not access the Website or use any services. If these terms and
conditions are considered an offer by Operator, acceptance is
expressly limited to these terms. The Website is available only to
individuals who are at least 13 years old.
<ol>
<li><strong>Your %%site.name%% Account and Site.</strong> If you
create a notice stream on the Website, you are responsible for
maintaining the security of your account and notice stream, and you
are fully responsible for all activities that occur under the account
and any other actions taken in connection with the notice stream. You
must not describe or assign keywords to your notice stream in a
misleading or unlawful manner, including in a manner intended to trade
on the name or reputation of others, and Operator may change or remove
any description or keyword that it considers inappropriate or
unlawful, or otherwise likely to cause Operator liability. You must
immediately notify Operator of any unauthorized uses of your notice
stream, your account or any other breaches of security. Operator will
not be liable for any acts or omissions by You, including any damages
of any kind incurred as a result of such acts or omissions.</li>
<li><strong>Responsibility of Contributors.</strong> If you operate a
notice stream, comment on a notice stream, post material to the
Website, post links on the Website, or otherwise make (or allow any
third party to make) material available by means of the Website (any
such material, “Content”), You are entirely responsible for the
content of, and any harm resulting from, that Content. That is the
case regardless of whether the Content in question constitutes text,
graphics, an audio file, or computer software. By making Content
available, you represent and warrant that:
<ul>
<li>the downloading, copying and use of the Content will not infringe
the proprietary rights, including but not limited to the copyright,
patent, trademark or trade secret rights, of any third party;</li>
<li>if your employer has rights to intellectual property you create,
you have either (i) received permission from your employer to post or
make available the Content, including but not limited to any software,
or (ii) secured from your employer a waiver as to all rights in or to
the Content;</li>
<li>you have fully complied with any third-party licenses
relating to the Content, and have done all things necessary to
successfully pass through to end users any required terms;</li>
<li>the Content does not contain or install any viruses, worms, malware,
Trojan horses or other harmful or destructive content;</li>
<li>the Content is not spam, and does not contain unethical or
unwanted commercial content designed to drive traffic to third party
sites or boost the search engine rankings of third party sites, or to
further unlawful acts (such as phishing) or mislead recipients as to
the source of the material (such as spoofing);</li>
<li>if the Content is machine- or randomly-generated, it is for
purposes of direct entertainment, information and/or utility for you
or other users, and not for spam,</li>
<li>the Content is not libelous or defamatory (more info on
what that means), does not contain threats or incite violence towards
individuals or entities, and does not violate the privacy or publicity
rights of any third party;</li>
<li>your notice stream is not getting advertised via unwanted electronic
messages such as spam links on newsgroups, email lists, other notice streams
and web sites, and similar unsolicited promotional methods;</li>
<li>your notice stream is not named in a manner that misleads your
readers into thinking that you are another person or company. For
example, your notice streams URL or name is not the name of a person other
than yourself or company other than your own; and</li>
<li>you have, in the case of Content that includes computer code,
accurately categorized and/or described the type, nature, uses and
effects of the materials, whether requested to do so by Operator or
otherwise.</li>
</ul>
<p>By submitting Content to Operator for inclusion on your Website, you
grant Operator a world-wide, royalty-free, and non-exclusive license
to reproduce, modify, adapt and publish the Content solely for the
purpose of displaying, distributing and promoting your notice
stream.</p>
<p>By submitting Content to Operator for inclusion on your Website,
you grant all readers the right to use, re-use, modify and/or
re-distribute the Content under the terms of the <a
href="%%license.url%%">%%license.title%%</a>.</p>
<p>If you delete Content, Operator will use reasonable efforts to remove it from
the Website, but you acknowledge that caching or references to the
Content may not be made immediately unavailable.</p>
<p>Without limiting any of those representations or warranties, Operator
has the right (though not the obligation) to, in Operators sole
discretion (i) refuse or remove any content that, in Operators
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
Operators sole discretion.</p>
</li>
<li><strong>Responsibility of Website Visitors.</strong> Operator has not reviewed,
and cannot review, all of the material, including computer software,
posted to the Website, and cannot therefore be responsible for that
materials content, use or effects. By operating the Website,
Operator does not represent or imply that it endorses the material
there posted, or that it believes such material to be accurate, useful
or non-harmful. You are responsible for taking precautions as
necessary to protect yourself and your computer systems from viruses,
worms, Trojan horses, and other harmful or destructive content. The
Website may contain content that is offensive, indecent, or otherwise
objectionable, as well as content containing technical inaccuracies,
typographical mistakes, and other errors. The Website may also contain
material that violates the privacy or publicity rights, or infringes
the intellectual property and other proprietary rights, of third
parties, or the downloading, copying or use of which is subject to
additional terms and conditions, stated or unstated. Operator
disclaims any responsibility for any harm resulting from the use by
visitors of the Website, or from any downloading by those visitors of
content there posted.</li>
<li><strong>Content Posted on Other Websites.</strong> We have not reviewed, and
cannot review, all of the material, including computer software, made
available through the websites and webpages to which %%site.name%%
links, and that link to %%site.name%%. Operator does not have any
control over those external websites and webpages, and is not
responsible for their contents or their use. By linking to a
external website or webpage, Operator does not represent or
imply that it endorses such website or webpage. You are responsible
for taking precautions as necessary to protect yourself and your
computer systems from viruses, worms, Trojan horses, and other harmful
or destructive content. Operator disclaims any responsibility for
any harm resulting from your use of external websites and
webpages.</li>
<li><strong>Copyright Infringement and DMCA Policy.</strong> As Operator asks
others to respect its intellectual property rights, it respects the
intellectual property rights of others. If you believe that material
located on or linked to by %%site.name%% violates your copyright, you
are encouraged to notify Operator in accordance with Operators
Digital Millennium Copyright Act (”DMCA”) Policy. Operator will
respond to all such notices, including as required or appropriate by
removing the infringing material or disabling all links to the
infringing material. In the case of a visitor who may infringe or
repeatedly infringes the copyrights or other intellectual property
rights of Operator or others, Operator may, in its discretion,
terminate or deny access to and use of the Website. In the case of
such termination, Operator will have no obligation to provide a
refund of any amounts previously paid to Operator.</li>
<li><strong>Intellectual Property.</strong> This Agreement does not
transfer from Operator to you any Operator or third party intellectual
property, and all right, title and interest in and to such property
will remain (as between the parties) solely with Operator.
%%site.name%%, the %%site.name%% logo, and all other trademarks,
service marks, graphics and logos used in connection with
%%site.name%%, or the Website are trademarks or registered trademarks
of Operator or Operators licensors. Other trademarks, service marks,
graphics and logos used in connection with the Website may be the
trademarks of other third parties. Your use of the Website grants you
no right or license to reproduce or otherwise use any Operator or
third-party trademarks.</li>
<li><strong>Changes.</strong> Operator reserves the right, at its sole
discretion, to modify or replace any part of this Agreement. It is
your responsibility to check this Agreement periodically for changes.
Your continued use of or access to the Website following the posting
of any changes to this Agreement constitutes acceptance of those
changes. Operator may also, in the future, offer new services and/or
features through the Website (including, the release of new tools and
resources). Such new features and/or services shall be subject to the
terms and conditions of this Agreement.</li>
<li><strong>Termination.</strong> Operator may terminate your access
to all or any part of the Website at any time, with or without cause,
with or without notice, effective immediately. If you wish to
terminate this Agreement or your %%site.name%% account (if you have
one), you may simply discontinue using the Website. All provisions of
this Agreement which by their nature should survive termination shall
survive termination, including, without limitation, ownership
provisions, warranty disclaimers, indemnity and limitations of
liability.</li>
<li><strong>Disclaimer of Warranties.</strong> The Website is provided
“as is”. Operator and its suppliers and licensors hereby disclaim all
warranties of any kind, express or implied, including, without
limitation, the warranties of merchantability, fitness for a
particular purpose and non-infringement. Neither Operator nor its
suppliers and licensors, makes any warranty that the Website will be
error free or that access thereto will be continuous or uninterrupted.
If youre actually reading this, heres a treat. You understand that
you download from, or otherwise obtain content or services through,
the Website at your own discretion and risk.</li>
<li><strong>Limitation of Liability.</strong> In no event will
Operator, or its suppliers or licensors, be liable with respect to any
subject matter of this agreement under any contract, negligence,
strict liability or other legal or equitable theory for: (i) any
special, incidental or consequential damages; (ii) the cost of
procurement or substitute products or services; (iii) for interruption
of use or loss or corruption of data; or (iv) for any amounts that
exceed the fees paid by you to Operator under this agreement during
the twelve (12) month period prior to the cause of action. Operator
shall have no liability for any failure or delay due to matters beyond
their reasonable control. The foregoing shall not apply to the extent
prohibited by applicable law.</li>
<li><strong>General Representation and Warranty.</strong> You
represent and warrant that (i) your use of the Website will be in
strict accordance with the Operator Privacy Policy, with this
Agreement and with all applicable laws and regulations (including
without limitation any local laws or regulations in your country,
state, city, or other governmental area, regarding online conduct and
acceptable content, and including all applicable laws regarding the
transmission of technical data exported from the United States or the
country in which you reside) and (ii) your use of the Website will not
infringe or misappropriate the intellectual property rights of any
third party.</li>
<li><strong>Indemnification.</strong> You agree to indemnify and hold
harmless Operator, its contractors, and its licensors, and their
respective directors, officers, employees and agents from and against
any and all claims and expenses, including attorneys fees, arising
out of your use of the Website, including but not limited to out of
your violation this Agreement.</li>
<li><strong>Miscellaneous.</strong> This Agreement constitutes the
entire agreement between Operator and you concerning the subject
matter hereof, and they may only be modified by a written amendment
signed by an authorized executive of Operator, or by the posting by
Operator of a revised version. If any part of this Agreement is held
invalid or unenforceable, that part will be construed to reflect the
parties original intent, and the remaining portions will remain in
full force and effect. A waiver by either party of any term or
condition of this Agreement or any breach thereof, in any one
instance, will not waive such term or condition or any subsequent
breach thereof. You may assign your rights under this Agreement to any
party that consents to, and agrees to be bound by, its terms and
conditions; Operator may assign its rights under this Agreement
without condition. This Agreement will be binding upon and will inure
to the benefit of the parties, their successors and permitted
assigns.</li> </ol>
*Originally published by Automattic, Inc. as the [WordPress.com Terms
of Service](http://en.wordpress.com/tos/) and made available by them
under the [Creative Commons Attribution-ShareAlike 3.0
License](http://creativecommons.org/licenses/by-sa/3.0/).
Modifications to remove reference to "VIP services", rename "blog" to
"notice stream", remove the choice-of-venue clause, and add variables
specific to instances of this software made by Control Yourself, Inc.
and made available under the terms of the same license.*

View File

@ -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() {

View File

@ -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',

View File

@ -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".

View File

@ -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).

View File

@ -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();
}
}

166
lib/dbqueuemanager.php Normal file
View File

@ -0,0 +1,166 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Simple-minded queue manager for storing items in the database
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category QueueManager
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Sarven Capadisli <csarven@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class DBQueueManager extends QueueManager
{
var $qis = array();
function enqueue($object, $queue)
{
$notice = $object;
$qi = new Queue_item();
$qi->notice_id = $notice->id;
$qi->transport = $queue;
$qi->created = $notice->created;
$result = $qi->insert();
if (!$result) {
common_log_db_error($qi, 'INSERT', __FILE__);
throw new ServerException('DB error inserting queue item');
}
return true;
}
function service($queue, $handler)
{
while (true) {
$this->_log(LOG_DEBUG, 'Checking for notices...');
$notice = $this->_nextItem($queue, null);
if (empty($notice)) {
$this->_log(LOG_DEBUG, 'No notices waiting; idling.');
// Nothing in the queue. Do you
// have other tasks, like servicing your
// XMPP connection, to do?
$handler->idle(QUEUE_HANDLER_MISS_IDLE);
} else {
$this->_log(LOG_INFO, 'Got notice '. $notice->id);
// Yay! Got one!
if ($handler->handle_notice($notice)) {
$this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id);
$this->_done($notice, $queue);
} else {
$this->_log(LOG_INFO, 'Failed to handle notice '. $notice->id);
$this->_fail($notice, $queue);
}
// Chance to e.g. service your XMPP connection
$this->_log(LOG_DEBUG, 'Idling after success.');
$handler->idle(QUEUE_HANDLER_HIT_IDLE);
}
// XXX: when do we give up?
}
}
function _nextItem($queue, $timeout=null)
{
$start = time();
$result = null;
do {
$qi = Queue_item::top($queue);
if (!empty($qi)) {
$notice = Notice::staticGet('id', $qi->notice_id);
if (!empty($notice)) {
$result = $notice;
} else {
$this->_log(LOG_INFO, 'dequeued non-existent notice ' . $notice->id);
$qi->delete();
$qi->free();
$qi = null;
}
}
} while (empty($result) && (is_null($timeout) || (time() - $start) < $timeout));
return $result;
}
function _done($object, $queue)
{
// XXX: right now, we only handle notices
$notice = $object;
$qi = Queue_item::pkeyGet(array('notice_id' => $notice->id,
'transport' => $queue));
if (empty($qi)) {
$this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue);
} else {
if (empty($qi->claimed)) {
$this->_log(LOG_WARNING, 'Reluctantly releasing unclaimed queue item '.
'for '.$notice->id.', queue '.$queue);
}
$qi->delete();
$qi->free();
$qi = null;
}
$this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id);
$notice->free();
$notice = null;
}
function _fail($object, $queue)
{
// XXX: right now, we only handle notices
$notice = $object;
$qi = Queue_item::pkeyGet(array('notice_id' => $notice->id,
'transport' => $queue));
if (empty($qi)) {
$this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue);
} else {
if (empty($qi->claimed)) {
$this->_log(LOG_WARNING, 'Ignoring failure for unclaimed queue item '.
'for '.$notice->id.', queue '.$queue);
} else {
$orig = clone($qi);
$qi->claimed = null;
$qi->update($orig);
$qi = null;
}
}
$this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id);
$notice->free();
$notice = null;
}
function _log($level, $msg)
{
common_log($level, 'DBQueueManager: '.$msg);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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'),

View File

@ -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);
}

View File

@ -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);
}
}
}
/**

View File

@ -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])");

View File

@ -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;

View File

@ -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');

View File

@ -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);

View File

@ -17,14 +17,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('CLAIM_TIMEOUT', 1200);
if (!defined('LACONICA')) { exit(1); }
require_once(INSTALLDIR.'/lib/daemon.php');
require_once(INSTALLDIR.'/classes/Queue_item.php');
require_once(INSTALLDIR.'/classes/Notice.php');
define('CLAIM_TIMEOUT', 1200);
define('QUEUE_HANDLER_MISS_IDLE', 10);
define('QUEUE_HANDLER_HIT_IDLE', 0);
class QueueHandler extends Daemon
{
var $_id = 'generic';
@ -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();
}
}

74
lib/queuemanager.php Normal file
View File

@ -0,0 +1,74 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Abstract class for queue managers
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category QueueManager
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Sarven Capadisli <csarven@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class QueueManager
{
static $qm = null;
static function get()
{
if (empty(self::$qm)) {
if (Event::handle('StartNewQueueManager', array(&self::$qm))) {
$enabled = common_config('queue', 'enabled');
$type = common_config('queue', 'subsystem');
if (!$enabled) {
// does everything immediately
self::$qm = new UnQueueManager();
} else {
switch ($type) {
case 'db':
self::$qm = new DBQueueManager();
break;
case 'stomp':
self::$qm = new StompQueueManager();
break;
default:
throw new ServerException("No queue manager class for type '$type'");
}
}
}
}
return self::$qm;
}
function enqueue($object, $queue)
{
throw ServerException("Unimplemented function 'enqueue' called");
}
function service($queue, $handler)
{
throw ServerException("Unimplemented function 'service' called");
}
}

View File

@ -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'));

View File

@ -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' =>

169
lib/stompqueuemanager.php Normal file
View File

@ -0,0 +1,169 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Abstract class for queue managers
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category QueueManager
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Sarven Capadisli <csarven@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
require_once 'Stomp.php';
class LiberalStomp extends Stomp
{
function getSocket()
{
return $this->_socket;
}
}
class StompQueueManager
{
var $server = null;
var $username = null;
var $password = null;
var $base = null;
var $con = null;
function __construct()
{
$this->server = common_config('queue', 'stomp_server');
$this->username = common_config('queue', 'stomp_username');
$this->password = common_config('queue', 'stomp_password');
$this->base = common_config('queue', 'queue_basename');
}
function _connect()
{
if (empty($this->con)) {
$this->_log(LOG_INFO, "Connecting to '$this->server' as '$this->username'...");
$this->con = new LiberalStomp($this->server);
if ($this->con->connect($this->username, $this->password)) {
$this->_log(LOG_INFO, "Connected.");
} else {
$this->_log(LOG_ERR, 'Failed to connect to queue server');
throw new ServerException('Failed to connect to queue server');
}
}
}
function enqueue($object, $queue)
{
$notice = $object;
$this->_connect();
// XXX: serialize and send entire notice
$result = $this->con->send($this->_queueName($queue),
$notice->id, // BODY of the message
array ('created' => $notice->created));
if (!$result) {
common_log(LOG_ERR, 'Error sending to '.$queue.' queue');
return false;
}
common_log(LOG_DEBUG, 'complete remote queueing notice ID = '
. $notice->id . ' for ' . $queue);
}
function service($queue, $handler)
{
$result = null;
$this->_connect();
$this->con->setReadTimeout($handler->timeout());
$this->con->subscribe($this->_queueName($queue));
while (true) {
// Wait for something on one of our sockets
$stompsock = $this->con->getSocket();
$handsocks = $handler->getSockets();
$socks = array_merge(array($stompsock), $handsocks);
$read = $socks;
$write = array();
$except = array();
$ready = stream_select($read, $write, $except, $handler->timeout(), 0);
if ($ready === false) {
$this->_log(LOG_ERR, "Error selecting on sockets");
} else if ($ready > 0) {
if (in_array($stompsock, $read)) {
$this->_handleNotice($queue, $handler);
}
$handler->idle(QUEUE_HANDLER_HIT_IDLE);
}
}
$this->con->unsubscribe($this->_queueName($queue));
}
function _handleNotice($queue, $handler)
{
$frame = $this->con->readFrame();
if (!empty($frame)) {
$notice = Notice::staticGet('id', $frame->body);
if (empty($notice)) {
$this->_log(LOG_WARNING, 'Got ID '. $frame->body .' for non-existent notice in queue '. $queue);
$this->con->ack($frame);
} else {
if ($handler->handle_notice($notice)) {
$this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue);
$this->con->ack($frame);
} else {
$this->_log(LOG_WARNING, 'Failed handling notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue);
// FIXME we probably shouldn't have to do
// this kind of queue management ourselves
$this->con->ack($frame);
$this->enqueue($notice, $queue);
}
unset($notice);
}
unset($frame);
}
}
function _queueName($queue)
{
return common_config('queue', 'queue_basename') . $queue;
}
function _log($level, $msg)
{
common_log($level, 'StompQueueManager: '.$msg);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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)) {

85
lib/unqueuemanager.php Normal file
View File

@ -0,0 +1,85 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* A queue manager interface for just doing things immediately
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category QueueManager
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Sarven Capadisli <csarven@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class UnQueueManager
{
function enqueue($object, $queue)
{
$notice = $object;
switch ($queue)
{
case 'omb':
if ($this->_isLocal($notice)) {
require_once(INSTALLDIR.'/lib/omb.php');
omb_broadcast_remote_subscribers($notice);
}
break;
case 'public':
if ($this->_isLocal($notice)) {
require_once(INSTALLDIR.'/lib/jabber.php');
jabber_public_notice($notice);
}
break;
case 'twitter':
if ($this->_isLocal($notice)) {
broadcast_twitter($notice);
}
break;
case 'facebook':
if ($this->_isLocal($notice)) {
require_once INSTALLDIR . '/lib/facebookutil.php';
return facebookBroadcastNotice($notice);
}
break;
case 'ping':
if ($this->_isLocal($notice)) {
require_once INSTALLDIR . '/lib/ping.php';
return ping_broadcast_notice($notice);
}
case 'sms':
require_once(INSTALLDIR.'/lib/mail.php');
mail_broadcast_notice_sms($notice);
break;
case 'jabber':
require_once(INSTALLDIR.'/lib/jabber.php');
jabber_broadcast_notice($notice);
break;
default:
throw ServerException("UnQueueManager: Unknown queue: $type");
}
}
function _isLocal($notice)
{
return ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
$notice->is_local == NOTICE_LOCAL_NONPUBLIC);
}
}

View File

@ -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) {

View File

@ -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("<iq from='{$jid}' to='{$server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
}
function forward_message(&$pl)
{
if ($pl['type'] != 'chat') {
@ -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());
}
}

View File

@ -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');
}

View File

@ -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)

View File

@ -1,66 +1,97 @@
#!/usr/bin/env php
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, Control Yourself, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Abort if called from a web server
if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
print "This script must be run from the command line\n";
exit();
}
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
define('LACONICA', true);
require_once(INSTALLDIR . '/lib/common.php');
// Master Laconica .pot file location (created by update_pot.sh)
$laconica_pot = INSTALLDIR . '/locale/laconica.po';
set_time_limit(60);
chdir(dirname(__FILE__) . '/..');
/* Languages to pull */
$languages = array(
'da_DK' => 'http://laconi.ca/translate/download.php?file_id=93',
'nl_NL' => 'http://laconi.ca/translate/download.php?file_id=97',
'en_NZ' => 'http://laconi.ca/translate/download.php?file_id=87',
'eo' => 'http://laconi.ca/translate/download.php?file_id=88',
'fr_FR' => 'http://laconi.ca/translate/download.php?file_id=99',
'de_DE' => 'http://laconi.ca/translate/download.php?file_id=100',
'it_IT' => 'http://laconi.ca/translate/download.php?file_id=101',
'ko' => 'http://laconi.ca/translate/download.php?file_id=102',
'no_NB' => 'http://laconi.ca/translate/download.php?file_id=104',
'pt' => 'http://laconi.ca/translate/download.php?file_id=106',
'pt_BR' => 'http://laconi.ca/translate/download.php?file_id=107',
'ru_RU' => 'http://laconi.ca/translate/download.php?file_id=109',
'es' => 'http://laconi.ca/translate/download.php?file_id=110',
'tr_TR' => 'http://laconi.ca/translate/download.php?file_id=114',
'uk_UA' => 'http://laconi.ca/translate/download.php?file_id=115',
'he_IL' => 'http://laconi.ca/translate/download.php?file_id=116',
'mk_MK' => 'http://laconi.ca/translate/download.php?file_id=103',
'ja_JP' => 'http://laconi.ca/translate/download.php?file_id=117',
'cs_CZ' => 'http://laconi.ca/translate/download.php?file_id=96',
'ca_ES' => 'http://laconi.ca/translate/download.php?file_id=95',
'pl_PL' => 'http://laconi.ca/translate/download.php?file_id=105',
'sv_SE' => 'http://laconi.ca/translate/download.php?file_id=128'
);
$languages = get_all_languages();
/* Update the languages */
foreach ($languages as $code => $file) {
$lcdir='locale/'.$code;
$msgdir=$lcdir.'/LC_MESSAGES';
$pofile=$msgdir.'/laconica.po';
$mofile=$msgdir.'/laconica.mo';
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;
}

View File

@ -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("<iq from='{$this->jid}' to='{$this->server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
}
function handle_reconnect(&$pl)
{
$this->log(LOG_DEBUG, "Got reconnection callback.");

View File

@ -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,

View File

@ -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;
}