From 69a1ecec9b4b7168fb570d2d07bcfaa0f29fc856 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 15:04:12 -0400 Subject: [PATCH 01/74] check for a post --- lib/action.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/action.php b/lib/action.php index 01bb0f7e92..d8f1392466 100644 --- a/lib/action.php +++ b/lib/action.php @@ -1354,4 +1354,15 @@ class Action extends HTMLOutputter // lawsuit $this->clientError(_('There was a problem with your session token.')); } } + + /** + * Check if the current request is a POST + * + * @return boolean true if POST; otherwise false. + */ + + function isPost() + { + return ($_SERVER['REQUEST_METHOD'] == 'POST'); + } } From 43a67b150a4e4285224ccf695171df731c736a1e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 15:58:53 -0400 Subject: [PATCH 02/74] show a single notice in atom entry format --- actions/apistatusesshow.php | 16 ++++++++++++---- lib/apiaction.php | 6 ++++++ lib/router.php | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index 84f8079db5..c0eab15a44 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -105,8 +105,8 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction { parent::handle($args); - if (!in_array($this->format, array('xml', 'json'))) { - $this->clientError(_('API method not found.'), $code = 404); + if (!in_array($this->format, array('xml', 'json', 'atom'))) { + $this->clientError(_('API method not found.'), 404); return; } @@ -122,10 +122,18 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction function showNotice() { if (!empty($this->notice)) { - if ($this->format == 'xml') { + switch ($this->format) { + case 'xml': $this->showSingleXmlStatus($this->notice); - } elseif ($this->format == 'json') { + break; + case 'json': $this->show_single_json_status($this->notice); + break; + case 'atom': + $this->showSingleAtomStatus($this->notice); + break; + default: + throw new Exception(sprintf(_("Unsupported format: %s"), $this->format)); } } else { diff --git a/lib/apiaction.php b/lib/apiaction.php index 4e9dbb310b..8a7be31502 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -726,6 +726,12 @@ class ApiAction extends Action $this->endDocument('xml'); } + function showSingleAtomStatus($notice) + { + header('Content-Type: application/atom+xml; charset=utf-8'); + print $notice->asAtomEntry(true, true, true, $this->auth_user); + } + function show_single_json_status($notice) { $this->initDocument('json'); diff --git a/lib/router.php b/lib/router.php index 9aaac7dfe3..834445f09b 100644 --- a/lib/router.php +++ b/lib/router.php @@ -399,12 +399,12 @@ class Router $m->connect('api/statuses/show.:format', array('action' => 'ApiStatusesShow', - 'format' => '(xml|json)')); + 'format' => '(xml|json|atom)')); $m->connect('api/statuses/show/:id.:format', array('action' => 'ApiStatusesShow', 'id' => '[0-9]+', - 'format' => '(xml|json)')); + 'format' => '(xml|json|atom)')); $m->connect('api/statuses/update.:format', array('action' => 'ApiStatusesUpdate', From 292e789584df47834f30d4de1ef143670c079b24 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 21:24:23 -0400 Subject: [PATCH 03/74] delete a notice using AtomPub --- actions/apistatusesshow.php | 38 ++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index c0eab15a44..86ffd6862e 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -110,7 +110,17 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction return; } - $this->showNotice(); + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + $this->showNotice(); + break; + case 'DELETE': + $this->deleteNotice(); + break; + default: + $this->clientError(_('HTTP method not supported.'), 405); + return; + } } /** @@ -213,4 +223,30 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction return null; } + function deleteNotice() + { + if ($this->format != 'atom') { + $this->clientError(_("Can only delete using the Atom format.")); + return; + } + + if (empty($this->auth_user) || + ($this->notice->profile_id != $this->auth_user->id && + !$this->auth_user->hasRight(Right::DELETEOTHERSNOTICE))) { + $this->clientError(_('Can\'t delete this notice.'), 403); + return; + } + + if (Event::handle('StartDeleteOwnNotice', array($this->auth_user, $this->notice))) { + $this->notice->delete(); + Event::handle('EndDeleteOwnNotice', array($this->auth_user, $this->notice)); + } + + // @fixme is there better output we could do here? + + header('HTTP/1.1 200 OK'); + header('Content-Type: text/plain'); + print(sprintf(_('Deleted notice %d'), $this->notice->id)); + print("\n"); + } } From c0664599aa5a90f99d462d7e9d9930e1aaf5dcbc Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 22:50:13 -0400 Subject: [PATCH 04/74] allow posting to user timeline using AtomPub --- actions/apitimelineuser.php | 227 ++++++++++++++++++++++++++++++++---- 1 file changed, 203 insertions(+), 24 deletions(-) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 0c97aad21c..cb7c847d62 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -101,7 +101,12 @@ class ApiTimelineUserAction extends ApiBareAuthAction function handle($args) { parent::handle($args); - $this->showTimeline(); + + if ($this->isPost()) { + $this->handlePost(); + } else { + $this->showTimeline(); + } } /** @@ -119,9 +124,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom = new AtomUserNoticeFeed($this->user, $this->auth_user); $link = common_local_url( - 'showstream', - array('nickname' => $this->user->nickname) - ); + 'showstream', + array('nickname' => $this->user->nickname) + ); $self = $this->getSelfUri(); @@ -137,14 +142,14 @@ class ApiTimelineUserAction extends ApiBareAuthAction break; case 'rss': $this->showRssTimeline( - $this->notices, - $atom->title, - $link, - $atom->subtitle, - $suplink, - $atom->logo, - $self - ); + $this->notices, + $atom->title, + $link, + $atom->subtitle, + $suplink, + $atom->logo, + $self + ); break; case 'atom': @@ -177,9 +182,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction $notices = array(); $notice = $this->user->getNotices( - ($this->page-1) * $this->count, $this->count, - $this->since_id, $this->max_id - ); + ($this->page-1) * $this->count, $this->count, + $this->since_id, $this->max_id + ); while ($notice->fetch()) { $notices[] = clone($notice); @@ -232,18 +237,192 @@ class ApiTimelineUserAction extends ApiBareAuthAction $last = count($this->notices) - 1; return '"' . implode( - ':', - array($this->arg('action'), - common_user_cache_hash($this->auth_user), - common_language(), - $this->user->id, - strtotime($this->notices[0]->created), - strtotime($this->notices[$last]->created)) - ) - . '"'; + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->user->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; } return null; } + function handlePost() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->user->id) { + $this->clientError(_("Only the user can add to their own timeline.")); + return; + } + + if ($this->format != 'atom') { + // Only handle posts for Atom + $this->clientError(_("Only accept AtomPub for atom feeds.")); + return; + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + $this->clientError(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + if ($activity->verb != ActivityVerb::POST) { + $this->clientError(_('Can only handle post activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + $this->clientError(sprintf(_('Cannot handle activity object type "%s"', + $note->type))); + return; + } + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // @todo i18n FIXME: use sprintf and add i18n. + $this->clientError("No content for notice {$note->id}."); + return; + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = common_shorten_links($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // accept remote URI (not necessarily a good idea) + + common_debug("Note ID is {$note->id}"); + + if (!empty($note->id)) { + $notice = Notice::staticGet('uri', trim($note->id)); + + if (!empty($notice)) { + $this->clientError(sprintf(_('Notice with URI "%s" already exists.'), + $note->id)); + return; + } + common_log(LOG_NOTICE, "Saving client-supplied notice URI '$note->id'"); + $options['uri'] = $note->id; + } + + // accept remote create time (also maybe not such a good idea) + + if (!empty($activity->time)) { + common_log(LOG_NOTICE, "Saving client-supplied create time {$activity->time}"); + $options['created'] = common_sql_date($activity->time); + } + + // Check for optional attributes... + + if (!empty($activity->context)) { + + foreach ($activity->context->attention as $uri) { + + $profile = Profile::fromURI($uri); + + if (!empty($profile)) { + $options['replies'] = $uri; + } else { + $group = User_group::staticGet('uri', $uri); + if (!empty($group)) { + $options['groups'] = $uri; + } else { + // @fixme: hook for discovery here + common_log(LOG_WARNING, sprintf(_('AtomPub post with unknown attention URI %s'), $uri)); + } + } + } + + // Maintain direct reply associations + // @fixme what about conversation ID? + + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($this->user->id, + $content, + 'atompub', // TODO: deal with this + $options); + + if (!empty($saved)) { + header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, + 'format' => 'atom'))); + $this->showSingleAtomStatus($saved); + } + } + + function purify($content) + { + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } } From 698818bd7eee94e92f23967897862857a190c6ea Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 23:05:33 -0400 Subject: [PATCH 05/74] show rel=edit links in notices for authenticated users --- classes/Notice.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index 60989f9bac..676e4cb546 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1613,6 +1613,20 @@ class Notice extends Memcached_DataObject Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon)); } + // @fixme check this logic + + if ($this->isLocal() && !empty($cur) && $cur->id == $this->profile_id) { + $relEditUrl = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + + if (Event::handle('StartActivityRelEdit', array(&$this, &$xs, &$relEditUrl))) { + $xs->element('link', array('rel' => 'edit', + 'type' => 'application/atom+xml', + 'href' => $relEditUrl)); + Event::handle('EndActivityRelEdit', array(&$this, &$xs, $relEditUrl)); + } + } + if (Event::handle('StartActivityEnd', array(&$this, &$xs))) { $xs->elementEnd('entry'); Event::handle('EndActivityEnd', array(&$this, &$xs)); From 59a7d78acb09c92622814d55c14e266f8f460fdf Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 23:43:26 -0400 Subject: [PATCH 06/74] Atom Service Document --- actions/apiatomservice.php | 100 +++++++++++++++++++++++++++++++++++++ actions/rsd.php | 14 ++++++ lib/router.php | 7 +++ 3 files changed, 121 insertions(+) create mode 100644 actions/apiatomservice.php diff --git a/actions/apiatomservice.php b/actions/apiatomservice.php new file mode 100644 index 0000000000..fb9d6aee82 --- /dev/null +++ b/actions/apiatomservice.php @@ -0,0 +1,100 @@ +. + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +require_once INSTALLDIR.'/lib/apibareauth.php'; + +/** + * Shows an AtomPub service document for a user + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ApiAtomServiceAction extends ApiBareAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + $this->user = $this->getTargetUser($this->arg('id')); + + if (empty($this->user)) { + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + return true; + } + + /** + * Handle the arguments. In our case, show a service document. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + header('Content-Type: application/atomsvc+xml'); + + $this->startXML(); + $this->elementStart('service', array('xmlns' => 'http://www.w3.org/2007/app', + 'xmlns:atom' => 'http://www.w3.org/2005/Atom')); + $this->elementStart('workspace'); + $this->element('atom:title', null, _('Main')); + $this->elementStart('collection', + array('href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')))); + $this->element('atom:title', + null, + sprintf(_("%s timeline"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->elementEnd('collection'); + $this->elementEnd('workspace'); + $this->elementEnd('service'); + $this->endXML(); + } +} diff --git a/actions/rsd.php b/actions/rsd.php index f88bf2e9a8..e02c85c41b 100644 --- a/actions/rsd.php +++ b/actions/rsd.php @@ -162,6 +162,20 @@ class RsdAction extends Action 'true'); $this->elementEnd('settings'); $this->elementEnd('api'); + + // Atom API + + if (empty($this->user)) { + $service = common_local_url('ApiAtomService'); + } else { + $service = common_local_url('ApiAtomService', array('id' => $this->user->nickname)); + } + + $this->element('api', array('name' => 'Atom', + 'preferred' => 'false', + 'apiLink' => $service, + 'blogID' => $blogID)); + Event::handle('EndRsdListApis', array($this, $this->user)); } $this->elementEnd('apis'); diff --git a/lib/router.php b/lib/router.php index 834445f09b..c0f3bf31d7 100644 --- a/lib/router.php +++ b/lib/router.php @@ -686,6 +686,13 @@ class Router $m->connect('api/oauth/authorize', array('action' => 'ApiOauthAuthorize')); + $m->connect('api/statusnet/app/service/:id.xml', + array('action' => 'ApiAtomService', + 'id' => '[a-zA-Z0-9]+')); + + $m->connect('api/statusnet/app/service.xml', + array('action' => 'ApiAtomService')); + // Admin $m->connect('admin/site', array('action' => 'siteadminpanel')); From e51ed96b89eb0229f890a3924c653eda51972a76 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 25 Oct 2010 09:48:01 -0400 Subject: [PATCH 07/74] add rel=self links to atom entries --- classes/Notice.php | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 676e4cb546..90ea811b6b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1615,15 +1615,30 @@ class Notice extends Memcached_DataObject // @fixme check this logic - if ($this->isLocal() && !empty($cur) && $cur->id == $this->profile_id) { - $relEditUrl = common_local_url('ApiStatusesShow', array('id' => $this->id, - 'format' => 'atom')); + if ($this->isLocal()) { - if (Event::handle('StartActivityRelEdit', array(&$this, &$xs, &$relEditUrl))) { - $xs->element('link', array('rel' => 'edit', + $selfUrl = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + + if (Event::handle('StartActivityRelSelf', array(&$this, &$xs, &$selfUrl))) { + $xs->element('link', array('rel' => 'self', 'type' => 'application/atom+xml', - 'href' => $relEditUrl)); - Event::handle('EndActivityRelEdit', array(&$this, &$xs, $relEditUrl)); + 'href' => $selfUrl)); + Event::handle('EndActivityRelSelf', array(&$this, &$xs, $selfUrl)); + } + + if (!empty($cur) && $cur->id == $this->profile_id) { + + // note: $selfUrl may have been changed by a plugin + $relEditUrl = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + + if (Event::handle('StartActivityRelEdit', array(&$this, &$xs, &$relEditUrl))) { + $xs->element('link', array('rel' => 'edit', + 'type' => 'application/atom+xml', + 'href' => $relEditUrl)); + Event::handle('EndActivityRelEdit', array(&$this, &$xs, $relEditUrl)); + } } } From e6ba379c8bfcf6a057a9cdfc161cae84d031401f Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 25 Oct 2010 11:08:10 -0400 Subject: [PATCH 08/74] navigation links in user timeline (for AtomPub) --- actions/apitimelineuser.php | 58 +++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index cb7c847d62..69cd9c2cb4 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -157,6 +157,49 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom->setId($self); $atom->setSelfLink($self); + + // Add navigation links: next, prev, first + // Note: we use IDs rather than pages for navigation; page boundaries + // change too quickly! + + if (!empty($this->next_id)) { + $nextUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('max_id' => $this->next_id)); + + $atom->addLink($nextUrl, + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) { + + $lastNotice = $this->notices[0]; + $lastId = $lastNotice->id; + + $prevUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('since_id' => $lastId)); + + $atom->addLink($prevUrl, + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) { + + $firstUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id)); + + $atom->addLink($firstUrl, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + } + $atom->addEntryFromNotices($this->notices); $this->raw($atom->getString()); @@ -181,13 +224,18 @@ class ApiTimelineUserAction extends ApiBareAuthAction { $notices = array(); - $notice = $this->user->getNotices( - ($this->page-1) * $this->count, $this->count, - $this->since_id, $this->max_id - ); + $notice = $this->user->getNotices(($this->page-1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id); while ($notice->fetch()) { - $notices[] = clone($notice); + if (count($notices) < $this->count) { + $notices[] = clone($notice); + } else { + $this->next_id = $notice->id; + break; + } } return $notices; From 35931e3a0e3b78f222e8aa6eb02e71ca7a3b8410 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 8 Nov 2010 10:36:19 -0500 Subject: [PATCH 09/74] first steps for email summary --- plugins/EmailSummary/EmailSummaryPlugin.php | 119 +++++++++++++ plugins/EmailSummary/Email_summary_status.php | 167 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 plugins/EmailSummary/EmailSummaryPlugin.php create mode 100644 plugins/EmailSummary/Email_summary_status.php diff --git a/plugins/EmailSummary/EmailSummaryPlugin.php b/plugins/EmailSummary/EmailSummaryPlugin.php new file mode 100644 index 0000000000..dd72329f88 --- /dev/null +++ b/plugins/EmailSummary/EmailSummaryPlugin.php @@ -0,0 +1,119 @@ +. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugin for sending email summaries to users + * + * @category Email + * @package StatusNet + * @author Brion Vibber + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class EmailSummaryPlugin extends Plugin +{ + /** + * Database schema setup + * + * @return boolean hook value + */ + + function onCheckSchema() + { + $schema = Schema::get(); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('email_summary_status', + array(new ColumnDef('user_id', 'integer', null, + false, 'PRI'), + new ColumnDef('send_summary', 'tinyint', null, + false, null, 1), + new ColumnDef('last_summary', 'datetime', null, + true), + new ColumnDef('created', 'datetime', null, + false), + new ColumnDef('modified', 'datetime', null, + false), + ) + ); + return true; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + * + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'Email_summary_status': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Version info for this plugin + * + * @param array &$versions array of version data + * + * @return boolean hook value; true means continue processing, false means stop. + * + */ + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'EmailSummary', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:EmailSummary', + 'rawdescription' => + _m('Send an email summary of the inbox to users.')); + return true; + } +} diff --git a/plugins/EmailSummary/Email_summary_status.php b/plugins/EmailSummary/Email_summary_status.php new file mode 100644 index 0000000000..a0462fd04c --- /dev/null +++ b/plugins/EmailSummary/Email_summary_status.php @@ -0,0 +1,167 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for email summaries + * + * Email summary information for users + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Email_summary_status extends Memcached_DataObject +{ + public $__table = 'email_summary_status'; // table name + public $user_id; // int(4) primary_key not_null + public $send_summary; // tinyint not_null + public $last_summary; // int(4) primary_key not_null + public $created; // int(4) primary_key not_null + public $modified; // int(4) primary_key not_null + + /** + * Get an instance by key + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return Email_summary_status object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('email_summary_status', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'send_summary' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'last_summary' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'created' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'modified' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME); + } + + /** + * return key definitions for DB_DataObject + * + * @return array list of key field names + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + function keyTypes() + { + return array('user_id' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Helper function + * + * @param integer $user_id ID of the user to get a count for + * + * @return int flag for whether to send this user a summary email + */ + + static function getSendSummary($user_id) + { + $ess = Email_summary_status::staticGet('user_id', $user_id); + + if (!empty($ess)) { + return $ess->send_summary; + } else { + return 1; + } + } + + /** + * Get email summary status for a user + * + * @param integer $user_id ID of the user to get a count for + * + * @return Email_summary_status instance for this user, with count already incremented. + */ + + static function getLastSummary($user_id) + { + $ess = Email_summary_status::staticGet('user_id', $user_id); + + if (!empty($ess)) { + return $ess->last_summary; + } else { + return 1; + } + } +} From 797059340e304a707dda5596f30651ffc727f3c3 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 8 Nov 2010 13:10:09 -0500 Subject: [PATCH 10/74] Complete email summary sending system Added the necessary classes to send email summaries. First, added a script to run on a daily basis. Second, added a queue handler for sending email summaries for users, and another to queue summaries for all users on the site. Fixed up the email_summary_status table to store the last-sent notice id, rather than a datetime (since we don't support 'since' parameters anymore). Finally, made the plugin class load the right modules when needed. --- plugins/EmailSummary/EmailSummaryPlugin.php | 21 ++- plugins/EmailSummary/Email_summary_status.php | 18 +- plugins/EmailSummary/sendemailsummary.php | 47 +++++ .../EmailSummary/siteemailsummaryhandler.php | 96 ++++++++++ .../EmailSummary/useremailsummaryhandler.php | 172 ++++++++++++++++++ 5 files changed, 344 insertions(+), 10 deletions(-) create mode 100644 plugins/EmailSummary/sendemailsummary.php create mode 100644 plugins/EmailSummary/siteemailsummaryhandler.php create mode 100644 plugins/EmailSummary/useremailsummaryhandler.php diff --git a/plugins/EmailSummary/EmailSummaryPlugin.php b/plugins/EmailSummary/EmailSummaryPlugin.php index dd72329f88..03577bb4a7 100644 --- a/plugins/EmailSummary/EmailSummaryPlugin.php +++ b/plugins/EmailSummary/EmailSummaryPlugin.php @@ -63,7 +63,7 @@ class EmailSummaryPlugin extends Plugin false, 'PRI'), new ColumnDef('send_summary', 'tinyint', null, false, null, 1), - new ColumnDef('last_summary', 'datetime', null, + new ColumnDef('last_summary_id', 'integer', null, true), new ColumnDef('created', 'datetime', null, false), @@ -89,6 +89,10 @@ class EmailSummaryPlugin extends Plugin switch ($cls) { + case 'SiteEmailSummaryHandler': + case 'UserEmailSummaryHandler': + include_once $dir . '/'.strtolower($cls).'.php'; + return false; case 'Email_summary_status': include_once $dir . '/'.$cls.'.php'; return false; @@ -116,4 +120,19 @@ class EmailSummaryPlugin extends Plugin _m('Send an email summary of the inbox to users.')); return true; } + + /** + * Register our queue handlers + * + * @param QueueManager $qm Current queue manager + * + * @return boolean hook value + */ + + function onEndInitializeQueueManager($qm) + { + $qm->connect('sitesum', 'SiteEmailSummaryHandler'); + $qm->connect('usersum', 'UserEmailSummaryHandler'); + return true; + } } diff --git a/plugins/EmailSummary/Email_summary_status.php b/plugins/EmailSummary/Email_summary_status.php index a0462fd04c..5b5b231e34 100644 --- a/plugins/EmailSummary/Email_summary_status.php +++ b/plugins/EmailSummary/Email_summary_status.php @@ -52,9 +52,9 @@ class Email_summary_status extends Memcached_DataObject public $__table = 'email_summary_status'; // table name public $user_id; // int(4) primary_key not_null public $send_summary; // tinyint not_null - public $last_summary; // int(4) primary_key not_null - public $created; // int(4) primary_key not_null - public $modified; // int(4) primary_key not_null + public $last_summary_id; // int(4) null + public $created; // datetime not_null + public $modified; // datetime not_null /** * Get an instance by key @@ -83,9 +83,9 @@ class Email_summary_status extends Memcached_DataObject { return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, 'send_summary' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'last_summary' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, - 'created' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, - 'modified' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME); + 'last_summary_id' => DB_DATAOBJECT_INT, + 'created' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); } /** @@ -154,14 +154,14 @@ class Email_summary_status extends Memcached_DataObject * @return Email_summary_status instance for this user, with count already incremented. */ - static function getLastSummary($user_id) + static function getLastSummaryID($user_id) { $ess = Email_summary_status::staticGet('user_id', $user_id); if (!empty($ess)) { - return $ess->last_summary; + return $ess->last_summary_id; } else { - return 1; + return null; } } } diff --git a/plugins/EmailSummary/sendemailsummary.php b/plugins/EmailSummary/sendemailsummary.php new file mode 100644 index 0000000000..37bfdcfbd1 --- /dev/null +++ b/plugins/EmailSummary/sendemailsummary.php @@ -0,0 +1,47 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..')); + +$shortoptions = 'i:n:a'; +$longoptions = array('id=', 'nickname=', 'all'); + +$helptext = <<enqueue($user->id, 'usersum'); +} catch (NoUserArgumentException $nuae) { + $qm->enqueue(null, 'sitesum'); +} diff --git a/plugins/EmailSummary/siteemailsummaryhandler.php b/plugins/EmailSummary/siteemailsummaryhandler.php new file mode 100644 index 0000000000..595c3267a1 --- /dev/null +++ b/plugins/EmailSummary/siteemailsummaryhandler.php @@ -0,0 +1,96 @@ +. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * + * Handler for queue items of type 'sitesum', sends email summaries + * to all users on the site. + * + * @category Email + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class SiteEmailSummaryHandler extends QueueHandler +{ + + /** + * Return transport keyword which identifies items this queue handler + * services; must be defined for all subclasses. + * + * Must be 8 characters or less to fit in the queue_item database. + * ex "email", "jabber", "sms", "irc", ... + * + * @return string + */ + + function transport() + { + return 'sitesum'; + } + + /** + * Handle the site + * + * @param mixed $object + * @return boolean true on success, false on failure + */ + + function handle($object) + { + $qm = QueueManager::get(); + + try { + // Enqueue a summary for all users + + $user = new User(); + $user->find(); + + while ($user->fetch()) { + try { + $qm->enqueue($user->id, 'usersum'); + } catch (Exception $e) { + common_log(LOG_WARNING, $e->getMessage()); + continue; + } + } + } catch (Exception $e) { + common_log(LOG_WARNING, $e->getMessage()); + } + + return true; + } +} + diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php new file mode 100644 index 0000000000..f3151bd476 --- /dev/null +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -0,0 +1,172 @@ +. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Handler for queue items of type 'usersum', sends an email summaries + * to a particular user. + * + * @category Email + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class UserEmailSummaryHandler extends QueueHandler +{ + // Maximum number of notices to include by default. This is probably too much. + + const MAX_NOTICES = 200; + + /** + * Return transport keyword which identifies items this queue handler + * services; must be defined for all subclasses. + * + * Must be 8 characters or less to fit in the queue_item database. + * ex "email", "jabber", "sms", "irc", ... + * + * @return string + */ + + function transport() + { + return 'sitesum'; + } + + /** + * Send a summary email to the user + * + * @param mixed $object + * @return boolean true on success, false on failure + */ + + function handle($user_id) + { + // Skip if they've asked not to get summaries + + $ess = Email_summary_status::staticGet('user_id', $user_id); + + if (!empty($ess) && !$ess->send_summary) { + common_log(LOG_INFO, sprintf('Not sending email summary for user %s by request.', $user_id)); + return true; + } + + $since_id = null; + + if (!empty($ess)) { + $since_id = $ess->last_summary_id; + } + + $user = User::staticGet('id', $user_id); + + if (empty($user)) { + common_log(LOG_INFO, sprintf('Not sending email summary for user %s; no such user.', $user_id)); + return true; + } + + if (empty($user->email)) { + common_log(LOG_INFO, sprintf('Not sending email summary for user %s; no email address.', $user_id)); + return true; + } + + $profile = $user->getProfile(); + + if (empty($profile)) { + common_log(LOG_WARNING, sprintf('Not sending email summary for user %s; no profile.', $user_id)); + return true; + } + + $notice = $user->ownFriendsTimeline(0, self::MAX_NOTICES, $since_id); + + if (empty($notice) || $notice->N == 0) { + common_log(LOG_WARNING, sprintf('Not sending email summary for user %s; no notices.', $user_id)); + return true; + } + + // XXX: This is risky fingerpoken in der objektvars, but I didn't feel like + // figuring out a better way. -ESP + + $new_top = null; + + if ($notice instanceof ArrayWrapper) { + $new_top = $notice->_items[0]->id; + } + + $out = new XMLStringer(); + + $out->raw(sprintf(_('

Recent updates from %1s for %2s:

'), + common_config('site', 'name'), + $profile->getBestName())); + + $nl = new NoticeList($notice, $out); + + // Outputs to the string + + $nl->show(); + + $out->raw(sprintf(_('

change your email settings for %2s

'), + common_local_url('emailsettings'), + common_config('site', 'name'))); + + $body = $out->getString(); + + // FIXME: do something for people who don't like HTML email + + mail_to_user($user, _('Updates from your network'), $body, + array('Content-Type', 'text/html; charset=UTF-8')); + + if (empty($ess)) { + + $ess = new Email_summary_status(); + + $ess->user_id = $user_id; + $ess->created = common_sql_now(); + $ess->last_summary_id = $new_top; + $ess->modified = common_sql_now(); + + $ess->insert(); + + } else { + + $orig = clone($ess); + + $ess->last_summary_id = $new_top; + $ess->modified = common_sql_now(); + + $ess->update(); + } + + return true; + } +} + From 883f7a6c0b1464f6723e51bf99d06641a612f968 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 13:27:54 -0800 Subject: [PATCH 11/74] Avoid marking files as attachments that are not locally uploaded, unless they're really oembedable. HTML-y things now excluded properly. --- classes/File.php | 3 +++ lib/util.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/classes/File.php b/classes/File.php index 16e00024a5..d71403e649 100644 --- a/classes/File.php +++ b/classes/File.php @@ -352,6 +352,9 @@ class File extends Memcached_DataObject $mimetype = substr($mimetype,0,$semicolon); } if(in_array($mimetype,$notEnclosureMimeTypes)){ + // Never treat HTML as an enclosure type! + return false; + } else { $oembed = File_oembed::staticGet('file_id',$this->id); if($oembed){ $mimetype = strtolower($oembed->mimetype); diff --git a/lib/util.php b/lib/util.php index 8f2a9f1738..e6b62f750f 100644 --- a/lib/util.php +++ b/lib/util.php @@ -877,7 +877,7 @@ function common_linkify($url) { } if (!empty($f)) { - if ($f->getEnclosure() || File_oembed::staticGet('file_id',$f->id)) { + if ($f->getEnclosure()) { $is_attachment = true; $attachment_id = $f->id; From 32321de5e048ee50417a6d91b651180c28a801b1 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 14:20:23 -0800 Subject: [PATCH 12/74] Some initial testing w/ thumb gen --- js/util.js | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/js/util.js b/js/util.js index 1be3f30535..a4eb0fc284 100644 --- a/js/util.js +++ b/js/util.js @@ -428,30 +428,15 @@ var SN = { // StatusNet }).attr('title', SN.msg('showmore_tooltip')); } else { - $.fn.jOverlay.options = { - method : 'GET', - data : '', - url : '', - color : '#000', - opacity : '0.6', - zIndex : 9999, - center : false, - imgLoading : $('address .url')[0].href+'theme/base/images/illustrations/illu_progress_loading-01.gif', - bgClickToClose : true, - success : function() { - $('#jOverlayContent').append(''); - $('#jOverlayContent button').click($.closeOverlay); - }, - timeout : 0, - autoHide : true, - css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'} - }; + //imgLoading : $('address .url')[0].href+'theme/base/images/illustrations/illu_progress_loading-01.gif', - notice.find('a.attachment').click(function() { + notice.find('a.attachment').each(function() { var attachId = ($(this).attr('id').substring('attachment'.length + 1)); if (attachId) { - $().jOverlay({url: $('address .url')[0].href+'attachment/' + attachId + '/ajax'}); - return false; + var thumbUrl = $('address .url')[0].href+'attachment/' + attachId + '/thumb'; + var thumb = $('
Thumb:
'); + thumb.find('img').attr('src', thumbUrl).last(); + notice.append(thumb); } }); From 0a56523461ec2b7abce652d18129c043eddb6394 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 8 Nov 2010 17:22:16 -0500 Subject: [PATCH 13/74] Fixup headers for HTML email --- plugins/EmailSummary/useremailsummaryhandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index f3151bd476..4bbbb861c0 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -143,7 +143,7 @@ class UserEmailSummaryHandler extends QueueHandler // FIXME: do something for people who don't like HTML email mail_to_user($user, _('Updates from your network'), $body, - array('Content-Type', 'text/html; charset=UTF-8')); + array('Content-Type' => 'text/html; charset=UTF-8')); if (empty($ess)) { From 37407d8c7751b1966f41484678e7695157a460c3 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 8 Nov 2010 17:31:21 -0500 Subject: [PATCH 14/74] stylesheet for outgoing email --- .../EmailSummary/useremailsummaryhandler.php | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index 4bbbb861c0..9ef56a20d4 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -124,6 +124,8 @@ class UserEmailSummaryHandler extends QueueHandler $out = new XMLStringer(); + $out->raw(''); + $out->raw(sprintf(_('

Recent updates from %1s for %2s:

'), common_config('site', 'name'), $profile->getBestName())); @@ -168,5 +170,295 @@ class UserEmailSummaryHandler extends QueueHandler return true; } + + function stylesheet() + { + $ss = << Date: Mon, 8 Nov 2010 18:14:13 -0500 Subject: [PATCH 15/74] change to a table for HTML output --- .../EmailSummary/useremailsummaryhandler.php | 350 +++--------------- 1 file changed, 54 insertions(+), 296 deletions(-) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index 9ef56a20d4..1ac1eaedbe 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -124,17 +124,66 @@ class UserEmailSummaryHandler extends QueueHandler $out = new XMLStringer(); - $out->raw(''); - $out->raw(sprintf(_('

Recent updates from %1s for %2s:

'), common_config('site', 'name'), $profile->getBestName())); - $nl = new NoticeList($notice, $out); - // Outputs to the string + $out->elementStart('table', array('width' => '100%', 'style' => 'border: none')); - $nl->show(); + while ($notice->fetch()) { + + $profile = Profile::staticGet('id', $notice->profile_id); + + if (empty($profile)) { + continue; + } + + $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); + + $out->elementStart('tr'); + $out->elementStart('td'); + $out->element('img', array('src' => ($avatar) ? + $avatar->displayUrl() : + Avatar::defaultImage($avatar_size), + 'class' => 'avatar photo', + 'width' => $avatar_size, + 'height' => $avatar_size, + 'alt' => $profile->getBestName())); + $out->elementEnd('td'); + $out->elementStart('td'); + $out->element('a', array('href' => $profile->profileurl), + $profile->nickname); + $out->text(' '); + $out->raw($notice->rendered); + $out->element('br'); // yeah, you know it. I just wrote a
in the middle of my table layout. + $noticeurl = $notice->bestUrl(); + // above should always return an URL + assert(!empty($noticeurl)); + $out->elementStart('a', array('rel' => 'bookmark', + 'class' => 'timestamp', + 'href' => $noticeurl)); + $dt = common_date_iso8601($notice->created); + $out->element('abbr', array('class' => 'published', + 'title' => $dt), + common_date_string($notice->created)); + $out->elementEnd('a'); + if ($notice->hasConversation()) { + $conv = Conversation::staticGet('id', $notice->conversation); + $convurl = $conv->uri; + if (!empty($convurl)) { + $out->text(' '); + $out->element('a', + array('href' => $convurl.'#notice-'.$notice->id, + 'class' => 'response'), + _('in context')); + } + } + $out->elementEnd('td'); + $out->elementEnd('tr'); + } + + $out->elementEnd('table'); $out->raw(sprintf(_('

change your email settings for %2s

'), common_local_url('emailsettings'), @@ -170,295 +219,4 @@ class UserEmailSummaryHandler extends QueueHandler return true; } - - function stylesheet() - { - $ss = << Date: Mon, 8 Nov 2010 15:32:41 -0800 Subject: [PATCH 16/74] doomy doom doom --- actions/shownotice.php | 5 --- js/util.js | 8 +++-- lib/attachmentlist.php | 64 +++++++++++++++++++++++++++----------- lib/noticelist.php | 6 ++++ theme/base/css/display.css | 20 +++++++++--- 5 files changed, 73 insertions(+), 30 deletions(-) diff --git a/actions/shownotice.php b/actions/shownotice.php index b7e61a1375..7a11787b66 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -356,9 +356,4 @@ class SingleNoticeItem extends DoFollowListItem $this->profile->fullname : $this->profile->nickname)); } - - function showNoticeAttachments() { - $al = new AttachmentList($this->notice, $this->out); - $al->show(); - } } diff --git a/js/util.js b/js/util.js index a4eb0fc284..15fb163103 100644 --- a/js/util.js +++ b/js/util.js @@ -431,16 +431,19 @@ var SN = { // StatusNet //imgLoading : $('address .url')[0].href+'theme/base/images/illustrations/illu_progress_loading-01.gif', notice.find('a.attachment').each(function() { + /* var attachId = ($(this).attr('id').substring('attachment'.length + 1)); if (attachId) { var thumbUrl = $('address .url')[0].href+'attachment/' + attachId + '/thumb'; - var thumb = $('
Thumb:
'); + var thumb = $('
Thumb:
'); thumb.find('img').attr('src', thumbUrl).last(); - notice.append(thumb); + notice.find('.entry-title .entry-content').append(thumb); } + */ }); if ($('#shownotice').length == 0) { + /* var t; notice.find('a.thumbnail').hover( function() { @@ -465,6 +468,7 @@ var SN = { // StatusNet $(this).closest('.entry-title').removeClass('ov'); } ); + */ } } }, diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index f6b09fb491..f29d32ada3 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -181,9 +181,11 @@ class AttachmentListItem extends Widget */ function show() { - $this->showStart(); - $this->showNoticeAttachment(); - $this->showEnd(); + if ($this->attachment->isEnclosure()) { + $this->showStart(); + $this->showNoticeAttachment(); + $this->showEnd(); + } } function linkAttr() { @@ -203,9 +205,44 @@ class AttachmentListItem extends Widget } function showRepresentation() { + $thumb = $this->getThumbInfo(); + if ($thumb) { + $thumb = $this->sizeThumb($thumb); + $this->out->element('img', array('alt' => '', 'src' => $thumb->url, 'width' => $thumb->width, 'height' => $thumb->height)); + } + } + + function getThumbInfo() + { $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id); - if (!empty($thumbnail)) { - $this->out->element('img', array('alt' => '', 'src' => $thumbnail->url, 'width' => $thumbnail->width, 'height' => $thumbnail->height)); + if ($thumbnail) { + return $thumbnail; + } else { + switch ($this->attachment->mimetype) { + case 'image/gif': + case 'image/png': + case 'image/jpg': + case 'image/jpeg': + $thumb = (object)array(); + $thumb->url = $this->attachment->url; + $thumb->width = 100; + $thumb->height = 75; // @fixme + return $thumb; + } + } + return false; + } + + function sizeThumb($thumbnail) { + $maxWidth = 100; + $maxHeight = 75; + if ($thumbnail->width > $maxWidth) { + $thumb = clone($thumbnail); + $thumb->width = $maxWidth; + $thumb->height = intval($thumbnail->height * $maxWidth / $thumbnail->width); + return $thumb; + } else { + return $thumbnail; } } @@ -234,6 +271,9 @@ class AttachmentListItem extends Widget } } +/** + * used for one-off attachment action + */ class Attachment extends AttachmentListItem { function showLink() { @@ -414,18 +454,4 @@ class Attachment extends AttachmentListItem return $scrubbed; } - - function showFallback() - { - // If we don't know how to display an attachment inline, we probably - // shouldn't have gotten to this point. - // - // But, here we are... displaying details on a file or remote URL - // either on the main view or in an ajax-loaded lightbox. As a lesser - // of several evils, we'll try redirecting to the actual target via - // client-side JS. - - common_log(LOG_ERR, "Empty or unknown type for file id {$this->attachment->id}; falling back to client-side redirect."); - $this->out->raw(''); - } } diff --git a/lib/noticelist.php b/lib/noticelist.php index 6f82c9269b..fb5db2374c 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -208,6 +208,7 @@ class NoticeListItem extends Widget $this->showStart(); if (Event::handle('StartShowNoticeItem', array($this))) { $this->showNotice(); + $this->showNoticeAttachments(); $this->showNoticeInfo(); $this->showNoticeOptions(); Event::handle('EndShowNoticeItem', array($this)); @@ -383,6 +384,11 @@ class NoticeListItem extends Widget $this->out->elementEnd('p'); } + function showNoticeAttachments() { + $al = new AttachmentList($this->notice, $this->out); + $al->show(); + } + /** * show the link to the main page for the notice * diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 7ac66095a8..29f7d0ae0d 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1150,7 +1150,8 @@ border-radius:4px; -webkit-border-radius:4px; } -.notice div.entry-content { +.notice div.entry-content, +.notice dl.entry-content { clear:left; float:left; font-size:0.95em; @@ -1325,6 +1326,7 @@ margin-left:4px; .notice .attachment.more { padding-left:0; } +/* .notice .attachment img { position:absolute; top:18px; @@ -1334,20 +1336,30 @@ z-index:99; #shownotice .notice .attachment img { position:static; } +*/ -#attachments { + +/* Small inline attachment list */ +#attachments ol li { + list-style-type: none; +} +#attachments dt { + display: none; +} + +#shownotice #attachments { clear:both; float:left; width:100%; margin-top:18px; } -#attachments dt { +#shownotice #attachments dt { font-weight:bold; font-size:1.3em; margin-bottom:4px; } -#attachments ol li { +#shownotice #attachments ol li { margin-bottom:18px; list-style-type:decimal; float:left; From a2994e3aa232106f39a6c36c9176608467b8822e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 15:50:06 -0800 Subject: [PATCH 17/74] Testing... using photo info for temp thumbnails --- classes/File.php | 7 +++---- lib/attachmentlist.php | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/classes/File.php b/classes/File.php index d71403e649..e3b922d13b 100644 --- a/classes/File.php +++ b/classes/File.php @@ -352,11 +352,10 @@ class File extends Memcached_DataObject $mimetype = substr($mimetype,0,$semicolon); } if(in_array($mimetype,$notEnclosureMimeTypes)){ - // Never treat HTML as an enclosure type! - return false; - } else { + // Never treat generic HTML links as an enclosure type! + // But if we have oEmbed info, we'll consider it golden. $oembed = File_oembed::staticGet('file_id',$this->id); - if($oembed){ + if($oembed && in_array($oembed->type, array('photo', 'video'))){ $mimetype = strtolower($oembed->mimetype); $semicolon = strpos($mimetype,';'); if($semicolon){ diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index f29d32ada3..8e6ad038a3 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -212,19 +212,30 @@ class AttachmentListItem extends Widget } } + /** + * Pull a thumbnail image reference for the given file. + * In order we check: + * 1) file_thumbnail table (thumbnails found via oEmbed) + * 2) image URL from direct dereference or oEmbed 'photo' type URL + * 3) ??? + * + * @return mixed object with (url, width, height) properties, or false + */ function getThumbInfo() { $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id); if ($thumbnail) { return $thumbnail; - } else { - switch ($this->attachment->mimetype) { + } + $enc = $this->attachment->getEnclosure(); + if ($enc) { + switch ($enc->mimetype) { case 'image/gif': case 'image/png': case 'image/jpg': case 'image/jpeg': $thumb = (object)array(); - $thumb->url = $this->attachment->url; + $thumb->url = $enc->url; $thumb->width = 100; $thumb->height = 75; // @fixme return $thumb; From dc497ed090d94561db195983a4346a2f66503422 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 16:51:31 -0800 Subject: [PATCH 18/74] Break out ImageFile->resizeTo() from ImageFile->resize(); allows resizing images to non-square sizes and to arbitrary destinations. Will be used for creating thumbnails as well as the originala use of cropping/sizing avatars. --- lib/imagefile.php | 103 +++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 34 deletions(-) diff --git a/lib/imagefile.php b/lib/imagefile.php index b70fd248e1..159deead61 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -115,10 +115,46 @@ class ImageFile return new ImageFile(null, $_FILES[$param]['tmp_name']); } + /** + * Compat interface for old code generating avatar thumbnails... + * Saves the scaled file directly into the avatar area. + * + * @param int $size target width & height -- must be square + * @param int $x (default 0) upper-left corner to crop from + * @param int $y (default 0) upper-left corner to crop from + * @param int $w (default full) width of image area to crop + * @param int $h (default full) height of image area to crop + * @return string filename + */ function resize($size, $x = 0, $y = 0, $w = null, $h = null) + { + $targetType = $this->preferredType($this->type); + $outname = Avatar::filename($this->id, + image_type_to_extension($targetType), + $size, + common_timestamp()); + $outpath = Avatar::path($outname); + $this->resizeTo($outpath, $size, $size, $x, $y, $w, $h); + return $outname; + } + + /** + * Create and save a thumbnail image. + * + * @param string $outpath + * @param int $width target width + * @param int $height target height + * @param int $x (default 0) upper-left corner to crop from + * @param int $y (default 0) upper-left corner to crop from + * @param int $w (default full) width of image area to crop + * @param int $h (default full) height of image area to crop + * @return string full local filesystem filename + */ + function resizeTo($outpath, $width, $height, $x=0, $y=0, $w=null, $h=null) { $w = ($w === null) ? $this->width:$w; $h = ($h === null) ? $this->height:$h; + $targetType = $this->preferredType($this->type); if (!file_exists($this->filepath)) { throw new Exception(_('Lost our file.')); @@ -126,20 +162,16 @@ class ImageFile } // Don't crop/scale if it isn't necessary - if ($size === $this->width - && $size === $this->height + if ($width === $this->width + && $height === $this->height && $x === 0 && $y === 0 && $w === $this->width - && $h === $this->height) { + && $h === $this->height + && $this->type == $targetType) { - $outname = Avatar::filename($this->id, - image_type_to_extension($this->type), - $size, - common_timestamp()); - $outpath = Avatar::path($outname); @copy($this->filepath, $outpath); - return $outname; + return $outpath; } switch ($this->type) { @@ -166,7 +198,7 @@ class ImageFile return; } - $image_dest = imagecreatetruecolor($size, $size); + $image_dest = imagecreatetruecolor($width, $height); if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) { @@ -189,30 +221,9 @@ class ImageFile } } - imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $size, $size, $w, $h); + imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $width, $height, $w, $h); - if($this->type == IMAGETYPE_BMP) { - //we don't want to save BMP... it's an inefficient, rare, antiquated format - //save png instead - $this->type = IMAGETYPE_PNG; - } else if($this->type == IMAGETYPE_WBMP) { - //we don't want to save WBMP... it's a rare format that we can't guarantee clients will support - //save png instead - $this->type = IMAGETYPE_PNG; - } else if($this->type == IMAGETYPE_XBM) { - //we don't want to save XBM... it's a rare format that we can't guarantee clients will support - //save png instead - $this->type = IMAGETYPE_PNG; - } - - $outname = Avatar::filename($this->id, - image_type_to_extension($this->type), - $size, - common_timestamp()); - - $outpath = Avatar::path($outname); - - switch ($this->type) { + switch ($targetType) { case IMAGETYPE_GIF: imagegif($image_dest, $outpath); break; @@ -230,7 +241,31 @@ class ImageFile imagedestroy($image_src); imagedestroy($image_dest); - return $outname; + return $outpath; + } + + /** + * Several obscure file types should be normalized to PNG on resize. + * + * @param int $type + * @return int + */ + function preferredType($type) + { + if($type == IMAGETYPE_BMP) { + //we don't want to save BMP... it's an inefficient, rare, antiquated format + //save png instead + return IMAGETYPE_PNG; + } else if($type == IMAGETYPE_WBMP) { + //we don't want to save WBMP... it's a rare format that we can't guarantee clients will support + //save png instead + return IMAGETYPE_PNG; + } else if($type == IMAGETYPE_XBM) { + //we don't want to save XBM... it's a rare format that we can't guarantee clients will support + //save png instead + return IMAGETYPE_PNG; + } + return $type; } function unlink() From c36fecb79431218016615cde45215337d67fee67 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 17:20:04 -0800 Subject: [PATCH 19/74] Save a thumbnail image when uploading an image file into the file attachments system. Currently hardcoded to 100x75, needs aspect-sensitivity etc. --- classes/File_thumbnail.php | 30 ++++++++++++++++++++++++++---- lib/mediafile.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index edae8ac21a..d371b9e8aa 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -48,12 +48,34 @@ class File_thumbnail extends Memcached_DataObject return array(false, false, false); } - function saveNew($data, $file_id) { + /** + * Save oEmbed-provided thumbnail data + * + * @param object $data + * @param int $file_id + */ + public static function saveNew($data, $file_id) { + self::saveThumbnail($file_id, + $data->thumbnail_url, + $data->thumbnail_width, + $data->thumbnail_height); + } + + /** + * Save a thumbnail record for the referenced file record. + * + * @param int $file_id + * @param string $url + * @param int $width + * @param int $height + */ + static function saveThumbnail($file_id, $url, $width, $height) + { $tn = new File_thumbnail; $tn->file_id = $file_id; - $tn->url = $data->thumbnail_url; - $tn->width = intval($data->thumbnail_width); - $tn->height = intval($data->thumbnail_height); + $tn->url = $url; + $tn->width = intval($width); + $tn->height = intval($height); $tn->insert(); } } diff --git a/lib/mediafile.php b/lib/mediafile.php index aad3575d72..2c04b46501 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -48,11 +48,14 @@ class MediaFile { if ($user == null) { $this->user = common_current_user(); + } else { + $this->user = $user; } $this->filename = $filename; $this->mimetype = $mimetype; $this->fileRecord = $this->storeFile(); + $this->thumbnailRecord = $this->storeThumbnail(); $this->fileurl = common_local_url('attachment', array('attachment' => $this->fileRecord->id)); @@ -102,6 +105,38 @@ class MediaFile return $file; } + /** + * Generate and store a thumbnail image for the uploaded file, if applicable. + * + * @return File_thumbnail or null + */ + function storeThumbnail() + { + if (substr($this->mimetype, 0, strlen('image/')) != 'image/') { + // @fixme video thumbs would be nice! + return null; + } + try { + $image = new ImageFile($this->fileRecord->id, + File::path($this->filename)); + } catch (Exception $e) { + // Unsupported image type. + return null; + } + + $outname = File::filename($this->user->getProfile(), 'thumb-' . $this->filename, $this->mimetype); + $outpath = File::path($outname); + + $width = 100; + $height = 75; + + $image->resizeTo($outpath, $width, $height); + File_thumbnail::saveThumbnail($this->fileRecord->id, + File::url($outname), + $width, + $height); + } + function rememberFile($file, $short) { $this->maybeAddRedir($file->id, $short); From 6d7f02ff31c6c929223030b051541b1bf103f3a8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 17:22:01 -0800 Subject: [PATCH 20/74] Pass file attachment thumbnails along with oEmbed data. --- actions/oembed.php | 6 ++++++ classes/File.php | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/actions/oembed.php b/actions/oembed.php index da3aa0c716..11d8145837 100644 --- a/actions/oembed.php +++ b/actions/oembed.php @@ -112,6 +112,12 @@ class OembedAction extends Action //$oembed['width']= //$oembed['height']= $oembed['url']=$attachment->url; + $thumb = $attachment->getThumbnail(); + if ($thumb) { + $oembed['thumbnail_url'] = $thumb->url; + $oembed['thumbnail_width'] = $thumb->width; + $oembed['thumbnail_height'] = $thumb->height; + } }else{ $oembed['type']='link'; $oembed['url']=common_local_url('attachment', diff --git a/classes/File.php b/classes/File.php index e3b922d13b..56bc73ab25 100644 --- a/classes/File.php +++ b/classes/File.php @@ -384,4 +384,14 @@ class File extends Memcached_DataObject $enclosure = $this->getEnclosure(); return !empty($enclosure); } + + /** + * Get the attachment's thumbnail record, if any. + * + * @return File_thumbnail + */ + function getThumbnail() + { + return File_thumbnail::staticGet('file_id', $this->id); + } } From 694448e0aa81edb7b010f102ee9ee0e6961f6f7c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 17:36:02 -0800 Subject: [PATCH 21/74] Add attachments 'thumb_width' and 'thumb_height' settings for inline thumbs, defaulting to 100x75. This is used as the max thumb width/height for oEmbed requests (replacing the old default of 500x400 which was more suitable for the lightbox). --- classes/File_oembed.php | 6 +++--- lib/default.php | 2 ++ lib/mediafile.php | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/classes/File_oembed.php b/classes/File_oembed.php index 4813d5dda5..a5540ecfee 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -58,11 +58,11 @@ class File_oembed extends Memcached_DataObject return array(false, false, false); } - function _getOembed($url, $maxwidth = 500, $maxheight = 400) { + function _getOembed($url) { require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; $parameters = array( - 'maxwidth'=>$maxwidth, - 'maxheight'=>$maxheight, + 'maxwidth' => common_config('attachments', 'thumb_width'), + 'maxheight' => common_config('attachments', 'thumb_height'), ); try{ $oEmbed = new Services_oEmbed($url); diff --git a/lib/default.php b/lib/default.php index a19453fce4..87f4e45c0e 100644 --- a/lib/default.php +++ b/lib/default.php @@ -250,6 +250,8 @@ $default = 'monthly_quota' => 15000000, 'uploads' => true, 'filecommand' => '/usr/bin/file', + 'thumb_width' => 100, + 'thumb_height' => 75, ), 'application' => array('desclimit' => null), diff --git a/lib/mediafile.php b/lib/mediafile.php index 2c04b46501..febf4603a7 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -127,8 +127,8 @@ class MediaFile $outname = File::filename($this->user->getProfile(), 'thumb-' . $this->filename, $this->mimetype); $outpath = File::path($outname); - $width = 100; - $height = 75; + $width = common_config('attachments', 'thumb_width'); + $height = common_config('attachments', 'thumb_height'); $image->resizeTo($outpath, $width, $height); File_thumbnail::saveThumbnail($this->fileRecord->id, From 504529e8cd8fbaf5e8e1b980260d1d87d9e880ac Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Nov 2010 17:51:53 -0800 Subject: [PATCH 22/74] Keep aspect ratio when generating local thumbnails --- lib/mediafile.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/mediafile.php b/lib/mediafile.php index febf4603a7..a41d7c76b5 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -127,8 +127,9 @@ class MediaFile $outname = File::filename($this->user->getProfile(), 'thumb-' . $this->filename, $this->mimetype); $outpath = File::path($outname); - $width = common_config('attachments', 'thumb_width'); - $height = common_config('attachments', 'thumb_height'); + $maxWidth = common_config('attachments', 'thumb_width'); + $maxHeight = common_config('attachments', 'thumb_height'); + list($width, $height) = $this->scaleToFit($image->width, $image->height, $maxWidth, $maxHeight); $image->resizeTo($outpath, $width, $height); File_thumbnail::saveThumbnail($this->fileRecord->id, @@ -137,6 +138,19 @@ class MediaFile $height); } + function scaleToFit($width, $height, $maxWidth, $maxHeight) + { + $aspect = $maxWidth / $maxHeight; + $w1 = $maxWidth; + $h1 = intval($height * $maxWidth / $width); + if ($h1 > $maxHeight) { + $w2 = intval($width * $maxHeight / $height); + $h2 = $maxHeight; + return array($w2, $h2); + } + return array($w1, $h1); + } + function rememberFile($file, $short) { $this->maybeAddRedir($file->id, $short); From 76aed36f38b6ee0f713dfdeb0e8afe2ad5723594 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 9 Nov 2010 06:59:16 -0500 Subject: [PATCH 23/74] set height and width of avatar td in email summary --- plugins/EmailSummary/useremailsummaryhandler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index 1ac1eaedbe..d6de6aa055 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -142,7 +142,8 @@ class UserEmailSummaryHandler extends QueueHandler $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); $out->elementStart('tr'); - $out->elementStart('td'); + $out->elementStart('td', array('width' => AVATAR_STREAM_SIZE, + 'height' => AVATAR_STREAM_SIZE)); $out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage($avatar_size), From 17ab5c31ed255216a2b734e15e9d11bd4c6fce8b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 9 Nov 2010 07:04:50 -0500 Subject: [PATCH 24/74] some alignment in the table layout --- plugins/EmailSummary/useremailsummaryhandler.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index d6de6aa055..95b7fdc477 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -143,16 +143,19 @@ class UserEmailSummaryHandler extends QueueHandler $out->elementStart('tr'); $out->elementStart('td', array('width' => AVATAR_STREAM_SIZE, - 'height' => AVATAR_STREAM_SIZE)); + 'height' => AVATAR_STREAM_SIZE, + 'align' => 'left', + 'valign' => 'top')); $out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage($avatar_size), 'class' => 'avatar photo', - 'width' => $avatar_size, - 'height' => $avatar_size, + 'width' => AVATAR_STREAM_SIZE, + 'height' => AVATAR_STREAM_SIZE, 'alt' => $profile->getBestName())); $out->elementEnd('td'); - $out->elementStart('td'); + $out->elementStart('td', array('align' => 'left', + 'valign' => 'top')); $out->element('a', array('href' => $profile->profileurl), $profile->nickname); $out->text(' '); From e87323e426f032504a01a01ea0854065413fb177 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 9 Nov 2010 13:04:11 -0500 Subject: [PATCH 25/74] change width of notices table to display better --- plugins/EmailSummary/useremailsummaryhandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index 95b7fdc477..fb004b5558 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -129,7 +129,7 @@ class UserEmailSummaryHandler extends QueueHandler $profile->getBestName())); - $out->elementStart('table', array('width' => '100%', 'style' => 'border: none')); + $out->elementStart('table', array('width' => '541px', 'style' => 'border: none')); while ($notice->fetch()) { From f25accc43ea1e66f290c8bc1d284ae04b4bf004f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 9 Nov 2010 10:45:19 -0800 Subject: [PATCH 26/74] split out InlineAttachmentList from AttachmentList --- actions/shownotice.php | 9 ++++ lib/attachmentlist.php | 34 ++++++++----- lib/inlineattachmentlist.php | 97 ++++++++++++++++++++++++++++++++++++ lib/noticelist.php | 2 +- theme/base/css/display.css | 6 +++ 5 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 lib/inlineattachmentlist.php diff --git a/actions/shownotice.php b/actions/shownotice.php index 7a11787b66..b4af7dbaa2 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -331,6 +331,15 @@ class SingleNoticeItem extends DoFollowListItem $this->showEnd(); } + /** + * For our zoomed-in special case we'll use a fuller list + * for the attachment info. + */ + function showNoticeAttachments() { + $al = new AttachmentList($this->notice, $this->out); + $al->show(); + } + /** * show the avatar of the notice's author * diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 8e6ad038a3..f9ef7499e1 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -79,23 +79,33 @@ class AttachmentList extends Widget $atts = new File; $att = $atts->getAttachments($this->notice->id); if (empty($att)) return 0; - $this->out->elementStart('dl', array('id' =>'attachments', - 'class' => 'entry-content')); - // TRANS: DT element label in attachment list. - $this->out->element('dt', null, _('Attachments')); - $this->out->elementStart('dd'); - $this->out->elementStart('ol', array('class' => 'attachments')); + $this->showListStart(); foreach ($att as $n=>$attachment) { $item = $this->newListItem($attachment); $item->show(); } + $this->showListEnd(); + + return count($att); + } + + function showListStart() + { + $this->out->elementStart('dl', array('id' =>'attachments', + 'class' => 'entry-content')); + // TRANS: DT element label in attachment list. + $this->out->element('dt', null, _('Attachments')); + $this->out->elementStart('dd'); + $this->out->elementStart('ol', array('class' => 'attachments')); + } + + function showListEnd() + { $this->out->elementEnd('dd'); $this->out->elementEnd('ol'); $this->out->elementEnd('dl'); - - return count($att); } /** @@ -181,11 +191,9 @@ class AttachmentListItem extends Widget */ function show() { - if ($this->attachment->isEnclosure()) { - $this->showStart(); - $this->showNoticeAttachment(); - $this->showEnd(); - } + $this->showStart(); + $this->showNoticeAttachment(); + $this->showEnd(); } function linkAttr() { diff --git a/lib/inlineattachmentlist.php b/lib/inlineattachmentlist.php new file mode 100644 index 0000000000..8b1a1cd9bb --- /dev/null +++ b/lib/inlineattachmentlist.php @@ -0,0 +1,97 @@ +. + * + * @category UI + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 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')) { + exit(1); +} + +class InlineAttachmentList extends AttachmentList +{ + function showListStart() + { + $this->out->elementStart('div', array('class' => 'entry-content thumbnails')); + } + + function showListEnd() + { + $this->out->elementEnd('div'); + } + + /** + * returns a new list item for the current attachment + * + * @param File $notice the current attachment + * + * @return ListItem a list item for displaying the attachment + */ + function newListItem($attachment) + { + return new InlineAttachmentListItem($attachment, $this->out); + } +} + +class InlineAttachmentListItem extends AttachmentListItem +{ + function show() + { + if ($this->attachment->isEnclosure()) { + parent::show(); + } + } + + function showLink() { + $this->out->elementStart('a', $this->linkAttr()); + $this->showRepresentation(); + $this->out->elementEnd('a'); + } + + /** + * start a single notice. + * + * @return void + */ + function showStart() + { + // XXX: RDFa + // TODO: add notice_type class e.g., notice_video, notice_image + $this->out->elementStart('span', array('class' => 'inline-attachment')); + } + + /** + * finish the notice + * + * Close the last elements in the notice list item + * + * @return void + */ + function showEnd() + { + $this->out->elementEnd('span'); + } +} diff --git a/lib/noticelist.php b/lib/noticelist.php index fb5db2374c..d2ac7ed84a 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -385,7 +385,7 @@ class NoticeListItem extends Widget } function showNoticeAttachments() { - $al = new AttachmentList($this->notice, $this->out); + $al = new InlineAttachmentList($this->notice, $this->out); $al->show(); } diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 29f7d0ae0d..6615e13eb4 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1728,6 +1728,12 @@ width:auto; min-width:0; } +.inline-attachment img { + /* Why on earth is this changed to block at the top? */ + display: inline; + border: solid 1px #aaa; + padding: 1px; +} }/*end of @media screen, projection, tv*/ From dbb95b76a4d384fd62fd24b4d427baefd007cb90 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 9 Nov 2010 12:04:07 -0800 Subject: [PATCH 27/74] Allow YouTube-style media links to be counted as enclosures for purposes of listing attachments/thumbs --- classes/File.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/classes/File.php b/classes/File.php index 56bc73ab25..499c8d72c3 100644 --- a/classes/File.php +++ b/classes/File.php @@ -361,15 +361,19 @@ class File extends Memcached_DataObject if($semicolon){ $mimetype = substr($mimetype,0,$semicolon); } - if(in_array($mimetype,$notEnclosureMimeTypes)){ - return false; - }else{ + // @fixme uncertain if this is right. + // we want to expose things like YouTube videos as + // viewable attachments, but don't expose them as + // downloadable enclosures.....? + //if (in_array($mimetype, $notEnclosureMimeTypes)) { + // return false; + //} else { if($oembed->mimetype) $enclosure->mimetype=$oembed->mimetype; if($oembed->url) $enclosure->url=$oembed->url; if($oembed->title) $enclosure->title=$oembed->title; if($oembed->modified) $enclosure->modified=$oembed->modified; unset($oembed->size); - } + //} } else { return false; } From a4654bfe9f950fa3ca76d109f21030a03507c3da Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 9 Nov 2010 12:53:57 -0500 Subject: [PATCH 28/74] session table was missing from upgrade scripts --- db/074to080.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/db/074to080.sql b/db/074to080.sql index ff08191596..e3631e214a 100644 --- a/db/074to080.sql +++ b/db/074to080.sql @@ -107,3 +107,15 @@ create table group_alias ( index group_alias_group_id_idx (group_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table session ( + + id varchar(32) primary key comment 'session ID', + session_data text comment 'session data', + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified', + + index session_modified_idx (modified) + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + From 3afb031d9270a29db7f1ac4a964bb4b796759827 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 9 Nov 2010 17:08:11 -0500 Subject: [PATCH 29/74] Missing one close-paren in newgroup.php --- actions/newgroup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/newgroup.php b/actions/newgroup.php index 2951920362..371e508379 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -147,7 +147,7 @@ class NewgroupAction extends Action $this->showForm(sprintf(_m('Description is too long (maximum %d character).', 'Description is too long (maximum %d characters).', User_group::maxDescription(), - User_group::maxDescription())); + User_group::maxDescription()))); return; } else if (!is_null($location) && mb_strlen($location) > 255) { $this->showForm(_('Location is too long (maximum 255 characters).')); From 5a3d01423d378272218072136f2b3e46b5aa5269 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 9 Nov 2010 16:28:33 -0800 Subject: [PATCH 30/74] Cleanup on the CSS for inline attachments; removed some unneeded changes since the split of the inline and regular attachment lists. Removing commented-out code that seems to be for some old thumbnailing system where the thumbnails were hidden popups within the core text (wtf!) --- theme/base/css/display.css | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 6615e13eb4..8c364febce 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1150,8 +1150,7 @@ border-radius:4px; -webkit-border-radius:4px; } -.notice div.entry-content, -.notice dl.entry-content { +.notice div.entry-content { clear:left; float:left; font-size:0.95em; @@ -1326,40 +1325,20 @@ margin-left:4px; .notice .attachment.more { padding-left:0; } -/* -.notice .attachment img { -position:absolute; -top:18px; -left:0; -z-index:99; -} -#shownotice .notice .attachment img { -position:static; -} -*/ - -/* Small inline attachment list */ -#attachments ol li { - list-style-type: none; -} -#attachments dt { - display: none; -} - -#shownotice #attachments { +#attachments { clear:both; float:left; width:100%; margin-top:18px; } -#shownotice #attachments dt { +#attachments dt { font-weight:bold; font-size:1.3em; margin-bottom:4px; } -#shownotice #attachments ol li { +#attachments ol li { margin-bottom:18px; list-style-type:decimal; float:left; From 592e0bc505c52a38952469bae0a081c224180bd8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 9 Nov 2010 16:43:37 -0800 Subject: [PATCH 31/74] add title attribute on attachment list items --- lib/attachmentlist.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index f9ef7499e1..6e127af864 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -197,7 +197,10 @@ class AttachmentListItem extends Widget } function linkAttr() { - return array('class' => 'attachment', 'href' => $this->attachment->url, 'id' => 'attachment-' . $this->attachment->id); + return array('class' => 'attachment', + 'href' => $this->attachment->url, + 'id' => 'attachment-' . $this->attachment->id, + 'title' => $this->title()); } function showLink() { @@ -244,8 +247,9 @@ class AttachmentListItem extends Widget case 'image/jpeg': $thumb = (object)array(); $thumb->url = $enc->url; - $thumb->width = 100; - $thumb->height = 75; // @fixme + // @fixme use the given width/height aspect + $thumb->width = common_config('attachments', 'thumb_width'); + $thumb->height = common_config('attachments', 'thumb_height'); return $thumb; } } From cc0038d47c3e2e2817b1ae588c65f6f0e4347742 Mon Sep 17 00:00:00 2001 From: Craig Andrews Date: Wed, 10 Nov 2010 15:53:20 -0500 Subject: [PATCH 32/74] Fix isHTTPS to work correctly for Cherokee and IIS --- lib/statusnet.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/statusnet.php b/lib/statusnet.php index 33bf32b10e..85b46bbb3f 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -377,7 +377,11 @@ class StatusNet static function isHTTPS() { // There are some exceptions to this; add them here! - return !empty($_SERVER['HTTPS']); + if(empty($_SERVER['HTTPS'])) { + return false; + } else { + return $_SERVER['HTTPS'] !== 'off'; + } } } From 46223da59433e602343169a948bc895977ea253f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Nov 2010 14:31:55 -0800 Subject: [PATCH 33/74] CSS class tweak for inline attachment thumbnails to avoid things thinking they're content links --- lib/inlineattachmentlist.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/inlineattachmentlist.php b/lib/inlineattachmentlist.php index 8b1a1cd9bb..de5008e87b 100644 --- a/lib/inlineattachmentlist.php +++ b/lib/inlineattachmentlist.php @@ -71,6 +71,17 @@ class InlineAttachmentListItem extends AttachmentListItem $this->out->elementEnd('a'); } + /** + * Build HTML attributes for the link + * @return array + */ + function linkAttr() + { + $attr = parent::linkAttr(); + $attr['class'] = 'attachment-thumbnail'; + return $attr; + } + /** * start a single notice. * From fbd8052d05fda7a967d8440574d2b5013d4e7be1 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 10 Nov 2010 15:26:18 -0800 Subject: [PATCH 34/74] Add error logging for a couple send-fail cases in XMPP out --- lib/xmppmanager.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/xmppmanager.php b/lib/xmppmanager.php index 7acd7663a2..238696664a 100644 --- a/lib/xmppmanager.php +++ b/lib/xmppmanager.php @@ -198,10 +198,12 @@ class XmppManager extends IoManager $this->conn->processTime(0); return true; } else { + common_log(LOG_ERR, __METHOD__ . ' failed: 0 bytes sent'); return false; } } else { // Can't send right now... + common_log(LOG_ERR, __METHOD__ . ' failed: XMPP server connection currently down'); return false; } } From 09aaf21e8d9be5fc0ed82df0028125c9fd96e48d Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 11 Nov 2010 10:33:26 -0800 Subject: [PATCH 35/74] Fix missing close of comment block --- actions/allrss.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions/allrss.php b/actions/allrss.php index d398c8a6ad..573bb4eb2f 100644 --- a/actions/allrss.php +++ b/actions/allrss.php @@ -56,6 +56,8 @@ class AllrssAction extends Rss10Action * @param array $args Web and URL arguments * * @return boolean false if user doesn't exist + * + */ function prepare($args) { parent::prepare($args); From 0ed572ff3f3401f6faf376136a3e17cba1259922 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 11 Nov 2010 10:33:26 -0800 Subject: [PATCH 36/74] Fix missing close of comment block --- actions/allrss.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions/allrss.php b/actions/allrss.php index d398c8a6ad..573bb4eb2f 100644 --- a/actions/allrss.php +++ b/actions/allrss.php @@ -56,6 +56,8 @@ class AllrssAction extends Rss10Action * @param array $args Web and URL arguments * * @return boolean false if user doesn't exist + * + */ function prepare($args) { parent::prepare($args); From adb16b8098dbe8870b7f4a9dcde49930a41666ff Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Nov 2010 14:50:53 -0500 Subject: [PATCH 37/74] fix update of email prefs in queue handler --- plugins/EmailSummary/useremailsummaryhandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/EmailSummary/useremailsummaryhandler.php b/plugins/EmailSummary/useremailsummaryhandler.php index fb004b5558..b1ebd0c425 100644 --- a/plugins/EmailSummary/useremailsummaryhandler.php +++ b/plugins/EmailSummary/useremailsummaryhandler.php @@ -218,7 +218,7 @@ class UserEmailSummaryHandler extends QueueHandler $ess->last_summary_id = $new_top; $ess->modified = common_sql_now(); - $ess->update(); + $ess->update($orig); } return true; From 2d55bc0e5b89012e700bbeb68411e51477862b23 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 11 Nov 2010 14:51:14 -0500 Subject: [PATCH 38/74] give users a chance to opt out of email summaries --- plugins/EmailSummary/EmailSummaryPlugin.php | 64 +++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/plugins/EmailSummary/EmailSummaryPlugin.php b/plugins/EmailSummary/EmailSummaryPlugin.php index 03577bb4a7..58c40e43c5 100644 --- a/plugins/EmailSummary/EmailSummaryPlugin.php +++ b/plugins/EmailSummary/EmailSummaryPlugin.php @@ -135,4 +135,68 @@ class EmailSummaryPlugin extends Plugin $qm->connect('usersum', 'UserEmailSummaryHandler'); return true; } + + /** + * Add a checkbox to turn off email summaries + * + * @param Action $action Action being executed (emailsettings) + * + * @return boolean hook value + */ + + function onEndEmailFormData($action) + { + $user = common_current_user(); + + $action->elementStart('li'); + $action->checkbox('emailsummary', + // TRANS: Checkbox label in e-mail preferences form. + _('Send me a periodic summary of updates from my network.'), + Email_summary_status::getSendSummary($user->id)); + $action->elementEnd('li'); + return true; + } + + /** + * Add a checkbox to turn off email summaries + * + * @param Action $action Action being executed (emailsettings) + * + * @return boolean hook value + */ + + function onEndEmailSaveForm($action) + { + $sendSummary = $action->boolean('emailsummary'); + + $user = common_current_user(); + + if (!empty($user)) { + + $ess = Email_summary_status::staticGet('user_id', $user->id); + + if (empty($ess)) { + + $ess = new Email_summary_status(); + + $ess->user_id = $user->id; + $ess->send_summary = $sendSummary; + $ess->created = common_sql_now(); + $ess->modified = common_sql_now(); + + $ess->insert(); + + } else { + + $orig = clone($ess); + + $ess->send_summary = $sendSummary; + $ess->modified = common_sql_now(); + + $ess->update($orig); + } + } + + return true; + } } From fdf3a23da7769586a818ca2219ec6bc1b46587de Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 12 Nov 2010 11:46:45 -0500 Subject: [PATCH 39/74] don't try to initialize the mapstraction canvas if it doesn't exist --- plugins/Mapstraction/MapstractionPlugin.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php index c4ba6464ea..d5261d8bc7 100644 --- a/plugins/Mapstraction/MapstractionPlugin.php +++ b/plugins/Mapstraction/MapstractionPlugin.php @@ -156,7 +156,8 @@ class MapstractionPlugin extends Plugin ' var user = null; '. (($actionName == 'showstream') ? ' user = scrapeUser(); ' : '') . ' var notices = scrapeNotices(user); ' . - ' showMapstraction($("#map_canvas"), notices); '. + ' var canvas = $("#map_canvas")[0]; ' . + ' if (typeof(canvas) != "undefined") { showMapstraction(canvas, notices); } '. '});'); } From b6af5a25ba61ef3c86a3772abea7ac95778689f7 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 12 Nov 2010 11:46:45 -0500 Subject: [PATCH 40/74] don't try to initialize the mapstraction canvas if it doesn't exist --- plugins/Mapstraction/MapstractionPlugin.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php index c4ba6464ea..d5261d8bc7 100644 --- a/plugins/Mapstraction/MapstractionPlugin.php +++ b/plugins/Mapstraction/MapstractionPlugin.php @@ -156,7 +156,8 @@ class MapstractionPlugin extends Plugin ' var user = null; '. (($actionName == 'showstream') ? ' user = scrapeUser(); ' : '') . ' var notices = scrapeNotices(user); ' . - ' showMapstraction($("#map_canvas"), notices); '. + ' var canvas = $("#map_canvas")[0]; ' . + ' if (typeof(canvas) != "undefined") { showMapstraction(canvas, notices); } '. '});'); } From 62467f51e520439d3ec44ceb6a66a91ad54d77b6 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 12:10:29 -0800 Subject: [PATCH 41/74] Drop commented-out code from old lightbox & thumbnail popup stuff --- js/util.js | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/js/util.js b/js/util.js index 15fb163103..bf3c43fd8a 100644 --- a/js/util.js +++ b/js/util.js @@ -427,50 +427,6 @@ var SN = { // StatusNet return false; }).attr('title', SN.msg('showmore_tooltip')); } - else { - //imgLoading : $('address .url')[0].href+'theme/base/images/illustrations/illu_progress_loading-01.gif', - - notice.find('a.attachment').each(function() { - /* - var attachId = ($(this).attr('id').substring('attachment'.length + 1)); - if (attachId) { - var thumbUrl = $('address .url')[0].href+'attachment/' + attachId + '/thumb'; - var thumb = $('
Thumb:
'); - thumb.find('img').attr('src', thumbUrl).last(); - notice.find('.entry-title .entry-content').append(thumb); - } - */ - }); - - if ($('#shownotice').length == 0) { - /* - var t; - notice.find('a.thumbnail').hover( - function() { - var anchor = $(this); - $('a.thumbnail').children('img').hide(); - anchor.closest(".entry-title").addClass('ov'); - - if (anchor.children('img').length === 0) { - t = setTimeout(function() { - $.get($('address .url')[0].href+'attachment/' + (anchor.attr('id').substring('attachment'.length + 1)) + '/thumbnail', null, function(data) { - anchor.append(data); - }); - }, 500); - } - else { - anchor.children('img').show(); - } - }, - function() { - clearTimeout(t); - $('a.thumbnail').children('img').hide(); - $(this).closest('.entry-title').removeClass('ov'); - } - ); - */ - } - } }, NoticeDataAttach: function() { From cda59dc1771c2911491c9271662c32f56103347f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 12:10:51 -0800 Subject: [PATCH 42/74] drop a comma which isn't actually an error but keeps throwing annoying warnings in netbeans --- js/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/util.js b/js/util.js index bf3c43fd8a..74eef4df17 100644 --- a/js/util.js +++ b/js/util.js @@ -56,7 +56,7 @@ var SN = { // StatusNet NoticeDataGeoCookie: 'NoticeDataGeo', NoticeDataGeoSelected: 'notice_data-geo_selected', StatusNetInstance:'StatusNetInstance' - }, + } }, messages: {}, From cb124fe831a3c77dfca89590ebb8d691685bb573 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 12:24:55 -0800 Subject: [PATCH 43/74] Add a quick config setting to disable/enable display of thumbnails in regular notice lists (attachments/show_thumbs) - disabling gives the same display as before this feature was added (but changes to oembed handling are still there, and the lightbox popup is gone) --- lib/default.php | 1 + lib/noticelist.php | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/default.php b/lib/default.php index 87f4e45c0e..ece01f2a8b 100644 --- a/lib/default.php +++ b/lib/default.php @@ -250,6 +250,7 @@ $default = 'monthly_quota' => 15000000, 'uploads' => true, 'filecommand' => '/usr/bin/file', + 'show_thumbs' => true, // show thumbnails in notice lists for uploaded images, and photos and videos linked remotely that provide oEmbed info 'thumb_width' => 100, 'thumb_height' => 75, ), diff --git a/lib/noticelist.php b/lib/noticelist.php index d2ac7ed84a..c6f964662f 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -385,8 +385,10 @@ class NoticeListItem extends Widget } function showNoticeAttachments() { - $al = new InlineAttachmentList($this->notice, $this->out); - $al->show(); + if (common_config('attachments', 'show_thumbs')) { + $al = new InlineAttachmentList($this->notice, $this->out); + $al->show(); + } } /** From 6291e8201f90ed50687f7670edc505645ea55bfb Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 13:06:41 -0800 Subject: [PATCH 44/74] Fix for failure edge case in TwitterBridge outgoing repeat/retweets. When the retweet failed with a 403 error (say due to it being a private tweet, which can't be retweeted) we would end up mishandling the return value from our internal error handling. Instead of correctly discarding the message and closing out the queue item, we ended up trying to save a bogus twitter<->local ID mapping, which threw another exception and lead the queue system to re-run it. - Fixed the logic check and return values for the retweet case in broadcast_twitter(). - Added doc comments explaining the return values on some functions in twitter.php - Added check on Notice_to_status::saveNew() for empty input -- throw an exception before we try to actually insert into db. :) --- plugins/TwitterBridge/Notice_to_status.php | 7 +++++ plugins/TwitterBridge/twitter.php | 32 +++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/plugins/TwitterBridge/Notice_to_status.php b/plugins/TwitterBridge/Notice_to_status.php index 2e32ba963c..3b8f816cfc 100644 --- a/plugins/TwitterBridge/Notice_to_status.php +++ b/plugins/TwitterBridge/Notice_to_status.php @@ -144,6 +144,7 @@ class Notice_to_status extends Memcached_DataObject /** * Save a mapping between a notice and a status + * Warning: status_id values may not fit in 32-bit integers. * * @param integer $notice_id ID of the notice in StatusNet * @param integer $status_id ID of the status in Twitter @@ -153,12 +154,18 @@ class Notice_to_status extends Memcached_DataObject static function saveNew($notice_id, $status_id) { + if (empty($notice_id)) { + throw new Exception("Invalid notice_id $notice_id"); + } $n2s = Notice_to_status::staticGet('notice_id', $notice_id); if (!empty($n2s)) { return $n2s; } + if (empty($status_id)) { + throw new Exception("Invalid status_id $status_id"); + } $n2s = Notice_to_status::staticGet('status_id', $status_id); if (!empty($n2s)) { diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php index cd1ad70b9b..b34488069a 100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@ -128,6 +128,16 @@ function is_twitter_notice($id) return (!empty($n2s)); } +/** + * Check if we need to broadcast a notice over the Twitter bridge, and + * do so if necessary. Will determine whether to do a straight post or + * a repeat/retweet + * + * This function is meant to be called directly from TwitterQueueHandler. + * + * @param Notice $notice + * @return boolean true if complete or successful, false if we should retry + */ function broadcast_twitter($notice) { $flink = Foreign_link::getByUserID($notice->profile_id, @@ -137,8 +147,13 @@ function broadcast_twitter($notice) if (!empty($flink) && TwitterOAuthClient::isPackedToken($flink->credentials)) { if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) { $retweet = retweet_notice($flink, Notice::staticGet('id', $notice->repeat_of)); - if (!empty($retweet)) { + if (is_object($retweet)) { Notice_to_status::saveNew($notice->id, $retweet->id); + return true; + } else { + // Our error processing will have decided if we need to requeue + // this or can discard safely. + return $retweet; } } else if (is_twitter_bound($notice, $flink)) { return broadcast_oauth($notice, $flink); @@ -148,6 +163,21 @@ function broadcast_twitter($notice) return true; } +/** + * Send a retweet to Twitter for a notice that has been previously bridged + * in or out. + * + * Warning: the return value is not guaranteed to be an object; some error + * conditions will return a 'true' which should be passed on to a calling + * queue handler. + * + * No local information about the resulting retweet is saved: it's up to + * caller to save new mappings etc if appropriate. + * + * @param Foreign_link $flink + * @param Notice $notice + * @return mixed object with resulting Twitter status data on success, or true/false/null on error conditions. + */ function retweet_notice($flink, $notice) { $token = TwitterOAuthClient::unpackToken($flink->credentials); From 9621904cac010bdf31f1a8128517722172ff8df4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 13:34:04 -0800 Subject: [PATCH 45/74] Revert "Missing one close-paren in newgroup.php" - incorrect fix for paren bug This reverts commit 3afb031d9270a29db7f1ac4a964bb4b796759827. --- actions/newgroup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/newgroup.php b/actions/newgroup.php index 371e508379..2951920362 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -147,7 +147,7 @@ class NewgroupAction extends Action $this->showForm(sprintf(_m('Description is too long (maximum %d character).', 'Description is too long (maximum %d characters).', User_group::maxDescription(), - User_group::maxDescription()))); + User_group::maxDescription())); return; } else if (!is_null($location) && mb_strlen($location) > 255) { $this->showForm(_('Location is too long (maximum 255 characters).')); From e4913f97224bad0affd271e1666ca71e66aaa47e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 2 Nov 2010 14:03:50 -0700 Subject: [PATCH 46/74] fix syntax error introduced in i18n tweaks: newgroup action --- actions/newgroup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/newgroup.php b/actions/newgroup.php index 2951920362..05520223c0 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -146,7 +146,7 @@ class NewgroupAction extends Action // TRANS: %d is the maximum number of allowed characters. $this->showForm(sprintf(_m('Description is too long (maximum %d character).', 'Description is too long (maximum %d characters).', - User_group::maxDescription(), + User_group::maxDescription()), User_group::maxDescription())); return; } else if (!is_null($location) && mb_strlen($location) > 255) { From 2c4313467f07cae059798ac500ec2a1c31953877 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 14:03:08 -0800 Subject: [PATCH 47/74] Save oEmbed photo references as thumbnails if there's not a separate thumbnail_url entry in the return data. This fixes thumb saving for Flickr photo references. --- classes/File_oembed.php | 2 +- classes/File_thumbnail.php | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/classes/File_oembed.php b/classes/File_oembed.php index a5540ecfee..bcb2f7bacc 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -120,7 +120,7 @@ class File_oembed extends Memcached_DataObject } } $file_oembed->insert(); - if (!empty($data->thumbnail_url)) { + if (!empty($data->thumbnail_url) || ($data->type == 'photo')) { $ft = File_thumbnail::staticGet('file_id', $file_id); if (!empty($ft)) { common_log(LOG_WARNING, "Strangely, a File_thumbnail object exists for new file $file_id", diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index d371b9e8aa..17bac7f08c 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -55,10 +55,21 @@ class File_thumbnail extends Memcached_DataObject * @param int $file_id */ public static function saveNew($data, $file_id) { - self::saveThumbnail($file_id, - $data->thumbnail_url, - $data->thumbnail_width, - $data->thumbnail_height); + if (!empty($data->thumbnail_url)) { + // Non-photo types such as video will usually + // show us a thumbnail, though it's not required. + self::saveThumbnail($file_id, + $data->thumbnail_url, + $data->thumbnail_width, + $data->thumbnail_height); + } else if ($data->type == 'photo') { + // The inline photo URL given should also fit within + // our requested thumbnail size, per oEmbed spec. + self::saveThumbnail($file_id, + $data->url, + $data->width, + $data->height); + } } /** From 2c33fdd2fb98c37798a80a8600798caa9dabcb0e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 14:03:57 -0800 Subject: [PATCH 48/74] Only use saved thumbnails for notice list attachment thumbs -- don't attempt to search enclosures for photo types. We now save thumbs directly for oEmbed photos that don't list a separate thumb entry (like Flickr), so it's not needed. Keeps things cleaner :D --- lib/attachmentlist.php | 47 ++++++++++-------------------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 6e127af864..0d56791d70 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -218,55 +218,30 @@ class AttachmentListItem extends Widget function showRepresentation() { $thumb = $this->getThumbInfo(); if ($thumb) { - $thumb = $this->sizeThumb($thumb); $this->out->element('img', array('alt' => '', 'src' => $thumb->url, 'width' => $thumb->width, 'height' => $thumb->height)); } } /** - * Pull a thumbnail image reference for the given file. - * In order we check: - * 1) file_thumbnail table (thumbnails found via oEmbed) - * 2) image URL from direct dereference or oEmbed 'photo' type URL - * 3) ??? + * Pull a thumbnail image reference for the given file, and if necessary + * resize it to match currently thumbnail size settings. * - * @return mixed object with (url, width, height) properties, or false + * @return File_Thumbnail or false/null */ function getThumbInfo() { $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id); if ($thumbnail) { - return $thumbnail; - } - $enc = $this->attachment->getEnclosure(); - if ($enc) { - switch ($enc->mimetype) { - case 'image/gif': - case 'image/png': - case 'image/jpg': - case 'image/jpeg': - $thumb = (object)array(); - $thumb->url = $enc->url; - // @fixme use the given width/height aspect - $thumb->width = common_config('attachments', 'thumb_width'); - $thumb->height = common_config('attachments', 'thumb_height'); - return $thumb; + $maxWidth = common_config('attachments', 'thumb_width'); + $maxHeight = common_config('attachments', 'thumb_height'); + if ($thumbnail->width > $maxWidth) { + $thumb = clone($thumbnail); + $thumb->width = $maxWidth; + $thumb->height = intval($thumbnail->height * $maxWidth / $thumbnail->width); + return $thumb; } } - return false; - } - - function sizeThumb($thumbnail) { - $maxWidth = 100; - $maxHeight = 75; - if ($thumbnail->width > $maxWidth) { - $thumb = clone($thumbnail); - $thumb->width = $maxWidth; - $thumb->height = intval($thumbnail->height * $maxWidth / $thumbnail->width); - return $thumb; - } else { - return $thumbnail; - } + return $thumbnail; } /** From 398e622fecdb2b2b6bf6cde975e3978284db62b4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 17:40:34 -0800 Subject: [PATCH 49/74] Save attached URLs when importing a Twitter status: this lets our thumbnail detection handle photos and videos linked to by Twitter posters. --- plugins/TwitterBridge/twitterimport.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/TwitterBridge/twitterimport.php b/plugins/TwitterBridge/twitterimport.php index 07a9cf95f6..498e9b1fc5 100644 --- a/plugins/TwitterBridge/twitterimport.php +++ b/plugins/TwitterBridge/twitterimport.php @@ -189,6 +189,7 @@ class TwitterImport Notice_to_status::saveNew($notice->id, $status->id); $this->saveStatusMentions($notice, $status); + $this->saveStatusAttachments($notice, $status); $notice->blowOnInsert(); @@ -648,4 +649,20 @@ class TwitterImport } } } + + /** + * Record URL links from the notice. Needed to get thumbnail records + * for referenced photo and video posts, etc. + * + * @param Notice $notice + * @param object $status + */ + function saveStatusAttachments($notice, $status) + { + if (!empty($status->entities) && !empty($status->entities->urls)) { + foreach ($status->entities->urls as $url) { + File::processNew($url->url, $notice->id); + } + } + } } \ No newline at end of file From 4f323efdf7abc5452152a87241e320aca20ce486 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 17:41:35 -0800 Subject: [PATCH 50/74] Encapsulate the oEmbed -> oohembed fallback into oEmbedHelper class. Also added a chance to whitelist sites that don't show discovery info but do have oEmbed API endpoints, and to provide alternate APIs for some common services. Newly supported: - TwitPic: added a local function using TwitPic's API, since the oohembed implementation for TwitPic produced invalid output which Services_oEmbed rejects. (bug filed upstream) Tweaked... - Flickr: works, now using whitelist to use their endpoint directly instead of going through oohembed - Youtube: worked around a bug in Services_oEmbed which broke the direct use of API discovery info, so we don't have to use oohembed. Not currently working... - YFrog: whitelisting their endpoint directly as the oohembed output is broken, but this doesn't appear to work currently as I think things are confused by YFrog's servers giving a '204 No Content' response on our HEAD checks on the original link. --- classes/File_oembed.php | 20 +--- lib/oembedhelper.php | 246 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 lib/oembedhelper.php diff --git a/classes/File_oembed.php b/classes/File_oembed.php index bcb2f7bacc..b7bf3a5dae 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -59,25 +59,15 @@ class File_oembed extends Memcached_DataObject } function _getOembed($url) { - require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; $parameters = array( 'maxwidth' => common_config('attachments', 'thumb_width'), 'maxheight' => common_config('attachments', 'thumb_height'), ); - try{ - $oEmbed = new Services_oEmbed($url); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $e){ - try{ - $oEmbed = new Services_oEmbed($url, array( - Services_oEmbed::OPTION_API => common_config('oohembed', 'endpoint') - )); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $ex){ - return false; - } + try { + return oEmbedHelper::getObject($url, $parameters); + } catch (Exception $e) { + common_log(LOG_ERR, "Error during oembed lookup for $url - " . $e->getMessage()); + return false; } } diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php new file mode 100644 index 0000000000..ef2b59214c --- /dev/null +++ b/lib/oembedhelper.php @@ -0,0 +1,246 @@ +. + */ + +if (!defined('STATUSNET')) { + exit(1); +} +require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; + + +/** + * Utility class to wrap Services_oEmbed: + * + * Blacklisted hosts will use an alternate lookup method: + * - Twitpic + * + * Whitelisted hosts will use known oEmbed API endpoints: + * - Flickr, YFrog + * + * Sites that provide discovery links will use them directly; a bug + * in use of discovery links with query strings is worked around. + * + * Others will fall back to oohembed (unless disabled). + * The API endpoint can be configured or disabled through config + * as 'oohembed'/'endpoint'. + */ +class oEmbedHelper +{ + protected static $apiMap = array( + 'flickr.com' => 'http://www.flickr.com/services/oembed/', + 'yfrog.com' => 'http://www.yfrog.com/api/oembed', + ); + protected static $functionMap = array( + 'twitpic.com' => 'oEmbedHelper::twitPic', + ); + + /** + * Perform or fake an oEmbed lookup for the given resource. + * + * Some known hosts are whitelisted with API endpoints where we + * know they exist but autodiscovery data isn't available. + * If autodiscovery links are missing and we don't recognize the + * host, we'll pass it to oohembed.com's public service which + * will either proxy or fake info on a lot of sites. + * + * A few hosts are blacklisted due to known problems with oohembed, + * in which case we'll look up the info another way and return + * equivalent data. + * + * Throws exceptions on failure. + * + * @param string $url + * @param array $params + * @return object + */ + public static function getObject($url, $params=array()) + { + common_log(LOG_INFO, 'QQQ: wtf? ' . $url); + $host = parse_url($url, PHP_URL_HOST); + if (substr($host, 0, 4) == 'www.') { + $host = substr($host, 4); + } + + // Blacklist: systems with no oEmbed API of their own, which are + // either missing from or broken on oohembed.com's proxy. + // we know how to look data up in another way... + if (array_key_exists($host, self::$functionMap)) { + $func = self::$functionMap[$host]; + return call_user_func($func, $url, $params); + } + + // Whitelist: known API endpoints for sites that don't provide discovery... + if (array_key_exists($host, self::$apiMap)) { + $api = self::$apiMap[$host]; + common_log(LOG_INFO, 'QQQ: going to: ' . $api); + } else { + $api = false; + common_log(LOG_INFO, 'QQQ: no map for ' . $host); + } + return self::getObjectFrom($api, $url, $params); + } + + /** + * Actually do an oEmbed lookup to a particular API endpoint, + * or to the autodiscovered target, or to oohembed. + * + * @param mixed $api string or false: oEmbed API endpoint URL + * @param string $url target URL to look up info about + * @param array $params + * @return object + */ + static protected function getObjectFrom($api, $url, $params=array()) + { + $options = array(); + if ($api) { + $options[Services_oEmbed::OPTION_API] = $api; + } + + try { + $oEmbed = new Services_oEmbed_Tweaked($url, $options); + } catch (Services_oEmbed_Exception_NoSupport $e) { + // Discovery failed... fall back to oohembed if enabled. + $oohembed = common_config('oohembed', 'endpoint'); + if ($oohembed) { + $options[Services_oEmbed::OPTION_API] = $oohembed; + $oEmbed = new Services_oEmbed_Tweaked($url, $options); + } else { + throw $e; + } + } + + // And.... let's go look it up! + return $oEmbed->getObject($params); + } + + /** + * Using a local function for twitpic lookups, as oohembed's adapter + * doesn't return a valid result: + * http://code.google.com/p/oohembed/issues/detail?id=19 + * + * This code fetches metadata from Twitpic's own API, and attempts + * to guess proper thumbnail size from the original's size. + * + * @todo respect maxwidth and maxheight params + * + * @param string $url + * @param array $params + * @return object + */ + static function twitPic($url, $params=array()) + { + $matches = array(); + if (preg_match('!twitpic\.com/(\w+)!', $url, $matches)) { + $id = $matches[1]; + } else { + throw new Exception("Invalid twitpic URL"); + } + + // Grab metadata from twitpic's API... + // http://dev.twitpic.com/docs/2/media_show + $data = self::json('http://api.twitpic.com/2/media/show.json', + array('id' => $id)); + $oembed = (object)array('type' => 'photo', + 'url' => 'http://twitpic.com/show/full/' . $data->short_id, + 'width' => $data->width, + 'height' => $data->height); + if (!empty($data->message)) { + $oembed->title = $data->message; + } + + // Thumbnail is cropped and scaled to 150x150 box: + // http://dev.twitpic.com/docs/thumbnails/ + $thumbSize = 150; + $oembed->thumbnail_url = 'http://twitpic.com/show/thumb/' . $data->short_id; + $oembed->thumbnail_width = $thumbSize; + $oembed->thumbnail_height = $thumbSize; + + return $oembed; + } + + /** + * Fetch some URL and return JSON data. + * + * @param string $url + * @param array $params query-string params + * @return object + */ + static protected function json($url, $params=array()) + { + $data = self::http($url, $params); + return json_decode($data); + } + + /** + * Hit some web API and return data on success. + * @param string $url + * @param array $params + * @return string + */ + static protected function http($url, $params=array()) + { + $client = HTTPClient::start(); + if ($params) { + $query = http_build_query($params, null, '&'); + if (strpos($url, '?') === false) { + $url .= '?' . $query; + } else { + $url .= '&' . $query; + } + } + $response = $client->get($url); + if ($response->isOk()) { + return $response->getBody(); + } else { + throw new Exception('Bad HTTP response code: ' . $response->getStatus()); + } + } +} + +class Services_oEmbed_Tweaked extends Services_oEmbed +{ + protected function discover($url) + { + $api = parent::discover($url); + if (strpos($api, '?') !== false) { + // Services_oEmbed doesn't expect to find existing params + // on its API endpoint, which may surprise you since the + // spec says discovery URLs should include parameters... :) + // + // Appending a '&' on the end keeps the later-appended '?' + // from breaking whatever the first parameters was. + return $api . '&'; + } + return $api; + } + + public function getObject(array $params = array()) + { + $api = $this->options[self::OPTION_API]; + if (strpos($api, '?') !== false) { + // The Services_oEmbed code appends a '?' on the end, which breaks + // the next parameter which may be something important like + // maxwidth. + // + // Sticking this bogus entry into our parameters gets us past it. + $params = array_merge(array('statusnet' => 1), $params); + } + return parent::getObject($params); + } + +} \ No newline at end of file From cb371d65c18771f8fcdcbeb450c063b844c000df Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 15 Nov 2010 11:54:42 -0500 Subject: [PATCH 51/74] add hooks for atom pub posts --- EVENTS.txt | 8 +++++++ actions/apitimelineuser.php | 46 ++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/EVENTS.txt b/EVENTS.txt index 8e730945a4..2df21f01a3 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1158,3 +1158,11 @@ StartRevokeRole: when a role is being revoked EndRevokeRole: when a role has been revoked - $profile: profile that lost the role - $role: string name of the role + +StartAtomPubNewActivity: When a new activity comes in through Atom Pub API +- &$activity: received activity + +EndAtomPubNewActivity: When a new activity comes in through Atom Pub API +- $activity: received activity +- $notice: notice that was created + diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 69cd9c2cb4..7e76636460 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -325,21 +325,39 @@ class ApiTimelineUserAction extends ApiBareAuthAction $activity = new Activity($dom->documentElement); - if ($activity->verb != ActivityVerb::POST) { - $this->clientError(_('Can only handle post activities.')); - return; + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { + + if ($activity->verb != ActivityVerb::POST) { + $this->clientError(_('Can only handle post activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + $this->clientError(sprintf(_('Cannot handle activity object type "%s"', + $note->type))); + return; + } + + $saved = $this->postNote($activity); + + Event::handle('EndAtomPubNewActivity', array($activity, $saved)); } + if (!empty($saved)) { + header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, + 'format' => 'atom'))); + $this->showSingleAtomStatus($saved); + } + } + + function postNote($activity) + { $note = $activity->objects[0]; - if (!in_array($note->type, array(ActivityObject::NOTE, - ActivityObject::BLOGENTRY, - ActivityObject::STATUS))) { - $this->clientError(sprintf(_('Cannot handle activity object type "%s"', - $note->type))); - return; - } - // Use summary as fallback for content if (!empty($note->content)) { @@ -458,11 +476,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction 'atompub', // TODO: deal with this $options); - if (!empty($saved)) { - header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, - 'format' => 'atom'))); - $this->showSingleAtomStatus($saved); - } + return $saved; } function purify($content) From e1ffbfed0463b27b536cc86a70b206eb317f2a33 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 11:00:42 -0800 Subject: [PATCH 52/74] doc comments on File::processNew --- classes/File.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/classes/File.php b/classes/File.php index 499c8d72c3..ef9dbf14ab 100644 --- a/classes/File.php +++ b/classes/File.php @@ -116,10 +116,24 @@ class File extends Memcached_DataObject } /** + * Go look at a URL and possibly save data about it if it's new: + * - follow redirect chains and store them in file_redirection + * - look up oEmbed data and save it in file_oembed + * - if a thumbnail is available, save it in file_thumbnail + * - save file record with basic info + * - optionally save a file_to_post record + * - return the File object with the full reference + * * @fixme refactor this mess, it's gotten pretty scary. - * @param bool $followRedirects + * @param string $given_url the URL we're looking at + * @param int $notice_id (optional) + * @param bool $followRedirects defaults to true + * + * @return mixed File on success, -1 on some errors + * + * @throws ServerException on some errors */ - function processNew($given_url, $notice_id=null, $followRedirects=true) { + public function processNew($given_url, $notice_id=null, $followRedirects=true) { 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 From 1c90d09ec8bfbbbad80c74c3feb5bf70fd76d926 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 11:01:00 -0800 Subject: [PATCH 53/74] Workaround for yfrog.com photo attachments: fudge File_redirection::lookupWhere()'s HTTP handling -- when we get a 204 on a HEAD, double-check it by re-running as a GET. yfrog.com returns a 204 incorrectly for this case. --- classes/File_redirection.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/classes/File_redirection.php b/classes/File_redirection.php index 68fed77e8b..1976e3439c 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -91,9 +91,16 @@ class File_redirection extends Memcached_DataObject $request->setMethod(HTTP_Request2::METHOD_HEAD); $response = $request->send(); - if (405 == $response->getStatus()) { + if (405 == $response->getStatus() || 204 == $response->getStatus()) { + // HTTP 405 Unsupported Method // Server doesn't support HEAD method? Can this really happen? // We'll try again as a GET and ignore the response data. + // + // HTTP 204 No Content + // YFrog sends 204 responses back for our HEAD checks, which + // seems like it may be a logic error in their servers. If + // we get a 204 back, re-run it as a GET... if there's really + // no content it'll be cheap. :) $request = self::_commonHttp($short_url, $redirs); $response = $request->send(); } @@ -235,6 +242,18 @@ class File_redirection extends Memcached_DataObject return null; } + /** + * Basic attempt to canonicalize a URL, cleaning up some standard variants + * such as funny syntax or a missing path. Used internally when cleaning + * up URLs for storage and following redirect chains. + * + * Note that despite being on File_redirect, this function DOES NOT perform + * any dereferencing of redirects. + * + * @param string $in_url input URL + * @param string $default_scheme if given a bare link; defaults to 'http://' + * @return string + */ function _canonUrl($in_url, $default_scheme = 'http://') { if (empty($in_url)) return false; $out_url = $in_url; From d038d0fa465f76395f8e341a04a64661b631c122 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 15 Nov 2010 14:14:09 -0500 Subject: [PATCH 54/74] AtomPub-related actions are only read-only on GET --- actions/apistatusesshow.php | 7 ++++++- actions/apitimelineuser.php | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index f4a79ddbcb..e684a07eec 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -171,9 +171,14 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction * * @return boolean true */ + function isReadOnly($args) { - return true; + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + return true; + } else { + return false; + } } /** diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index e4a8b596ee..f716232e43 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -241,9 +241,14 @@ class ApiTimelineUserAction extends ApiBareAuthAction * * @return boolean true */ + function isReadOnly($args) { - return true; + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + return true; + } else { + return false; + } } /** From c8445299c71c17e8f652584a05aac65ef7b55ac5 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 11:25:38 -0800 Subject: [PATCH 55/74] Swap the Services_oEmbed wrapper in oEmbedHelper out for doing it ourselves... - workaround for providers that are skimpy on their data, such as missing width/height or thumbnail_width/thumbnail_height - workaround for YFrog listing "image" instead of "photo" type - generally more lax about formatting: if it comes back and looks kinda ok, we'll take it. - discovery uses system HTML parser, should be more robust if the links include things like ampersands with proper HTML-level escaping --- lib/oembedhelper.php | 175 +++++++++++++++++++++++++++++++------------ 1 file changed, 126 insertions(+), 49 deletions(-) diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php index ef2b59214c..0ffea468bf 100644 --- a/lib/oembedhelper.php +++ b/lib/oembedhelper.php @@ -20,11 +20,10 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; /** - * Utility class to wrap Services_oEmbed: + * Utility class to wrap basic oEmbed lookups. * * Blacklisted hosts will use an alternate lookup method: * - Twitpic @@ -89,43 +88,134 @@ class oEmbedHelper $api = self::$apiMap[$host]; common_log(LOG_INFO, 'QQQ: going to: ' . $api); } else { - $api = false; common_log(LOG_INFO, 'QQQ: no map for ' . $host); + try { + $api = self::discover($url); + } catch (Exception $e) { + // Discovery failed... fall back to oohembed if enabled. + $oohembed = common_config('oohembed', 'endpoint'); + if ($oohembed) { + $api = $oohembed; + } else { + throw $e; + } + } + common_log(LOG_INFO, 'QQQ: going to: ' . $api); } return self::getObjectFrom($api, $url, $params); } /** - * Actually do an oEmbed lookup to a particular API endpoint, - * or to the autodiscovered target, or to oohembed. + * Perform basic discovery. + * @return string + */ + static function discover($url) + { + // @fixme ideally skip this for non-HTML stuff! + $body = self::http($url); + return self::discoverFromHTML($url, $body); + } + + /** + * Partially ripped from OStatus' FeedDiscovery class. * - * @param mixed $api string or false: oEmbed API endpoint URL + * @param string $url source URL, used to resolve relative links + * @param string $body HTML body text + * @return mixed string with URL or false if no target found + */ + static function discoverFromHTML($url, $body) + { + // DOMDocument::loadHTML may throw warnings on unrecognized elements, + // and notices on unrecognized namespaces. + $old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE)); + $dom = new DOMDocument(); + $ok = $dom->loadHTML($body); + error_reporting($old); + + if (!$ok) { + throw new oEmbedHelper_BadHtmlException(); + } + + // Ok... now on to the links! + $feeds = array( + 'application/json+oembed' => false, + ); + + $nodes = $dom->getElementsByTagName('link'); + for ($i = 0; $i < $nodes->length; $i++) { + $node = $nodes->item($i); + if ($node->hasAttributes()) { + $rel = $node->attributes->getNamedItem('rel'); + $type = $node->attributes->getNamedItem('type'); + $href = $node->attributes->getNamedItem('href'); + if ($rel && $type && $href) { + $rel = array_filter(explode(" ", $rel->value)); + $type = trim($type->value); + $href = trim($href->value); + + if (in_array('alternate', $rel) && array_key_exists($type, $feeds) && empty($feeds[$type])) { + // Save the first feed found of each type... + $feeds[$type] = $href; + } + } + } + } + + // Return the highest-priority feed found + foreach ($feeds as $type => $url) { + if ($url) { + return $url; + } + } + + return false; + throw new oEmbedHelper_DiscoveryException(); + } + + /** + * Actually do an oEmbed lookup to a particular API endpoint. + * + * @param string $api oEmbed API endpoint URL * @param string $url target URL to look up info about * @param array $params * @return object */ - static protected function getObjectFrom($api, $url, $params=array()) + static function getObjectFrom($api, $url, $params=array()) { - $options = array(); - if ($api) { - $options[Services_oEmbed::OPTION_API] = $api; + $params['url'] = $url; + $params['format'] = 'json'; + $data = self::json($api, $params); + return self::normalize($data); + } + + /** + * Normalize oEmbed format. + * + * @param object $orig + * @return object + */ + static function normalize($orig) + { + $data = clone($orig); + + if (empty($data->type)) { + throw new Exception('Invalid oEmbed data: no type field.'); } - try { - $oEmbed = new Services_oEmbed_Tweaked($url, $options); - } catch (Services_oEmbed_Exception_NoSupport $e) { - // Discovery failed... fall back to oohembed if enabled. - $oohembed = common_config('oohembed', 'endpoint'); - if ($oohembed) { - $options[Services_oEmbed::OPTION_API] = $oohembed; - $oEmbed = new Services_oEmbed_Tweaked($url, $options); - } else { - throw $e; + if ($data->type == 'image') { + // YFrog does this. + $data->type = 'photo'; + } + + if (isset($data->thumbnail_url)) { + if (!isset($data->thumbnail_width)) { + // !?!?! + $data->thumbnail_width = common_config('attachments', 'thumb_width'); + $data->thumbnail_height = common_config('attachments', 'thumb_height'); } } - // And.... let's go look it up! - return $oEmbed->getObject($params); + return $data; } /** @@ -212,35 +302,22 @@ class oEmbedHelper } } -class Services_oEmbed_Tweaked extends Services_oEmbed +class oEmbedHelper_Exception extends Exception { - protected function discover($url) - { - $api = parent::discover($url); - if (strpos($api, '?') !== false) { - // Services_oEmbed doesn't expect to find existing params - // on its API endpoint, which may surprise you since the - // spec says discovery URLs should include parameters... :) - // - // Appending a '&' on the end keeps the later-appended '?' - // from breaking whatever the first parameters was. - return $api . '&'; - } - return $api; - } +} - public function getObject(array $params = array()) +class oEmbedHelper_BadHtmlException extends oEmbedHelper_Exception +{ + function __construct($previous=null) { - $api = $this->options[self::OPTION_API]; - if (strpos($api, '?') !== false) { - // The Services_oEmbed code appends a '?' on the end, which breaks - // the next parameter which may be something important like - // maxwidth. - // - // Sticking this bogus entry into our parameters gets us past it. - $params = array_merge(array('statusnet' => 1), $params); - } - return parent::getObject($params); + return parent::__construct('Bad HTML in discovery data.', 0, $previous); } +} -} \ No newline at end of file +class oEmbedHelper_DiscoveryException extends oEmbedHelper_Exception +{ + function __construct($previous=null) + { + return parent::__construct('No oEmbed discovery data.', 0, $previous); + } +} From 57ec01d0b8c6d46fba1c0aca258dec690f408c60 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 11:30:35 -0800 Subject: [PATCH 56/74] Drop some debug lines --- lib/oembedhelper.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php index 0ffea468bf..8cd4a9dc4a 100644 --- a/lib/oembedhelper.php +++ b/lib/oembedhelper.php @@ -69,7 +69,6 @@ class oEmbedHelper */ public static function getObject($url, $params=array()) { - common_log(LOG_INFO, 'QQQ: wtf? ' . $url); $host = parse_url($url, PHP_URL_HOST); if (substr($host, 0, 4) == 'www.') { $host = substr($host, 4); @@ -86,9 +85,7 @@ class oEmbedHelper // Whitelist: known API endpoints for sites that don't provide discovery... if (array_key_exists($host, self::$apiMap)) { $api = self::$apiMap[$host]; - common_log(LOG_INFO, 'QQQ: going to: ' . $api); } else { - common_log(LOG_INFO, 'QQQ: no map for ' . $host); try { $api = self::discover($url); } catch (Exception $e) { @@ -100,7 +97,6 @@ class oEmbedHelper throw $e; } } - common_log(LOG_INFO, 'QQQ: going to: ' . $api); } return self::getObjectFrom($api, $url, $params); } From 87114a5c30ff50d75cb00fa0bffee1a47da35156 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 11:55:28 -0800 Subject: [PATCH 57/74] Add some basic oEmbed lookup test cases; fixed a bug in discovery fallback. --- lib/oembedhelper.php | 1 - tests/oEmbedTest.php | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/oEmbedTest.php diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php index 8cd4a9dc4a..84cf105867 100644 --- a/lib/oembedhelper.php +++ b/lib/oembedhelper.php @@ -164,7 +164,6 @@ class oEmbedHelper } } - return false; throw new oEmbedHelper_DiscoveryException(); } diff --git a/tests/oEmbedTest.php b/tests/oEmbedTest.php new file mode 100644 index 0000000000..eabee00ad9 --- /dev/null +++ b/tests/oEmbedTest.php @@ -0,0 +1,67 @@ +old_oohembed = common_config('oohembed', 'endpoint'); + } + + public function tearDown() + { + //$GLOBALS['config']['attachments']['supported'] = $this->old_attachments_supported; + } + + /** + * @dataProvider fileTypeCases + * + */ + public function testoEmbed($url, $expectedType) + { + try { + $data = oEmbedHelper::getObject($url); + $this->assertEquals($expectedType, $data->type); + } catch (Exception $e) { + if ($expectedType == 'none') { + $this->assertEquals($expectedType, 'none', 'Should not have data for this URL.'); + } else { + throw $e; + } + } + } + + static public function fileTypeCases() + { + $files = array( + 'http://www.flickr.com/photos/brionv/5172500179/' => 'photo', + 'http://twitpic.com/36adw6' => 'photo', + 'http://yfrog.com/fy42747177j' => 'photo', + 'http://identi.ca/attachment/34437400' => 'photo', + + 'http://www.youtube.com/watch?v=eUgLR232Cnw' => 'video', + 'http://vimeo.com/9283184' => 'video', + + 'http://en.wikipedia.org/wiki/File:Wiki.png' => 'link', // @fixme in future there may be a native provider -- will change to 'photo' + 'http://leuksman.com/log/2010/10/29/statusnet-0-9-6-release/' => 'none', + ); + + $dataset = array(); + foreach ($files as $url => $type) { + $dataset[] = array($url, $type); + } + return $dataset; + } + +} + From 68ff57f230c35a7b8ecfcb5c95f219b14f21f60f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 12:25:44 -0800 Subject: [PATCH 58/74] Restructure oembed test sources --- tests/oEmbedTest.php | 64 ++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/tests/oEmbedTest.php b/tests/oEmbedTest.php index eabee00ad9..0a8841606e 100644 --- a/tests/oEmbedTest.php +++ b/tests/oEmbedTest.php @@ -24,7 +24,7 @@ class oEmbedTest extends PHPUnit_Framework_TestCase } /** - * @dataProvider fileTypeCases + * @dataProvider fallbackSources * */ public function testoEmbed($url, $expectedType) @@ -41,27 +41,51 @@ class oEmbedTest extends PHPUnit_Framework_TestCase } } - static public function fileTypeCases() + /** + * Sample oEmbed targets for sites we know ourselves... + * @return array + */ + static public function knownSources() { - $files = array( - 'http://www.flickr.com/photos/brionv/5172500179/' => 'photo', - 'http://twitpic.com/36adw6' => 'photo', - 'http://yfrog.com/fy42747177j' => 'photo', - 'http://identi.ca/attachment/34437400' => 'photo', - - 'http://www.youtube.com/watch?v=eUgLR232Cnw' => 'video', - 'http://vimeo.com/9283184' => 'video', - - 'http://en.wikipedia.org/wiki/File:Wiki.png' => 'link', // @fixme in future there may be a native provider -- will change to 'photo' - 'http://leuksman.com/log/2010/10/29/statusnet-0-9-6-release/' => 'none', + $sources = array( + array('http://www.flickr.com/photos/brionv/5172500179/', 'photo'), + array('http://yfrog.com/fy42747177j', 'photo'), + array('http://twitpic.com/36adw6', 'photo'), ); - - $dataset = array(); - foreach ($files as $url => $type) { - $dataset[] = array($url, $type); - } - return $dataset; + return $sources; } -} + /** + * Sample oEmbed targets that can be found via discovery. + * Includes also knownSources() output. + * + * @return array + */ + static public function discoverableSources() + { + $sources = array( + array('http://identi.ca/attachment/34437400', 'photo'), + array('http://www.youtube.com/watch?v=eUgLR232Cnw', 'video'), + array('http://vimeo.com/9283184', 'video'), + + // Will fail discovery: + array('http://leuksman.com/log/2010/10/29/statusnet-0-9-6-release/', 'none'), + ); + return array_merge(self::knownSources(), $sources); + } + + /** + * Sample oEmbed targets that can be found via oohembed.com. + * Includes also discoverableSources() output. + * + * @return array + */ + static public function fallbackSources() + { + $sources = array( + array('http://en.wikipedia.org/wiki/File:Wiki.png', 'link'), // @fixme in future there may be a native provider -- will change to 'photo' + ); + return array_merge(self::discoverableSources(), $sources); + } +} From 727596f35d7b88691a2ac60eecc2575f2a3c33f9 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 12:32:29 -0800 Subject: [PATCH 59/74] Test oEmbed lookups with oohembed both on and off explicitly --- tests/oEmbedTest.php | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/tests/oEmbedTest.php b/tests/oEmbedTest.php index 0a8841606e..d00963003e 100644 --- a/tests/oEmbedTest.php +++ b/tests/oEmbedTest.php @@ -15,19 +15,56 @@ class oEmbedTest extends PHPUnit_Framework_TestCase public function setup() { - //$this->old_oohembed = common_config('oohembed', 'endpoint'); + $this->old_oohembed = common_config('oohembed', 'endpoint'); } public function tearDown() { - //$GLOBALS['config']['attachments']['supported'] = $this->old_attachments_supported; + $GLOBALS['config']['oohembed']['endpoint'] = $this->old_oohembed; } /** - * @dataProvider fallbackSources + * Test with oohembed DISABLED. * + * @dataProvider discoverableSources */ public function testoEmbed($url, $expectedType) + { + $GLOBALS['config']['oohembed']['endpoint'] = false; + $this->_doTest($url, $expectedType); + } + + /** + * Test with oohembed ENABLED. + * + * @dataProvider fallbackSources + */ + public function testoohEmbed($url, $expectedType) + { + $GLOBALS['config']['oohembed']['endpoint'] = $this->_endpoint(); + $this->_doTest($url, $expectedType); + } + + /** + * Get default oohembed endpoint. + * + * @return string + */ + function _endpoint() + { + $default = array(); + $_server = 'localhost'; $_path = ''; + require INSTALLDIR . '/lib/default.php'; + return $default['oohembed']['endpoint']; + } + + /** + * Actually run an individual test. + * + * @param string $url + * @param string $expectedType + */ + function _doTest($url, $expectedType) { try { $data = oEmbedHelper::getObject($url); From e317ec11ad470fe57dc12c0bde1566abde1c036a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 12:35:15 -0800 Subject: [PATCH 60/74] Drop PEAR Services_oEmbed -- ended up replaced by our oEmbedHelper wrapper. The extra validation available there is problematic, and their code for building HTML for us wasn't being used anyway. --- README | 6 +- extlib/Services/oEmbed.php | 357 ------------------ extlib/Services/oEmbed/Exception.php | 65 ---- .../Services/oEmbed/Exception/NoSupport.php | 63 ---- extlib/Services/oEmbed/Object.php | 126 ------- extlib/Services/oEmbed/Object/Common.php | 139 ------- extlib/Services/oEmbed/Object/Exception.php | 65 ---- extlib/Services/oEmbed/Object/Link.php | 73 ---- extlib/Services/oEmbed/Object/Photo.php | 89 ----- extlib/Services/oEmbed/Object/Rich.php | 82 ---- extlib/Services/oEmbed/Object/Video.php | 82 ---- 11 files changed, 2 insertions(+), 1145 deletions(-) delete mode 100644 extlib/Services/oEmbed.php delete mode 100644 extlib/Services/oEmbed/Exception.php delete mode 100644 extlib/Services/oEmbed/Exception/NoSupport.php delete mode 100644 extlib/Services/oEmbed/Object.php delete mode 100644 extlib/Services/oEmbed/Object/Common.php delete mode 100644 extlib/Services/oEmbed/Object/Exception.php delete mode 100644 extlib/Services/oEmbed/Object/Link.php delete mode 100644 extlib/Services/oEmbed/Object/Photo.php delete mode 100644 extlib/Services/oEmbed/Object/Rich.php delete mode 100644 extlib/Services/oEmbed/Object/Video.php diff --git a/README b/README index b36d8b7454..6343e3e024 100644 --- a/README +++ b/README @@ -220,14 +220,12 @@ and the URLs are listed here for your convenience. version may render your StatusNet site unable to send or receive XMPP messages. - Facebook library. Used for the Facebook application. -- PEAR Services_oEmbed. Used for some multimedia integration. -- PEAR HTTP_Request is an oEmbed dependency. -- PEAR Validate is an oEmbed dependency. -- PEAR Net_URL2 is an oEmbed dependency. +- PEAR Validate is used for URL and email validation. - Console_GetOpt for parsing command-line options. - libomb. a library for implementing OpenMicroBlogging 0.1, the predecessor to OStatus. - HTTP_Request2, a library for making HTTP requests. +- PEAR Net_URL2 is an HTTP_Request2 dependency. A design goal of StatusNet is that the basic Web functionality should work on even the most restrictive commercial hosting services. diff --git a/extlib/Services/oEmbed.php b/extlib/Services/oEmbed.php deleted file mode 100644 index 0dc8f01b2f..0000000000 --- a/extlib/Services/oEmbed.php +++ /dev/null @@ -1,357 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Validate.php'; -require_once 'Net/URL2.php'; -require_once 'HTTP/Request.php'; -require_once 'Services/oEmbed/Exception.php'; -require_once 'Services/oEmbed/Exception/NoSupport.php'; -require_once 'Services/oEmbed/Object.php'; - -/** - * Base class for consuming oEmbed objects - * - * - * 'http://www.flickr.com/services/oembed/' - * )); - * $object = $oEmbed->getObject(); - * - * // All of the objects have somewhat sane __toString() methods that allow - * // you to output them directly. - * echo (string)$object; - * - * ?> - * - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed -{ - /** - * HTTP timeout in seconds - * - * All HTTP requests made by Services_oEmbed will respect this timeout. - * This can be passed to {@link Services_oEmbed::setOption()} or to the - * options parameter in {@link Services_oEmbed::__construct()}. - * - * @var string OPTION_TIMEOUT Timeout in seconds - */ - const OPTION_TIMEOUT = 'http_timeout'; - - /** - * HTTP User-Agent - * - * All HTTP requests made by Services_oEmbed will be sent with the - * string set by this option. - * - * @var string OPTION_USER_AGENT The HTTP User-Agent string - */ - const OPTION_USER_AGENT = 'http_user_agent'; - - /** - * The API's URI - * - * If the API is known ahead of time this option can be used to explicitly - * set it. If not present then the API is attempted to be discovered - * through the auto-discovery mechanism. - * - * @var string OPTION_API - */ - const OPTION_API = 'oembed_api'; - - /** - * Options for oEmbed requests - * - * @var array $options The options for making requests - */ - protected $options = array( - self::OPTION_TIMEOUT => 3, - self::OPTION_API => null, - self::OPTION_USER_AGENT => 'Services_oEmbed 0.1.0' - ); - - /** - * URL of object to get embed information for - * - * @var object $url {@link Net_URL2} instance of URL of object - */ - protected $url = null; - - /** - * Constructor - * - * @param string $url The URL to fetch an oEmbed for - * @param array $options A list of options for the oEmbed lookup - * - * @throws {@link Services_oEmbed_Exception} if the $url is invalid - * @throws {@link Services_oEmbed_Exception} when no valid API is found - * @return void - */ - public function __construct($url, array $options = array()) - { - if (Validate::uri($url)) { - $this->url = new Net_URL2($url); - } else { - throw new Services_oEmbed_Exception('URL is invalid'); - } - - if (count($options)) { - foreach ($options as $key => $val) { - $this->setOption($key, $val); - } - } - - if ($this->options[self::OPTION_API] === null) { - $this->options[self::OPTION_API] = $this->discover($url); - } - } - - /** - * Set an option for the oEmbed request - * - * @param mixed $option The option name - * @param mixed $value The option value - * - * @see Services_oEmbed::OPTION_API, Services_oEmbed::OPTION_TIMEOUT - * @throws {@link Services_oEmbed_Exception} on invalid option - * @access public - * @return void - */ - public function setOption($option, $value) - { - switch ($option) { - case self::OPTION_API: - case self::OPTION_TIMEOUT: - break; - default: - throw new Services_oEmbed_Exception( - 'Invalid option "' . $option . '"' - ); - } - - $func = '_set_' . $option; - if (method_exists($this, $func)) { - $this->options[$option] = $this->$func($value); - } else { - $this->options[$option] = $value; - } - } - - /** - * Set the API option - * - * @param string $value The API's URI - * - * @throws {@link Services_oEmbed_Exception} on invalid API URI - * @see Validate::uri() - * @return string - */ - protected function _set_oembed_api($value) - { - if (!Validate::uri($value)) { - throw new Services_oEmbed_Exception( - 'API URI provided is invalid' - ); - } - - return $value; - } - - /** - * Get the oEmbed response - * - * @param array $params Optional parameters for - * - * @throws {@link Services_oEmbed_Exception} on cURL errors - * @throws {@link Services_oEmbed_Exception} on HTTP errors - * @throws {@link Services_oEmbed_Exception} when result is not parsable - * @return object The oEmbed response as an object - */ - public function getObject(array $params = array()) - { - $params['url'] = $this->url->getURL(); - if (!isset($params['format'])) { - $params['format'] = 'json'; - } - - $sets = array(); - foreach ($params as $var => $val) { - $sets[] = $var . '=' . urlencode($val); - } - - $url = $this->options[self::OPTION_API] . '?' . implode('&', $sets); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]); - $result = curl_exec($ch); - - if (curl_errno($ch)) { - throw new Services_oEmbed_Exception( - curl_error($ch), curl_errno($ch) - ); - } - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if (substr($code, 0, 1) != '2') { - throw new Services_oEmbed_Exception('Non-200 code returned. Got code ' . $code); - } - - curl_close($ch); - - switch ($params['format']) { - case 'json': - $res = json_decode($result); - if (!is_object($res)) { - throw new Services_oEmbed_Exception( - 'Could not parse JSON response' - ); - } - break; - case 'xml': - libxml_use_internal_errors(true); - $res = simplexml_load_string($result); - if (!$res instanceof SimpleXMLElement) { - $errors = libxml_get_errors(); - $err = array_shift($errors); - libxml_clear_errors(); - libxml_use_internal_errors(false); - throw new Services_oEmbed_Exception( - $err->message, $error->code - ); - } - break; - } - - return Services_oEmbed_Object::factory($res); - } - - /** - * Discover an oEmbed API - * - * @param string $url The URL to attempt to discover oEmbed for - * - * @throws {@link Services_oEmbed_Exception} if the $url is invalid - * @return string The oEmbed API endpoint discovered - */ - protected function discover($url) - { - $body = $this->sendRequest($url); - - // Find all tags that have a valid oembed type set. We then - // extract the href attribute for each type. - $regexp = '#]*)type[\s\n]*=[\s\n]*"' . - '(application/json|text/xml)\+oembed"([^>]*)>#im'; - - $m = $ret = array(); - if (!preg_match_all($regexp, $body, $m)) { - throw new Services_oEmbed_Exception_NoSupport( - 'No valid oEmbed links found on page' - ); - } - - foreach ($m[0] as $i => $link) { - $h = array(); - if (preg_match('/[\s\n]+href[\s\n]*=[\s\n]*"([^"]+)"/im', $link, $h)) { - $ret[$m[2][$i]] = $h[1]; - } - } - - return (isset($ret['application/json']) ? $ret['application/json'] : array_pop($ret)); - } - - /** - * Send a GET request to the provider - * - * @param mixed $url The URL to send the request to - * - * @throws {@link Services_oEmbed_Exception} on HTTP errors - * @return string The contents of the response - */ - private function sendRequest($url) - { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->options[self::OPTION_TIMEOUT]); - curl_setopt($ch, CURLOPT_USERAGENT, $this->options[self::OPTION_USER_AGENT]); - $result = curl_exec($ch); - if (curl_errno($ch)) { - throw new Services_oEmbed_Exception( - curl_error($ch), curl_errno($ch) - ); - } - - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if (substr($code, 0, 1) != '2') { - throw new Services_oEmbed_Exception('Non-200 code returned. Got code ' . $code); - } - - return $result; - } -} - -?> diff --git a/extlib/Services/oEmbed/Exception.php b/extlib/Services/oEmbed/Exception.php deleted file mode 100644 index 446ac2a706..0000000000 --- a/extlib/Services/oEmbed/Exception.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'PEAR/Exception.php'; - -/** - * Base exception class for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Exception extends PEAR_Exception -{ - -} - -?> diff --git a/extlib/Services/oEmbed/Exception/NoSupport.php b/extlib/Services/oEmbed/Exception/NoSupport.php deleted file mode 100644 index 384c7191f2..0000000000 --- a/extlib/Services/oEmbed/Exception/NoSupport.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -/** - * Exception class when no oEmbed support is discovered - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Exception_NoSupport extends Services_oEmbed_Exception -{ - -} - -?> diff --git a/extlib/Services/oEmbed/Object.php b/extlib/Services/oEmbed/Object.php deleted file mode 100644 index 9eedd7efb6..0000000000 --- a/extlib/Services/oEmbed/Object.php +++ /dev/null @@ -1,126 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Exception.php'; - -/** - * Base class for consuming oEmbed objects - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -abstract class Services_oEmbed_Object -{ - - /** - * Valid oEmbed object types - * - * @var array $types Array of valid object types - * @see Services_oEmbed_Object::factory() - */ - static protected $types = array( - 'photo' => 'Photo', - 'video' => 'Video', - 'link' => 'Link', - 'rich' => 'Rich' - ); - - /** - * Create an oEmbed object from result - * - * @param object $object Raw object returned from API - * - * @throws {@link Services_oEmbed_Object_Exception} on object error - * @return object Instance of object driver - * @see Services_oEmbed_Object_Link, Services_oEmbed_Object_Photo - * @see Services_oEmbed_Object_Rich, Services_oEmbed_Object_Video - */ - static public function factory($object) - { - if (!isset($object->type)) { - throw new Services_oEmbed_Object_Exception( - 'Object has no type' - ); - } - - $type = (string)$object->type; - if (!isset(self::$types[$type])) { - throw new Services_oEmbed_Object_Exception( - 'Object type is unknown or invalid: ' . $type - ); - } - - $file = 'Services/oEmbed/Object/' . self::$types[$type] . '.php'; - include_once $file; - - $class = 'Services_oEmbed_Object_' . self::$types[$type]; - if (!class_exists($class)) { - throw new Services_oEmbed_Object_Exception( - 'Object class is invalid or not present' - ); - } - - $instance = new $class($object); - return $instance; - } - - /** - * Instantiation is not allowed - * - * @return void - */ - private function __construct() - { - - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Common.php b/extlib/Services/oEmbed/Object/Common.php deleted file mode 100644 index f568ec89f5..0000000000 --- a/extlib/Services/oEmbed/Object/Common.php +++ /dev/null @@ -1,139 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -/** - * Base class for oEmbed objects - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -abstract class Services_oEmbed_Object_Common -{ - /** - * Raw object returned from API - * - * @var object $object The raw object from the API - */ - protected $object = null; - - /** - * Required fields per the specification - * - * @var array $required Array of required fields - * @link http://oembed.com - */ - protected $required = array(); - - /** - * Constructor - * - * @param object $object Raw object returned from the API - * - * @throws {@link Services_oEmbed_Object_Exception} on missing fields - * @return void - */ - public function __construct($object) - { - $this->object = $object; - - $this->required[] = 'version'; - foreach ($this->required as $field) { - if (!isset($this->$field)) { - throw new Services_oEmbed_Object_Exception( - 'Object is missing required ' . $field . ' attribute' - ); - } - } - } - - /** - * Get object variable - * - * @param string $var Variable to get - * - * @see Services_oEmbed_Object_Common::$object - * @return mixed Attribute's value or null if it's not set/exists - */ - public function __get($var) - { - if (property_exists($this->object, $var)) { - return $this->object->$var; - } - - return null; - } - - /** - * Is variable set? - * - * @param string $var Variable name to check - * - * @return boolean True if set, false if not - * @see Services_oEmbed_Object_Common::$object - */ - public function __isset($var) - { - if (property_exists($this->object, $var)) { - return (isset($this->object->$var)); - } - - return false; - } - - /** - * Require a sane __toString for all objects - * - * @return string - */ - abstract public function __toString(); -} - -?> diff --git a/extlib/Services/oEmbed/Object/Exception.php b/extlib/Services/oEmbed/Object/Exception.php deleted file mode 100644 index 6025ffd494..0000000000 --- a/extlib/Services/oEmbed/Object/Exception.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Exception.php'; - -/** - * Exception for {@link Services_oEmbed_Object} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Exception extends Services_oEmbed_Exception -{ - -} - -?> diff --git a/extlib/Services/oEmbed/Object/Link.php b/extlib/Services/oEmbed/Object/Link.php deleted file mode 100644 index 9b627a89ac..0000000000 --- a/extlib/Services/oEmbed/Object/Link.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Link object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Link extends Services_oEmbed_Object_Common -{ - /** - * Output a sane link - * - * @return string An HTML link of the object - */ - public function __toString() - { - return '' . $this->title . ''; - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Photo.php b/extlib/Services/oEmbed/Object/Photo.php deleted file mode 100644 index 5fbf4292fa..0000000000 --- a/extlib/Services/oEmbed/Object/Photo.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Photo object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Photo extends Services_oEmbed_Object_Common -{ - /** - * Required fields for photo objects - * - * @var array $required Required fields - */ - protected $required = array( - 'url', 'width', 'height' - ); - - /** - * Output a valid HTML tag for image - * - * @return string HTML tag for Photo - */ - public function __toString() - { - $img = 'title)) { - $img .= ' alt="' . $this->title . '"'; - } - - return $img . ' />'; - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Rich.php b/extlib/Services/oEmbed/Object/Rich.php deleted file mode 100644 index dbf6933ac7..0000000000 --- a/extlib/Services/oEmbed/Object/Rich.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Photo object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Rich extends Services_oEmbed_Object_Common -{ - /** - * Required fields for rich objects - * - * @var array $required Required fields - */ - protected $required = array( - 'html', 'width', 'height' - ); - - /** - * Output a the HTML tag for rich object - * - * @return string HTML for rich object - */ - public function __toString() - { - return $this->html; - } -} - -?> diff --git a/extlib/Services/oEmbed/Object/Video.php b/extlib/Services/oEmbed/Object/Video.php deleted file mode 100644 index 7461081151..0000000000 --- a/extlib/Services/oEmbed/Object/Video.php +++ /dev/null @@ -1,82 +0,0 @@ - - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version SVN: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ - -require_once 'Services/oEmbed/Object/Common.php'; - -/** - * Photo object for {@link Services_oEmbed} - * - * @category Services - * @package Services_oEmbed - * @author Joe Stump - * @copyright 2008 Digg.com, Inc. - * @license http://tinyurl.com/42zef New BSD License - * @version Release: @version@ - * @link http://code.google.com/p/digg - * @link http://oembed.com - */ -class Services_oEmbed_Object_Video extends Services_oEmbed_Object_Common -{ - /** - * Required fields for video objects - * - * @var array $required Required fields - */ - protected $required = array( - 'html', 'width', 'height' - ); - - /** - * Output a valid embed tag for video - * - * @return string HTML for video - */ - public function __toString() - { - return $this->html; - } -} - -?> From fe7cb35551053380a359c17a49402abeaf684f46 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 12:56:56 -0800 Subject: [PATCH 61/74] restore empty showFallback() for attachment display; still needed for one-offs --- lib/attachmentlist.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 0d56791d70..7e536925bf 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -452,4 +452,9 @@ class Attachment extends AttachmentListItem return $scrubbed; } + + function showFallback() + { + // still needed: should show a link? + } } From 89d59936748ee55bff31f161e6a0f2a1cbd44635 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 12:57:15 -0800 Subject: [PATCH 62/74] Include width/height of locally-uploaded images in our oembed provider data for attachment pages. --- actions/oembed.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/actions/oembed.php b/actions/oembed.php index 11d8145837..09d68a446e 100644 --- a/actions/oembed.php +++ b/actions/oembed.php @@ -108,9 +108,16 @@ class OembedAction extends Action $oembed['url']=$file_oembed->url; }else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){ $oembed['type']='photo'; - //TODO set width and height - //$oembed['width']= - //$oembed['height']= + if ($attachment->filename) { + $filepath = File::path($attachment->filename); + $gis = @getimagesize($filepath); + if ($gis) { + $oembed['width'] = $gis[0]; + $oembed['height'] = $gis[1]; + } else { + // TODO Either throw an error or find a fallback? + } + } $oembed['url']=$attachment->url; $thumb = $attachment->getThumbnail(); if ($thumb) { From 0735ca86d27b774481f294c5500fd4f1d00db04c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 12:58:00 -0800 Subject: [PATCH 63/74] Add some data integrity checks on oembed tests (shows a bug on identi.ca test case -- missing width/height in photo data) --- tests/oEmbedTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/oEmbedTest.php b/tests/oEmbedTest.php index d00963003e..b5e441c42f 100644 --- a/tests/oEmbedTest.php +++ b/tests/oEmbedTest.php @@ -69,6 +69,18 @@ class oEmbedTest extends PHPUnit_Framework_TestCase try { $data = oEmbedHelper::getObject($url); $this->assertEquals($expectedType, $data->type); + if ($data->type == 'photo') { + $this->assertTrue(!empty($data->url), 'Photo must have a URL.'); + $this->assertTrue(!empty($data->width), 'Photo must have a width.'); + $this->assertTrue(!empty($data->height), 'Photo must have a height.'); + } else if ($data->type == 'video') { + $this->assertTrue(!empty($data->html), 'Video must have embedding HTML.'); + $this->assertTrue(!empty($data->thumbnail_url), 'Video should have a thumbnail.'); + } + if (!empty($data->thumbnail_url)) { + $this->assertTrue(!empty($data->thumbnail_width), 'Thumbnail must list a width.'); + $this->assertTrue(!empty($data->thumbnail_height), 'Thumbnail must list a height.'); + } } catch (Exception $e) { if ($expectedType == 'none') { $this->assertEquals($expectedType, 'none', 'Should not have data for this URL.'); From defaa3b332c74741bb6df129cc51df825986a08b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 13:26:42 -0800 Subject: [PATCH 64/74] clear_jabber.php script to clear confirmed jabber/xmpp addresses from one or more accounts --- scripts/clear_jabber.php | 92 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100755 scripts/clear_jabber.php diff --git a/scripts/clear_jabber.php b/scripts/clear_jabber.php new file mode 100755 index 0000000000..5ec53caf0e --- /dev/null +++ b/scripts/clear_jabber.php @@ -0,0 +1,92 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$shortoptions = 'i::n::y'; +$longoptions = array('id=', 'nickname=', 'yes', 'all', 'dry-run'); + +$helptext = <<whereAdd("jabber != ''"); + $user->find(true); + if ($user->N == 0) { + print "No users with registered Jabber addresses in database.\n"; + exit(1); + } +} else { + print "You must provide either an ID or a nickname.\n"; + print "\n"; + print $helptext; + exit(1); +} + +function clear_jabber($id) +{ + $user = User::staticGet('id', $id); + if ($user && $user->jabber) { + echo "clearing user $id's user.jabber, was: $user->jabber"; + if (have_option('dry-run')) { + echo " (SKIPPING)"; + } else { + $original = clone($user); + $user->jabber = null; + $result = $user->updateKeys($original); + } + echo "\n"; + } else if (!$user) { + echo "Missing user for $id\n"; + } else { + echo "Cleared jabber already for $id\n"; + } +} + +do { + clear_jabber($user->id); +} while ($user->fetch()); + +print "DONE.\n"; From 227d4b688949a70c1dd3923628a101e1f0208e15 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 14:15:41 -0800 Subject: [PATCH 65/74] Stub ModPlus plugin: will hold experimental UI improvements for mod actions --- plugins/ModPlus/ModPlusPlugin.php | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 plugins/ModPlus/ModPlusPlugin.php diff --git a/plugins/ModPlus/ModPlusPlugin.php b/plugins/ModPlus/ModPlusPlugin.php new file mode 100644 index 0000000000..485b821813 --- /dev/null +++ b/plugins/ModPlus/ModPlusPlugin.php @@ -0,0 +1,43 @@ +. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Some UI extras for now... + * + * @package ModPlusPlugin + * @maintainer Brion Vibber + */ +class ModPlusPlugin extends Plugin +{ + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'ModPlus', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber', + 'homepage' => 'http://status.net/wiki/Plugin:ModPlus', + 'rawdescription' => + _m('UI extensions for profile moderation actions.')); + + return true; + } +} From 0d0e51292d97e0b3a07cf1d12031df0b8f69823f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 15:32:57 -0800 Subject: [PATCH 66/74] some User -> Profile cleanup to help in adapting the profile page action to show stuff for remote users. Subscriptions, groups, roles, etc are all on profiles now so go ahead and use em. --- classes/Profile.php | 23 +++++++++++++++++++++++ classes/User.php | 7 ++++--- lib/profileaction.php | 10 +++++----- lib/userprofile.php | 14 ++++++-------- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index 37d2c571f9..b11cffc776 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -473,6 +473,29 @@ class Profile extends Memcached_DataObject return $cnt; } + /** + * Is this profile subscribed to another profile? + * + * @param Profile $other + * @return boolean + */ + function isSubscribed($other) + { + return Subscription::exists($this, $other); + } + + /** + * Are these two profiles subscribed to each other? + * + * @param Profile $other + * @return boolean + */ + function mutuallySubscribed($other) + { + return $this->isSubscribed($other) && + $other->isSubscribed($this); + } + function hasFave($notice) { $cache = common_memcache(); diff --git a/classes/User.php b/classes/User.php index 7345dc7f94..964bc3e7f3 100644 --- a/classes/User.php +++ b/classes/User.php @@ -84,7 +84,8 @@ class User extends Memcached_DataObject function isSubscribed($other) { - return Subscription::exists($this->getProfile(), $other); + $profile = $this->getProfile(); + return $profile->isSubscribed($other); } // 'update' won't write key columns, so we have to do it ourselves. @@ -418,8 +419,8 @@ class User extends Memcached_DataObject function mutuallySubscribed($other) { - return $this->isSubscribed($other) && - $other->isSubscribed($this); + $profile = $this->getProfile(); + return $profile->mutuallySubscribed($other); } function mutuallySubscribedUsers() diff --git a/lib/profileaction.php b/lib/profileaction.php index 504b775669..4bfc4d48d9 100644 --- a/lib/profileaction.php +++ b/lib/profileaction.php @@ -101,7 +101,7 @@ class ProfileAction extends OwnerDesignAction function showSubscriptions() { - $profile = $this->user->getSubscriptions(0, PROFILES_PER_MINILIST + 1); + $profile = $this->profile->getSubscriptions(0, PROFILES_PER_MINILIST + 1); $this->elementStart('div', array('id' => 'entity_subscriptions', 'class' => 'section')); @@ -134,7 +134,7 @@ class ProfileAction extends OwnerDesignAction function showSubscribers() { - $profile = $this->user->getSubscribers(0, PROFILES_PER_MINILIST + 1); + $profile = $this->profile->getSubscribers(0, PROFILES_PER_MINILIST + 1); $this->elementStart('div', array('id' => 'entity_subscribers', 'class' => 'section')); @@ -173,7 +173,7 @@ class ProfileAction extends OwnerDesignAction $subs_count = $this->profile->subscriptionCount(); $subbed_count = $this->profile->subscriberCount(); $notice_count = $this->profile->noticeCount(); - $group_count = $this->user->getGroups()->N; + $group_count = $this->profile->getGroups()->N; $age_days = (time() - strtotime($this->profile->created)) / 86400; if ($age_days < 1) { // Rather than extrapolating out to a bajillion... @@ -241,7 +241,7 @@ class ProfileAction extends OwnerDesignAction function showGroups() { - $groups = $this->user->getGroups(0, GROUPS_PER_MINILIST + 1); + $groups = $this->profile->getGroups(0, GROUPS_PER_MINILIST + 1); $this->elementStart('div', array('id' => 'entity_groups', 'class' => 'section')); @@ -249,7 +249,7 @@ class ProfileAction extends OwnerDesignAction $this->element('h2', null, _('Groups')); if ($groups) { - $gml = new GroupMiniList($groups, $this->user, $this); + $gml = new GroupMiniList($groups, $this->profile, $this); $cnt = $gml->show(); if ($cnt == 0) { $this->element('p', null, _('(None)')); diff --git a/lib/userprofile.php b/lib/userprofile.php index ca060842b6..0f48078d17 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -109,10 +109,8 @@ class UserProfile extends Widget 'alt' => $this->profile->nickname)); $this->out->elementEnd('dd'); - $user = User::staticGet('id', $this->profile->id); - $cur = common_current_user(); - if ($cur && $cur->id == $user->id) { + if ($cur && $cur->id == $this->profile->id) { $this->out->elementStart('dd'); $this->out->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar')); $this->out->elementEnd('dd'); @@ -278,7 +276,7 @@ class UserProfile extends Widget } $this->out->elementEnd('li'); - if ($cur->mutuallySubscribed($this->user)) { + if ($cur->mutuallySubscribed($this->profile)) { // message @@ -290,7 +288,7 @@ class UserProfile extends Widget // nudge - if ($this->user->email && $this->user->emailnotifynudge) { + if ($this->user && $this->user->email && $this->user->emailnotifynudge) { $this->out->elementStart('li', 'entity_nudge'); $nf = new NudgeForm($this->out, $this->user); $nf->show(); @@ -327,7 +325,7 @@ class UserProfile extends Widget $this->out->elementStart('ul'); if ($cur->hasRight(Right::SANDBOXUSER)) { $this->out->elementStart('li', 'entity_sandbox'); - if ($this->user->isSandboxed()) { + if ($this->profile->isSandboxed()) { $usf = new UnSandboxForm($this->out, $this->profile, $r2args); $usf->show(); } else { @@ -339,7 +337,7 @@ class UserProfile extends Widget if ($cur->hasRight(Right::SILENCEUSER)) { $this->out->elementStart('li', 'entity_silence'); - if ($this->user->isSilenced()) { + if ($this->profile->isSilenced()) { $usf = new UnSilenceForm($this->out, $this->profile, $r2args); $usf->show(); } else { @@ -387,7 +385,7 @@ class UserProfile extends Widget $r2args['action'] = $action; $this->out->elementStart('li', "entity_role_$role"); - if ($this->user->hasRole($role)) { + if ($this->profile->hasRole($role)) { $rf = new RevokeRoleForm($role, $label, $this->out, $this->profile, $r2args); $rf->show(); } else { From 0e763b49024703efc3e2bbadad83b03d3dd62790 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 15:34:12 -0800 Subject: [PATCH 67/74] Stub RemoteprofileAction to show the standard profile header stuff for offsite users -- provides a way to get at the mod & block controls for remote users. --- plugins/ModPlus/ModPlusPlugin.php | 55 ++++++++++++++++++++ plugins/ModPlus/remoteprofileaction.php | 69 +++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 plugins/ModPlus/remoteprofileaction.php diff --git a/plugins/ModPlus/ModPlusPlugin.php b/plugins/ModPlus/ModPlusPlugin.php index 485b821813..89bbdf857f 100644 --- a/plugins/ModPlus/ModPlusPlugin.php +++ b/plugins/ModPlus/ModPlusPlugin.php @@ -40,4 +40,59 @@ class ModPlusPlugin extends Plugin return true; } + + /** + * Load JS at runtime if we're logged in. + * + * @param Action $action + * @return boolean hook result + */ + function onEndShowScripts($action) + { + $user = common_current_user(); + if ($user) { + $action->script('plugins/ModPlus/modplus.js'); + } + return true; + } + + /** + * Autoloader + * + * Loads our classes if they're requested. + * + * @param string $cls Class requested + * + * @return boolean hook return + */ + function onAutoload($cls) + { + switch ($cls) + { + case 'RemoteprofileAction': + case 'RemoteProfileAction': + require_once dirname(__FILE__) . '/remoteprofileaction.php'; + return false; + default: + return true; + } + } + + /** + * Add OpenID-related paths to the router table + * + * Hook for RouterInitialized event. + * + * @param Net_URL_Mapper $m URL mapper + * + * @return boolean hook return + */ + function onStartInitializeRouter($m) + { + $m->connect('user/remote/:id', + array('action' => 'remoteprofile'), + array('id' => '[\d]+')); + + return true; + } } diff --git a/plugins/ModPlus/remoteprofileaction.php b/plugins/ModPlus/remoteprofileaction.php new file mode 100644 index 0000000000..ca82045111 --- /dev/null +++ b/plugins/ModPlus/remoteprofileaction.php @@ -0,0 +1,69 @@ +arg('id'); + $this->user = false; + $this->profile = Profile::staticGet('id', $id); + + if (!$this->profile) { + $this->serverError(_('User has no profile.')); + return false; + } + + $this->tag = $this->trimmed('tag'); + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + common_set_returnto($this->selfUrl()); + return true; + } + + function handle($args) + { + // skip yadis thingy + $this->showPage(); + } + + function title() + { + // maybe fixed in 0.9.x + if (!empty($this->profile->fullname)) { + $base = $this->profile->fullname . ' (' . $this->profile->nickname . ') '; + } else { + $base = $this->profile->nickname; + } + } + + function showContent() + { + $this->showProfile(); + // don't show notices + } + + function getFeeds() + { + // none + } + + function extraHead() + { + // none + } + function showLocalNav() + { + // none...? + } + function showSections() + { + ProfileAction::showSections(); + // skip tag cloud + } + +} \ No newline at end of file From 6849b8f9e530deae776064d4489777a9fb4eb772 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 15:39:42 -0800 Subject: [PATCH 68/74] Workaround for display of Twitter remote users in remoteprofile (ModPlus plugin): use 73px avatar if no 96px present --- lib/userprofile.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/userprofile.php b/lib/userprofile.php index 0f48078d17..9124b7c94d 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -98,6 +98,10 @@ class UserProfile extends Widget if (Event::handle('StartProfilePageAvatar', array($this->out, $this->profile))) { $avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + if (!$avatar) { + // hack for remote Twitter users: no 96px, but large Twitter size is 73px + $avatar = $this->profile->getAvatar(73); + } $this->out->elementStart('dl', 'entity_depiction'); $this->out->element('dt', null, _('Photo')); From 16f1c764c0ac89f922654f9f663faaada611b8e2 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 15:40:07 -0800 Subject: [PATCH 69/74] RemoteProfileAction: redirect to the regular user profile page if given a local user. --- plugins/ModPlus/remoteprofileaction.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/ModPlus/remoteprofileaction.php b/plugins/ModPlus/remoteprofileaction.php index ca82045111..9c089ee23e 100644 --- a/plugins/ModPlus/remoteprofileaction.php +++ b/plugins/ModPlus/remoteprofileaction.php @@ -19,6 +19,14 @@ class RemoteProfileAction extends ShowstreamAction return false; } + $user = User::staticGet('id', $this->profile->id); + if ($user) { + // This is a local user -- send to their regular profile. + $url = common_local_url('showstream', array('nickname' => $user->nickname)); + common_redirect($url); + return false; + } + $this->tag = $this->trimmed('tag'); $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; common_set_returnto($this->selfUrl()); From 88c35c2ccea5edcd08708f30247dc51a7473ecce Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 15:57:57 -0800 Subject: [PATCH 70/74] visual tweaks for RemoteProfileAction --- plugins/ModPlus/remoteprofileaction.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/ModPlus/remoteprofileaction.php b/plugins/ModPlus/remoteprofileaction.php index 9c089ee23e..f3ddbc7c65 100644 --- a/plugins/ModPlus/remoteprofileaction.php +++ b/plugins/ModPlus/remoteprofileaction.php @@ -47,12 +47,23 @@ class RemoteProfileAction extends ShowstreamAction } else { $base = $this->profile->nickname; } + $host = parse_url($this->profile->profileurl, PHP_URL_HOST); + return sprintf(_m('%s on %s'), $base, $host); } - function showContent() + /** + * Instead of showing notices, link to the original offsite profile. + */ + function showNotices() { - $this->showProfile(); - // don't show notices + $url = $this->profile->profileurl; + $host = parse_url($url, PHP_URL_HOST); + $markdown = sprintf( + _m('This profile is registered on another site; see [the profile page on %s](%s).'), + $host, + $url); + $html = common_markup_to_html($markdown); + $this->raw($html); } function getFeeds() @@ -64,10 +75,13 @@ class RemoteProfileAction extends ShowstreamAction { // none } + function showLocalNav() { - // none...? + $nav = new PublicGroupNav($this); + $nav->show(); } + function showSections() { ProfileAction::showSections(); From 5fdcba472b8673380248ab0d5dce45967f774caa Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 16:12:16 -0800 Subject: [PATCH 71/74] RemoteProfileAction cleanup: - meta robots to prevent spidering - a little notice if silenced --- lib/userprofile.php | 7 +++++-- plugins/ModPlus/remoteprofileaction.php | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/userprofile.php b/lib/userprofile.php index 9124b7c94d..2813f735ea 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -321,6 +321,9 @@ class UserProfile extends Widget } $this->out->elementEnd('li'); + // Some actions won't be applicable to non-local users. + $isLocal = !empty($this->user); + if ($cur->hasRight(Right::SANDBOXUSER) || $cur->hasRight(Right::SILENCEUSER) || $cur->hasRight(Right::DELETEUSER)) { @@ -351,7 +354,7 @@ class UserProfile extends Widget $this->out->elementEnd('li'); } - if ($cur->hasRight(Right::DELETEUSER)) { + if ($isLocal && $cur->hasRight(Right::DELETEUSER)) { $this->out->elementStart('li', 'entity_delete'); $df = new DeleteUserForm($this->out, $this->profile, $r2args); $df->show(); @@ -361,7 +364,7 @@ class UserProfile extends Widget $this->out->elementEnd('li'); } - if ($cur->hasRight(Right::GRANTROLE)) { + if ($isLocal && $cur->hasRight(Right::GRANTROLE)) { $this->out->elementStart('li', 'entity_role'); $this->out->element('p', null, _('User role')); $this->out->elementStart('ul'); diff --git a/plugins/ModPlus/remoteprofileaction.php b/plugins/ModPlus/remoteprofileaction.php index f3ddbc7c65..5b25da1701 100644 --- a/plugins/ModPlus/remoteprofileaction.php +++ b/plugins/ModPlus/remoteprofileaction.php @@ -64,6 +64,11 @@ class RemoteProfileAction extends ShowstreamAction $url); $html = common_markup_to_html($markdown); $this->raw($html); + + if ($this->profile->hasRole(Profile_role::SILENCED)) { + $markdown = _m('Site moderators have silenced this profile, which prevents delivery of new messages to any users on this site.'); + $this->raw(common_markup_to_html($markdown)); + } } function getFeeds() @@ -71,9 +76,13 @@ class RemoteProfileAction extends ShowstreamAction // none } + /** + * Don't do various extra stuff, and also trim some things to avoid crawlers. + */ function extraHead() { - // none + $this->element('meta', array('name' => 'robots', + 'content' => 'noindex,nofollow')); } function showLocalNav() From fdcaac365354fb7315263373bae6094eab6aa3c8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 16:38:18 -0800 Subject: [PATCH 72/74] Tweak remote profile action: hide stats from sidebar, tweak wording on remote notice --- plugins/ModPlus/remoteprofileaction.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/ModPlus/remoteprofileaction.php b/plugins/ModPlus/remoteprofileaction.php index 5b25da1701..caa5e6fbf3 100644 --- a/plugins/ModPlus/remoteprofileaction.php +++ b/plugins/ModPlus/remoteprofileaction.php @@ -59,7 +59,8 @@ class RemoteProfileAction extends ShowstreamAction $url = $this->profile->profileurl; $host = parse_url($url, PHP_URL_HOST); $markdown = sprintf( - _m('This profile is registered on another site; see [the profile page on %s](%s).'), + _m('This remote profile is registered on another site; see [%s\'s original profile page on %s](%s).'), + $this->profile->nickname, $host, $url); $html = common_markup_to_html($markdown); @@ -97,4 +98,9 @@ class RemoteProfileAction extends ShowstreamAction // skip tag cloud } + function showStatistics() + { + // skip + } + } \ No newline at end of file From 25170f272ca853c585531894d282c7545f00bf16 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 17:32:33 -0800 Subject: [PATCH 73/74] visual cleanup on ModPlus remote profile info popup menu --- plugins/ModPlus/ModPlusPlugin.php | 18 ++++++++++++++++++ plugins/ModPlus/modplus.css | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 plugins/ModPlus/modplus.css diff --git a/plugins/ModPlus/ModPlusPlugin.php b/plugins/ModPlus/ModPlusPlugin.php index 89bbdf857f..3e7a8c7455 100644 --- a/plugins/ModPlus/ModPlusPlugin.php +++ b/plugins/ModPlus/ModPlusPlugin.php @@ -56,6 +56,11 @@ class ModPlusPlugin extends Plugin return true; } + function onEndShowStatusNetStyles($action) { + $action->cssLink('plugins/ModPlus/modplus.css'); + return true; + } + /** * Autoloader * @@ -95,4 +100,17 @@ class ModPlusPlugin extends Plugin return true; } + + function onStartShowNoticeItem($item) + { + $profile = $item->profile; + $isRemote = !(User::staticGet('id', $profile->id)); + if ($isRemote) { + $target = common_local_url('remoteprofile', array('id' => $profile->id)); + $label = _m('Remote profile options...'); + $item->out->elementStart('div', 'remote-profile-options'); + $item->out->element('a', array('href' => $target), $label); + $item->out->elementEnd('div'); + } + } } diff --git a/plugins/ModPlus/modplus.css b/plugins/ModPlus/modplus.css new file mode 100644 index 0000000000..8d2fc8fba1 --- /dev/null +++ b/plugins/ModPlus/modplus.css @@ -0,0 +1,23 @@ +.remote-profile-options { + position: absolute; + z-index: 999; + + background: url(../../theme/base/images/icons/twotone/green/admin.gif) no-repeat 8px 8px white; + border: solid 1px #c0c0c0; + + margin-top: 56px; + + padding: 6px 16px; + padding-left: 32px; + + -moz-border-radius: 8px; + -webkit-border-radius: 8px; + -msie-border-radius: 8px; + border-radius: 8px; + + box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); + -moz-box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); + -webkit-box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); + + display: none; +} From 54de6d3260e7fde77eb86079bd67a815930b8b43 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Nov 2010 17:45:58 -0800 Subject: [PATCH 74/74] Forgot to commit the JS for ModPlus. :) --- plugins/ModPlus/modplus.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 plugins/ModPlus/modplus.js diff --git a/plugins/ModPlus/modplus.js b/plugins/ModPlus/modplus.js new file mode 100644 index 0000000000..2e90de4f19 --- /dev/null +++ b/plugins/ModPlus/modplus.js @@ -0,0 +1,23 @@ +/** + * modplus.js + * (c) 2010 StatusNet, Inc + */ + +$(function() { + function ModPlus_setup(notice) { + if ($(notice).find('.remote-profile-options').size()) { + var $options = $(notice).find('.remote-profile-options'); + $options.prepend($()) + $(notice).find('.author').mouseenter(function(event) { + $(notice).find('.remote-profile-options').fadeIn(); + }); + $(notice).mouseleave(function(event) { + $(notice).find('.remote-profile-options').fadeOut(); + }); + } + } + + $('.notice').each(function() { + ModPlus_setup(this); + }); +});