Merge branch '0.9.x' into facebook-upgrade

This commit is contained in:
Zach Copley 2010-11-16 02:32:46 +00:00
commit bd566b6f85
58 changed files with 2602 additions and 1362 deletions

View File

@ -1180,3 +1180,11 @@ StartRevokeRole: when a role is being revoked
EndRevokeRole: when a role has been revoked EndRevokeRole: when a role has been revoked
- $profile: profile that lost the role - $profile: profile that lost the role
- $role: string name of 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

6
README
View File

@ -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 version may render your StatusNet site unable to send or receive XMPP
messages. messages.
- Facebook library. Used for the Facebook application. - Facebook library. Used for the Facebook application.
- PEAR Services_oEmbed. Used for some multimedia integration. - PEAR Validate is used for URL and email validation.
- PEAR HTTP_Request is an oEmbed dependency.
- PEAR Validate is an oEmbed dependency.
- PEAR Net_URL2 is an oEmbed dependency.
- Console_GetOpt for parsing command-line options. - Console_GetOpt for parsing command-line options.
- libomb. a library for implementing OpenMicroBlogging 0.1, the - libomb. a library for implementing OpenMicroBlogging 0.1, the
predecessor to OStatus. predecessor to OStatus.
- HTTP_Request2, a library for making HTTP requests. - 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 A design goal of StatusNet is that the basic Web functionality should
work on even the most restrictive commercial hosting services. work on even the most restrictive commercial hosting services.

View File

@ -56,6 +56,8 @@ class AllrssAction extends Rss10Action
* @param array $args Web and URL arguments * @param array $args Web and URL arguments
* *
* @return boolean false if user doesn't exist * @return boolean false if user doesn't exist
*
*/
function prepare($args) function prepare($args)
{ {
parent::prepare($args); parent::prepare($args);

100
actions/apiatomservice.php Normal file
View File

@ -0,0 +1,100 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* An AtomPub service document for a user
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category API
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @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 <evan@status.net>
* @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();
}
}

View File

@ -100,13 +100,23 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
{ {
parent::handle($args); parent::handle($args);
if (!in_array($this->format, array('xml', 'json'))) { if (!in_array($this->format, array('xml', 'json', 'atom'))) {
// TRANS: Client error displayed when trying to handle an unknown API method. // TRANS: Client error displayed when trying to handle an unknown API method.
$this->clientError(_('API method not found.'), $code = 404); $this->clientError(_('API method not found.'), 404);
return; return;
} }
switch ($_SERVER['REQUEST_METHOD']) {
case 'GET':
$this->showNotice(); $this->showNotice();
break;
case 'DELETE':
$this->deleteNotice();
break;
default:
$this->clientError(_('HTTP method not supported.'), 405);
return;
}
} }
/** /**
@ -117,10 +127,18 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
function showNotice() function showNotice()
{ {
if (!empty($this->notice)) { if (!empty($this->notice)) {
if ($this->format == 'xml') { switch ($this->format) {
case 'xml':
$this->showSingleXmlStatus($this->notice); $this->showSingleXmlStatus($this->notice);
} elseif ($this->format == 'json') { break;
case 'json':
$this->show_single_json_status($this->notice); $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 { } else {
// XXX: Twitter just sets a 404 header and doens't bother // XXX: Twitter just sets a 404 header and doens't bother
@ -153,9 +171,14 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
* *
* @return boolean true * @return boolean true
*/ */
function isReadOnly($args) function isReadOnly($args)
{ {
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
return true; return true;
} else {
return false;
}
} }
/** /**
@ -197,4 +220,31 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
return null; 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");
}
} }

View File

@ -97,8 +97,13 @@ class ApiTimelineUserAction extends ApiBareAuthAction
function handle($args) function handle($args)
{ {
parent::handle($args); parent::handle($args);
if ($this->isPost()) {
$this->handlePost();
} else {
$this->showTimeline(); $this->showTimeline();
} }
}
/** /**
* Show the timeline of notices * Show the timeline of notices
@ -146,6 +151,49 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$atom->setId($self); $atom->setId($self);
$atom->setSelfLink($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); $atom->addEntryFromNotices($this->notices);
$this->raw($atom->getString()); $this->raw($atom->getString());
@ -169,13 +217,18 @@ class ApiTimelineUserAction extends ApiBareAuthAction
{ {
$notices = array(); $notices = array();
$notice = $this->user->getNotices( $notice = $this->user->getNotices(($this->page-1) * $this->count,
($this->page-1) * $this->count, $this->count, $this->count + 1,
$this->since_id, $this->max_id $this->since_id,
); $this->max_id);
while ($notice->fetch()) { while ($notice->fetch()) {
if (count($notices) < $this->count) {
$notices[] = clone($notice); $notices[] = clone($notice);
} else {
$this->next_id = $notice->id;
break;
}
} }
return $notices; return $notices;
@ -188,9 +241,14 @@ class ApiTimelineUserAction extends ApiBareAuthAction
* *
* @return boolean true * @return boolean true
*/ */
function isReadOnly($args) function isReadOnly($args)
{ {
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
return true; return true;
} else {
return false;
}
} }
/** /**
@ -234,4 +292,193 @@ class ApiTimelineUserAction extends ApiBareAuthAction
return null; 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 (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];
// 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);
return $saved;
}
function purify($content)
{
require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
$config = array('safe' => 1,
'deny_attribute' => 'id,style,on*');
return htmLawed($content, $config);
}
} }

View File

@ -108,10 +108,23 @@ class OembedAction extends Action
$oembed['url']=$file_oembed->url; $oembed['url']=$file_oembed->url;
}else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){ }else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){
$oembed['type']='photo'; $oembed['type']='photo';
//TODO set width and height if ($attachment->filename) {
//$oembed['width']= $filepath = File::path($attachment->filename);
//$oembed['height']= $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; $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{ }else{
$oembed['type']='link'; $oembed['type']='link';
$oembed['url']=common_local_url('attachment', $oembed['url']=common_local_url('attachment',

View File

@ -162,6 +162,20 @@ class RsdAction extends Action
'true'); 'true');
$this->elementEnd('settings'); $this->elementEnd('settings');
$this->elementEnd('api'); $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)); Event::handle('EndRsdListApis', array($this, $this->user));
} }
$this->elementEnd('apis'); $this->elementEnd('apis');

View File

@ -331,6 +331,15 @@ class SingleNoticeItem extends DoFollowListItem
$this->showEnd(); $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 * show the avatar of the notice's author
* *
@ -356,9 +365,4 @@ class SingleNoticeItem extends DoFollowListItem
$this->profile->fullname : $this->profile->fullname :
$this->profile->nickname)); $this->profile->nickname));
} }
function showNoticeAttachments() {
$al = new AttachmentList($this->notice, $this->out);
$al->show();
}
} }

View File

@ -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. * @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 if (empty($given_url)) return -1; // error, no url to process
$given_url = File_redirection::_canonUrl($given_url); $given_url = File_redirection::_canonUrl($given_url);
if (empty($given_url)) return -1; // error, no url to process if (empty($given_url)) return -1; // error, no url to process
@ -352,22 +366,28 @@ class File extends Memcached_DataObject
$mimetype = substr($mimetype,0,$semicolon); $mimetype = substr($mimetype,0,$semicolon);
} }
if(in_array($mimetype,$notEnclosureMimeTypes)){ if(in_array($mimetype,$notEnclosureMimeTypes)){
// 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); $oembed = File_oembed::staticGet('file_id',$this->id);
if($oembed){ if($oembed && in_array($oembed->type, array('photo', 'video'))){
$mimetype = strtolower($oembed->mimetype); $mimetype = strtolower($oembed->mimetype);
$semicolon = strpos($mimetype,';'); $semicolon = strpos($mimetype,';');
if($semicolon){ if($semicolon){
$mimetype = substr($mimetype,0,$semicolon); $mimetype = substr($mimetype,0,$semicolon);
} }
if(in_array($mimetype,$notEnclosureMimeTypes)){ // @fixme uncertain if this is right.
return false; // we want to expose things like YouTube videos as
}else{ // 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->mimetype) $enclosure->mimetype=$oembed->mimetype;
if($oembed->url) $enclosure->url=$oembed->url; if($oembed->url) $enclosure->url=$oembed->url;
if($oembed->title) $enclosure->title=$oembed->title; if($oembed->title) $enclosure->title=$oembed->title;
if($oembed->modified) $enclosure->modified=$oembed->modified; if($oembed->modified) $enclosure->modified=$oembed->modified;
unset($oembed->size); unset($oembed->size);
} //}
} else { } else {
return false; return false;
} }
@ -382,4 +402,14 @@ class File extends Memcached_DataObject
$enclosure = $this->getEnclosure(); $enclosure = $this->getEnclosure();
return !empty($enclosure); return !empty($enclosure);
} }
/**
* Get the attachment's thumbnail record, if any.
*
* @return File_thumbnail
*/
function getThumbnail()
{
return File_thumbnail::staticGet('file_id', $this->id);
}
} }

View File

@ -58,28 +58,18 @@ class File_oembed extends Memcached_DataObject
return array(false, false, false); return array(false, false, false);
} }
function _getOembed($url, $maxwidth = 500, $maxheight = 400) { function _getOembed($url) {
require_once INSTALLDIR.'/extlib/Services/oEmbed.php';
$parameters = array( $parameters = array(
'maxwidth'=>$maxwidth, 'maxwidth' => common_config('attachments', 'thumb_width'),
'maxheight'=>$maxheight, 'maxheight' => common_config('attachments', 'thumb_height'),
); );
try{ try {
$oEmbed = new Services_oEmbed($url); return oEmbedHelper::getObject($url, $parameters);
$object = $oEmbed->getObject($parameters); } catch (Exception $e) {
return $object; common_log(LOG_ERR, "Error during oembed lookup for $url - " . $e->getMessage());
}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; return false;
} }
} }
}
/** /**
* Save embedding info for a new file. * Save embedding info for a new file.
@ -120,7 +110,7 @@ class File_oembed extends Memcached_DataObject
} }
} }
$file_oembed->insert(); $file_oembed->insert();
if (!empty($data->thumbnail_url)) { if (!empty($data->thumbnail_url) || ($data->type == 'photo')) {
$ft = File_thumbnail::staticGet('file_id', $file_id); $ft = File_thumbnail::staticGet('file_id', $file_id);
if (!empty($ft)) { if (!empty($ft)) {
common_log(LOG_WARNING, "Strangely, a File_thumbnail object exists for new file $file_id", common_log(LOG_WARNING, "Strangely, a File_thumbnail object exists for new file $file_id",

View File

@ -91,9 +91,16 @@ class File_redirection extends Memcached_DataObject
$request->setMethod(HTTP_Request2::METHOD_HEAD); $request->setMethod(HTTP_Request2::METHOD_HEAD);
$response = $request->send(); $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? // Server doesn't support HEAD method? Can this really happen?
// We'll try again as a GET and ignore the response data. // 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); $request = self::_commonHttp($short_url, $redirs);
$response = $request->send(); $response = $request->send();
} }
@ -235,6 +242,18 @@ class File_redirection extends Memcached_DataObject
return null; 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://') { function _canonUrl($in_url, $default_scheme = 'http://') {
if (empty($in_url)) return false; if (empty($in_url)) return false;
$out_url = $in_url; $out_url = $in_url;

View File

@ -48,12 +48,45 @@ class File_thumbnail extends Memcached_DataObject
return array(false, false, false); 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) {
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);
}
}
/**
* 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 = new File_thumbnail;
$tn->file_id = $file_id; $tn->file_id = $file_id;
$tn->url = $data->thumbnail_url; $tn->url = $url;
$tn->width = intval($data->thumbnail_width); $tn->width = intval($width);
$tn->height = intval($data->thumbnail_height); $tn->height = intval($height);
$tn->insert(); $tn->insert();
} }
} }

View File

@ -1611,6 +1611,35 @@ class Notice extends Memcached_DataObject
Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon)); Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon));
} }
// @fixme check this logic
if ($this->isLocal()) {
$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' => $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));
}
}
}
if (Event::handle('StartActivityEnd', array(&$this, &$xs))) { if (Event::handle('StartActivityEnd', array(&$this, &$xs))) {
$xs->elementEnd('entry'); $xs->elementEnd('entry');
Event::handle('EndActivityEnd', array(&$this, &$xs)); Event::handle('EndActivityEnd', array(&$this, &$xs));

View File

@ -494,6 +494,29 @@ class Profile extends Memcached_DataObject
return $cnt; 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) function hasFave($notice)
{ {
$cache = common_memcache(); $cache = common_memcache();

View File

@ -84,7 +84,8 @@ class User extends Memcached_DataObject
function isSubscribed($other) 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. // '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) function mutuallySubscribed($other)
{ {
return $this->isSubscribed($other) && $profile = $this->getProfile();
$other->isSubscribed($this); return $profile->mutuallySubscribed($other);
} }
function mutuallySubscribedUsers() function mutuallySubscribedUsers()

View File

@ -1,357 +0,0 @@
<?php
/**
* An interface for oEmbed consumption
*
* PHP version 5.1.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Validate.php';
require_once 'Net/URL2.php';
require_once 'HTTP/Request.php';
require_once 'Services/oEmbed/Exception.php';
require_once 'Services/oEmbed/Exception/NoSupport.php';
require_once 'Services/oEmbed/Object.php';
/**
* Base class for consuming oEmbed objects
*
* <code>
* <?php
*
* require_once 'Services/oEmbed.php';
*
* // The URL that we'd like to find out more information about.
* $url = 'http://flickr.com/photos/joestump/2848795611/';
*
* // The oEmbed API URI. Not all providers support discovery yet so we're
* // explicitly providing one here. If one is not provided Services_oEmbed
* // attempts to discover it. If none is found an exception is thrown.
* $oEmbed = new Services_oEmbed($url, array(
* Services_oEmbed::OPTION_API => 'http://www.flickr.com/services/oembed/'
* ));
* $object = $oEmbed->getObject();
*
* // All of the objects have somewhat sane __toString() methods that allow
* // you to output them directly.
* echo (string)$object;
*
* ?>
* </code>
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed
{
/**
* HTTP timeout in seconds
*
* All HTTP requests made by Services_oEmbed will respect this timeout.
* This can be passed to {@link Services_oEmbed::setOption()} or to the
* options parameter in {@link Services_oEmbed::__construct()}.
*
* @var string OPTION_TIMEOUT Timeout in seconds
*/
const OPTION_TIMEOUT = 'http_timeout';
/**
* HTTP User-Agent
*
* All HTTP requests made by Services_oEmbed will be sent with the
* string set by this option.
*
* @var string OPTION_USER_AGENT The HTTP User-Agent string
*/
const OPTION_USER_AGENT = 'http_user_agent';
/**
* The API's URI
*
* If the API is known ahead of time this option can be used to explicitly
* set it. If not present then the API is attempted to be discovered
* through the auto-discovery mechanism.
*
* @var string OPTION_API
*/
const OPTION_API = 'oembed_api';
/**
* Options for oEmbed requests
*
* @var array $options The options for making requests
*/
protected $options = array(
self::OPTION_TIMEOUT => 3,
self::OPTION_API => null,
self::OPTION_USER_AGENT => 'Services_oEmbed 0.1.0'
);
/**
* URL of object to get embed information for
*
* @var object $url {@link Net_URL2} instance of URL of object
*/
protected $url = null;
/**
* Constructor
*
* @param string $url The URL to fetch an oEmbed for
* @param array $options A list of options for the oEmbed lookup
*
* @throws {@link Services_oEmbed_Exception} if the $url is invalid
* @throws {@link Services_oEmbed_Exception} when no valid API is found
* @return void
*/
public function __construct($url, array $options = array())
{
if (Validate::uri($url)) {
$this->url = new Net_URL2($url);
} else {
throw new Services_oEmbed_Exception('URL is invalid');
}
if (count($options)) {
foreach ($options as $key => $val) {
$this->setOption($key, $val);
}
}
if ($this->options[self::OPTION_API] === null) {
$this->options[self::OPTION_API] = $this->discover($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 <link /> tags that have a valid oembed type set. We then
// extract the href attribute for each type.
$regexp = '#<link([^>]*)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;
}
}
?>

View File

@ -1,65 +0,0 @@
<?php
/**
* Base exception class for {@link Services_oEmbed}
*
* PHP version 5.1.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'PEAR/Exception.php';
/**
* Base exception class for {@link Services_oEmbed}
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Exception extends PEAR_Exception
{
}
?>

View File

@ -1,63 +0,0 @@
<?php
/**
* Exception class when no oEmbed support is discovered
*
* PHP version 5.2.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
/**
* Exception class when no oEmbed support is discovered
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Exception_NoSupport extends Services_oEmbed_Exception
{
}
?>

View File

@ -1,126 +0,0 @@
<?php
/**
* An interface for oEmbed consumption
*
* PHP version 5.1.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Services/oEmbed/Object/Exception.php';
/**
* Base class for consuming oEmbed objects
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
abstract class Services_oEmbed_Object
{
/**
* Valid oEmbed object types
*
* @var array $types Array of valid object types
* @see Services_oEmbed_Object::factory()
*/
static protected $types = array(
'photo' => 'Photo',
'video' => 'Video',
'link' => 'Link',
'rich' => 'Rich'
);
/**
* Create an oEmbed object from result
*
* @param object $object Raw object returned from API
*
* @throws {@link Services_oEmbed_Object_Exception} on object error
* @return object Instance of object driver
* @see Services_oEmbed_Object_Link, Services_oEmbed_Object_Photo
* @see Services_oEmbed_Object_Rich, Services_oEmbed_Object_Video
*/
static public function factory($object)
{
if (!isset($object->type)) {
throw new Services_oEmbed_Object_Exception(
'Object has no type'
);
}
$type = (string)$object->type;
if (!isset(self::$types[$type])) {
throw new Services_oEmbed_Object_Exception(
'Object type is unknown or invalid: ' . $type
);
}
$file = 'Services/oEmbed/Object/' . self::$types[$type] . '.php';
include_once $file;
$class = 'Services_oEmbed_Object_' . self::$types[$type];
if (!class_exists($class)) {
throw new Services_oEmbed_Object_Exception(
'Object class is invalid or not present'
);
}
$instance = new $class($object);
return $instance;
}
/**
* Instantiation is not allowed
*
* @return void
*/
private function __construct()
{
}
}
?>

View File

@ -1,139 +0,0 @@
<?php
/**
* Base class for oEmbed objects
*
* PHP version 5.1.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
/**
* Base class for oEmbed objects
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
abstract class Services_oEmbed_Object_Common
{
/**
* Raw object returned from API
*
* @var object $object The raw object from the API
*/
protected $object = null;
/**
* Required fields per the specification
*
* @var array $required Array of required fields
* @link http://oembed.com
*/
protected $required = array();
/**
* Constructor
*
* @param object $object Raw object returned from the API
*
* @throws {@link Services_oEmbed_Object_Exception} on missing fields
* @return void
*/
public function __construct($object)
{
$this->object = $object;
$this->required[] = 'version';
foreach ($this->required as $field) {
if (!isset($this->$field)) {
throw new Services_oEmbed_Object_Exception(
'Object is missing required ' . $field . ' attribute'
);
}
}
}
/**
* Get object variable
*
* @param string $var Variable to get
*
* @see Services_oEmbed_Object_Common::$object
* @return mixed Attribute's value or null if it's not set/exists
*/
public function __get($var)
{
if (property_exists($this->object, $var)) {
return $this->object->$var;
}
return null;
}
/**
* Is variable set?
*
* @param string $var Variable name to check
*
* @return boolean True if set, false if not
* @see Services_oEmbed_Object_Common::$object
*/
public function __isset($var)
{
if (property_exists($this->object, $var)) {
return (isset($this->object->$var));
}
return false;
}
/**
* Require a sane __toString for all objects
*
* @return string
*/
abstract public function __toString();
}
?>

View File

@ -1,65 +0,0 @@
<?php
/**
* Exception for {@link Services_oEmbed_Object}
*
* PHP version 5.1.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Services/oEmbed/Exception.php';
/**
* Exception for {@link Services_oEmbed_Object}
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Object_Exception extends Services_oEmbed_Exception
{
}
?>

View File

@ -1,73 +0,0 @@
<?php
/**
* Link object for {@link Services_oEmbed}
*
* PHP version 5.2.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Services/oEmbed/Object/Common.php';
/**
* Link object for {@link Services_oEmbed}
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Object_Link extends Services_oEmbed_Object_Common
{
/**
* Output a sane link
*
* @return string An HTML link of the object
*/
public function __toString()
{
return '<a href="' . $this->url . '">' . $this->title . '</a>';
}
}
?>

View File

@ -1,89 +0,0 @@
<?php
/**
* Photo object for {@link Services_oEmbed}
*
* PHP version 5.2.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Services/oEmbed/Object/Common.php';
/**
* Photo object for {@link Services_oEmbed}
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Object_Photo extends Services_oEmbed_Object_Common
{
/**
* Required fields for photo objects
*
* @var array $required Required fields
*/
protected $required = array(
'url', 'width', 'height'
);
/**
* Output a valid HTML tag for image
*
* @return string HTML <img /> tag for Photo
*/
public function __toString()
{
$img = '<img src="' . $this->url . '" width="' . $this->width . '" ' .
'height="' . $this->height . '"';
if (isset($this->title)) {
$img .= ' alt="' . $this->title . '"';
}
return $img . ' />';
}
}
?>

View File

@ -1,82 +0,0 @@
<?php
/**
* Photo object for {@link Services_oEmbed}
*
* PHP version 5.2.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Services/oEmbed/Object/Common.php';
/**
* Photo object for {@link Services_oEmbed}
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Object_Rich extends Services_oEmbed_Object_Common
{
/**
* Required fields for rich objects
*
* @var array $required Required fields
*/
protected $required = array(
'html', 'width', 'height'
);
/**
* Output a the HTML tag for rich object
*
* @return string HTML for rich object
*/
public function __toString()
{
return $this->html;
}
}
?>

View File

@ -1,82 +0,0 @@
<?php
/**
* Photo object for {@link Services_oEmbed}
*
* PHP version 5.2.0+
*
* Copyright (c) 2008, Digg.com, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of Digg.com, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version SVN: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
require_once 'Services/oEmbed/Object/Common.php';
/**
* Photo object for {@link Services_oEmbed}
*
* @category Services
* @package Services_oEmbed
* @author Joe Stump <joe@joestump.net>
* @copyright 2008 Digg.com, Inc.
* @license http://tinyurl.com/42zef New BSD License
* @version Release: @version@
* @link http://code.google.com/p/digg
* @link http://oembed.com
*/
class Services_oEmbed_Object_Video extends Services_oEmbed_Object_Common
{
/**
* Required fields for video objects
*
* @var array $required Required fields
*/
protected $required = array(
'html', 'width', 'height'
);
/**
* Output a valid embed tag for video
*
* @return string HTML for video
*/
public function __toString()
{
return $this->html;
}
}
?>

View File

@ -56,7 +56,7 @@ var SN = { // StatusNet
NoticeDataGeoCookie: 'NoticeDataGeo', NoticeDataGeoCookie: 'NoticeDataGeo',
NoticeDataGeoSelected: 'notice_data-geo_selected', NoticeDataGeoSelected: 'notice_data-geo_selected',
StatusNetInstance:'StatusNetInstance' StatusNetInstance:'StatusNetInstance'
}, }
}, },
messages: {}, messages: {},
@ -427,61 +427,6 @@ var SN = { // StatusNet
return false; return false;
}).attr('title', SN.msg('showmore_tooltip')); }).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('<button class="close">&#215;</button>');
$('#jOverlayContent button').click($.closeOverlay);
},
timeout : 0,
autoHide : true,
css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'}
};
notice.find('a.attachment').click(function() {
var attachId = ($(this).attr('id').substring('attachment'.length + 1));
if (attachId) {
$().jOverlay({url: $('address .url')[0].href+'attachment/' + attachId + '/ajax'});
return false;
}
});
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() { NoticeDataAttach: function() {

View File

@ -1404,4 +1404,15 @@ class Action extends HTMLOutputter // lawsuit
$this->clientError(_('There was a problem with your session token.')); $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');
}
} }

View File

@ -726,6 +726,12 @@ class ApiAction extends Action
$this->endDocument('xml'); $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) function show_single_json_status($notice)
{ {
$this->initDocument('json'); $this->initDocument('json');

View File

@ -79,23 +79,33 @@ class AttachmentList extends Widget
$atts = new File; $atts = new File;
$att = $atts->getAttachments($this->notice->id); $att = $atts->getAttachments($this->notice->id);
if (empty($att)) return 0; if (empty($att)) return 0;
$this->out->elementStart('dl', array('id' =>'attachments', $this->showListStart();
'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'));
foreach ($att as $n=>$attachment) { foreach ($att as $n=>$attachment) {
$item = $this->newListItem($attachment); $item = $this->newListItem($attachment);
$item->show(); $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('dd');
$this->out->elementEnd('ol'); $this->out->elementEnd('ol');
$this->out->elementEnd('dl'); $this->out->elementEnd('dl');
return count($att);
} }
/** /**
@ -187,7 +197,10 @@ class AttachmentListItem extends Widget
} }
function linkAttr() { 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() { function showLink() {
@ -203,12 +216,34 @@ class AttachmentListItem extends Widget
} }
function showRepresentation() { function showRepresentation() {
$thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id); $thumb = $this->getThumbInfo();
if (!empty($thumbnail)) { if ($thumb) {
$this->out->element('img', array('alt' => '', 'src' => $thumbnail->url, 'width' => $thumbnail->width, 'height' => $thumbnail->height)); $this->out->element('img', array('alt' => '', 'src' => $thumb->url, 'width' => $thumb->width, 'height' => $thumb->height));
} }
} }
/**
* Pull a thumbnail image reference for the given file, and if necessary
* resize it to match currently thumbnail size settings.
*
* @return File_Thumbnail or false/null
*/
function getThumbInfo()
{
$thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id);
if ($thumbnail) {
$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 $thumbnail;
}
/** /**
* start a single notice. * start a single notice.
* *
@ -234,6 +269,9 @@ class AttachmentListItem extends Widget
} }
} }
/**
* used for one-off attachment action
*/
class Attachment extends AttachmentListItem class Attachment extends AttachmentListItem
{ {
function showLink() { function showLink() {
@ -417,15 +455,6 @@ class Attachment extends AttachmentListItem
function showFallback() function showFallback()
{ {
// If we don't know how to display an attachment inline, we probably // still needed: should show a link?
// 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('<script>window.location = ' . json_encode($this->attachment->url) . ';</script>');
} }
} }

View File

@ -250,6 +250,9 @@ $default =
'monthly_quota' => 15000000, 'monthly_quota' => 15000000,
'uploads' => true, 'uploads' => true,
'filecommand' => '/usr/bin/file', '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,
), ),
'application' => 'application' =>
array('desclimit' => null), array('desclimit' => null),

View File

@ -115,10 +115,46 @@ class ImageFile
return new ImageFile(null, $_FILES[$param]['tmp_name']); 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) 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; $w = ($w === null) ? $this->width:$w;
$h = ($h === null) ? $this->height:$h; $h = ($h === null) ? $this->height:$h;
$targetType = $this->preferredType($this->type);
if (!file_exists($this->filepath)) { if (!file_exists($this->filepath)) {
throw new Exception(_('Lost our file.')); throw new Exception(_('Lost our file.'));
@ -126,20 +162,16 @@ class ImageFile
} }
// Don't crop/scale if it isn't necessary // Don't crop/scale if it isn't necessary
if ($size === $this->width if ($width === $this->width
&& $size === $this->height && $height === $this->height
&& $x === 0 && $x === 0
&& $y === 0 && $y === 0
&& $w === $this->width && $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); @copy($this->filepath, $outpath);
return $outname; return $outpath;
} }
switch ($this->type) { switch ($this->type) {
@ -166,7 +198,7 @@ class ImageFile
return; return;
} }
$image_dest = imagecreatetruecolor($size, $size); $image_dest = imagecreatetruecolor($width, $height);
if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG || $this->type == IMAGETYPE_BMP) { 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) { switch ($targetType) {
//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) {
case IMAGETYPE_GIF: case IMAGETYPE_GIF:
imagegif($image_dest, $outpath); imagegif($image_dest, $outpath);
break; break;
@ -230,7 +241,31 @@ class ImageFile
imagedestroy($image_src); imagedestroy($image_src);
imagedestroy($image_dest); 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() function unlink()

View File

@ -0,0 +1,108 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* widget for displaying notice attachments thumbnails
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category UI
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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');
}
/**
* Build HTML attributes for the link
* @return array
*/
function linkAttr()
{
$attr = parent::linkAttr();
$attr['class'] = 'attachment-thumbnail';
return $attr;
}
/**
* 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');
}
}

View File

@ -48,11 +48,14 @@ class MediaFile
{ {
if ($user == null) { if ($user == null) {
$this->user = common_current_user(); $this->user = common_current_user();
} else {
$this->user = $user;
} }
$this->filename = $filename; $this->filename = $filename;
$this->mimetype = $mimetype; $this->mimetype = $mimetype;
$this->fileRecord = $this->storeFile(); $this->fileRecord = $this->storeFile();
$this->thumbnailRecord = $this->storeThumbnail();
$this->fileurl = common_local_url('attachment', $this->fileurl = common_local_url('attachment',
array('attachment' => $this->fileRecord->id)); array('attachment' => $this->fileRecord->id));
@ -102,6 +105,52 @@ class MediaFile
return $file; 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);
$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,
File::url($outname),
$width,
$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) function rememberFile($file, $short)
{ {
$this->maybeAddRedir($file->id, $short); $this->maybeAddRedir($file->id, $short);

View File

@ -208,6 +208,7 @@ class NoticeListItem extends Widget
$this->showStart(); $this->showStart();
if (Event::handle('StartShowNoticeItem', array($this))) { if (Event::handle('StartShowNoticeItem', array($this))) {
$this->showNotice(); $this->showNotice();
$this->showNoticeAttachments();
$this->showNoticeInfo(); $this->showNoticeInfo();
$this->showNoticeOptions(); $this->showNoticeOptions();
Event::handle('EndShowNoticeItem', array($this)); Event::handle('EndShowNoticeItem', array($this));
@ -383,6 +384,13 @@ class NoticeListItem extends Widget
$this->out->elementEnd('p'); $this->out->elementEnd('p');
} }
function showNoticeAttachments() {
if (common_config('attachments', 'show_thumbs')) {
$al = new InlineAttachmentList($this->notice, $this->out);
$al->show();
}
}
/** /**
* show the link to the main page for the notice * show the link to the main page for the notice
* *

318
lib/oembedhelper.php Normal file
View File

@ -0,0 +1,318 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-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 <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Utility class to wrap basic oEmbed lookups.
*
* 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())
{
$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];
} else {
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;
}
}
}
return self::getObjectFrom($api, $url, $params);
}
/**
* 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 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;
}
}
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 function getObjectFrom($api, $url, $params=array())
{
$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.');
}
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');
}
}
return $data;
}
/**
* 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 oEmbedHelper_Exception extends Exception
{
}
class oEmbedHelper_BadHtmlException extends oEmbedHelper_Exception
{
function __construct($previous=null)
{
return parent::__construct('Bad HTML in discovery data.', 0, $previous);
}
}
class oEmbedHelper_DiscoveryException extends oEmbedHelper_Exception
{
function __construct($previous=null)
{
return parent::__construct('No oEmbed discovery data.', 0, $previous);
}
}

View File

@ -101,7 +101,7 @@ class ProfileAction extends OwnerDesignAction
function showSubscriptions() 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', $this->elementStart('div', array('id' => 'entity_subscriptions',
'class' => 'section')); 'class' => 'section'));
@ -134,7 +134,7 @@ class ProfileAction extends OwnerDesignAction
function showSubscribers() 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', $this->elementStart('div', array('id' => 'entity_subscribers',
'class' => 'section')); 'class' => 'section'));
@ -173,7 +173,7 @@ class ProfileAction extends OwnerDesignAction
$subs_count = $this->profile->subscriptionCount(); $subs_count = $this->profile->subscriptionCount();
$subbed_count = $this->profile->subscriberCount(); $subbed_count = $this->profile->subscriberCount();
$notice_count = $this->profile->noticeCount(); $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; $age_days = (time() - strtotime($this->profile->created)) / 86400;
if ($age_days < 1) { if ($age_days < 1) {
// Rather than extrapolating out to a bajillion... // Rather than extrapolating out to a bajillion...
@ -241,7 +241,7 @@ class ProfileAction extends OwnerDesignAction
function showGroups() 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', $this->elementStart('div', array('id' => 'entity_groups',
'class' => 'section')); 'class' => 'section'));
@ -249,7 +249,7 @@ class ProfileAction extends OwnerDesignAction
$this->element('h2', null, _('Groups')); $this->element('h2', null, _('Groups'));
if ($groups) { if ($groups) {
$gml = new GroupMiniList($groups, $this->user, $this); $gml = new GroupMiniList($groups, $this->profile, $this);
$cnt = $gml->show(); $cnt = $gml->show();
if ($cnt == 0) { if ($cnt == 0) {
$this->element('p', null, _('(None)')); $this->element('p', null, _('(None)'));

View File

@ -399,12 +399,12 @@ class Router
$m->connect('api/statuses/show.:format', $m->connect('api/statuses/show.:format',
array('action' => 'ApiStatusesShow', array('action' => 'ApiStatusesShow',
'format' => '(xml|json)')); 'format' => '(xml|json|atom)'));
$m->connect('api/statuses/show/:id.:format', $m->connect('api/statuses/show/:id.:format',
array('action' => 'ApiStatusesShow', array('action' => 'ApiStatusesShow',
'id' => '[0-9]+', 'id' => '[0-9]+',
'format' => '(xml|json)')); 'format' => '(xml|json|atom)'));
$m->connect('api/statuses/update.:format', $m->connect('api/statuses/update.:format',
array('action' => 'ApiStatusesUpdate', array('action' => 'ApiStatusesUpdate',
@ -686,6 +686,13 @@ class Router
$m->connect('api/oauth/authorize', $m->connect('api/oauth/authorize',
array('action' => 'ApiOauthAuthorize')); 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 // Admin
$m->connect('admin/site', array('action' => 'siteadminpanel')); $m->connect('admin/site', array('action' => 'siteadminpanel'));

View File

@ -377,7 +377,11 @@ class StatusNet
static function isHTTPS() static function isHTTPS()
{ {
// There are some exceptions to this; add them here! // There are some exceptions to this; add them here!
return !empty($_SERVER['HTTPS']); if(empty($_SERVER['HTTPS'])) {
return false;
} else {
return $_SERVER['HTTPS'] !== 'off';
}
} }
} }

View File

@ -98,6 +98,10 @@ class UserProfile extends Widget
if (Event::handle('StartProfilePageAvatar', array($this->out, $this->profile))) { if (Event::handle('StartProfilePageAvatar', array($this->out, $this->profile))) {
$avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); $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->elementStart('dl', 'entity_depiction');
$this->out->element('dt', null, _('Photo')); $this->out->element('dt', null, _('Photo'));
@ -109,10 +113,8 @@ class UserProfile extends Widget
'alt' => $this->profile->nickname)); 'alt' => $this->profile->nickname));
$this->out->elementEnd('dd'); $this->out->elementEnd('dd');
$user = User::staticGet('id', $this->profile->id);
$cur = common_current_user(); $cur = common_current_user();
if ($cur && $cur->id == $user->id) { if ($cur && $cur->id == $this->profile->id) {
$this->out->elementStart('dd'); $this->out->elementStart('dd');
$this->out->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar')); $this->out->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar'));
$this->out->elementEnd('dd'); $this->out->elementEnd('dd');
@ -278,7 +280,7 @@ class UserProfile extends Widget
} }
$this->out->elementEnd('li'); $this->out->elementEnd('li');
if ($cur->mutuallySubscribed($this->user)) { if ($cur->mutuallySubscribed($this->profile)) {
// message // message
@ -290,7 +292,7 @@ class UserProfile extends Widget
// nudge // nudge
if ($this->user->email && $this->user->emailnotifynudge) { if ($this->user && $this->user->email && $this->user->emailnotifynudge) {
$this->out->elementStart('li', 'entity_nudge'); $this->out->elementStart('li', 'entity_nudge');
$nf = new NudgeForm($this->out, $this->user); $nf = new NudgeForm($this->out, $this->user);
$nf->show(); $nf->show();
@ -319,6 +321,9 @@ class UserProfile extends Widget
} }
$this->out->elementEnd('li'); $this->out->elementEnd('li');
// Some actions won't be applicable to non-local users.
$isLocal = !empty($this->user);
if ($cur->hasRight(Right::SANDBOXUSER) || if ($cur->hasRight(Right::SANDBOXUSER) ||
$cur->hasRight(Right::SILENCEUSER) || $cur->hasRight(Right::SILENCEUSER) ||
$cur->hasRight(Right::DELETEUSER)) { $cur->hasRight(Right::DELETEUSER)) {
@ -327,7 +332,7 @@ class UserProfile extends Widget
$this->out->elementStart('ul'); $this->out->elementStart('ul');
if ($cur->hasRight(Right::SANDBOXUSER)) { if ($cur->hasRight(Right::SANDBOXUSER)) {
$this->out->elementStart('li', 'entity_sandbox'); $this->out->elementStart('li', 'entity_sandbox');
if ($this->user->isSandboxed()) { if ($this->profile->isSandboxed()) {
$usf = new UnSandboxForm($this->out, $this->profile, $r2args); $usf = new UnSandboxForm($this->out, $this->profile, $r2args);
$usf->show(); $usf->show();
} else { } else {
@ -339,7 +344,7 @@ class UserProfile extends Widget
if ($cur->hasRight(Right::SILENCEUSER)) { if ($cur->hasRight(Right::SILENCEUSER)) {
$this->out->elementStart('li', 'entity_silence'); $this->out->elementStart('li', 'entity_silence');
if ($this->user->isSilenced()) { if ($this->profile->isSilenced()) {
$usf = new UnSilenceForm($this->out, $this->profile, $r2args); $usf = new UnSilenceForm($this->out, $this->profile, $r2args);
$usf->show(); $usf->show();
} else { } else {
@ -349,7 +354,7 @@ class UserProfile extends Widget
$this->out->elementEnd('li'); $this->out->elementEnd('li');
} }
if ($cur->hasRight(Right::DELETEUSER)) { if ($isLocal && $cur->hasRight(Right::DELETEUSER)) {
$this->out->elementStart('li', 'entity_delete'); $this->out->elementStart('li', 'entity_delete');
$df = new DeleteUserForm($this->out, $this->profile, $r2args); $df = new DeleteUserForm($this->out, $this->profile, $r2args);
$df->show(); $df->show();
@ -359,7 +364,7 @@ class UserProfile extends Widget
$this->out->elementEnd('li'); $this->out->elementEnd('li');
} }
if ($cur->hasRight(Right::GRANTROLE)) { if ($isLocal && $cur->hasRight(Right::GRANTROLE)) {
$this->out->elementStart('li', 'entity_role'); $this->out->elementStart('li', 'entity_role');
$this->out->element('p', null, _('User role')); $this->out->element('p', null, _('User role'));
$this->out->elementStart('ul'); $this->out->elementStart('ul');
@ -387,7 +392,7 @@ class UserProfile extends Widget
$r2args['action'] = $action; $r2args['action'] = $action;
$this->out->elementStart('li', "entity_role_$role"); $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 = new RevokeRoleForm($role, $label, $this->out, $this->profile, $r2args);
$rf->show(); $rf->show();
} else { } else {

View File

@ -877,7 +877,7 @@ function common_linkify($url) {
} }
if (!empty($f)) { if (!empty($f)) {
if ($f->getEnclosure() || File_oembed::staticGet('file_id',$f->id)) { if ($f->getEnclosure()) {
$is_attachment = true; $is_attachment = true;
$attachment_id = $f->id; $attachment_id = $f->id;

View File

@ -198,10 +198,12 @@ class XmppManager extends IoManager
$this->conn->processTime(0); $this->conn->processTime(0);
return true; return true;
} else { } else {
common_log(LOG_ERR, __METHOD__ . ' failed: 0 bytes sent');
return false; return false;
} }
} else { } else {
// Can't send right now... // Can't send right now...
common_log(LOG_ERR, __METHOD__ . ' failed: XMPP server connection currently down');
return false; return false;
} }
} }

View File

@ -0,0 +1,202 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Sends an email summary of the inbox to users in the network
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Sample
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @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 <brionv@status.net>
* @author Evan Prodromou <evan@status.net>
* @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_id', 'integer', 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 'SiteEmailSummaryHandler':
case 'UserEmailSummaryHandler':
include_once $dir . '/'.strtolower($cls).'.php';
return false;
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;
}
/**
* 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;
}
/**
* 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;
}
}

View File

@ -0,0 +1,167 @@
<?php
/**
* Data class for email summary status
*
* PHP version 5
*
* @category Data
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @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 <http://www.gnu.org/licenses/>.
*/
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 <evan@status.net>
* @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_id; // int(4) null
public $created; // datetime not_null
public $modified; // datetime 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_id' => DB_DATAOBJECT_INT,
'created' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
'modified' => DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
}
/**
* 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 getLastSummaryID($user_id)
{
$ess = Email_summary_status::staticGet('user_id', $user_id);
if (!empty($ess)) {
return $ess->last_summary_id;
} else {
return null;
}
}
}

View File

@ -0,0 +1,47 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a 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 <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
$shortoptions = 'i:n:a';
$longoptions = array('id=', 'nickname=', 'all');
$helptext = <<<END_OF_SENDEMAILSUMMARY_HELP
sendemailsummary.php [options]
Send an email summary of the inbox to users
-i --id ID of user to send summary to
-n --nickname nickname of the user to send summary to
-a --all send summary to all users
END_OF_SENDEMAILSUMMARY_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
$qm = QueueManager::get();
// enqueue summary for user or all users
try {
$user = getUser();
$qm->enqueue($user->id, 'usersum');
} catch (NoUserArgumentException $nuae) {
$qm->enqueue(null, 'sitesum');
}

View File

@ -0,0 +1,96 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
*
* Handler for queue items of type 'sitesum', sends email summaries
* to all users on the site.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Sample
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @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 <evan@status.net>
* @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;
}
}

View File

@ -0,0 +1,226 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
*
* Handler for queue items of type 'usersum', sends an email summaries
* to a particular user.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Sample
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @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 <evan@status.net>
* @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(_('<p>Recent updates from %1s for %2s:</p>'),
common_config('site', 'name'),
$profile->getBestName()));
$out->elementStart('table', array('width' => '541px', 'style' => 'border: none'));
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', array('width' => 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_STREAM_SIZE,
'height' => AVATAR_STREAM_SIZE,
'alt' => $profile->getBestName()));
$out->elementEnd('td');
$out->elementStart('td', array('align' => 'left',
'valign' => 'top'));
$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 <br> 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(_('<p><a href="%1s">change your email settings for %2s</a></p>'),
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($orig);
}
return true;
}
}

View File

@ -156,7 +156,8 @@ class MapstractionPlugin extends Plugin
' var user = null; '. ' var user = null; '.
(($actionName == 'showstream') ? ' user = scrapeUser(); ' : '') . (($actionName == 'showstream') ? ' user = scrapeUser(); ' : '') .
' var notices = scrapeNotices(user); ' . ' var notices = scrapeNotices(user); ' .
' showMapstraction($("#map_canvas"), notices); '. ' var canvas = $("#map_canvas")[0]; ' .
' if (typeof(canvas) != "undefined") { showMapstraction(canvas, notices); } '.
'});'); '});');
} }

View File

@ -0,0 +1,116 @@
<?php
/*
* 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 <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Some UI extras for now...
*
* @package ModPlusPlugin
* @maintainer Brion Vibber <brion@status.net>
*/
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;
}
/**
* 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;
}
function onEndShowStatusNetStyles($action) {
$action->cssLink('plugins/ModPlus/modplus.css');
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;
}
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');
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,106 @@
<?php
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
class RemoteProfileAction extends ShowstreamAction
{
function prepare($args)
{
OwnerDesignAction::prepare($args); // skip the ProfileAction code and replace it...
$id = $this->arg('id');
$this->user = false;
$this->profile = Profile::staticGet('id', $id);
if (!$this->profile) {
$this->serverError(_('User has no profile.'));
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());
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;
}
$host = parse_url($this->profile->profileurl, PHP_URL_HOST);
return sprintf(_m('%s on %s'), $base, $host);
}
/**
* Instead of showing notices, link to the original offsite profile.
*/
function showNotices()
{
$url = $this->profile->profileurl;
$host = parse_url($url, PHP_URL_HOST);
$markdown = sprintf(
_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);
$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()
{
// none
}
/**
* Don't do various extra stuff, and also trim some things to avoid crawlers.
*/
function extraHead()
{
$this->element('meta', array('name' => 'robots',
'content' => 'noindex,nofollow'));
}
function showLocalNav()
{
$nav = new PublicGroupNav($this);
$nav->show();
}
function showSections()
{
ProfileAction::showSections();
// skip tag cloud
}
function showStatistics()
{
// skip
}
}

View File

@ -144,6 +144,7 @@ class Notice_to_status extends Memcached_DataObject
/** /**
* Save a mapping between a notice and a status * 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 $notice_id ID of the notice in StatusNet
* @param integer $status_id ID of the status in Twitter * @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) 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); $n2s = Notice_to_status::staticGet('notice_id', $notice_id);
if (!empty($n2s)) { if (!empty($n2s)) {
return $n2s; return $n2s;
} }
if (empty($status_id)) {
throw new Exception("Invalid status_id $status_id");
}
$n2s = Notice_to_status::staticGet('status_id', $status_id); $n2s = Notice_to_status::staticGet('status_id', $status_id);
if (!empty($n2s)) { if (!empty($n2s)) {

View File

@ -128,6 +128,16 @@ function is_twitter_notice($id)
return (!empty($n2s)); 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) function broadcast_twitter($notice)
{ {
$flink = Foreign_link::getByUserID($notice->profile_id, $flink = Foreign_link::getByUserID($notice->profile_id,
@ -137,8 +147,13 @@ function broadcast_twitter($notice)
if (!empty($flink) && TwitterOAuthClient::isPackedToken($flink->credentials)) { if (!empty($flink) && TwitterOAuthClient::isPackedToken($flink->credentials)) {
if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) { if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) {
$retweet = retweet_notice($flink, Notice::staticGet('id', $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); 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)) { } else if (is_twitter_bound($notice, $flink)) {
return broadcast_oauth($notice, $flink); return broadcast_oauth($notice, $flink);
@ -148,6 +163,21 @@ function broadcast_twitter($notice)
return true; 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) function retweet_notice($flink, $notice)
{ {
$token = TwitterOAuthClient::unpackToken($flink->credentials); $token = TwitterOAuthClient::unpackToken($flink->credentials);

View File

@ -189,6 +189,7 @@ class TwitterImport
Notice_to_status::saveNew($notice->id, $status->id); Notice_to_status::saveNew($notice->id, $status->id);
$this->saveStatusMentions($notice, $status); $this->saveStatusMentions($notice, $status);
$this->saveStatusAttachments($notice, $status);
$notice->blowOnInsert(); $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);
}
}
}
} }

92
scripts/clear_jabber.php Executable file
View File

@ -0,0 +1,92 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, 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 <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$shortoptions = 'i::n::y';
$longoptions = array('id=', 'nickname=', 'yes', 'all', 'dry-run');
$helptext = <<<END_OF_DELETEUSER_HELP
clear_jabber.php [options]
Deletes a user's confirmed Jabber/XMPP address from the database.
-i --id ID of the user
-n --nickname nickname of the user
--all all users with confirmed Jabber addresses
--dry-run Don't actually delete info.
END_OF_DELETEUSER_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (have_option('i', 'id')) {
$id = get_option_value('i', 'id');
$user = User::staticGet('id', $id);
if (empty($user)) {
print "Can't find user with ID $id\n";
exit(1);
}
} else if (have_option('n', 'nickname')) {
$nickname = get_option_value('n', 'nickname');
$user = User::staticGet('nickname', $nickname);
if (empty($user)) {
print "Can't find user with nickname '$nickname'\n";
exit(1);
}
} else if (have_option('all')) {
$user = new User();
$user->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";

140
tests/oEmbedTest.php Normal file
View File

@ -0,0 +1,140 @@
<?php
if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
print "This script must be run from the command line\n";
exit();
}
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
define('STATUSNET', true);
require_once INSTALLDIR . '/lib/common.php';
class oEmbedTest extends PHPUnit_Framework_TestCase
{
public function setup()
{
$this->old_oohembed = common_config('oohembed', 'endpoint');
}
public function tearDown()
{
$GLOBALS['config']['oohembed']['endpoint'] = $this->old_oohembed;
}
/**
* 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);
$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.');
} else {
throw $e;
}
}
}
/**
* Sample oEmbed targets for sites we know ourselves...
* @return array
*/
static public function knownSources()
{
$sources = array(
array('http://www.flickr.com/photos/brionv/5172500179/', 'photo'),
array('http://yfrog.com/fy42747177j', 'photo'),
array('http://twitpic.com/36adw6', 'photo'),
);
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);
}
}

View File

@ -1325,15 +1325,6 @@ margin-left:4px;
.notice .attachment.more { .notice .attachment.more {
padding-left:0; padding-left:0;
} }
.notice .attachment img {
position:absolute;
top:18px;
left:0;
z-index:99;
}
#shownotice .notice .attachment img {
position:static;
}
#attachments { #attachments {
clear:both; clear:both;
@ -1716,6 +1707,12 @@ width:auto;
min-width:0; 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*/ }/*end of @media screen, projection, tv*/