diff --git a/README b/README index 173ea4c253..f3b2528b85 100644 --- a/README +++ b/README @@ -146,6 +146,7 @@ Your PHP installation must include the following PHP extensions: - GD. For scaling down avatar images. - mbstring. For handling Unicode (UTF-8) encoded strings. - gettext. For multiple languages. Default on many PHP installs. +- tidy. Used to clean up HTML/URLs for the URL shortener to consume. For some functionality, you will also need the following extensions: diff --git a/actions/api.php b/actions/api.php index 93e33085f9..f425a8dcd7 100644 --- a/actions/api.php +++ b/actions/api.php @@ -133,6 +133,8 @@ class ApiAction extends Action 'groups/show', 'groups/timeline', 'groups/list_all', + 'groups/membership', + 'groups/is_member', 'groups/timeline'); static $bareauth = array('statuses/user_timeline', diff --git a/actions/foaf.php b/actions/foaf.php index 4dae9dfc19..356393304e 100644 --- a/actions/foaf.php +++ b/actions/foaf.php @@ -146,8 +146,10 @@ class FoafAction extends Action while ($sub->fetch()) { if ($sub->token) { $other = Remote_profile::staticGet('id', $sub->subscriber); + $profile = Profile::staticGet('id', $sub->subscriber); } else { $other = User::staticGet('id', $sub->subscriber); + $profile = Profile::staticGet('id', $sub->subscriber); } if (!$other) { common_debug('Got a bad subscription: '.print_r($sub,true)); @@ -158,12 +160,15 @@ class FoafAction extends Action } else { $person[$other->uri] = array(LISTENER, $other->id, - $other->nickname, + $profile->nickname, (empty($sub->token)) ? 'User' : 'Remote_profile'); } $other->free(); $other = null; unset($other); + $profile->free(); + $profile = null; + unset($profile); } } @@ -254,8 +259,10 @@ class FoafAction extends Action while ($sub->fetch()) { if (!empty($sub->token)) { $other = Remote_profile::staticGet('id', $sub->subscribed); + $profile = Profile::staticGet('id', $sub->subscribed); } else { $other = User::staticGet('id', $sub->subscribed); + $profile = Profile::staticGet('id', $sub->subscribed); } if (empty($other)) { common_debug('Got a bad subscription: '.print_r($sub,true)); @@ -264,11 +271,14 @@ class FoafAction extends Action $this->element('sioc:follows', array('rdf:resource' => $other->uri.'#acct')); $person[$other->uri] = array(LISTENEE, $other->id, - $other->nickname, + $profile->nickname, (empty($sub->token)) ? 'User' : 'Remote_profile'); $other->free(); $other = null; unset($other); + $profile->free(); + $profile = null; + unset($profile); } } diff --git a/actions/twitapigroups.php b/actions/twitapigroups.php index 214fa8214f..4deb1b764a 100644 --- a/actions/twitapigroups.php +++ b/actions/twitapigroups.php @@ -21,7 +21,7 @@ * * @category Twitter * @package StatusNet - * @author Craig Andrews + * @author Craig Andrews * @author Zach Copley * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 @@ -41,7 +41,7 @@ require_once INSTALLDIR.'/lib/twitterapi.php'; * * @category Twitter * @package StatusNet - * @author Craig Andrews + * @author Craig Andrews * @author Zach Copley * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 @@ -233,4 +233,97 @@ require_once INSTALLDIR.'/lib/twitterapi.php'; } } + function membership($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(_("Members of %s group"), $group->nickname); + $taguribase = common_config('integration', 'taguri'); + $id = "tag:$taguribase:GroupMembership:".$group->id; + $link = common_local_url('showgroup', + array('nickname' => $group->nickname)); + $subtitle = sprintf(_('Members of %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'); + + $member = $group->getMembers(($page-1)*$count, + $count, $since_id, $max_id, $since); + + switch($apidata['content-type']) { + case 'xml': + $this->show_twitter_xml_users($member); + break; + //TODO implement the RSS and ATOM content types + /*case 'rss': + $this->show_rss_users($member, $title, $link, $subtitle); + break;*/ + /*case 'atom': + if (isset($apidata['api_arg'])) { + $selfuri = common_root_url() . + 'api/statusnet/groups/membership/' . + $apidata['api_arg'] . '.atom'; + } else { + $selfuri = common_root_url() . + 'api/statusnet/groups/membership.atom'; + } + $this->show_atom_users($member, $title, $id, $link, + $subtitle, null, $selfuri); + break;*/ + case 'json': + $this->show_json_users($member); + break; + default: + $this->clientError(_('API method not found!'), $code = 404); + } + } + + function is_member($args, $apidata) + { + parent::handle($args); + + common_debug("in groups api action"); + + $this->auth_user = $apidata['user']; + $group = User_group::staticGet($args['group_id']); + if(! $group){ + $this->clientError(_('Group not found'), $code = 500); + } + $user = User::staticGet('id', $args['user_id']); + if(! $user){ + $this->clientError(_('User not found'), $code = 500); + } + + $is_member=$user->isMember($group); + + switch($apidata['content-type']) { + case 'xml': + $this->init_document('xml'); + $this->element('is_member', null, $is_member); + $this->end_document('xml'); + break; + case 'json': + $this->init_document('json'); + $this->show_json_objects(array('is_member'=>$is_member)); + $this->end_document('json'); + break; + default: + $this->clientError(_('API method not found!'), $code = 404); + } + } } diff --git a/actions/twitapitags.php b/actions/twitapitags.php index 2bb7ee01ab..0bcc55d378 100644 --- a/actions/twitapitags.php +++ b/actions/twitapitags.php @@ -21,7 +21,7 @@ * * @category Twitter * @package StatusNet - * @author Craig Andrews + * @author Craig Andrews * @author Zach Copley * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 @@ -41,7 +41,7 @@ require_once INSTALLDIR.'/lib/twitterapi.php'; * * @category Twitter * @package StatusNet - * @author Craig Andrews + * @author Craig Andrews * @author Zach Copley * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 diff --git a/actions/twittersettings.php b/actions/twittersettings.php index 563d867a49..89169941eb 100644 --- a/actions/twittersettings.php +++ b/actions/twittersettings.php @@ -165,7 +165,7 @@ class TwittersettingsAction extends ConnectSettingsAction ($flink->noticesync & FOREIGN_NOTICE_RECV) : false); $this->elementEnd('li'); - + } else { // preserve setting even if bidrection bridge toggled off if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { diff --git a/classes/File.php b/classes/File.php index dc668ab235..9758cf7f55 100644 --- a/classes/File.php +++ b/classes/File.php @@ -78,14 +78,14 @@ class File extends Memcached_DataObject $file_id = $x->insert(); if (isset($redir_data['type']) - && ('text/html' === substr($redir_data['type'], 0, 9)) + && (('text/html' === substr($redir_data['type'], 0, 9) || 'application/xhtml+xml' === substr($redir_data['type'], 0, 21))) && ($oembed_data = File_oembed::_getOembed($given_url))) { File_oembed::saveNew($oembed_data, $file_id); } return $x; } - function processNew($given_url, $notice_id) { + function processNew($given_url, $notice_id=null) { if (empty($given_url)) return -1; // error, no url to process $given_url = File_redirection::_canonUrl($given_url); if (empty($given_url)) return -1; // error, no url to process @@ -96,7 +96,7 @@ class File extends Memcached_DataObject $redir_data = File_redirection::where($given_url); $redir_url = $redir_data['url']; // TODO: max field length - if ($redir_url === $given_url || strlen($redir_url) > 255) { + if ($redir_url === $given_url || strlen($redir_url) > 255) { $x = File::saveNew($redir_data, $given_url); $file_id = $x->id; } else { @@ -119,7 +119,9 @@ class File extends Memcached_DataObject } } - File_to_post::processNew($file_id, $notice_id); + if (!empty($notice_id)) { + File_to_post::processNew($file_id, $notice_id); + } return $x; } diff --git a/classes/Notice.php b/classes/Notice.php index c3c8d13c85..2138e05612 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -56,7 +56,7 @@ class Notice extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - /* Notice types */ + /* Notice types */ const LOCAL_PUBLIC = 1; const REMOTE_OMB = 0; const LOCAL_NONPUBLIC = -1; @@ -260,17 +260,6 @@ class Notice extends Memcached_DataObject $notice->saveUrls(); - // FIXME: why do we have to re-render the content? - // Remove this if it's not necessary. - - $orig2 = clone($notice); - - $notice->rendered = common_render_content($final, $notice); - if (!$notice->update($orig2)) { - common_log_db_error($notice, 'UPDATE', __FILE__); - return _('Problem saving notice.'); - } - $notice->query('COMMIT'); Event::handle('EndNoticeSave', array($notice)); diff --git a/classes/Status_network.php b/classes/Status_network.php index d526cb4d61..fe4f0b0c58 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -54,7 +54,7 @@ class Status_network extends DB_DataObject global $config; $config['db']['database_'.$dbname] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname"; - $config['db']['ini_'.$dbname] = INSTALLDIR.'/classes/statusnet.ini'; + $config['db']['ini_'.$dbname] = INSTALLDIR.'/classes/status_network.ini'; $config['db']['table_status_network'] = $dbname; self::$cache = new Memcache(); diff --git a/doc-src/contact b/doc-src/contact index 31f3a4d2b3..c63fcd01ad 100644 --- a/doc-src/contact +++ b/doc-src/contact @@ -13,7 +13,7 @@ Bugs ---- If you think you've found a bug in the [StatusNet](http://status.net/) software, -or if there's a new feature you'd like to see, add it into the [StatusNet bug database](http://status.net/PITS/HomePage). Don't forget to check the list of +or if there's a new feature you'd like to see, add it into the [StatusNet bug database](http://status.net/bugs/). Don't forget to check the list of existing bugs to make sure it hasn't already been reported! Email diff --git a/extlib/Services/oEmbed.php b/extlib/Services/oEmbed.php index 7d507b6f62..0dc8f01b2f 100644 --- a/extlib/Services/oEmbed.php +++ b/extlib/Services/oEmbed.php @@ -256,7 +256,7 @@ class Services_oEmbed $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (substr($code, 0, 1) != '2') { - throw new Services_oEmbed_Exception('Non-200 code returned'); + throw new Services_oEmbed_Exception('Non-200 code returned. Got code ' . $code); } curl_close($ch); @@ -302,8 +302,8 @@ class Services_oEmbed // Find all tags that have a valid oembed type set. We then // extract the href attribute for each type. - $regexp = '#]*)type="' . - '(application/json|text/xml)\+oembed"([^>]*)>#i'; + $regexp = '#]*)type[\s\n]*=[\s\n]*"' . + '(application/json|text/xml)\+oembed"([^>]*)>#im'; $m = $ret = array(); if (!preg_match_all($regexp, $body, $m)) { @@ -314,7 +314,7 @@ class Services_oEmbed foreach ($m[0] as $i => $link) { $h = array(); - if (preg_match('/href="([^"]+)"/i', $link, $h)) { + if (preg_match('/[\s\n]+href[\s\n]*=[\s\n]*"([^"]+)"/im', $link, $h)) { $ret[$m[2][$i]] = $h[1]; } } @@ -347,7 +347,7 @@ class Services_oEmbed $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (substr($code, 0, 1) != '2') { - throw new Services_oEmbed_Exception('Non-200 code returned'); + throw new Services_oEmbed_Exception('Non-200 code returned. Got code ' . $code); } return $result; diff --git a/extlib/Stomp.php b/extlib/Stomp.php index 9e1c97b3b3..abd9cba62b 100644 --- a/extlib/Stomp.php +++ b/extlib/Stomp.php @@ -454,7 +454,7 @@ class Stomp */ public function disconnect () { - $header = array(); + $headers = array(); if ($this->clientId != null) { $headers["client-id"] = $this->clientId; diff --git a/install.php b/install.php index f4dcef2e85..e828fa814b 100644 --- a/install.php +++ b/install.php @@ -230,7 +230,7 @@ function checkPrereqs() } $reqs = array('gd', 'curl', - 'xmlwriter', 'mbstring'); + 'xmlwriter', 'mbstring','tidy'); foreach ($reqs as $req) { if (!checkExtension($req)) { diff --git a/lib/command.php b/lib/command.php index cae7a82987..11d40b8e15 100644 --- a/lib/command.php +++ b/lib/command.php @@ -114,7 +114,6 @@ class StatsCommand extends Command class FavCommand extends Command { - var $other = null; function __construct($user, $other) @@ -158,6 +157,108 @@ class FavCommand extends Command $channel->output($this->user, _('Notice marked as fave.')); } + +} +class JoinCommand extends Command +{ + var $other = null; + + function __construct($user, $other) + { + parent::__construct($user); + $this->other = $other; + } + + function execute($channel) + { + + $nickname = common_canonical_nickname($this->other); + $group = User_group::staticGet('nickname', $nickname); + $cur = $this->user; + + if (!$group) { + $channel->error($cur, _('No such group.')); + return; + } + + if ($cur->isMember($group)) { + $channel->error($cur, _('You are already a member of that group')); + return; + } + if (Group_block::isBlocked($group, $cur->getProfile())) { + $channel->error($cur, _('You have been blocked from that group by the admin.')); + return; + } + + $member = new Group_member(); + + $member->group_id = $group->id; + $member->profile_id = $cur->id; + $member->created = common_sql_now(); + + $result = $member->insert(); + if (!$result) { + common_log_db_error($member, 'INSERT', __FILE__); + $channel->error($cur, sprintf(_('Could not join user %s to group %s'), + $cur->nickname, $group->nickname)); + return; + } + + $channel->output($cur, sprintf(_('%s joined group %s'), + $cur->nickname, + $group->nickname)); + } + +} +class DropCommand extends Command +{ + var $other = null; + + function __construct($user, $other) + { + parent::__construct($user); + $this->other = $other; + } + + function execute($channel) + { + + $nickname = common_canonical_nickname($this->other); + $group = User_group::staticGet('nickname', $nickname); + $cur = $this->user; + + if (!$group) { + $channel->error($cur, _('No such group.')); + return; + } + + if (!$cur->isMember($group)) { + $channel->error($cur, _('You are not a member of that group.')); + return; + } + + $member = new Group_member(); + + $member->group_id = $group->id; + $member->profile_id = $cur->id; + + if (!$member->find(true)) { + $channel->error($cur,_('Could not find membership record.')); + return; + } + $result = $member->delete(); + if (!$result) { + common_log_db_error($member, 'INSERT', __FILE__); + $channel->error($cur, sprintf(_('Could not remove user %s to group %s'), + $cur->nickname, $group->nickname)); + return; + } + + $channel->output($cur, sprintf(_('%s left group %s'), + $cur->nickname, + $group->nickname)); + } + } class WhoisCommand extends Command @@ -396,6 +497,8 @@ class HelpCommand extends Command "get - get last notice from user\n". "whois - get profile info on user\n". "fav - add user's last notice as a 'fave'\n". + "join - join group\n". + "drop - leave group\n". "stats - get your stats\n". "stop - same as 'off'\n". "quit - same as 'off'\n". diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index ac6bf73c8e..6e4340e5dc 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -70,6 +70,26 @@ class CommandInterpreter } else { return new OffCommand($user); } + case 'join': + if (!$arg) { + return null; + } + list($other, $extra) = explode(' ', $arg, 2); + if ($extra) { + return null; + } else { + return new JoinCommand($user, $other); + } + case 'drop': + if (!$arg) { + return null; + } + list($other, $extra) = explode(' ', $arg, 2); + if ($extra) { + return null; + } else { + return new DropCommand($user, $other); + } case 'follow': case 'sub': if (!$arg) { diff --git a/lib/mailbox.php b/lib/mailbox.php index a09bf1060a..e1d384a063 100644 --- a/lib/mailbox.php +++ b/lib/mailbox.php @@ -213,26 +213,20 @@ class MailboxAction extends CurrentUserDesignAction } $this->elementStart('div', 'entry-content'); - $this->elementStart('dl', 'timestamp'); - $this->element('dt', null, _('Published')); - $this->elementStart('dd', null); - $dt = common_date_iso8601($message->created); $this->elementStart('a', array('rel' => 'bookmark', + 'class' => 'timestamp', 'href' => $messageurl)); + $dt = common_date_iso8601($message->created); $this->element('abbr', array('class' => 'published', 'title' => $dt), common_date_string($message->created)); $this->elementEnd('a'); - $this->elementEnd('dd'); - $this->elementEnd('dl'); if ($message->source) { - $this->elementStart('dl', 'device'); - $this->elementStart('dt'); - $this->text(_('From')); - $this->elementEnd('dt'); - $this->showSource($message->source); - $this->elementEnd('dl'); + $this->elementStart('span', 'source'); + $this->text(_('from')); + $this->element('span', 'device', $this->showSource($message->source)); + $this->elementEnd('span'); } $this->elementEnd('div'); @@ -277,18 +271,18 @@ class MailboxAction extends CurrentUserDesignAction case 'mail': case 'omb': case 'api': - $this->element('dd', null, $source_name); + $this->element('span', 'device', $source_name); break; default: $ns = Notice_source::staticGet($source); if ($ns) { - $this->elementStart('dd', null); + $this->elementStart('span', 'device'); $this->element('a', array('href' => $ns->url, - 'rel' => 'external'), - $ns->name); - $this->elementEnd('dd'); + 'rel' => 'external'), + $ns->name); + $this->elementEnd('span'); } else { - $this->element('dd', null, $source_name); + $this->out->element('span', 'device', $source_name); } break; } diff --git a/lib/oauthclient.php b/lib/oauthclient.php index cc10cea8f9..f1827726e7 100644 --- a/lib/oauthclient.php +++ b/lib/oauthclient.php @@ -22,7 +22,7 @@ * @category Action * @package StatusNet * @author Zach Copley - * @copyright 2008 StatusNet, Inc. + * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ diff --git a/lib/twitter.php b/lib/twitter.php index 2d2b08f730..b49e2e1190 100644 --- a/lib/twitter.php +++ b/lib/twitter.php @@ -154,83 +154,134 @@ function broadcast_twitter($notice) TWITTER_SERVICE); if (is_twitter_bound($notice, $flink)) { - - $user = $flink->getUser(); - - // XXX: Hack to get around PHP cURL's use of @ being a a meta character - $statustxt = preg_replace('/^@/', ' @', $notice->content); - - // Convert !groups to #hashes - $statustxt = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/', "\\1#\\2", $statustxt); - - $token = TwitterOAuthClient::unpackToken($flink->credentials); - - $client = new TwitterOAuthClient($token->key, $token->secret); - - $status = null; - - try { - $status = $client->statusesUpdate($statustxt); - } catch (OAuthClientCurlException $e) { - - if ($e->getMessage() == 'The requested URL returned error: 401') { - - $errmsg = sprintf('User %1$s (user id: %2$s) has an invalid ' . - 'Twitter OAuth access token.', - $user->nickname, $user->id); - common_log(LOG_WARNING, $errmsg); - - // Bad auth token! 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 probably - // try to send again later. - - $errmsg = sprintf('cURL error trying to send notice to Twitter ' . - 'for user %1$s (user id: %2$s) - ' . - 'code: %3$s message: $4$s.', - $user->nickname, $user->id, - $e->getCode(), $e->getMessage()); - common_log(LOG_WARNING, $errmsg); - - return false; - } + if (TwitterOAuthClient::isPackedToken($flink->credentials)) { + return broadcast_oauth($notice, $flink); + } else { + return broadcast_basicauth($notice, $flink); } + } - if (empty($status)) { + return true; +} - // This could represent a failure posting, - // or the Twitter API might just be behaving flakey. +function broadcast_oauth($notice, $flink) { + $user = $flink->getUser(); + $statustxt = format_status($notice); + // Convert !groups to #hashes + $statustxt = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/', "\\1#\\2", $statustxt); + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + $status = null; + + try { + $status = $client->statusesUpdate($statustxt); + } catch (OAuthClientCurlException $e) { + return process_error($e, $flink); + } + + if (empty($status)) { + + // This could represent a failure posting, + // or the Twitter API might just be behaving flakey. + + $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' . + 'trying to send update for %1$s (user id %2$s).', + $user->nickname, $user->id); + common_log(LOG_WARNING, $errmsg); + + return false; + } + + // Notice crossed the great divide + + $msg = sprintf('Twitter bridge - posted notice %s to Twitter using OAuth.', + $notice->id); + common_log(LOG_INFO, $msg); + + return true; +} + +function broadcast_basicauth($notice, $flink) +{ + $user = $flink->getUser(); + + $statustxt = format_status($notice); + + $client = new TwitterBasicAuthClient($flink); + $status = null; + + try { + $status = $client->statusesUpdate($statustxt); + } catch (BasicAuthCurlException $e) { + return process_error($e, $flink); + } + + if (empty($status)) { + + $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' . + 'trying to send update for %1$s (user id %2$s).', + $user->nickname, $user->id); + common_log(LOG_WARNING, $errmsg); $errmsg = sprintf('No data returned by Twitter API when ' . 'trying to send update for %1$s (user id %2$s).', $user->nickname, $user->id); common_log(LOG_WARNING, $errmsg); - - return false; - } - - // Notice crossed the great divide - - $msg = sprintf('Twitter bridge posted notice %s to Twitter.', - $notice->id); - common_log(LOG_INFO, $msg); + return false; } + $msg = sprintf('Twitter bridge - posted notice %s to Twitter using basic auth.', + $notice->id); + common_log(LOG_INFO, $msg); + return true; } +function process_error($e, $flink) +{ + $user = $flink->getUser(); + $errmsg = $e->getMessage(); + $delivered = false; + + switch($errmsg) { + case 'The requested URL returned error: 401': + $logmsg = sprintf('Twiter bridge - User %1$s (user id: %2$s) has an invalid ' . + 'Twitter screen_name/password combo or an invalid acesss token.', + $user->nickname, $user->id); + $delivered = true; + remove_twitter_link($flink); + break; + case 'The requested URL returned error: 403': + $logmsg = sprintf('Twitter bridge - User %1$s (user id: %2$s) has exceeded ' . + 'his/her Twitter request limit.', + $user->nickname, $user->id); + break; + default: + $logmsg = sprintf('Twitter bridge - cURL error trying to send notice to Twitter ' . + 'for user %1$s (user id: %2$s) - ' . + 'code: %3$s message: %4$s.', + $user->nickname, $user->id, + $e->getCode(), $e->getMessage()); + break; + } + + common_log(LOG_WARNING, $logmsg); + + return $delivered; +} + +function format_status($notice) +{ + // XXX: Hack to get around PHP cURL's use of @ being a a meta character + return preg_replace('/^@/', ' @', $notice->content); +} + 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)."); + "user $user->nickname (user id: $user->id)."); $result = $flink->delete(); diff --git a/lib/twitterapi.php b/lib/twitterapi.php index 4612f74e9f..d199e4dee2 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -788,6 +788,52 @@ class TwitterapiAction extends Action $this->end_document('xml'); } + function show_twitter_xml_users($user) + { + + $this->init_document('xml'); + $this->elementStart('users', array('type' => 'array')); + + if (is_array($user)) { + foreach ($group as $g) { + $twitter_user = $this->twitter_user_array($g); + $this->show_twitter_xml_user($twitter_user,'user'); + } + } else { + while ($user->fetch()) { + $twitter_user = $this->twitter_user_array($user); + $this->show_twitter_xml_user($twitter_user); + } + } + + $this->elementEnd('users'); + $this->end_document('xml'); + } + + function show_json_users($user) + { + + $this->init_document('json'); + + $users = array(); + + if (is_array($user)) { + foreach ($user as $u) { + $twitter_user = $this->twitter_user_array($u); + array_push($users, $twitter_user); + } + } else { + while ($user->fetch()) { + $twitter_user = $this->twitter_user_array($user); + array_push($users, $twitter_user); + } + } + + $this->show_json_objects($users); + + $this->end_document('json'); + } + function show_single_json_group($group) { $this->init_document('json'); diff --git a/lib/twitterbasicauthclient.php b/lib/twitterbasicauthclient.php new file mode 100644 index 0000000000..fd331fbdc9 --- /dev/null +++ b/lib/twitterbasicauthclient.php @@ -0,0 +1,236 @@ +. + * + * @category Integration + * @package StatusNet + * @author Zach Copley + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Exception wrapper for cURL errors + * + * @category Integration + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + */ +class BasicAuthCurlException extends Exception +{ +} + +/** + * Class for talking to the Twitter API with HTTP Basic Auth. + * + * @category Integration + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + */ +class TwitterBasicAuthClient +{ + var $screen_name = null; + var $password = null; + + /** + * constructor + * + * @param Foreign_link $flink a Foreign_link storing the + * Twitter user's password, etc. + */ + function __construct($flink) + { + $fuser = $flink->getForeignUser(); + $this->screen_name = $fuser->nickname; + $this->password = $flink->credentials; + } + + /** + * Calls Twitter's /statuses/update API method + * + * @param string $status text of the status + * @param int $in_reply_to_status_id optional id of the status it's + * a reply to + * + * @return mixed the status + */ + function statusesUpdate($status, $in_reply_to_status_id = null) + { + $url = 'https://twitter.com/statuses/update.json'; + $params = array('status' => $status, + 'source' => common_config('integration', 'source'), + 'in_reply_to_status_id' => $in_reply_to_status_id); + $response = $this->httpRequest($url, $params); + $status = json_decode($response); + return $status; + } + + /** + * Calls Twitter's /statuses/friends_timeline API method + * + * @param int $since_id show statuses after this id + * @param int $max_id show statuses before this id + * @param int $cnt number of statuses to show + * @param int $page page number + * + * @return mixed an array of statuses + */ + function statusesFriendsTimeline($since_id = null, $max_id = null, + $cnt = null, $page = null) + { + $url = 'https://twitter.com/statuses/friends_timeline.json'; + $params = array('since_id' => $since_id, + 'max_id' => $max_id, + 'count' => $cnt, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->httpRequest($url); + $statuses = json_decode($response); + return $statuses; + } + + /** + * Calls Twitter's /statuses/friends API method + * + * @param int $id id of the user whom you wish to see friends of + * @param int $user_id numerical user id + * @param int $screen_name screen name + * @param int $page page number + * + * @return mixed an array of twitter users and their latest status + */ + function statusesFriends($id = null, $user_id = null, $screen_name = null, + $page = null) + { + $url = "https://twitter.com/statuses/friends.json"; + + $params = array('id' => $id, + 'user_id' => $user_id, + 'screen_name' => $screen_name, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->httpRequest($url); + $friends = json_decode($response); + return $friends; + } + + /** + * Calls Twitter's /statuses/friends/ids API method + * + * @param int $id id of the user whom you wish to see friends of + * @param int $user_id numerical user id + * @param int $screen_name screen name + * @param int $page page number + * + * @return mixed a list of ids, 100 per page + */ + function friendsIds($id = null, $user_id = null, $screen_name = null, + $page = null) + { + $url = "https://twitter.com/friends/ids.json"; + + $params = array('id' => $id, + 'user_id' => $user_id, + 'screen_name' => $screen_name, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->httpRequest($url); + $ids = json_decode($response); + return $ids; + } + + /** + * Make a HTTP request using cURL. + * + * @param string $url Where to make the request + * @param array $params post parameters + * + * @return mixed the request + */ + function httpRequest($url, $params = null, $auth = true) + { + $options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FAILONERROR => true, + CURLOPT_HEADER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_USERAGENT => 'StatusNet', + CURLOPT_CONNECTTIMEOUT => 120, + CURLOPT_TIMEOUT => 120, + CURLOPT_HTTPAUTH => CURLAUTH_ANY, + CURLOPT_SSL_VERIFYPEER => false, + + // Twitter is strict about accepting invalid "Expect" headers + + CURLOPT_HTTPHEADER => array('Expect:') + ); + + if (isset($params)) { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = $params; + } + + if ($auth) { + $options[CURLOPT_USERPWD] = $this->screen_name . + ':' . $this->password; + } + + $ch = curl_init($url); + curl_setopt_array($ch, $options); + $response = curl_exec($ch); + + if ($response === false) { + $msg = curl_error($ch); + $code = curl_errno($ch); + throw new BasicAuthCurlException($msg, $code); + } + + curl_close($ch); + + return $response; + } + +} diff --git a/lib/twitteroauthclient.php b/lib/twitteroauthclient.php index e4a89c4e9a..bad2b74ca3 100644 --- a/lib/twitteroauthclient.php +++ b/lib/twitteroauthclient.php @@ -22,7 +22,7 @@ * @category Integration * @package StatusNet * @author Zach Copley - * @copyright 2008 StatusNet, Inc. + * @copyright 2009 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -81,6 +81,15 @@ class TwitterOAuthClient extends OAuthClient return new OAuthToken($vals[0], $vals[1]); } + static function isPackedToken($str) + { + if (strpos($str, chr(0)) === false) { + return false; + } else { + return true; + } + } + /** * Builds a link to Twitter's endpoint for authorizing a request token * diff --git a/lib/util.php b/lib/util.php index 337113c701..9cf4625150 100644 --- a/lib/util.php +++ b/lib/util.php @@ -413,7 +413,7 @@ function common_replace_urls_callback($text, $callback, $notice_id = null) { // Start off with a regex $regex = '#'. '(?:^|[\s\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'. - '(?P'. + '('. '(?:'. '(?:'. //Known protocols '(?:'. @@ -421,7 +421,7 @@ function common_replace_urls_callback($text, $callback, $notice_id = null) { '|'. '(?:(?:mailto|aim|tel|xmpp):)'. ')'. - '(?:[\pN\pL\-\_\+]+(?::[\pN\pL\-\_\+]+)?\@)?'. //user:pass@ + '(?:[\pN\pL\-\_\+\%\~]+(?::[\pN\pL\-\_\+\%\~]+)?\@)?'. //user:pass@ '(?:'. '(?:'. '\[[\pN\pL\-\_\:\.]+(?'(', @@ -491,13 +491,11 @@ function callback_helper($matches, $callback, $notice_id) { $url=substr($url,0,-1); } }while($original_url!=$url); - - - + if(empty($notice_id)){ $result = call_user_func_array($callback,$url); }else{ - $result = call_user_func_array($callback, array($url,$notice_id) ); + $result = call_user_func_array($callback, array(array($url,$notice_id)) ); } return substr($matches[0],0,$left) . $result . substr($matches[0],$right); } @@ -508,16 +506,13 @@ function curry($fn) { array_shift($args); $id = uniqid('_partial'); $GLOBALS[$id] = array($fn, $args); - return create_function( - '', - ' - $args = func_get_args(); - return call_user_func_array( - $GLOBALS["'.$id.'"][0], - array_merge( - $args, - $GLOBALS["'.$id.'"][1])); - '); + return create_function('', + '$args = func_get_args(); '. + 'return call_user_func_array('. + '$GLOBALS["'.$id.'"][0],'. + 'array_merge('. + '$args,'. + '$GLOBALS["'.$id.'"][1]));'); } function common_linkify($url) { @@ -525,7 +520,7 @@ function common_linkify($url) { // functions $url = htmlspecialchars_decode($url); - if(strpos($url, '@')!==false && strpos($url, ':')===false){ + if(strpos($url, '@') !== false && strpos($url, ':') === false) { //url is an email address without the mailto: protocol return XMLStringer::estring('a', array('href' => "mailto:$url", 'rel' => 'external'), $url); } @@ -547,42 +542,30 @@ function common_linkify($url) { $attachment_id = null; $has_thumb = false; - // Check to see whether there's a filename associated with this URL. - // If there is, it's an upload and qualifies as an attachment + // Check to see whether this is a known "attachment" URL. - $localfile = File::staticGet('url', $longurl); + $f = File::staticGet('url', $longurl); - if (!empty($localfile)) { - if (isset($localfile->filename)) { - $is_attachment = true; - $attachment_id = $localfile->id; - } + if (empty($f)) { + // XXX: this writes to the database. :< + $f = File::processNew($longurl); } - // if this URL is an attachment, then we set class='attachment' and id='attahcment-ID' - // where ID is the id of the attachment for the given URL. - // - // we need a better test telling what can be shown as an attachment - // we're currently picking up oembeds only. - // I think the best option is another file_view table in the db - // and associated dbobject. + if (!empty($f)) { + if (isset($f->filename)) { + $is_attachment = true; + $attachment_id = $f->id; + } else { // if it has OEmbed info, it's an attachment, too + $foe = File_oembed::staticGet('file_id', $f->id); + if (!empty($foe)) { + $is_attachment = true; + $attachment_id = $f->id; - $query = "select file_oembed.file_id as file_id from file join file_oembed on file.id = file_oembed.file_id where file.url='$longurl'"; - $file = new File; - $file->query($query); - $file->fetch(); - - if (!empty($file->file_id)) { - $is_attachment = true; - $attachment_id = $file->file_id; - - $query = "select file_thumbnail.file_id as file_id from file join file_thumbnail on file.id = file_thumbnail.file_id where file.url='$longurl'"; - $file2 = new File; - $file2->query($query); - $file2->fetch(); - - if (!empty($file2)) { - $has_thumb = true; + $thumb = File_thumbnail::staticGet('file_id', $f->id); + if (!empty($thumb)) { + $has_thumb = true; + } + } } } diff --git a/scripts/synctwitterfriends.php b/scripts/synctwitterfriends.php index 545cb23b3c..b30e700a1c 100755 --- a/scripts/synctwitterfriends.php +++ b/scripts/synctwitterfriends.php @@ -142,13 +142,20 @@ class SyncTwitterFriendsDaemon extends ParallelizingDaemon { $friends = array(); - $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = null; - $client = new TwitterOAuthClient($token->key, $token->secret); + if (TwitterOAuthClient::isPackedToken($flink->credentials)) { + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + common_debug($this->name() . '- Grabbing friends IDs with OAuth.'); + } else { + $client = new TwitterBasicAuthClient($flink); + common_debug($this->name() . '- Grabbing friends IDs with basic auth.'); + } try { $friends_ids = $client->friendsIds(); - } catch (OAuthCurlException $e) { + } catch (Exception $e) { common_log(LOG_WARNING, $this->name() . ' - cURL error getting friend ids ' . $e->getCode() . ' - ' . $e->getMessage()); @@ -177,7 +184,7 @@ class SyncTwitterFriendsDaemon extends ParallelizingDaemon try { $more_friends = $client->statusesFriends(null, null, null, $i); - } catch (OAuthCurlException $e) { + } catch (Exception $e) { common_log(LOG_WARNING, $this->name() . ' - cURL error getting Twitter statuses/friends ' . "page $i - " . $e->getCode() . ' - ' . diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php index 68f7e9bf75..3cdf1867a1 100755 --- a/scripts/twitterstatusfetcher.php +++ b/scripts/twitterstatusfetcher.php @@ -148,9 +148,9 @@ class TwitterStatusFetcher extends ParallelizingDaemon function getTimeline($flink) { - if (empty($flink)) { + if (empty($flink)) { common_log(LOG_WARNING, $this->name() . - " - Can't retrieve Foreign_link for foreign ID $fid"); + " - Can't retrieve Foreign_link for foreign ID $fid"); return; } @@ -161,17 +161,24 @@ class TwitterStatusFetcher extends ParallelizingDaemon // to start importing? How many statuses? Right now I'm going // with the default last 20. - $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = null; - $client = new TwitterOAuthClient($token->key, $token->secret); + if (TwitterOAuthClient::isPackedToken($flink->credentials)) { + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + common_debug($this->name() . ' - Grabbing friends timeline with OAuth.'); + } else { + $client = new TwitterBasicAuthClient($flink); + common_debug($this->name() . ' - Grabbing friends timeline with basic auth.'); + } $timeline = null; try { $timeline = $client->statusesFriendsTimeline(); - } catch (OAuthClientCurlException $e) { + } catch (Exception $e) { common_log(LOG_WARNING, $this->name() . - ' - OAuth client unable to get friends timeline for user ' . + ' - Twitter client unable to get friends timeline for user ' . $flink->user_id . ' - code: ' . $e->getCode() . 'msg: ' . $e->getMessage()); } diff --git a/tests/URLDetectionTest.php b/tests/URLDetectionTest.php index 767f895bba..9a103b6f7c 100644 --- a/tests/URLDetectionTest.php +++ b/tests/URLDetectionTest.php @@ -31,8 +31,12 @@ class URLDetectionTest extends PHPUnit_Framework_TestCase '127.0.0.1'), array('127.0.0.1:99', '127.0.0.1:99'), - array('127.0.0.1/test.php', - '127.0.0.1/test.php'), + array('127.0.0.1/Name:test.php', + '127.0.0.1/Name:test.php'), + array('127.0.0.1/~test', + '127.0.0.1/~test'), + array('127.0.0.1/test%20stuff', + '127.0.0.1/test%20stuff'), array('http://[::1]:99/test.php', 'http://[::1]:99/test.php'), array('http://::1/test.php',