Event::handle('EndNoticeSaveWeb', array($this, $notice));
}
+
+ assert($notice instanceof Notice);
+
Event::handle('EndSaveNewNoticeWeb', array($this, $user, &$content_shortened, &$options));
if (StatusNet::isAjax()) {
*
* @return void
*/
- function showNotice($notice)
+ function showNotice(Notice $notice)
{
$nli = new NoticeListItem($notice, $this);
$nli->show();
$id = $this->arg('notice');
$notice = Notice::getKV('id', $id);
+ if ($notice instanceof Notice) {
+ // Alright, got it!
+ return $notice;
+ }
- if (!$notice instanceof Notice) {
- // Did we used to have it, and it got deleted?
- $deleted = Deleted_notice::getKV($id);
- if ($deleted instanceof Deleted_notice) {
- // TRANS: Client error displayed trying to show a deleted notice.
- $this->clientError(_('Notice deleted.'), 410);
- } else {
- // TRANS: Client error displayed trying to show a non-existing notice.
- $this->clientError(_('No such notice.'), 404);
- }
- return false;
+ // Did we use to have it, and it got deleted?
+ $deleted = Deleted_notice::getKV('id', $id);
+ if ($deleted instanceof Deleted_notice) {
+ // TRANS: Client error displayed trying to show a deleted notice.
+ $this->clientError(_('Notice deleted.'), 410);
}
- return $notice;
+ // TRANS: Client error displayed trying to show a non-existing notice.
+ $this->clientError(_('No such notice.'), 404);
}
/**
}
Event::handle('EndFileSaveNew', array($file, $redir_data, $given_url));
+ assert ($file instanceof File);
return $file;
}
*
* @return mixed File on success, -1 on some errors
*
- * @throws ServerException on some errors
+ * @throws ServerException on failure
*/
- public function processNew($given_url, $notice_id=null, $followRedirects=true) {
- if (empty($given_url)) return -1; // error, no url to process
+ public static function processNew($given_url, $notice_id=null, $followRedirects=true) {
+ if (empty($given_url)) {
+ throw new ServerException('No given URL to process');
+ }
+
$given_url = File_redirection::_canonUrl($given_url);
- if (empty($given_url)) return -1; // error, no url to process
+ if (empty($given_url)) {
+ throw new ServerException('No canonical URL from given URL to process');
+ }
+
$file = File::getKV('url', $given_url);
- if (empty($file)) {
+ if (!$file instanceof File) {
+ // First check if we have a lookup trace for this URL already
$file_redir = File_redirection::getKV('url', $given_url);
- if (empty($file_redir)) {
+ if ($file_redir instanceof File_redirection) {
+ $file = File::getKV('id', $file_redir->file_id);
+ if (!$file instanceof File) {
+ // File did not exist, let's clean up the File_redirection entry
+ $file_redir->delete();
+ }
+ }
+
+ // If we still don't have a File object, let's create one now!
+ if (!$file instanceof File) {
// @fixme for new URLs this also looks up non-redirect data
// such as target content type, size, etc, which we need
// for File::saveNew(); so we call it even if not following
// TRANS: Server exception thrown when a URL cannot be processed.
throw new ServerException(sprintf(_("Cannot process URL '%s'"), $given_url));
}
+
// TODO: max field length
if ($redir_url === $given_url || strlen($redir_url) > 255 || !$followRedirects) {
- $x = File::saveNew($redir_data, $given_url);
- $file_id = $x->id;
+ // Save the File object based on our lookup trace
+ $file = File::saveNew($redir_data, $given_url);
} else {
// This seems kind of messed up... for now skipping this part
// if we're already under a redirect, so we don't go into
//
// Seen in the wild with clojure.org, which redirects through
// wikispaces for auth and appends session data in the URL params.
- $x = File::processNew($redir_url, $notice_id, /*followRedirects*/false);
- $file_id = $x->id;
- File_redirection::saveNew($redir_data, $file_id, $given_url);
+ $file = self::processNew($redir_url, $notice_id, /*followRedirects*/false);
+ File_redirection::saveNew($redir_data, $file->id, $given_url);
}
- } else {
- $file_id = $file_redir->file_id;
}
- } else {
- $file_id = $file->id;
- $x = $file;
- }
- if (empty($x)) {
- $x = File::getKV('id', $file_id);
- if (empty($x)) {
- // @todo FIXME: This could possibly be a clearer message :)
- // TRANS: Server exception thrown when... Robin thinks something is impossible!
- throw new ServerException(_('Robin thinks something is impossible.'));
+ if (!$file instanceof File) {
+ // This should only happen if File::saveNew somehow did not return a File object,
+ // though we have an assert for that in case the event there might've gone wrong.
+ // If anything else goes wrong, there should've been an exception thrown.
+ throw new ServerException('URL processing failed without new File object');
}
}
if (!empty($notice_id)) {
- File_to_post::processNew($file_id, $notice_id);
+ File_to_post::processNew($file->id, $notice_id);
}
- return $x;
+ return $file;
}
public static function respectsQuota(Profile $scoped, $fileSize) {
Event::handle('FileEnclosureMetadata', array($this, &$enclosure));
}
}
+ if (empty($enclosure->mimetype)) {
+ // This means we don't know what it is, so it can't be an enclosure!
+ throw new ServerException('Unknown enclosure mimetype, not enough metadata');
+ }
return $enclosure;
}
- // quick back-compat hack, since there's still code using this
- function isEnclosure()
- {
- $enclosure = $this->getEnclosure();
- return !empty($enclosure);
- }
-
/**
* Get the attachment's thumbnail record, if any.
* Make sure you supply proper 'int' typed variables (or null).
* @param string $cls Class to fetch
* @param string $keyCol name of column for key
* @param array $keyVals key values to fetch
- * @param boolean $skipNulls return only non-null results?
*
* @return array Array of objects, in order
*/
- static function multiGetClass($cls, $keyCol, array $keyVals, $skipNulls=true)
+ static function multiGetClass($cls, $keyCol, array $keyVals)
{
- $result = self::pivotGetClass($cls, $keyCol, $keyVals);
+ $obj = new $cls;
- $values = array_values($result);
+ // php-compatible, for settype(), datatype
+ $colType = $obj->columnType($keyCol);
- if ($skipNulls) {
- $tmp = array();
- foreach ($values as $value) {
- if (!empty($value)) {
- $tmp[] = $value;
- }
- }
- $values = $tmp;
+ if (!in_array($colType, array('integer', 'int'))) {
+ // This is because I'm afraid to escape strings incorrectly
+ // in the way we use them below in FIND_IN_SET for MariaDB
+ throw new ServerException('Cannot do multiGet on anything but integer columns');
+ }
+
+ $obj->whereAddIn($keyCol, $keyVals, $colType);
+
+ // Since we're inputting straight to a query: format and escape
+ foreach ($keyVals as $key=>$val) {
+ settype($val, $colType);
+ $keyVals[$key] = $obj->escape($val);
}
- return new ArrayWrapper($values);
+ // FIND_IN_SET will make sure we keep the desired order
+ $obj->orderBy(sprintf("FIND_IN_SET(%s, '%s')", $keyCol, implode(',', $keyVals)));
+ $obj->find();
+
+ return $obj;
}
/**
const GROUP_SCOPE = 4;
const FOLLOWER_SCOPE = 8;
- protected $_profile = -1;
+ protected $_profile = array();
+ /**
+ * Will always return a profile, if anything fails it will
+ * (through _setProfile) throw a NoProfileException.
+ */
public function getProfile()
{
- if ($this->_profile === -1) {
+ if (!isset($this->_profile[$this->profile_id])) {
$this->_setProfile(Profile::getKV('id', $this->profile_id));
}
- return $this->_profile;
+ return $this->_profile[$this->profile_id];
}
public function _setProfile(Profile $profile=null)
if (!$profile instanceof Profile) {
throw new NoProfileException($this->profile_id);
}
- $this->_profile = $profile;
+ $this->_profile[$this->profile_id] = $profile;
}
function delete($useWhere=false)
throw new ClientException(_('You already repeated that notice.'));
}
- $notice->repeat_of = $repeat_of;
+ $notice->repeat_of = $repeat->id;
+ $notice->conversation = $repeat->conversation;
} else {
- $reply = self::getReplyTo($reply_to, $profile_id, $source, $final);
+ $reply = null;
- if (!empty($reply)) {
+ // If $reply_to is specified, we check that it exists, and then
+ // return it if it does
+ if (!empty($reply_to)) {
+ $reply = Notice::getKV('id', $reply_to);
+ } elseif (in_array($source, array('xmpp', 'mail', 'sms'))) {
+ // If the source lacks capability of sending the "reply_to"
+ // metadata, let's try to find an inline replyto-reference.
+ $reply = self::getInlineReplyTo($profile, $final);
+ }
+ if ($reply instanceof Notice) {
if (!$reply->inScope($profile)) {
// TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
// TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
$profile->nickname, $reply->id), 403);
}
- $notice->reply_to = $reply->id;
+ // If it's a repeat, the reply_to should be to the original
+ if (!empty($reply->repeat_of)) {
+ $notice->reply_to = $reply->repeat_of;
+ } else {
+ $notice->reply_to = $reply->id;
+ }
+ // But the conversation ought to be the same :)
$notice->conversation = $reply->conversation;
- // If the original is private to a group, and notice has no group specified,
- // make it to the same group(s)
+ // If the original is private to a group, and notice has
+ // no group specified, make it to the same group(s)
if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) {
$groups = array();
if (common_config('attachments', 'process_links')) {
// @fixme validation?
foreach (array_unique($urls) as $url) {
- File::processNew($url, $this->id);
+ try {
+ File::processNew($url, $this->id);
+ } catch (ServerException $e) {
+ // Could not save URL. Log it?
+ }
}
}
}
* @private callback
*/
function saveUrl($url, $notice_id) {
- File::processNew($url, $notice_id);
+ try {
+ File::processNew($url, $notice_id);
+ } catch (ServerException $e) {
+ // Could not save URL. Log it?
+ }
}
static function checkDupes($profile_id, $content) {
return true;
}
- protected $_attachments = -1;
+ protected $_attachments = array();
function attachments() {
-
- if ($this->_attachments != -1) {
- return $this->_attachments;
+ if (isset($this->_attachments[$this->id])) {
+ return $this->_attachments[$this->id];
}
$f2ps = File_to_post::listGet('post_id', array($this->id));
$files = File::multiGet('id', $ids);
- $this->_attachments = $files->fetchAll();
+ $this->_attachments[$this->id] = $files->fetchAll();
- return $this->_attachments;
+ return $this->_attachments[$this->id];
}
function _setAttachments($attachments)
{
- $this->_attachments = $attachments;
+ $this->_attachments[$this->id] = $attachments;
}
function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
return array();
}
- $sender = Profile::getKV($this->profile_id);
+ $sender = $this->getProfile();
$replied = array();
// If it's a reply, save for the replied-to author
try {
$parent = $this->getParent();
- $author = $parent->getProfile();
- if ($author instanceof Profile) {
- $this->saveReply($author->id);
- $replied[$author->id] = 1;
- self::blow('reply:stream:%d', $author->id);
- }
+ $parentauthor = $parent->getProfile();
+ $this->saveReply($parentauthor->id);
+ $replied[$parentauthor->id] = 1;
+ self::blow('reply:stream:%d', $parentauthor->id);
} catch (Exception $e) {
// Not a reply, since it has no parent!
}
return $reply;
}
- protected $_replies = -1;
+ protected $_replies = array();
/**
* Pull the complete list of @-reply targets for this notice.
*/
function getReplies()
{
- if ($this->_replies != -1) {
- return $this->_replies;
+ if (isset($this->_replies[$this->id])) {
+ return $this->_replies[$this->id];
}
$replyMap = Reply::listGet('notice_id', array($this->id));
$ids[] = $reply->profile_id;
}
- $this->_replies = $ids;
+ $this->_replies[$this->id] = $ids;
return $ids;
}
function _setReplies($replies)
{
- $this->_replies = $replies;
+ $this->_replies[$this->id] = $replies;
}
/**
* @return array of Group objects
*/
- protected $_groups = -1;
+ protected $_groups = array();
function getGroups()
{
return array();
}
- if ($this->_groups != -1)
- {
- return $this->_groups;
+ if (isset($this->_groups[$this->id])) {
+ return $this->_groups[$this->id];
}
$gis = Group_inbox::listGet('notice_id', array($this->id));
$groups = User_group::multiGet('id', $ids);
- $this->_groups = $groups->fetchAll();
+ $this->_groups[$this->id] = $groups->fetchAll();
- return $this->_groups;
+ return $this->_groups[$this->id];
}
function _setGroups($groups)
{
- $this->_groups = $groups;
+ $this->_groups[$this->id] = $groups;
}
/**
* Determine which notice, if any, a new notice is in reply to.
*
* For conversation tracking, we try to see where this notice fits
- * in the tree. Rough algorithm is:
- *
- * if (reply_to is set and valid) {
- * return reply_to;
- * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) {
- * return ID of last notice by initial @name in content;
- * }
+ * in the tree. Beware that this may very well give false positives
+ * and add replies to wrong threads (if there have been newer posts
+ * by the same user as we're replying to).
*
- * Note that all @nickname instances will still be used to save "reply" records,
- * so the notice shows up in the mentioned users' "replies" tab.
- *
- * @param integer $reply_to ID passed in by Web or API
- * @param integer $profile_id ID of author
- * @param string $source Source tag, like 'web' or 'gwibber'
+ * @param Profile $sender Author profile
* @param string $content Final notice content
*
* @return integer ID of replied-to notice, or null for not a reply.
*/
- static function getReplyTo($reply_to, $profile_id, $source, $content)
+ static function getInlineReplyTo(Profile $sender, $content)
{
- static $lb = array('xmpp', 'mail', 'sms', 'omb');
-
- // If $reply_to is specified, we check that it exists, and then
- // return it if it does
-
- if (!empty($reply_to)) {
- $reply_notice = Notice::getKV('id', $reply_to);
- if ($reply_notice instanceof Notice) {
- return $reply_notice;
- }
- }
-
- // If it's not a "low bandwidth" source (one where you can't set
- // a reply_to argument), we return. This is mostly web and API
- // clients.
-
- if (!in_array($source, $lb)) {
- return null;
- }
-
// Is there an initial @ or T?
-
- if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) ||
- preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) {
+ if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match)
+ || preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) {
$nickname = common_canonical_nickname($match[1]);
} else {
return null;
}
// Figure out who that is.
-
- $sender = Profile::getKV('id', $profile_id);
- if (!$sender instanceof Profile) {
- return null;
- }
-
$recipient = common_relative_profile($sender, $nickname, common_sql_now());
- if (!$recipient instanceof Profile) {
- return null;
- }
-
- // Get their last notice
-
- $last = $recipient->getCurrentNotice();
-
- if ($last instanceof Notice) {
- return $last;
+ if ($recipient instanceof Profile) {
+ // Get their last notice
+ $last = $recipient->getCurrentNotice();
+ if ($last instanceof Notice) {
+ return $last;
+ }
+ // Maybe in the future we want to handle something else below
+ // so don't return getCurrentNotice() immediately.
}
return null;
}
}
- protected $_faves;
+ protected $_faves = array();
/**
* All faves of this notice
function getFaves()
{
- if (isset($this->_faves) && is_array($this->_faves)) {
- return $this->_faves;
+ if (isset($this->_faves[$this->id])) {
+ return $this->_faves[$this->id];
}
$faveMap = Fave::listGet('notice_id', array($this->id));
- $this->_faves = $faveMap[$this->id];
- return $this->_faves;
+ $this->_faves[$this->id] = $faveMap[$this->id];
+ return $this->_faves[$this->id];
}
function _setFaves($faves)
{
- $this->_faves = $faves;
+ $this->_faves[$this->id] = $faves;
}
static function fillFaves(&$notices)
}
}
- protected $_repeats;
+ protected $_repeats = array();
function getRepeats()
{
- if (isset($this->_repeats) && is_array($this->_repeats)) {
- return $this->_repeats;
+ if (isset($this->_repeats[$this->id])) {
+ return $this->_repeats[$this->id];
}
$repeatMap = Notice::listGet('repeat_of', array($this->id));
- $this->_repeats = $repeatMap[$this->id];
- return $this->_repeats;
+ $this->_repeats[$this->id] = $repeatMap[$this->id];
+ return $this->_repeats[$this->id];
}
function _setRepeats($repeats)
{
- $this->_repeats = $repeats;
+ $this->_repeats[$this->id] = $repeats;
}
static function fillRepeats(&$notices)
return null;
}
- foreach (array(AVATAR_PROFILE_SIZE, AVATAR_STREAM_SIZE, AVATAR_MINI_SIZE) as $size) {
- // We don't do a scaled one if original is our scaled size
- if (!($avatar->width == $size && $avatar->height == $size)) {
- try {
- Avatar::newSize($this, $size);
- } catch (Exception $e) {
- // should we abort the generation and live without smaller avatars?
- }
- }
- }
-
return $avatar;
}
return $notice->_items[0];
}
return $notice;
- } else {
- return null;
}
+
+ return null;
}
function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
exit(1);
}
+require_once(INSTALLDIR.'/lib/activitystreamjsondocument.php');
+
/**
* A noun-ish thing in the activity universe
*
$twitter_status['attachments'] = array();
foreach ($attachments as $attachment) {
- $enclosure_o=$attachment->getEnclosure();
- if ($enclosure_o) {
+ try {
+ $enclosure_o = $attachment->getEnclosure();
$enclosure = array();
$enclosure['url'] = $enclosure_o->url;
$enclosure['mimetype'] = $enclosure_o->mimetype;
$enclosure['size'] = $enclosure_o->size;
$twitter_status['attachments'][] = $enclosure;
+ } catch (ServerException $e) {
+ // There was not enough metadata available
}
}
}
$enclosures = array();
foreach ($attachments as $attachment) {
- $enclosure_o=$attachment->getEnclosure();
- if ($enclosure_o) {
+ try {
+ $enclosure_o = $attachment->getEnclosure();
$enclosure = array();
$enclosure['url'] = $enclosure_o->url;
$enclosure['mimetype'] = $enclosure_o->mimetype;
$enclosure['size'] = $enclosure_o->size;
$enclosures[] = $enclosure;
+ } catch (ServerException $e) {
+ // There was not enough metadata available
}
}
*
* @param Notice $notice stream of notices from DB_DataObject
*/
- function __construct($notice, $out=null)
+ function __construct(Notice $notice, $out=null)
{
parent::__construct($out);
$this->notice = $notice;
*
* @return NoticeListItem a list item for displaying the notice
*/
- function newListItem($notice)
+ function newListItem(Notice $notice)
{
return new NoticeListItem($notice, $this->out);
}
*
* @param Notice $notice The notice we'll display
*/
- function __construct($notice, $out=null)
+ function __construct(Notice $notice, HTMLOutputter $out=null)
{
parent::__construct($out);
if (!empty($notice->repeat_of)) {
$original = Notice::getKV('id', $notice->repeat_of);
- if (empty($original)) { // could have been deleted
+ if (!$original instanceof Notice) { // could have been deleted
$this->notice = $notice;
} else {
$this->notice = $original;
return null;
}
- function showNotice($notice)
+ function showNotice(Notice $notice)
{
$profile = $notice->getProfile();
if (empty($profile)) {
$attachments = $notice->attachments();
if($attachments){
foreach($attachments as $attachment){
- $enclosure=$attachment->getEnclosure();
- if ($enclosure) {
+ try {
+ $enclosure = $attachment->getEnclosure();
$attribs = array('rdf:resource' => $enclosure->url);
if ($enclosure->title) {
$attribs['dc:title'] = $enclosure->title;
$attribs['enc:type'] = $enclosure->mimetype;
}
$this->element('enc:enclosure', $attribs);
+ } catch (ServerException $e) {
+ // There was not enough metadata available
}
$this->element('sioc:links_to', array('rdf:resource'=>$attachment->url));
}
{
protected $userProfile;
- function __construct($notice, $out=null, $profile=-1)
+ function __construct(Notice $notice, HTMLOutputter $out=null, $profile=-1)
{
parent::__construct($notice, $out);
if (is_int($profile) && $profile == -1) {
if ($notice->repeat_of) {
$orig = Notice::getKV('id', $notice->repeat_of);
- if ($orig) {
+ if ($orig instanceof Notice) {
$notice = $orig;
}
}
// Get the convo's root notice
$root = $notice->conversationRoot($this->userProfile);
- if ($root) {
+ if ($root instanceof Notice) {
$notice = $root;
}
*
* @return NoticeListItem a list item for displaying the notice
*/
- function newListItem($notice)
+ function newListItem(Notice $notice)
{
return new ThreadedNoticeListItem($notice, $this->out, $this->userProfile);
}
{
protected $userProfile = null;
- function __construct($notice, $out=null, $profile=null)
+ function __construct(Notice $notice, HTMLOutputter $out=null, $profile=null)
{
parent::__construct($notice, $out);
$this->userProfile = $profile;
$moreCutoff = null;
while ($notice->fetch()) {
if (Event::handle('StartAddNoticeReply', array($this, $this->notice, $notice))) {
+ // Don't list repeats as separate notices in a conversation
+ if (!empty($notice->repeat_of)) {
+ continue;
+ }
+
if ($notice->id == $this->notice->id) {
// Skip!
continue;
{
protected $root = null;
- function __construct($notice, $root, $out)
+ function __construct(Notice $notice, $root, $out)
{
$this->root = $root;
parent::__construct($notice, $out);
{
$item = new ThreadedNoticeListInlineFavesItem($this->notice, $this->out);
$hasFaves = $item->show();
+ $item = new ThreadedNoticeListInlineRepeatsItem($this->notice, $this->out);
+ $hasRepeats = $item->show();
parent::showEnd();
}
}
{
protected $cnt;
- function __construct($notice, $out, $cnt)
+ function __construct(Notice $notice, HTMLOutputter $out, $cnt)
{
parent::__construct($notice, $out);
$this->cnt = $cnt;
}
/**
- * Placeholder for showing faves...
+ * Placeholder for showing repeats...
*/
class ThreadedNoticeListRepeatsItem extends NoticeListActorsItem
{
$this->out->elementEnd('li');
}
}
+
+// @todo FIXME: needs documentation.
+class ThreadedNoticeListInlineRepeatsItem extends ThreadedNoticeListRepeatsItem
+{
+ function showStart()
+ {
+ $this->out->elementStart('div', array('class' => 'entry-content notice-repeats'));
+ }
+
+ function showEnd()
+ {
+ $this->out->elementEnd('div');
+ }
+}
$f = File::getKV('url', $longurl);
- if (empty($f)) {
+ if (!$f instanceof File) {
if (common_config('attachments', 'process_links')) {
// XXX: this writes to the database. :<
- $f = File::processNew($longurl);
+ try {
+ $f = File::processNew($longurl);
+ } catch (ServerException $e) {
+ $f = null;
+ }
}
}
- if (!empty($f)) {
- if ($f->getEnclosure()) {
+ if ($f instanceof File) {
+ try {
+ $enclosure = $f->getEnclosure();
$is_attachment = true;
$attachment_id = $f->id;
$thumb = File_thumbnail::getKV('file_id', $f->id);
- if (!empty($thumb)) {
- $has_thumb = true;
- }
+ $has_thumb = ($thumb instanceof File_thumbnail);
+ } catch (ServerException $e) {
+ // There was not enough metadata available
}
}
if (Event::handle('StartShortenUrl',
array($long_url, $shortenerName, &$shortenedUrl))) {
if ($shortenerName == 'internal') {
- $f = File::processNew($long_url);
- if (empty($f)) {
- return $long_url;
- } else {
- $shortenedUrl = common_local_url('redirecturl',
- array('id' => $f->id));
+ try {
+ $f = File::processNew($long_url);
+ $shortenedUrl = common_local_url('redirecturl', array('id' => $f->id));
if ((mb_strlen($shortenedUrl) < mb_strlen($long_url)) || $force) {
return $shortenedUrl;
} else {
return $long_url;
}
+ } catch (ServerException $e) {
+ return $long_url;
}
} else {
return $long_url;
* @param HTMLOutputter $out output helper, defaults to null
*/
- function __construct($out=null)
+ function __construct(HTMLOutputter $out=null)
{
$this->out = $out;
}
*/
function onEndShowStyles($action)
{
- $action->cssLink($this->path('bookmark.css'));
+ $action->cssLink($this->path('css/bookmark.css'));
return true;
}
throw new ClientException(_('Invalid URL.'), 400);
}
- $f = File::getKV('url', $this->url);
-
- if (empty($url)) {
- $f = File::processNew($this->url);
+ try {
+ // processNew will first try to fetch a locally stored File entry
+ $f = File::processNew($this->url);
+ } catch (ServerException $e) {
+ $f = null;
}
// How about now?
- if (!empty($f)) {
+ if ($f instanceof File) {
+ // FIXME: Use some File metadata Event instead
$this->oembed = File_oembed::getKV('file_id', $f->id);
- if (!empty($this->oembed)) {
+ if ($this->oembed instanceof File_oembed) {
$this->title = $this->oembed->title;
}
$this->thumbnail = File_thumbnail::getKV('file_id', $f->id);
*
* @return void
*/
- function showNotice($notice)
+ function showNotice(Notice $notice)
{
class_exists('NoticeList'); // @fixme hack for autoloader
$nli = new NoticeListItem($notice, $this);
+++ /dev/null
-.bookmark-tags li { display: inline; }
--- /dev/null
+/* Bookmark specific styles */
+
+.bookmark-tags li { display: inline; }
+
+.bookmark h3 {
+ margin: 0px 0px 8px 0px;
+ line-height: 3em;
+}
+
+.bookmark-notice-count {
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ padding: 1px 6px;
+ font-size: 1.2em;
+ line-height: 1.2em;
+ background: #fff;
+ border: 1px solid #7b8dbb;
+ color: #3e3e8c !important;
+ position: relative;
+ right: 4px;
+ margin-left: 10px;
+}
+
+.bookmark-notice-count:hover {
+ text-decoration: none;
+ background: #f2f2f2;
+ border: 1px solid #7b8dbb;
+ text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.5);
+}
+
+.notice .bookmark-description {
+ clear: both;
+ margin-left: 0px;
+ margin-bottom: 0px;
+}
+
+.notice .bookmark-author {
+ margin-left: 0px;
+ float: left;
+}
+
+.bookmark-tags {
+ clear: both;
+ margin-bottom: 8px;
+ line-height: 1.6em;
+}
+
+ul.bookmark-tags a {
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ padding: 1px 6px;
+ background: #f2f2f2;
+ color: #3e3e8c !important;
+ text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.5);
+ font-size: 0.88em;
+}
+
+ul.bookmark-tags a:hover {
+ background-color: #cdd1dd;
+ text-decoration: none;
+}
+
+.bookmark-avatar {
+ float: none !important;
+ position: relative;
+ top: 2px;
+}
+
+.bookmark div.entry-content {
+ font-size: 0.88em;
+ line-height: 1.2em;
+ margin-top: 6px;
+ opacity: 0.6;
+ margin-bottom: 0px;
+}
+
+.bookmark:hover div.entry-content {
+ opacity: 1;
+}
+
+#bookmarkpopup {
+ min-width: 600px;
+ margin-top: 0px;
+ height: 100%;
+ border: 10px solid #364A84;
+ background: #364A84;
+}
+
+#bookmarkpopup #wrap {
+ width: auto;
+ min-width: 560px;
+ padding: 40px 0px 25px 0px;
+ margin-right: 2px;
+ background: #fff url(../mobilelogo.png) no-repeat 6px 6px;
+}
+
+#bookmarkpopup #header {
+ width: auto;
+ padding: 0px 10px;
+}
+
+#bookmarkpopup .form_settings label {
+ margin-top: 2px;
+ text-align: right;
+ width: 24%;
+ font-size: 1.2em;
+}
+
+#bookmarkpopup .form_settings .form_data input {
+ width: 60%;
+}
+
+#bookmarkpopup .form_guide {
+ color: #777;
+}
+
+#bookmarkpopup #bookmark-submit {
+ min-width: 100px;
+}
+
+#bookmarkpopup fieldset fieldset {
+ margin-bottom: 10px;
+}
+
+#form_initial_bookmark.form_settings .form_data li {
+ margin-bottom: 0px;
+}
+
+#form_new_bookmark.form_settings .bookmarkform-thumbnail {
+ position: absolute;
+ top: 50px;
+ right: 0px;
+}
function onEndShowStyles($action)
{
- $action->cssLink($this->path('event.css'));
+ $action->cssLink($this->path('css/event.css'));
return true;
}
*
* @return void
*/
- function showNotice($notice)
+ function showNotice(Notice $notice)
{
$nli = new NoticeListItem($notice, $this);
$nli->show();
--- /dev/null
+/* Event specific styles */
+
+.event-tags li { display: inline; }
+.event-mentions li { display: inline; }
+.event-avatar { float: left; }
+.event-notice-count { float: right; }
+.event-info { float: left; }
+.event-title { margin-left: 0px; }
+#content .event .entry-title { margin-left: 0px; }
+#content .event .entry-content { margin-left: 0px; }
+.ui-autocomplete {
+ max-height: 100px;
+ overflow-y: auto;
+ /* prevent horizontal scrollbar */
+ overflow-x: hidden;
+ /* add padding to account for vertical scrollbar */
+ padding-right: 20px;
+}
+
+.attending-list { list-style-type: none; float: left; width: 100%; }
+
+#form_event_rsvp { clear: left; }
+
+li.rsvp-list { float: left; clear: left; }
+
+li.rsvp-list ul.entities {
+ display:inline;
+}
+li.rsvp-list .entities li {
+ list-style-type: none;
+ margin-right: 3px;
+ margin-bottom: 8px;
+ display: inline;
+}
+li.rsvp-list .entities li .photo {
+ margin: 0 !important;
+ float: none !important;
+}
+li.rsvp-list .entities li .fn {
+ display: none;
+}
+
+.notice .vevent div {
+ margin-bottom: 8px;
+}
+
+.event-info {
+ margin-left: 0px !important;
+ margin-top: 2px !important;
+}
+
+.notice .event-info + .notice-options {
+ margin-top: 14px;
+}
+
+.notice .threaded-replies .event-info + .notice-options {
+ margin-top: 20px;
+}
+
+#form_event_rsvp #new_rsvp_data {
+ display: inline;
+ margin: 10px 0px;
+}
+
+#form_event_rsvp input.submit {
+ height: auto;
+ padding: 0px 10px;
+ margin-left: 10px;
+ color:#fff;
+ font-weight: bold;
+ text-transform: uppercase;
+ font-size: 1.1em;
+ text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.2);
+ border: 1px solid #d7621c;
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ background: #FB6104;
+ background: -moz-linear-gradient(top, #ff9d63 0%, #fb6104 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff9d63), color-stop(100%,#fb6104));
+ background: -webkit-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+ background: -o-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+ background: -ms-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff9d63', endColorstr='#fb6104',GradientType=0 );
+ background: linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+}
+
+#form_event_rsvp input.submit:hover {
+ text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.6);
+ background: #ff9d63;
+ background: -moz-linear-gradient(top, #fb6104 0%, #fc8035 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fb6104), color-stop(100%,#fc8035));
+ background: -webkit-linear-gradient(top, #fb6104 0%,#fc8035 100%);
+ background: -o-linear-gradient(top, #fb6104 0%,#fc8035 100%);
+ background: -ms-linear-gradient(top, #fb6104 0%,#fc8035 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fb6104', endColorstr='#fc8035',GradientType=0 );
+ background: linear-gradient(top, #fb6104 0%,#fc8035 100%);
+}
+
+#wrap .vevent form.processing input.submit {
+ text-indent: 0;
+ background: #ff9d63;
+}
+
+#input_form_event .form_settings .form_data {
+ float: left;
+}
+
+#input_form_event .form_settings .form_data li {
+ float: left;
+ width: auto;
+}
+
+#input_form_event .form_settings .form_data label {
+ width: auto;
+}
+
+label[for=event-starttime], label[for=event-endtime] {
+ display: none !important;
+}
+
+#event-starttime, #event-endtime {
+ margin-top: -1px;
+ margin-bottom: -1px;
+ height: 2em;
+}
+
+#event-startdate, #event-enddate {
+ margin-right: 20px;
+ width: 120px;
+}
+++ /dev/null
-.event-tags li { display: inline; }
-.event-mentions li { display: inline; }
-.event-avatar { float: left; }
-.event-notice-count { float: right; }
-.event-info { float: left; }
-.event-title { margin-left: 0px; }
-#content .event .entry-title { margin-left: 0px; }
-#content .event .entry-content { margin-left: 0px; }
-.ui-autocomplete {
- max-height: 100px;
- overflow-y: auto;
- /* prevent horizontal scrollbar */
- overflow-x: hidden;
- /* add padding to account for vertical scrollbar */
- padding-right: 20px;
-}
-
-.attending-list { list-style-type: none; float: left; width: 100%; }
-
-#form_event_rsvp { clear: left; }
-
-li.rsvp-list { float: left; clear: left; }
-
-li.rsvp-list ul.entities {
- display:inline;
-}
-li.rsvp-list .entities li {
- list-style-type: none;
- margin-right: 3px;
- margin-bottom: 8px;
- display: inline;
-}
-li.rsvp-list .entities li .photo {
- margin: 0 !important;
- float: none !important;
-}
-li.rsvp-list .entities li .fn {
- display: none;
-}
foreach($attachments as $attachment)
{
- if($enclosure = $attachment->getEnclosure()){
+ try {
+ $enclosure = $attachment->getEnclosure();
$fbmedia = $this->getFacebookMedia($enclosure);
- }else{
+ } catch (ServerException $e) {
$fbmedia = $this->getFacebookMedia($attachment);
}
if($fbmedia){
$xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url);
$xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url);
- // Get this user's keypair
- $magickey = Magicsig::getKV('user_id', $target->id);
- if (!($magickey instanceof Magicsig)) {
- // No keypair yet, let's generate one.
- $magickey = new Magicsig();
- $magickey->generate($target->id);
+ // Get this profile's keypair
+ $magicsig = Magicsig::getKV('user_id', $target->id);
+ if (!$magicsig instanceof Magicsig && $target->isLocal()) {
+ $magicsig = Magicsig::generate($target->getUser());
}
- $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL,
- 'data:application/magic-public-key,'. $magickey->toString(false));
+ if ($magicsig instanceof Magicsig) {
+ $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL,
+ 'data:application/magic-public-key,'. $magicsig->toString());
+ }
// TODO - finalize where the redirect should go on the publisher
$xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe',
$oprofile = $this->ensureProfile();
if ($oprofile instanceof Ostatus_profile) {
common_log(LOG_INFO, sprintf('Setting up subscription from remote %s to local %s', $oprofile->getUri(), $this->target->getNickname()));
- Subscription::start($oprofile->localProfile(),
- $this->target);
+ Subscription::start($oprofile->localProfile(), $this->target);
} else {
common_log(LOG_INFO, "Can't set up subscription from remote; missing profile.");
}
Subscription::cancel($oprofile->localProfile(), $this->target);
} catch (NoProfileException $e) {
common_debug('Could not find profile for Subscription: '.$e->getMessage());
- } catch (AlreadyFulfilledException $e) {
- common_debug('Subscription did not exist, so there was nothing to cancel');
}
} else {
common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile");
if ($old instanceof Fave) {
// TRANS: Client exception.
- throw new ClientException(_m('This is already a favorite.'));
+ throw new AlreadyFulfilledException(_m('This is already a favorite.'));
}
if (!Fave::addNew($profile, $notice)) {
'notice_id' => $notice->id));
if (!$fave instanceof Fave) {
// TRANS: Client exception.
- throw new ClientException(_m('Notice was not favorited!'));
+ throw new AlreadyFulfilledException(_m('Notice was not favorited!'));
}
$fave->delete();
// legacy very-old-StatusNet generated keypairs.
if (strlen($obj->publicKey->modulus->toBits()) < 1024) {
common_log(LOG_WARNING, sprintf('Salmon key with <1024 bits (%d) belongs to profile with id==%d',
- strlen($this->publicKey->modulus->toBits()),
+ strlen($obj->publicKey->modulus->toBits()),
$obj->user_id));
}
}
*/
function insert()
{
- $this->keypair = $this->toString();
+ $this->keypair = $this->toString(true);
return parent::insert();
}
* Warning: this can be very slow on systems without the GMP module.
* Runtimes of 20-30 seconds are not unheard-of.
*
- * @param int $user_id id of local user we're creating a key for
+ * @param User $user the local user (since we don't have remote private keys)
*/
- public function generate($user_id, $bits=1024)
+ public static function generate(User $user, $bits=1024, $alg='RSA-SHA256')
{
+ $magicsig = new Magicsig($alg);
+ $magicsig->user_id = $user->id;
+
$rsa = new Crypt_RSA();
$keypair = $rsa->createKey($bits);
- $rsa->loadKey($keypair['privatekey']);
+ $magicsig->privateKey = new Crypt_RSA();
+ $magicsig->privateKey->loadKey($keypair['privatekey']);
- $this->privateKey = new Crypt_RSA();
- $this->privateKey->loadKey($keypair['privatekey']);
+ $magicsig->publicKey = new Crypt_RSA();
+ $magicsig->publicKey->loadKey($keypair['publickey']);
- $this->publicKey = new Crypt_RSA();
- $this->publicKey->loadKey($keypair['publickey']);
+ $magicsig->insert(); // will do $this->keypair = $this->toString(true);
+ $magicsig->importKeys(); // seems it's necessary to re-read keys from text keypair
- $this->user_id = $user_id;
- $this->insert();
+ return $magicsig;
}
/**
* Encode the keypair or public key as a string.
*
- * @param boolean $full_pair set to false to leave out the private key.
+ * @param boolean $full_pair set to true to include the private key.
* @return string
*/
- public function toString($full_pair = true)
+ public function toString($full_pair=false)
{
$mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes());
$exp = Magicsig::base64_url_encode($this->publicKey->exponent->toBytes());
$private_exp = '';
- if ($full_pair && $this->privateKey->exponent->toBytes()) {
+ if ($full_pair && $this->privateKey instanceof Crypt_RSA && $this->privateKey->exponent->toBytes()) {
$private_exp = '.' . Magicsig::base64_url_encode($this->privateKey->exponent->toBytes());
}
return 'RSA.' . $mod . '.' . $exp . $private_exp;
}
- /**
- * Decode a string representation of an RSA public key or keypair
- * as a Magicsig object which can be used to sign or verify.
- *
- * @param string $text
- * @return Magicsig
- */
- public static function fromString($text)
- {
- $magic_sig = new Magicsig();
-
- // remove whitespace
- $magic_sig->keypair = preg_replace('/\s+/', '', $text);
- $magic_sig->importKeys();
-
- // Please note this object will be missing the user_id field
- return $magic_sig;
- }
-
/**
* importKeys will load the object's keypair string, which initiates
* loadKey() and configures Crypt_RSA objects.
+ *
+ * @param string $keypair optional, otherwise the object's "keypair" property will be used
*/
- public function importKeys()
+ public function importKeys($keypair=null)
{
+ $this->keypair = $keypair===null ? $this->keypair : preg_replace('/\s+/', '', $keypair);
+
// parse components
- if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(.([^\.]+))?/', $this->keypair, $matches)) {
+ if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(\.([^\.]+))?/', $this->keypair, $matches)) {
common_debug('Magicsig error: RSA key not found in provided string.');
throw new ServerException('RSA key not found in keypair string.');
}
*/
public function loadKey($mod, $exp, $type = 'public')
{
- common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")");
-
$rsa = new Crypt_RSA();
- $rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1;
- $rsa->setHash('sha256');
+ $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
+ $rsa->setHash($this->getHash());
$rsa->modulus = new Math_BigInteger(Magicsig::base64_url_decode($mod), 256);
$rsa->k = strlen($rsa->modulus->toBytes());
$rsa->exponent = new Math_BigInteger(Magicsig::base64_url_decode($exp), 256);
* Returns the name of a hash function to use for signing with this key.
*
* @return string
- * @fixme is this used? doesn't seem to be called by name.
*/
public function getHash()
{
switch ($this->alg) {
-
case 'RSA-SHA256':
return 'sha256';
}
+ throw new ServerException('Unknown or unsupported hash algorithm for Salmon');
}
/**
* using our private key.
*
* @param string $bytes as raw byte string
- * @return string base64-encoded signature
+ * @return string base64url-encoded signature
*/
public function sign($bytes)
{
/**
*
* @param string $signed_bytes as raw byte string
- * @param string $signature as base64
+ * @param string $signature as base64url encoded
* @return boolean
*/
public function verify($signed_bytes, $signature)
{
- $signature = Magicsig::base64_url_decode($signature);
+ $signature = self::base64_url_decode($signature);
return $this->publicKey->verify($signed_bytes, $signature);
}
public function garbageCollect()
{
$feedsub = FeedSub::getKV('uri', $this->feeduri);
- return $feedsub->garbageCollect();
+ if ($feedsub instanceof FeedSub) {
+ return $feedsub->garbageCollect();
+ }
+ // Since there's no FeedSub we can assume it's already garbage collected
+ return true;
}
/**
* @param string $verb Activity::SUBSCRIBE or Activity::JOIN
* @param Object $object object of the action; must define asActivityNoun($tag)
*/
- public function notify($actor, $verb, $object=null, $target=null)
+ public function notify(Profile $actor, $verb, $object=null, $target=null)
{
- if (!($actor instanceof Profile)) {
- $type = gettype($actor);
- if ($type == 'object') {
- $type = get_class($actor);
- }
- // TRANS: Server exception.
- // TRANS: %1$s is the method name the exception occured in, %2$s is the actor type.
- throw new ServerException(sprintf(_m('Invalid actor passed to %1$s: %2$s.'),__METHOD__,$type));
- }
if ($object == null) {
$object = $this;
}
- if ($this->salmonuri) {
- $text = 'update';
- $id = TagURI::mint('%s:%s:%s',
- $verb,
- $actor->getURI(),
- common_date_iso8601(time()));
-
- // @todo FIXME: Consolidate all these NS settings somewhere.
- $attributes = array('xmlns' => Activity::ATOM,
- 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
- 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
- 'xmlns:georss' => 'http://www.georss.org/georss',
- 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0',
- 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0',
- 'xmlns:media' => 'http://purl.org/syndication/atommedia');
-
- $entry = new XMLStringer();
- $entry->elementStart('entry', $attributes);
- $entry->element('id', null, $id);
- $entry->element('title', null, $text);
- $entry->element('summary', null, $text);
- $entry->element('published', null, common_date_w3dtf(common_sql_now()));
-
- $entry->element('activity:verb', null, $verb);
- $entry->raw($actor->asAtomAuthor());
- $entry->raw($actor->asActivityActor());
- $entry->raw($object->asActivityNoun('object'));
- if ($target != null) {
- $entry->raw($target->asActivityNoun('target'));
- }
- $entry->elementEnd('entry');
-
- $xml = $entry->getString();
- common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml");
-
- $salmon = new Salmon(); // ?
- return $salmon->post($this->salmonuri, $xml, $actor);
+ if (empty($this->salmonuri)) {
+ return false;
}
- return false;
+ $text = 'update';
+ $id = TagURI::mint('%s:%s:%s',
+ $verb,
+ $actor->getURI(),
+ common_date_iso8601(time()));
+
+ // @todo FIXME: Consolidate all these NS settings somewhere.
+ $attributes = array('xmlns' => Activity::ATOM,
+ 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+ 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+ 'xmlns:georss' => 'http://www.georss.org/georss',
+ 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0',
+ 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0',
+ 'xmlns:media' => 'http://purl.org/syndication/atommedia');
+
+ $entry = new XMLStringer();
+ $entry->elementStart('entry', $attributes);
+ $entry->element('id', null, $id);
+ $entry->element('title', null, $text);
+ $entry->element('summary', null, $text);
+ $entry->element('published', null, common_date_w3dtf(common_sql_now()));
+
+ $entry->element('activity:verb', null, $verb);
+ $entry->raw($actor->asAtomAuthor());
+ $entry->raw($actor->asActivityActor());
+ $entry->raw($object->asActivityNoun('object'));
+ if ($target != null) {
+ $entry->raw($target->asActivityNoun('target'));
+ }
+ $entry->elementEnd('entry');
+
+ $xml = $entry->getString();
+ common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml");
+
+ Salmon::post($this->salmonuri, $xml, $actor->getUser());
}
/**
public function notifyActivity($entry, Profile $actor)
{
if ($this->salmonuri) {
- $salmon = new Salmon();
- return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry), $actor);
+ return Salmon::post($this->salmonuri, $this->notifyPrepXml($entry), $actor->getUser());
}
common_debug(__CLASS__.' error: No salmonuri for Ostatus_profile uri: '.$this->uri);
*/
public function getKeyPair(Profile $profile, $discovery=false) {
$magicsig = Magicsig::getKV('user_id', $profile->id);
+
if ($discovery && !$magicsig instanceof Magicsig) {
- $signer_uri = $profile->getUri();
- if (empty($signer_uri)) {
- throw new ServerException(sprintf('Profile missing URI (id==%d)', $profile->id));
- }
- $magicsig = $this->discoverKeyPair($signer_uri);
- // discoverKeyPair should've thrown exception if it failed
- assert($magicsig instanceof Magicsig);
+ // Throws exception on failure, but does not try to _load_ the keypair string.
+ $keypair = $this->discoverKeyPair($profile);
+
+ $magicsig = new Magicsig();
+ $magicsig->user_id = $profile->id;
+ $magicsig->importKeys($keypair);
} elseif (!$magicsig instanceof Magicsig) { // No discovery request, so we'll give up.
throw new ServerException(sprintf('No public key found for profile (id==%d)', $profile->id));
}
+
+ assert($magicsig->publicKey instanceof Crypt_RSA);
+
return $magicsig;
}
/**
- * Get the Salmon keypair from a URI, uses XRD Discovery etc.
+ * Get the Salmon keypair from a URI, uses XRD Discovery etc. Reasonably
+ * you'll only get the public key ;)
+ *
+ * The string will (hopefully) be formatted as described in Magicsig specification:
+ * https://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html#anchor13
*
- * @return Magicsig with loaded keypair
+ * @return string formatted as Magicsig keypair
*/
- public function discoverKeyPair($signer_uri)
+ public function discoverKeyPair(Profile $profile)
{
+ $signer_uri = $profile->getUri();
+ if (empty($signer_uri)) {
+ throw new ServerException(sprintf('Profile missing URI (id==%d)', $profile->id));
+ }
+
$disco = new Discovery();
// Throws exception on lookup problems
throw new Exception(_m('Incorrectly formatted public key element.'));
}
- $magicsig = Magicsig::fromString($keypair);
- if (!$magicsig instanceof Magicsig) {
- common_debug('Salmon error: unable to parse keypair: '.var_export($keypair,true));
- // TRANS: Exception when public key was properly formatted but not parsable.
- throw new ServerException(_m('Retrieved Salmon keypair could not be parsed.'));
- }
-
- return $magicsig;
+ return $keypair;
}
/**
* @param <type> $text
* @param <type> $mimetype
* @param Magicsig $magicsig Magicsig with private key available.
+ *
* @return MagicEnvelope object with all properties set
+ *
+ * @throws Exception of various kinds on signing failure
*/
- public static function signMessage($text, $mimetype, Magicsig $magicsig)
+ public function signMessage($text, $mimetype, Magicsig $magicsig)
{
- $magic_env = new MagicEnvelope();
+ assert($magicsig->privateKey instanceof Crypt_RSA);
// Prepare text and metadata for signing
- $magic_env->data = Magicsig::base64_url_encode($text);
- $magic_env->data_type = $mimetype;
- $magic_env->encoding = self::ENCODING;
- $magic_env->alg = $magicsig->getName();
- // Get the actual signature
- $magic_env->sig = $magicsig->sign($magic_env->signingText());
+ $this->data = Magicsig::base64_url_encode($text);
+ $this->data_type = $mimetype;
+ $this->encoding = self::ENCODING;
+ $this->alg = $magicsig->getName();
- return $magic_env;
+ // Get the actual signature
+ $this->sig = $magicsig->sign($this->signingText());
}
/**
$xs->element('me:data', array('type' => $this->data_type), $this->data);
$xs->element('me:encoding', null, $this->encoding);
$xs->element('me:alg', null, $this->alg);
- $xs->element('me:sig', null, $this->sig);
+ $xs->element('me:sig', null, $this->getSignature());
$xs->elementEnd('me:env');
$string = $xs->getString();
- common_debug('MagicEnvelope XML: ' . $string);
return $string;
}
$prov->appendChild($enc);
$alg = $dom->createElementNS(self::NS, 'me:alg', $this->alg);
$prov->appendChild($alg);
- $sig = $dom->createElementNS(self::NS, 'me:sig', $this->sig);
+ $sig = $dom->createElementNS(self::NS, 'me:sig', $this->getSignature());
$prov->appendChild($sig);
$dom->documentElement->appendChild($prov);
return $dom;
}
+ public function getSignature()
+ {
+ return $this->sig;
+ }
+
/**
* Find the author URI referenced in the payload Atom entry.
*
*
* Details of failure conditions are dumped to output log and not exposed to caller.
*
- * @param Profile $profile optional profile used to get locally cached public signature key.
+ * @param Profile $profile profile used to get locally cached public signature key
+ * or if necessary perform discovery on.
*
* @return boolean
*/
- public function verify(Profile $profile=null)
+ public function verify(Profile $profile)
{
if ($this->alg != 'RSA-SHA256') {
common_log(LOG_DEBUG, "Salmon error: bad algorithm");
}
try {
- if ($profile instanceof Profile) {
- $magicsig = $this->getKeyPair($profile, true); // Do discovery too if necessary
- } else {
- $signer_uri = $this->getAuthorUri();
- $magicsig = $this->discoverKeyPair($signer_uri);
- }
+ $magicsig = $this->getKeyPair($profile, true); // Do discovery too if necessary
} catch (Exception $e) {
common_log(LOG_DEBUG, "Salmon error: ".$e->getMessage());
return false;
}
- return $magicsig->verify($this->signingText(), $this->sig);
+ return $magicsig->verify($this->signingText(), $this->getSignature());
}
/**
* on some systems.
*
* @param string $text XML fragment to sign, assumed to be Atom
- * @param Profile $actor Profile of a local user to use as signer
+ * @param User $user User who cryptographically signs $text
*
- * @return string XML string representation of magic envelope
+ * @return MagicEnvelope object complete with signature
*
* @throws Exception on bad profile input or key generation problems
- * @fixme if signing fails, this seems to return the original text without warning. Is there a reason for this?
*/
- public static function signForProfile($text, Profile $actor)
+ public static function signAsUser($text, User $user)
{
- // We only generate keys for our local users of course, so let
- // getUser throw an exception if the profile is not local.
- $user = $actor->getUser();
-
// Find already stored key
$magicsig = Magicsig::getKV('user_id', $user->id);
if (!$magicsig instanceof Magicsig) {
- // No keypair yet, let's generate one.
- $magicsig = new Magicsig();
- $magicsig->generate($user->id);
+ $magicsig = Magicsig::generate($user);
}
+ assert($magicsig instanceof Magicsig);
+ assert($magicsig->privateKey instanceof Crypt_RSA);
- $magic_env = self::signMessage($text, 'application/atom+xml', $magicsig);
-
- assert($magic_env instanceof MagicEnvelope);
+ $magic_env = new MagicEnvelope();
+ $magic_env->signMessage($text, 'application/atom+xml', $magicsig);
return $magic_env;
}
*
* @param string $endpoint_uri
* @param string $xml string representation of payload
- * @param Profile $actor local user profile whose keys to sign with
+ * @param User $user local user profile whose keys we sign with
* @return boolean success
*/
- public function post($endpoint_uri, $xml, Profile $actor)
+ public static function post($endpoint_uri, $xml, User $user)
{
if (empty($endpoint_uri)) {
- common_debug('No endpoint URI for Salmon post to '.$actor->getUri());
+ common_debug('No endpoint URI for Salmon post to '.$user->getUri());
return false;
}
try {
- $magic_env = MagicEnvelope::signForProfile($xml, $actor);
+ $magic_env = MagicEnvelope::signAsUser($xml, $user);
$envxml = $magic_env->toXML();
} catch (Exception $e) {
common_log(LOG_ERR, "Salmon unable to sign: " . $e->getMessage());
}
if ($response->getStatus() != 200) {
common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s',
- $actor->id, $endpoint_uri, $response->getStatus(), $response->getBody()));
+ $user->id, $endpoint_uri, $response->getStatus(), $response->getBody()));
return false;
}
{
protected $needPost = true;
- protected $verified = false;
-
var $xml = null;
var $activity = null;
var $target = null;
$this->clientError(_m('Salmon requires "application/magic-envelope+xml".'));
}
- $envxml = file_get_contents('php://input');
- $magic_env = new MagicEnvelope($envxml); // parse incoming XML as a MagicEnvelope
-
- $entry = $magic_env->getPayload(); // Not cryptographically verified yet!
- $this->activity = new Activity($entry->documentElement);
-
try {
+ $envxml = file_get_contents('php://input');
+ $magic_env = new MagicEnvelope($envxml); // parse incoming XML as a MagicEnvelope
+
+ $entry = $magic_env->getPayload(); // Not cryptographically verified yet!
+ $this->activity = new Activity($entry->documentElement);
$profile = Profile::fromUri($this->activity->actor->id);
- $this->verified = $magic_env->verify($profile);
- } catch (UnknownUriException $e) {
- // If we don't know the profile, perform some discovery instead
- $this->verified = $magic_env->verify();
+ assert($profile instanceof Profile);
+ } catch (Exception $e) {
+ common_debug('Salmon envelope parsing failed with: '.$e->getMessage());
+ $this->clientError($e->getMessage());
}
- if (!$this->verified) {
+ // Cryptographic verification test
+ if (!$magic_env->verify($profile)) {
common_log(LOG_DEBUG, "Salmon signature verification failed.");
// TRANS: Client error.
$this->clientError(_m('Salmon signature verification failed.'));
parent::handle();
common_log(LOG_DEBUG, "Got a " . $this->activity->verb);
- if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) &&
- Event::handle('StartHandleSalmon', array($this->activity))) {
- switch ($this->activity->verb)
- {
- case ActivityVerb::POST:
- $this->handlePost();
- break;
- case ActivityVerb::SHARE:
- $this->handleShare();
- break;
- case ActivityVerb::FAVORITE:
- $this->handleFavorite();
- break;
- case ActivityVerb::UNFAVORITE:
- $this->handleUnfavorite();
- break;
- case ActivityVerb::FOLLOW:
- case ActivityVerb::FRIEND:
- $this->handleFollow();
- break;
- case ActivityVerb::UNFOLLOW:
- $this->handleUnfollow();
- break;
- case ActivityVerb::JOIN:
- $this->handleJoin();
- break;
- case ActivityVerb::LEAVE:
- $this->handleLeave();
- break;
- case ActivityVerb::TAG:
- $this->handleTag();
- break;
- case ActivityVerb::UNTAG:
- $this->handleUntag();
- break;
- case ActivityVerb::UPDATE_PROFILE:
- $this->handleUpdateProfile();
- break;
- default:
- // TRANS: Client exception.
- throw new ClientException(_m('Unrecognized activity type.'));
+ try {
+ if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) &&
+ Event::handle('StartHandleSalmon', array($this->activity))) {
+ switch ($this->activity->verb) {
+ case ActivityVerb::POST:
+ $this->handlePost();
+ break;
+ case ActivityVerb::SHARE:
+ $this->handleShare();
+ break;
+ case ActivityVerb::FAVORITE:
+ $this->handleFavorite();
+ break;
+ case ActivityVerb::UNFAVORITE:
+ $this->handleUnfavorite();
+ break;
+ case ActivityVerb::FOLLOW:
+ case ActivityVerb::FRIEND:
+ $this->handleFollow();
+ break;
+ case ActivityVerb::UNFOLLOW:
+ $this->handleUnfollow();
+ break;
+ case ActivityVerb::JOIN:
+ $this->handleJoin();
+ break;
+ case ActivityVerb::LEAVE:
+ $this->handleLeave();
+ break;
+ case ActivityVerb::TAG:
+ $this->handleTag();
+ break;
+ case ActivityVerb::UNTAG:
+ $this->handleUntag();
+ break;
+ case ActivityVerb::UPDATE_PROFILE:
+ $this->handleUpdateProfile();
+ break;
+ default:
+ // TRANS: Client exception.
+ throw new ClientException(_m('Unrecognized activity type.'));
+ }
+ Event::handle('EndHandleSalmon', array($this->activity));
+ Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
}
- Event::handle('EndHandleSalmon', array($this->activity));
- Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
+ } catch (AlreadyFulfilledException $e) {
+ // The action's results are already fulfilled. Maybe it was a
+ // duplicate? Maybe someone's database is out of sync?
+ // Let's just accept it and move on.
+ common_log(LOG_INFO, 'Salmon slap carried an event which had already been fulfilled.');
}
}
$actor = Profile::getKV($data['actor']);
- $salmon = new Salmon();
- $salmon->post($data['salmonuri'], $data['entry'], $actor);
+ Salmon::post($data['salmonuri'], $data['entry'], $actor->getUser());
// @fixme detect failure and attempt to resend
return true;
print $entry;
print "\n\n";
-$magic_env = MagicEnvelope::signForProfile($entry, $profile);
+$magic_env = MagicEnvelope::signAsUser($entry, $profile->getUser());
$envxml = $magic_env->toXML();
echo "== Signed envelope ==\n\n";
echo "== Testing local verification ==\n\n";
$magic_env = new MagicEnvelope($envxml);
-$ok = $magic_env->verify();
+$activity = new Activity($magic_env->getPayload()->documentElement);
+$actprofile = Profile::fromUri($activity->actor->id);
+$ok = $magic_env->verify($actprofile);
if ($ok) {
print "OK\n\n";
} else {
echo "== Remote salmon slap ==\n\n";
print "Sending signed Salmon slap to $url ...\n";
- $ok = $salmon->post($url, $entry, $profile);
+ $ok = Salmon::post($url, $entry, $profile->getUser());
if ($ok) {
print "OK\n\n";
} else {
*/
function onEndShowStyles($action)
{
- $action->cssLink($this->path('poll.css'));
+ $action->cssLink($this->path('css/poll.css'));
return true;
}
* open here, but we probably shouldn't open it here. Check parent class
* and Bookmark plugin for if that's right.
*/
- function showNotice($notice, $out)
+ function showNotice(Notice $notice, $out)
{
switch ($notice->object_type) {
case self::POLL_OBJECT:
}
}
- function showNoticePoll($notice, $out)
+ function showNoticePoll(Notice $notice, $out)
{
$user = common_current_user();
$out->elementStart('div', array('class' => 'entry-content'));
}
- function showNoticePollResponse($notice, $out)
+ function showNoticePollResponse(Notice $notice, $out)
{
$user = common_current_user();
*
* @return void
*/
- function showNotice($notice)
+ function showNotice(Notice $notice)
{
class_exists('NoticeList'); // @fixme hack for autoloader
$nli = new NoticeListItem($notice, $this);
--- /dev/null
+/* Poll specific styles */
+
+.poll-block {
+ float: left;
+ height: 16px;
+ background: #8aa;
+ margin-right: 8px;
+}
+
+.poll-winner {
+ background: #4af;
+}
+
+.notice div.poll-content {
+ opacity: 1;
+}
+
+#poll-response-submit {
+ min-width: 100px;
+}
+++ /dev/null
-.poll-block {
- float: left;
- height: 16px;
- background: #8aa;
- margin-right: 8px;
-}
-
-.poll-winner {
- background: #4af;
-}
* @param Notice $notice
* @param HTMLOutputter $out
*/
- function showNotice($notice, $out)
+ function showNotice(Notice $notice, $out)
{
switch ($notice->object_type) {
case QnA_Question::OBJECT_TYPE:
}
}
- function showNoticeQuestion($notice, $out)
+ function showNoticeQuestion(Notice $notice, $out)
{
$user = common_current_user();
return false;
}
- function showNoticeAnswer($notice, $out)
+ function showNoticeAnswer(Notice $notice, $out)
{
$user = common_current_user();
+/* QnA */
+
.question .answer-count, .qna-full-question .answer-count {
display: block;
clear: left;
display: block;
font-style: italic;
}
+
+.question div.question-description {
+ font-size: 1em;
+ line-height: 1.36em;
+ margin-top: 0px;
+ opacity: 1;
+}
+
+.question div.answer-content, .qna-full-question div.answer-content {
+ font-size: 1em;
+ opacity: 1;
+}
+
+.qna-dummy-placeholder input, .question #qna-answer, .qna-full-question #qna-answer {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
+ -webkit-box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
+}
+
+.question-description input.submit, .answer-content input.submit {
+ height: auto;
+ padding: 0px 10px;
+ margin: 6px 0px 10px 0px;
+ color:#fff;
+ font-weight: bold;
+ text-transform: uppercase;
+ font-size: 1.1em;
+ text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.2);
+ border: 1px solid #d7621c;
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ background: #FB6104;
+ background: -moz-linear-gradient(top, #ff9d63 0%, #fb6104 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff9d63), color-stop(100%,#fb6104));
+ background: -webkit-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+ background: -o-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+ background: -ms-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff9d63', endColorstr='#fb6104',GradientType=0 );
+ background: linear-gradient(top, #ff9d63 0%,#fb6104 100%);
+}
+
+#qna-answer-submit {
+ min-width: 100px;
+}
+
+.question .question-description input.submit:hover, .question .answer-content input.submit:hover {
+ text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.6);
+ background: #ff9d63;
+ background: -moz-linear-gradient(top, #fb6104 0%, #fc8035 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fb6104), color-stop(100%,#fc8035));
+ background: -webkit-linear-gradient(top, #fb6104 0%,#fc8035 100%);
+ background: -o-linear-gradient(top, #fb6104 0%,#fc8035 100%);
+ background: -ms-linear-gradient(top, #fb6104 0%,#fc8035 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fb6104', endColorstr='#fc8035',GradientType=0 );
+ background: linear-gradient(top, #fb6104 0%,#fc8035 100%);
+}
+
+.question .question-description #answer-form input.submit {
+ margin-top: 0px;
+}
+
+.question p.best, .answer p.best {
+ background: url(../images/rosette.png) no-repeat top left;
+ padding-left: 20px;
+}
+
+.question p.best:before, .answer p.best:before {
+ content: none !important;
+}
if (common_config('attachments', 'process_links')) {
if (!empty($status->entities) && !empty($status->entities->urls)) {
foreach ($status->entities->urls as $url) {
- File::processNew($url->url, $notice->id);
+ try {
+ File::processNew($url->url, $notice->id);
+ } catch (ServerException $e) {
+ // Could not process attached URL
+ }
}
}
}
// throws exception if resource is empty
$this->resource = Discovery::normalize($this->trimmed('resource'));
- if (Event::handle('StartGetWebFingerResource', array($this->resource, &$this->target, $this->args))) {
- Event::handle('EndGetWebFingerResource', array($this->resource, &$this->target, $this->args));
+ try {
+ if (Event::handle('StartGetWebFingerResource', array($this->resource, &$this->target, $this->args))) {
+ Event::handle('EndGetWebFingerResource', array($this->resource, &$this->target, $this->args));
+ }
+ } catch (NoSuchUserException $e) {
+ throw new ServerException($e->getMessage(), 404);
+ }
+
+ if (!$this->target instanceof WebFingerResource) {
+ // TRANS: Error message when an object URI which we cannot find was requested
+ throw new ServerException(_m('Resource not found in local database.'), 404);
}
return true;
protected function setXRD()
{
- if (!($this->target instanceof WebFingerResource)) {
- throw new Exception(_('Target not set for resource descriptor'));
- }
-
$this->xrd->subject = $this->resource;
foreach ($this->target->getAliases() as $alias) {
line-height: 1.2em;
}
-/* Bookmark specific styles */
-/* TODO separate base styles and move to plugin */
-
-.bookmark h3 {
- margin: 0px 0px 8px 0px;
- float: left;
- line-height: 1.2em;
- max-width: 92%;
-}
-
-.bookmark-notice-count {
- border-radius: 4px;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- padding: 1px 6px;
- font-size: 1.2em;
- line-height: 1.2em;
- background: #fff;
- border: 1px solid #7b8dbb;
- color: #3e3e8c !important;
- position: relative;
- right: 4px;
- margin-left: 10px;
-}
-
-.bookmark-notice-count:hover {
- text-decoration: none;
- background: #f2f2f2;
- border: 1px solid #7b8dbb;
- text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.5);
-}
-
-.notice .bookmark-description {
- clear: both;
- margin-left: 0px;
- margin-bottom: 0px;
-}
-
-.notice .bookmark-author {
- margin-left: 0px;
- float: left;
-}
-
-.bookmark-tags {
- clear: both;
- margin-bottom: 8px;
- line-height: 1.6em;
-}
-
-ul.bookmark-tags a {
- border-radius: 4px;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- padding: 1px 6px;
- background: #f2f2f2;
- color: #3e3e8c !important;
- text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.5);
- font-size: 0.88em;
-}
-
-ul.bookmark-tags a:hover {
- background-color: #cdd1dd;
- text-decoration: none;
-}
-
-.bookmark-avatar {
- float: none !important;
- position: relative;
- top: 2px;
-}
-
-.bookmark div.entry-content {
- font-size: 0.88em;
- line-height: 1.2em;
- margin-top: 6px;
- opacity: 0.6;
- margin-bottom: 0px;
-}
-
-.bookmark:hover div.entry-content {
- opacity: 1;
-}
-
-#bookmarkpopup {
- min-width: 600px;
- margin-top: 0px;
- height: 100%;
- border: 10px solid #364A84;
- background: #364A84;
-}
-
-#bookmarkpopup #wrap {
- width: auto;
- min-width: 560px;
- padding: 40px 0px 25px 0px;
- margin-right: 2px;
- background: #fff url(../mobilelogo.png) no-repeat 6px 6px;
-}
-
-#bookmarkpopup #header {
- width: auto;
- padding: 0px 10px;
-}
-
-#bookmarkpopup .form_settings label {
- margin-top: 2px;
- text-align: right;
- width: 24%;
- font-size: 1.2em;
-}
-
-#bookmarkpopup .form_settings .form_data input {
- width: 60%;
-}
-
-#bookmarkpopup .form_guide {
- color: #777;
-}
-
-#bookmarkpopup #bookmark-submit {
- min-width: 100px;
-}
-
-#bookmarkpopup fieldset fieldset {
- margin-bottom: 10px;
-}
-
-#form_initial_bookmark.form_settings .form_data li {
- margin-bottom: 0px;
-}
-
-#form_new_bookmark.form_settings .bookmarkform-thumbnail {
- position: absolute;
- top: 50px;
- right: 0px;
-}
-
/* Onboard specific styles */
/* TODO move to plugin */
-webkit-box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
}
-/* Event specific styles */
-/* TODO separate base styles and move to plugin */
-
-.notice .vevent div {
- margin-bottom: 8px;
-}
-
-.event-info {
- margin-left: 0px !important;
- margin-top: 2px !important;
-}
-
-.notice .event-info + .notice-options {
- margin-top: 14px;
-}
-
-.notice .threaded-replies .event-info + .notice-options {
- margin-top: 20px;
-}
-
-#form_event_rsvp #new_rsvp_data {
- display: inline;
- margin: 10px 0px;
-}
-
-#form_event_rsvp input.submit {
- height: auto;
- padding: 0px 10px;
- margin-left: 10px;
- color:#fff;
- font-weight: bold;
- text-transform: uppercase;
- font-size: 1.1em;
- text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.2);
- border: 1px solid #d7621c;
- border-radius: 4px;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- background: #FB6104;
- background: -moz-linear-gradient(top, #ff9d63 0%, #fb6104 100%);
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff9d63), color-stop(100%,#fb6104));
- background: -webkit-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
- background: -o-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
- background: -ms-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff9d63', endColorstr='#fb6104',GradientType=0 );
- background: linear-gradient(top, #ff9d63 0%,#fb6104 100%);
-}
-
-#form_event_rsvp input.submit:hover {
- text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.6);
- background: #ff9d63;
- background: -moz-linear-gradient(top, #fb6104 0%, #fc8035 100%);
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fb6104), color-stop(100%,#fc8035));
- background: -webkit-linear-gradient(top, #fb6104 0%,#fc8035 100%);
- background: -o-linear-gradient(top, #fb6104 0%,#fc8035 100%);
- background: -ms-linear-gradient(top, #fb6104 0%,#fc8035 100%);
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fb6104', endColorstr='#fc8035',GradientType=0 );
- background: linear-gradient(top, #fb6104 0%,#fc8035 100%);
-}
-
-#wrap .vevent form.processing input.submit {
- text-indent: 0;
- background: #ff9d63;
-}
-
-#input_form_event .form_settings .form_data {
- float: left;
-}
-
-#input_form_event .form_settings .form_data li {
- float: left;
- width: auto;
-}
-
-#input_form_event .form_settings .form_data label {
- width: auto;
-}
-
-label[for=event-starttime], label[for=event-endtime] {
- display: none !important;
-}
-
-#event-starttime, #event-endtime {
- margin-top: -1px;
- margin-bottom: -1px;
- height: 2em;
-}
-
-#event-startdate, #event-enddate {
- margin-right: 20px;
- width: 120px;
-}
-
-/* QnA */
-
-.question div.question-description {
- font-size: 1em;
- line-height: 1.36em;
- margin-top: 0px;
- opacity: 1;
-}
-
-.question div.answer-content, .qna-full-question div.answer-content {
- font-size: 1em;
- opacity: 1;
-}
-
-.qna-dummy-placeholder input, .question #qna-answer, .qna-full-question #qna-answer {
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
- -moz-box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
- -webkit-box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
-}
-
-.question-description input.submit, .answer-content input.submit {
- height: auto;
- padding: 0px 10px;
- margin: 6px 0px 10px 0px;
- color:#fff;
- font-weight: bold;
- text-transform: uppercase;
- font-size: 1.1em;
- text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.2);
- border: 1px solid #d7621c;
- border-radius: 4px;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- background: #FB6104;
- background: -moz-linear-gradient(top, #ff9d63 0%, #fb6104 100%);
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff9d63), color-stop(100%,#fb6104));
- background: -webkit-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
- background: -o-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
- background: -ms-linear-gradient(top, #ff9d63 0%,#fb6104 100%);
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff9d63', endColorstr='#fb6104',GradientType=0 );
- background: linear-gradient(top, #ff9d63 0%,#fb6104 100%);
-}
-
-#qna-answer-submit {
- min-width: 100px;
-}
-
-.question .question-description input.submit:hover, .question .answer-content input.submit:hover {
- text-shadow: 0px -1px 0px rgba(0, 0, 0, 0.6);
- background: #ff9d63;
- background: -moz-linear-gradient(top, #fb6104 0%, #fc8035 100%);
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fb6104), color-stop(100%,#fc8035));
- background: -webkit-linear-gradient(top, #fb6104 0%,#fc8035 100%);
- background: -o-linear-gradient(top, #fb6104 0%,#fc8035 100%);
- background: -ms-linear-gradient(top, #fb6104 0%,#fc8035 100%);
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fb6104', endColorstr='#fc8035',GradientType=0 );
- background: linear-gradient(top, #fb6104 0%,#fc8035 100%);
-}
-
-.question .question-description #answer-form input.submit {
- margin-top: 0px;
-}
-
-.question p.best, .answer p.best {
- background: url(../images/rosette.png) no-repeat top left;
- padding-left: 20px;
-}
-
-.question p.best:before, .answer p.best:before {
- content: none !important;
-}
-
-/* Poll specific styles */
-
-.notice div.poll-content {
- opacity: 1;
-}
-
-#poll-response-submit {
- min-width: 100px;
-}
-
/* SNOD CompanyLogo styling */
/* TODO move to plugin */