diff options
Diffstat (limited to 'classes')
-rw-r--r-- | classes/File.php | 83 | ||||
-rw-r--r-- | classes/File_oembed.php | 28 | ||||
-rw-r--r-- | classes/File_redirection.php | 47 | ||||
-rw-r--r-- | classes/File_thumbnail.php | 41 | ||||
-rw-r--r-- | classes/Message.php | 11 | ||||
-rw-r--r-- | classes/Notice.php | 525 | ||||
-rw-r--r-- | classes/Profile.php | 37 | ||||
-rw-r--r-- | classes/Queue_item.php | 2 | ||||
-rw-r--r-- | classes/User.php | 68 |
9 files changed, 418 insertions, 424 deletions
diff --git a/classes/File.php b/classes/File.php index da029e39b..ef9dbf14a 100644 --- a/classes/File.php +++ b/classes/File.php @@ -116,10 +116,24 @@ class File extends Memcached_DataObject } /** + * Go look at a URL and possibly save data about it if it's new: + * - follow redirect chains and store them in file_redirection + * - look up oEmbed data and save it in file_oembed + * - if a thumbnail is available, save it in file_thumbnail + * - save file record with basic info + * - optionally save a file_to_post record + * - return the File object with the full reference + * * @fixme refactor this mess, it's gotten pretty scary. - * @param bool $followRedirects + * @param string $given_url the URL we're looking at + * @param int $notice_id (optional) + * @param bool $followRedirects defaults to true + * + * @return mixed File on success, -1 on some errors + * + * @throws ServerException on some errors */ - function processNew($given_url, $notice_id=null, $followRedirects=true) { + public function processNew($given_url, $notice_id=null, $followRedirects=true) { if (empty($given_url)) return -1; // error, no url to process $given_url = File_redirection::_canonUrl($given_url); if (empty($given_url)) return -1; // error, no url to process @@ -169,9 +183,9 @@ class File extends Memcached_DataObject if (empty($x)) { $x = File::staticGet($file_id); if (empty($x)) { - // FIXME: This could possibly be a clearer message :) + // @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.")); + throw new ServerException(_('Robin thinks something is impossible.')); } } @@ -186,8 +200,10 @@ class File extends Memcached_DataObject if ($fileSize > common_config('attachments', 'file_quota')) { // TRANS: Message given if an upload is larger than the configured maximum. // TRANS: %1$d is the byte limit for uploads, %2$d is the byte count for the uploaded file. - return sprintf(_('No file may be larger than %1$d bytes ' . - 'and the file you sent was %2$d bytes. Try to upload a smaller version.'), + // TRANS: %1$s is used for plural. + return sprintf(_m('No file may be larger than %1$d byte and the file you sent was %2$d bytes. Try to upload a smaller version.', + 'No file may be larger than %1$d bytes and the file you sent was %2$d bytes. Try to upload a smaller version.', + common_config('attachments', 'file_quota')), common_config('attachments', 'file_quota'), $fileSize); } @@ -197,8 +213,11 @@ class File extends Memcached_DataObject $total = $this->total + $fileSize; if ($total > common_config('attachments', 'user_quota')) { // TRANS: Message given if an upload would exceed user quota. - // TRANS: %d (number) is the user quota in bytes. - return sprintf(_('A file this large would exceed your user quota of %d bytes.'), common_config('attachments', 'user_quota')); + // TRANS: %d (number) is the user quota in bytes and is used for plural. + return sprintf(_m('A file this large would exceed your user quota of %d byte.', + 'A file this large would exceed your user quota of %d bytes.', + common_config('attachments', 'user_quota')), + common_config('attachments', 'user_quota')); } $query .= ' AND EXTRACT(month FROM file.modified) = EXTRACT(month FROM now()) and EXTRACT(year FROM file.modified) = EXTRACT(year FROM now())'; $this->query($query); @@ -206,8 +225,11 @@ class File extends Memcached_DataObject $total = $this->total + $fileSize; if ($total > common_config('attachments', 'monthly_quota')) { // TRANS: Message given id an upload would exceed a user's monthly quota. - // TRANS: $d (number) is the monthly user quota in bytes. - return sprintf(_('A file this large would exceed your monthly quota of %d bytes.'), common_config('attachments', 'monthly_quota')); + // TRANS: $d (number) is the monthly user quota in bytes and is used for plural. + return sprintf(_m('A file this large would exceed your monthly quota of %d byte.', + 'A file this large would exceed your monthly quota of %d bytes.', + common_config('attachments', 'monthly_quota')), + common_config('attachments', 'monthly_quota')); } return true; } @@ -217,12 +239,19 @@ class File extends Memcached_DataObject static function filename($profile, $basename, $mimetype) { require_once 'MIME/Type/Extension.php'; + + // We have to temporarily disable auto handling of PEAR errors... + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $mte = new MIME_Type_Extension(); - try { - $ext = $mte->getExtension($mimetype); - } catch ( Exception $e) { + $ext = $mte->getExtension($mimetype); + if (PEAR::isError($ext)) { $ext = strtolower(preg_replace('/\W/', '', $mimetype)); } + + // Restore error handling. + PEAR::staticPopErrorHandling(); + $nickname = $profile->nickname; $datestamp = strftime('%Y%m%dT%H%M%S', time()); $random = strtolower(common_confirmation_code(32)); @@ -292,9 +321,7 @@ class File extends Memcached_DataObject } $protocol = 'https'; - } else { - $path = common_config('attachments', 'path'); $server = common_config('attachments', 'server'); @@ -339,22 +366,28 @@ class File extends Memcached_DataObject $mimetype = substr($mimetype,0,$semicolon); } 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); - if($oembed){ + if($oembed && in_array($oembed->type, array('photo', 'video'))){ $mimetype = strtolower($oembed->mimetype); $semicolon = strpos($mimetype,';'); if($semicolon){ $mimetype = substr($mimetype,0,$semicolon); } - if(in_array($mimetype,$notEnclosureMimeTypes)){ - return false; - }else{ + // @fixme uncertain if this is right. + // we want to expose things like YouTube videos as + // viewable attachments, but don't expose them as + // downloadable enclosures.....? + //if (in_array($mimetype, $notEnclosureMimeTypes)) { + // return false; + //} else { if($oembed->mimetype) $enclosure->mimetype=$oembed->mimetype; if($oembed->url) $enclosure->url=$oembed->url; if($oembed->title) $enclosure->title=$oembed->title; if($oembed->modified) $enclosure->modified=$oembed->modified; unset($oembed->size); - } + //} } else { return false; } @@ -369,4 +402,14 @@ class File extends Memcached_DataObject $enclosure = $this->getEnclosure(); return !empty($enclosure); } + + /** + * Get the attachment's thumbnail record, if any. + * + * @return File_thumbnail + */ + function getThumbnail() + { + return File_thumbnail::staticGet('file_id', $this->id); + } } diff --git a/classes/File_oembed.php b/classes/File_oembed.php index 4813d5dda..b7bf3a5da 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -58,26 +58,16 @@ class File_oembed extends Memcached_DataObject return array(false, false, false); } - function _getOembed($url, $maxwidth = 500, $maxheight = 400) { - require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; + function _getOembed($url) { $parameters = array( - 'maxwidth'=>$maxwidth, - 'maxheight'=>$maxheight, + 'maxwidth' => common_config('attachments', 'thumb_width'), + 'maxheight' => common_config('attachments', 'thumb_height'), ); - try{ - $oEmbed = new Services_oEmbed($url); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $e){ - try{ - $oEmbed = new Services_oEmbed($url, array( - Services_oEmbed::OPTION_API => common_config('oohembed', 'endpoint') - )); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $ex){ - return false; - } + try { + return oEmbedHelper::getObject($url, $parameters); + } catch (Exception $e) { + common_log(LOG_ERR, "Error during oembed lookup for $url - " . $e->getMessage()); + return false; } } @@ -120,7 +110,7 @@ class File_oembed extends Memcached_DataObject } } $file_oembed->insert(); - if (!empty($data->thumbnail_url)) { + if (!empty($data->thumbnail_url) || ($data->type == 'photo')) { $ft = File_thumbnail::staticGet('file_id', $file_id); if (!empty($ft)) { common_log(LOG_WARNING, "Strangely, a File_thumbnail object exists for new file $file_id", diff --git a/classes/File_redirection.php b/classes/File_redirection.php index 92f0125a4..1096f500b 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -91,9 +91,16 @@ class File_redirection extends Memcached_DataObject $request->setMethod(HTTP_Request2::METHOD_HEAD); $response = $request->send(); - if (405 == $response->getStatus()) { + if (405 == $response->getStatus() || 204 == $response->getStatus()) { + // HTTP 405 Unsupported Method // Server doesn't support HEAD method? Can this really happen? // We'll try again as a GET and ignore the response data. + // + // HTTP 204 No Content + // YFrog sends 204 responses back for our HEAD checks, which + // seems like it may be a logic error in their servers. If + // we get a 204 back, re-run it as a GET... if there's really + // no content it'll be cheap. :) $request = self::_commonHttp($short_url, $redirs); $response = $request->send(); } @@ -132,6 +139,7 @@ class File_redirection extends Memcached_DataObject * reached. * * @param string $in_url + * @param boolean $discover true to attempt dereferencing the redirect if we don't know it already * @return mixed one of: * string - target URL, if this is a direct link or a known redirect * array - redirect info if this is an *unknown* redirect: @@ -143,7 +151,7 @@ class File_redirection extends Memcached_DataObject * size (optional): byte size from Content-Length header * time (optional): timestamp from Last-Modified header */ - public function where($in_url) { + public function where($in_url, $discover=true) { // let's see if we know this... $a = File::staticGet('url', $in_url); @@ -159,8 +167,13 @@ class File_redirection extends Memcached_DataObject } } - $ret = File_redirection::lookupWhere($in_url); - return $ret; + if ($discover) { + $ret = File_redirection::lookupWhere($in_url); + return $ret; + } else { + // No manual dereferencing; leave the unknown URL as is. + return $in_url; + } } /** @@ -174,14 +187,14 @@ class File_redirection extends Memcached_DataObject * may be saved. * * @param string $long_url + * @param User $user whose shortening options to use; defaults to the current web session user * @return string */ - - function makeShort($long_url) + function makeShort($long_url, $user=null) { $canon = File_redirection::_canonUrl($long_url); - $short_url = File_redirection::_userMakeShort($canon); + $short_url = File_redirection::_userMakeShort($canon, $user); // Did we get one? Is it shorter? @@ -206,11 +219,11 @@ class File_redirection extends Memcached_DataObject * @return string */ - function forceShort($long_url) + function forceShort($long_url, $user) { $canon = File_redirection::_canonUrl($long_url); - $short_url = File_redirection::_userMakeShort($canon, true); + $short_url = File_redirection::_userMakeShort($canon, $user, true); // Did we get one? Is it shorter? if (!empty($short_url)) { @@ -220,8 +233,8 @@ class File_redirection extends Memcached_DataObject } } - function _userMakeShort($long_url, $force = false) { - $short_url = common_shorten_url($long_url, $force); + function _userMakeShort($long_url, User $user=null, $force = false) { + $short_url = common_shorten_url($long_url, $user, $force); if (!empty($short_url) && $short_url != $long_url) { $short_url = (string)$short_url; // store it @@ -265,6 +278,18 @@ class File_redirection extends Memcached_DataObject return null; } + /** + * Basic attempt to canonicalize a URL, cleaning up some standard variants + * such as funny syntax or a missing path. Used internally when cleaning + * up URLs for storage and following redirect chains. + * + * Note that despite being on File_redirect, this function DOES NOT perform + * any dereferencing of redirects. + * + * @param string $in_url input URL + * @param string $default_scheme if given a bare link; defaults to 'http://' + * @return string + */ function _canonUrl($in_url, $default_scheme = 'http://') { if (empty($in_url)) return false; $out_url = $in_url; diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index edae8ac21..17bac7f08 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -48,12 +48,45 @@ class File_thumbnail extends Memcached_DataObject return array(false, false, false); } - function saveNew($data, $file_id) { + /** + * Save oEmbed-provided thumbnail data + * + * @param object $data + * @param int $file_id + */ + public static function saveNew($data, $file_id) { + 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->file_id = $file_id; - $tn->url = $data->thumbnail_url; - $tn->width = intval($data->thumbnail_width); - $tn->height = intval($data->thumbnail_height); + $tn->url = $url; + $tn->width = intval($width); + $tn->height = intval($height); $tn->insert(); } } diff --git a/classes/Message.php b/classes/Message.php index 353dc01f9..484d1f724 100644 --- a/classes/Message.php +++ b/classes/Message.php @@ -45,12 +45,19 @@ class Message extends Memcached_DataObject throw new ClientException(_('You are banned from sending direct messages.')); } + $user = User::staticGet('id', $sender->id); + $msg = new Message(); $msg->from_profile = $from; $msg->to_profile = $to; - $msg->content = common_shorten_links($content); - $msg->rendered = common_render_text($content); + if ($user) { + // Use the sender's URL shortening options. + $msg->content = $user->shortenLinks($content); + } else { + $msg->content = common_shorten_links($content); + } + $msg->rendered = common_render_text($msg->content); $msg->created = common_sql_now(); $msg->source = $source; diff --git a/classes/Notice.php b/classes/Notice.php index 2f8c7d5d5..c36fb702b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -256,9 +256,14 @@ class Notice extends Memcached_DataObject $is_local = Notice::LOCAL_PUBLIC; } - $profile = Profile::staticGet($profile_id); - - $final = common_shorten_links($content); + $profile = Profile::staticGet('id', $profile_id); + $user = User::staticGet('id', $profile_id); + if ($user) { + // Use the local user's shortening preferences, if applicable. + $final = $user->shortenLinks($content); + } else { + $final = common_shorten_links($content); + } if (Notice::contentTooLong($final)) { // TRANS: Client exception thrown if a notice contains too many characters. @@ -476,7 +481,9 @@ class Notice extends Memcached_DataObject * @return void */ function saveUrls() { - common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + if (common_config('attachments', 'process_links')) { + common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); + } } /** @@ -489,17 +496,18 @@ class Notice extends Memcached_DataObject */ function saveKnownUrls($urls) { - // @fixme validation? - foreach (array_unique($urls) as $url) { - File::processNew($url, $this->id); + if (common_config('attachments', 'process_links')) { + // @fixme validation? + foreach (array_unique($urls) as $url) { + File::processNew($url, $this->id); + } } } /** * @private callback */ - function saveUrl($data) { - list($url, $notice_id) = $data; + function saveUrl($url, $notice_id) { File::processNew($url, $notice_id); } @@ -524,10 +532,8 @@ class Notice extends Memcached_DataObject $notice = new Notice(); $notice->profile_id = $profile_id; $notice->content = $content; - if (common_config('db','type') == 'pgsql') - $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit')); - else - $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit')); + $threshold = common_sql_date(time() - common_config('site', 'dupelimit')); + $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold))); $cnt = $notice->count(); return ($cnt == 0); @@ -904,7 +910,7 @@ class Notice extends Memcached_DataObject { if (!is_array($group_ids)) { // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups(). - throw new ServerException(_("Bad type provided to saveKnownGroups")); + throw new ServerException(_('Bad type provided to saveKnownGroups.')); } $groups = array(); @@ -1220,406 +1226,200 @@ class Notice extends Memcached_DataObject return $groups; } - function asActivity() + /** + * Convert a notice into an activity for export. + * + * @param User $cur Current user + * + * @return Activity activity object representing this Notice. + */ + + function asActivity($cur = null, $source = false) { - $profile = $this->getProfile(); + $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); - $act = new Activity(); + if (!empty($act)) { + return $act; + } - $act->actor = ActivityObject::fromProfile($profile); - $act->verb = ActivityVerb::POST; - $act->objects[] = ActivityObject::fromNotice($this); + $act = new Activity(); + + if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { - $act->time = strtotime($this->created); - $act->link = $this->bestUrl(); + $profile = $this->getProfile(); + + $act->actor = ActivityObject::fromProfile($profile); + $act->verb = ActivityVerb::POST; + $act->objects[] = ActivityObject::fromNotice($this); - $act->content = common_xml_safe_str($this->rendered); - $act->id = $this->uri; - $act->title = common_xml_safe_str($this->content); + // XXX: should this be handled by default processing for object entry? - $ctx = new ActivityContext(); + $act->time = strtotime($this->created); + $act->link = $this->bestUrl(); + + $act->content = common_xml_safe_str($this->rendered); + $act->id = $this->uri; + $act->title = common_xml_safe_str($this->content); - if (!empty($this->reply_to)) { - $reply = Notice::staticGet('id', $this->reply_to); - if (!empty($reply)) { - $ctx->replyToID = $reply->uri; - $ctx->replyToUrl = $reply->bestUrl(); - } - } + // Categories - $ctx->location = $this->getLocation(); + $tags = $this->getTags(); - $conv = null; + foreach ($tags as $tag) { + $cat = new AtomCategory(); + $cat->term = $tag; - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); - if (!empty($conv)) { - $ctx->conversation = $conv->uri; + $act->categories[] = $cat; } - } - - $reply_ids = $this->getReplies(); - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $ctx->attention[] = $profile->getUri(); - } - } + // Enclosures + // XXX: use Atom Media and/or File activity objects instead - $groups = $this->getGroups(); + $attachments = $this->attachments(); - foreach ($groups as $group) { - $ctx->attention[] = $group->uri; - } + foreach ($attachments as $attachment) { + $enclosure = $attachment->getEnclosure(); + if ($enclosure) { + $act->enclosures[] = $enclosure; + } + } + + $ctx = new ActivityContext(); + + if (!empty($this->reply_to)) { + $reply = Notice::staticGet('id', $this->reply_to); + if (!empty($reply)) { + $ctx->replyToID = $reply->uri; + $ctx->replyToUrl = $reply->bestUrl(); + } + } + + $ctx->location = $this->getLocation(); + + $conv = null; + + if (!empty($this->conversation)) { + $conv = Conversation::staticGet('id', $this->conversation); + if (!empty($conv)) { + $ctx->conversation = $conv->uri; + } + } + + $reply_ids = $this->getReplies(); + + foreach ($reply_ids as $id) { + $profile = Profile::staticGet('id', $id); + if (!empty($profile)) { + $ctx->attention[] = $profile->getUri(); + } + } + + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $ctx->attention[] = $group->uri; + } - $act->context = $ctx; + // XXX: deprecated; use ActivityVerb::SHARE instead - return $act; - } + $repeat = null; - // This has gotten way too long. Needs to be sliced up into functional bits - // or ideally exported to a utility class. + if (!empty($this->repeat_of)) { + $repeat = Notice::staticGet('id', $this->repeat_of); + $ctx->forwardID = $repeat->uri; + $ctx->forwardUrl = $repeat->bestUrl(); + } + + $act->context = $ctx; + + $noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering) + + $ns = $this->getSource(); + + if (!empty($ns)) { + $noticeInfoAttr['source'] = $ns->code; + if (!empty($ns->url)) { + $noticeInfoAttr['source_link'] = $ns->url; + if (!empty($ns->name)) { + $noticeInfoAttr['source'] = '<a href="' + . htmlspecialchars($ns->url) + . '" rel="nofollow">' + . htmlspecialchars($ns->name) + . '</a>'; + } + } + } - function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null) - { - $profile = $this->getProfile(); + if (!empty($cur)) { + $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; + $cp = $cur->getProfile(); + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; + } - $xs = new XMLStringer(true); - - if ($namespace) { - $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', - 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', - 'xmlns:georss' => 'http://www.georss.org/georss', - 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', - 'xmlns:media' => 'http://purl.org/syndication/atommedia', - 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', - 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', - 'xmlns:statusnet' => 'http://status.net/schema/api/1/'); - } else { - $attrs = array(); - } + if (!empty($this->repeat_of)) { + $noticeInfoAttr['repeat_of'] = $this->repeat_of; + } - if (Event::handle('StartActivityStart', array(&$this, &$xs, &$attrs))) { - $xs->elementStart('entry', $attrs); - Event::handle('EndActivityStart', array(&$this, &$xs, &$attrs)); - } + $act->extra[] = array('statusnet:notice_info', $noticeInfoAttr, null); - if (Event::handle('StartActivitySource', array(&$this, &$xs))) { if ($source) { + $atom_feed = $profile->getAtomFeed(); if (!empty($atom_feed)) { - $xs->elementStart('source'); + $act->source = new ActivitySource(); + // XXX: we should store the actual feed ID - $xs->element('id', null, $atom_feed); + $act->source->id = $atom_feed; // XXX: we should store the actual feed title - $xs->element('title', null, $profile->getBestName()); - - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $profile->profileurl)); - - $xs->element('link', array('rel' => 'self', - 'type' => 'application/atom+xml', - 'href' => $atom_feed)); + $act->source->title = $profile->getBestName(); - $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE)); + $act->source->links['alternate'] = $profile->profileurl; + $act->source->links['self'] = $atom_feed; + $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE); + $notice = $profile->getCurrentNotice(); if (!empty($notice)) { - $xs->element('updated', null, self::utcDate($notice->created)); + $act->source->updated = self::utcDate($notice->created); } $user = User::staticGet('id', $profile->id); if (!empty($user)) { - $xs->element('link', array('rel' => 'license', - 'href' => common_config('license', 'url'))); + $act->source->links['license'] = common_config('license', 'url'); } - - $xs->elementEnd('source'); - } - } - Event::handle('EndActivitySource', array(&$this, &$xs)); - } - - $title = common_xml_safe_str($this->content); - - if (Event::handle('StartActivityTitle', array(&$this, &$xs, &$title))) { - $xs->element('title', null, $title); - Event::handle('EndActivityTitle', array($this, &$xs, $title)); - } - - $atomAuthor = ''; - - if ($author) { - $atomAuthor = $profile->asAtomAuthor($cur); - } - - if (Event::handle('StartActivityAuthor', array(&$this, &$xs, &$atomAuthor))) { - if (!empty($atomAuthor)) { - $xs->raw($atomAuthor); - Event::handle('EndActivityAuthor', array(&$this, &$xs, &$atomAuthor)); - } - } - - $actor = ''; - - if ($author) { - $actor = $profile->asActivityActor(); - } - - if (Event::handle('StartActivityActor', array(&$this, &$xs, &$actor))) { - if (!empty($actor)) { - $xs->raw($actor); - Event::handle('EndActivityActor', array(&$this, &$xs, &$actor)); - } - } - - $url = $this->bestUrl(); - - if (Event::handle('StartActivityLink', array(&$this, &$xs, &$url))) { - $xs->element('link', array('rel' => 'alternate', - 'type' => 'text/html', - 'href' => $url)); - Event::handle('EndActivityLink', array(&$this, &$xs, $url)); - } - - $id = $this->uri; - - if (Event::handle('StartActivityId', array(&$this, &$xs, &$id))) { - $xs->element('id', null, $id); - Event::handle('EndActivityId', array(&$this, &$xs, $id)); - } - - $published = self::utcDate($this->created); - - if (Event::handle('StartActivityPublished', array(&$this, &$xs, &$published))) { - $xs->element('published', null, $published); - Event::handle('EndActivityPublished', array(&$this, &$xs, $published)); - } - - $updated = $published; // XXX: notices are usually immutable - - if (Event::handle('StartActivityUpdated', array(&$this, &$xs, &$updated))) { - $xs->element('updated', null, $updated); - Event::handle('EndActivityUpdated', array(&$this, &$xs, $updated)); - } - - $content = common_xml_safe_str($this->rendered); - - if (Event::handle('StartActivityContent', array(&$this, &$xs, &$content))) { - $xs->element('content', array('type' => 'html'), $content); - Event::handle('EndActivityContent', array(&$this, &$xs, $content)); - } - - // Most of our notices represent POSTing a NOTE. This is the default verb - // for activity streams, so we normally just leave it out. - - $verb = ActivityVerb::POST; - - if (Event::handle('StartActivityVerb', array(&$this, &$xs, &$verb))) { - $xs->element('activity:verb', null, $verb); - Event::handle('EndActivityVerb', array(&$this, &$xs, $verb)); - } - - // We use the default behavior for activity streams: if there's no activity:object, - // then treat the entry itself as the object. Here, you can set the type of that object, - // which is normally a NOTE. - - $type = ActivityObject::NOTE; - - if (Event::handle('StartActivityDefaultObjectType', array(&$this, &$xs, &$type))) { - $xs->element('activity:object-type', null, $type); - Event::handle('EndActivityDefaultObjectType', array(&$this, &$xs, $type)); - } - - // Since we usually use the entry itself as an object, we don't have an explicit - // object. Some extensions may want to add them (for photo, event, music, etc.). - - $objects = array(); - - if (Event::handle('StartActivityObjects', array(&$this, &$xs, &$objects))) { - foreach ($objects as $object) { - $xs->raw($object->asString()); - } - Event::handle('EndActivityObjects', array(&$this, &$xs, $objects)); - } - - $noticeInfoAttr = array('local_id' => $this->id); // local notice ID (useful to clients for ordering) - - $ns = $this->getSource(); - - if (!empty($ns)) { - $noticeInfoAttr['source'] = $ns->code; - if (!empty($ns->url)) { - $noticeInfoAttr['source_link'] = $ns->url; - if (!empty($ns->name)) { - $noticeInfoAttr['source'] = '<a href="' - . htmlspecialchars($ns->url) - . '" rel="nofollow">' - . htmlspecialchars($ns->name) - . '</a>'; } } - } - - if (!empty($cur)) { - $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; - $profile = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($profile->hasRepeated($this->id)) ? "true" : "false"; - } - if (!empty($this->repeat_of)) { - $noticeInfoAttr['repeat_of'] = $this->repeat_of; - } - - if (Event::handle('StartActivityNoticeInfo', array(&$this, &$xs, &$noticeInfoAttr))) { - $xs->element('statusnet:notice_info', $noticeInfoAttr, null); - Event::handle('EndActivityNoticeInfo', array(&$this, &$xs, $noticeInfoAttr)); - } - - $replyNotice = null; - - if ($this->reply_to) { - $replyNotice = Notice::staticGet('id', $this->reply_to); - } - - if (Event::handle('StartActivityInReplyTo', array(&$this, &$xs, &$replyNotice))) { - if (!empty($replyNotice)) { - $xs->element('link', array('rel' => 'related', - 'href' => $replyNotice->bestUrl())); - $xs->element('thr:in-reply-to', - array('ref' => $replyNotice->uri, - 'href' => $replyNotice->bestUrl())); - Event::handle('EndActivityInReplyTo', array(&$this, &$xs, $replyNotice)); + if ($this->isLocal()) { + $act->selfLink = common_local_url('ApiStatusesShow', array('id' => $this->id, + 'format' => 'atom')); + $act->editLink = $act->selfLink; } - } - $conv = null; - - if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); + Event::handle('EndNoticeAsActivity', array($this, &$act)); } + + self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); - if (Event::handle('StartActivityConversation', array(&$this, &$xs, &$conv))) { - if (!empty($conv)) { - $xs->element('link', array('rel' => 'ostatus:conversation', - 'href' => $conv->uri)); - } - Event::handle('EndActivityConversation', array(&$this, &$xs, $conv)); - } - - $replyProfiles = array(); - - $reply_ids = $this->getReplies(); - - foreach ($reply_ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $replyProfiles[] = $profile; - } - } - - if (Event::handle('StartActivityAttentionProfiles', array(&$this, &$xs, &$replyProfiles))) { - foreach ($replyProfiles as $profile) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $profile->getUri())); - $xs->element('link', array('rel' => 'mentioned', - 'href' => $profile->getUri())); - } - Event::handle('EndActivityAttentionProfiles', array(&$this, &$xs, $replyProfiles)); - } - - $groups = $this->getGroups(); - - if (Event::handle('StartActivityAttentionGroups', array(&$this, &$xs, &$groups))) { - foreach ($groups as $group) { - $xs->element('link', array('rel' => 'ostatus:attention', - 'href' => $group->permalink())); - $xs->element('link', array('rel' => 'mentioned', - 'href' => $group->permalink())); - } - Event::handle('EndActivityAttentionGroups', array(&$this, &$xs, $groups)); - } - - $repeat = null; - - if (!empty($this->repeat_of)) { - $repeat = Notice::staticGet('id', $this->repeat_of); - } - - if (Event::handle('StartActivityForward', array(&$this, &$xs, &$repeat))) { - if (!empty($repeat)) { - $xs->element('ostatus:forward', - array('ref' => $repeat->uri, - 'href' => $repeat->bestUrl())); - } - - Event::handle('EndActivityForward', array(&$this, &$xs, $repeat)); - } - - $tags = $this->getTags(); - - if (Event::handle('StartActivityCategories', array(&$this, &$xs, &$tags))) { - foreach ($tags as $tag) { - $xs->element('category', array('term' => $tag)); - } - Event::handle('EndActivityCategories', array(&$this, &$xs, $tags)); - } - - // Enclosures - - $enclosures = array(); - - $attachments = $this->attachments(); - - foreach ($attachments as $attachment) { - $enclosure = $attachment->getEnclosure(); - if ($enclosure) { - $enclosures[] = $enclosure; - } - } - - if (Event::handle('StartActivityEnclosures', array(&$this, &$xs, &$enclosures))) { - foreach ($enclosures as $enclosure) { - $attributes = array('rel' => 'enclosure', - 'href' => $enclosure->url, - 'type' => $enclosure->mimetype, - 'length' => $enclosure->size); - - if ($enclosure->title) { - $attributes['title'] = $enclosure->title; - } - - $xs->element('link', $attributes, null); - } - Event::handle('EndActivityEnclosures', array(&$this, &$xs, $enclosures)); - } - - $lat = $this->lat; - $lon = $this->lon; - - if (Event::handle('StartActivityGeo', array(&$this, &$xs, &$lat, &$lon))) { - if (!empty($lat) && !empty($lon)) { - $xs->element('georss:point', null, $lat . ' ' . $lon); - } - Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon)); - } + return $act; + } - if (Event::handle('StartActivityEnd', array(&$this, &$xs))) { - $xs->elementEnd('entry'); - Event::handle('EndActivityEnd', array(&$this, &$xs)); - } + // This has gotten way too long. Needs to be sliced up into functional bits + // or ideally exported to a utility class. - return $xs->getString(); + function asAtomEntry($namespace=false, $source=false, $author=true, $cur=null) + { + $act = $this->asActivity($cur, $source); + return $act->asString($namespace, $author); } + /** * Returns an XML string fragment with a reference to a notice as an @@ -1630,6 +1430,7 @@ class Notice extends Memcached_DataObject * @param string $element one of 'subject', 'object', 'target' * @return string */ + function asActivityNoun($element) { $noun = ActivityObject::fromNotice($this); diff --git a/classes/Profile.php b/classes/Profile.php index b56e508c6..05df8899e 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -125,6 +125,14 @@ class Profile extends Memcached_DataObject return $avatar; } + /** + * Delete attached avatars for this user from the database and filesystem. + * This should be used instead of a batch delete() to ensure that files + * get removed correctly. + * + * @param boolean $original true to delete only the original-size file + * @return <type> + */ function delete_avatars($original=true) { $avatar = new Avatar(); @@ -494,6 +502,29 @@ class Profile extends Memcached_DataObject return $cnt; } + /** + * Is this profile subscribed to another profile? + * + * @param Profile $other + * @return boolean + */ + function isSubscribed($other) + { + return Subscription::exists($this, $other); + } + + /** + * Are these two profiles subscribed to each other? + * + * @param Profile $other + * @return boolean + */ + function mutuallySubscribed($other) + { + return $this->isSubscribed($other) && + $other->isSubscribed($this); + } + function hasFave($notice) { $cache = Cache::instance(); @@ -641,9 +672,11 @@ class Profile extends Memcached_DataObject $this->_deleteMessages(); $this->_deleteTags(); $this->_deleteBlocks(); + $this->delete_avatars(); - $related = array('Avatar', - 'Reply', + // Warning: delete() will run on the batch objects, + // not on individual objects. + $related = array('Reply', 'Group_member', ); Event::handle('ProfileDeleteRelated', array($this, &$related)); diff --git a/classes/Queue_item.php b/classes/Queue_item.php index c7e17be6e..007d4ed23 100644 --- a/classes/Queue_item.php +++ b/classes/Queue_item.php @@ -32,7 +32,7 @@ class Queue_item extends Memcached_DataObject if ($transports) { if (is_array($transports)) { // @fixme use safer escaping - $list = implode("','", array_map('addslashes', $transports)); + $list = implode("','", array_map(array($qi, 'escape'), $transports)); $qi->whereAdd("transport in ('$list')"); } else { $qi->transport = $transports; diff --git a/classes/User.php b/classes/User.php index 5914f0b80..d4f182f7e 100644 --- a/classes/User.php +++ b/classes/User.php @@ -79,7 +79,8 @@ class User extends Memcached_DataObject function isSubscribed($other) { - return Subscription::exists($this->getProfile(), $other); + $profile = $this->getProfile(); + return $profile->isSubscribed($other); } // 'update' won't write key columns, so we have to do it ourselves. @@ -110,6 +111,16 @@ class User extends Memcached_DataObject return $result; } + /** + * Check whether the given nickname is potentially usable, or if it's + * excluded by any blacklists on this system. + * + * WARNING: INPUT IS NOT VALIDATED OR NORMALIZED. NON-NORMALIZED INPUT + * OR INVALID INPUT MAY LEAD TO FALSE RESULTS. + * + * @param string $nickname + * @return boolean true if clear, false if blacklisted + */ static function allowed_nickname($nickname) { // XXX: should already be validated for size, content, etc. @@ -413,8 +424,8 @@ class User extends Memcached_DataObject function mutuallySubscribed($other) { - return $this->isSubscribed($other) && - $other->isSubscribed($this); + $profile = $this->getProfile(); + return $profile->mutuallySubscribed($other); } function mutuallySubscribedUsers() @@ -911,4 +922,55 @@ class User extends Memcached_DataObject throw new ServerException(_('Single-user mode code called when not enabled.')); } } + + /** + * This is kind of a hack for using external setup code that's trying to + * build single-user sites. + * + * Will still return a username if the config singleuser/nickname is set + * even if the account doesn't exist, which normally indicates that the + * site is horribly misconfigured. + * + * At the moment, we need to let it through so that router setup can + * complete, otherwise we won't be able to create the account. + * + * This will be easier when we can more easily create the account and + * *then* switch the site to 1user mode without jumping through hoops. + * + * @return string + * @throws ServerException if no valid single user account is present + * @throws ServerException if called when not in single-user mode + */ + static function singleUserNickname() + { + try { + $user = User::singleUser(); + return $user->nickname; + } catch (Exception $e) { + if (common_config('singleuser', 'enabled') && common_config('singleuser', 'nickname')) { + common_log(LOG_WARN, "Warning: code attempting to pull single-user nickname when the account does not exist. If this is not setup time, this is probably a bug."); + return common_config('singleuser', 'nickname'); + } + throw $e; + } + } + + /** + * Find and shorten links in the given text using this user's URL shortening + * settings. + * + * By default, links will be left untouched if the text is shorter than the + * configured maximum notice length. Pass true for the $always parameter + * to force all links to be shortened regardless. + * + * Side effects: may save file and file_redirection records for referenced URLs. + * + * @param string $text + * @param boolean $always + * @return string + */ + public function shortenLinks($text, $always=false) + { + return common_shorten_links($text, $always, $this); + } } |