From: Roland Haeder Date: Tue, 10 Nov 2015 18:05:53 +0000 (+0100) Subject: Merge remote-tracking branch 'upstream/master' into social-master X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=a0b9aeb43ef1b08e8dd7fe25101c515a0df53e7f;hp=22f6cec9b7b9a04e61ba355730ffab9db9bc8005;p=quix0rs-gnu-social.git Merge remote-tracking branch 'upstream/master' into social-master Signed-off-by: Roland Haeder --- diff --git a/.gitignore b/.gitignore index b9289b7c2a..51ac39e6f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,11 @@ -avatar/* -background/* -files/* -file/* -local/* -_darcs/* -logs/* -log/* -run/* -config.php -.htaccess -httpd.conf -*.tmproj -dataobject.ini -*~ -*.bak -*.orig -*.rej -.#* -*.swp -.buildpath -.project -.settings -TODO.rym -config-*.php -good-config.php -lac08.log -php.log -.DS_Store -nbproject -*.mo -*log* -htaccess-sample -installer.txt -extlib/DB.php -.gitmodules +/nbproject/private/ +/nbproject/*~ +/manifest.mf +/build/ +/dist/ +/data/* +/*.properties +/*-ejb/nbproject/private/ +/*-ejb/nbproject/*~ +/*-ejb/build/ +/*-ejb/dist/ diff --git a/UPGRADE b/UPGRADE index d2cd365e55..3a1dd8a9b9 100644 --- a/UPGRADE +++ b/UPGRADE @@ -27,13 +27,13 @@ and follow this procedure: The upgrade script will likely take a long time because it will upgrade the tables to another character encoding and make other automated upgrades. Make sure it ends without errors. If you get - errors, create a new task on https://bugz.foocorp.net/ + errors, create a new task on https://git.gnu.io/gnu/gnu-social/issues 4. Start your queue daemons again (you can run this command even if you do not use the queue daemons): $ bash scripts/startdaemons.sh -5. Report any issues at https://bugz.foocorp.net/ (tag GNU social) +5. Report any issues at https://git.gnu.io/gnu/gnu-social/issues If you are using ssh keys to log in to your server, you can make this procedure pretty painless (assuming you have automated backups already). @@ -69,7 +69,7 @@ variant of this command (you will be prompted for the database password): 2. Unpack your GNU social code to a fresh directory. You can do this by cloning our git repository: - $ git clone https://gitorious.org/social/mainline.git gnusocial + $ git clone https://git.gnu.io/gnu/gnu-social.git gnusocial 3. Synchronize your local files to the GNU social directory. These will be the local files such as avatars, config and files: @@ -91,8 +91,8 @@ variant of this command (you will be prompted for the database password): The upgrade script will likely take a long time because it will upgrade the tables to another character encoding and make other automated upgrades. Make sure it ends without errors. If you get - errors, create a new task on https://bugz.foocorp.net/ + errors, create a new task on https://git.gnu.io/gnu/gnu-social/issues 6. Start your queue daemons: 'bash scripts/startdaemons.sh' -7. Report any issues at https://bugz.foocorp.net/ (tag GNU social) +7. Report any issues at https://git.gnu.io/gnu/gnu-social/issues diff --git a/actions/doc.php b/actions/doc.php index d59c63631a..d897b4e58c 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -28,9 +28,7 @@ * along with this program. If not, see . */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Documentation class. @@ -42,16 +40,14 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ -class DocAction extends Action +class DocAction extends ManagedAction { var $output = null; var $filename = null; var $title = null; - function prepare(array $args=array()) + protected function doPreparation() { - parent::prepare($args); - $this->title = $this->trimmed('title'); if (!preg_match('/^[a-zA-Z0-9_-]*$/', $this->title)) { $this->title = 'help'; @@ -59,52 +55,11 @@ class DocAction extends Action $this->output = null; $this->loadDoc(); - return true; - } - - /** - * Handle a request - * - * @param array $args array of arguments - * - * @return nothing - */ - function handle(array $args=array()) - { - parent::handle($args); - $this->showPage(); - } - - /** - * Page title - * - * Gives the page title of the document. Override default for hAtom entry. - * - * @return void - */ - function showPageTitle() - { - $this->element('h1', array('class' => 'entry-title'), $this->title()); } - /** - * Block for content. - * - * Overrides default from Action to wrap everything in an hAtom entry. - * - * @return void. - */ - function showContentBlock() + public function title() { - $this->elementStart('div', array('id' => 'content', 'class' => 'h-entry')); - $this->showPageTitle(); - $this->showPageNoticeBlock(); - $this->elementStart('div', array('id' => 'content_inner', - 'class' => 'e-content')); - // show the actual content (forms, lists, whatever) - $this->showContent(); - $this->elementEnd('div'); - $this->elementEnd('div'); + return ucfirst($this->title); } /** @@ -119,16 +74,9 @@ class DocAction extends Action $this->raw($this->output); } - /** - * Page title. - * - * Uses the title of the document. - * - * @return page title - */ - function title() + function showNoticeForm() { - return ucfirst($this->title); + // no notice form } /** diff --git a/classes/Deleted_notice.php b/classes/Deleted_notice.php index a9167f19a4..23bbea1bab 100644 --- a/classes/Deleted_notice.php +++ b/classes/Deleted_notice.php @@ -17,30 +17,21 @@ * along with this program. If not, see . */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** - * Table Definition for notice + * Table Definition for deleted_notice */ -require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; class Deleted_notice extends Managed_DataObject { - ###START_AUTOCODE - /* the code below is auto generated do not remove the above tag */ - - public $__table = 'deleted_notice'; // table name + public $__table = 'deleted_notice'; // table name public $id; // int(4) primary_key not_null public $profile_id; // int(4) not_null public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space public $created; // datetime() not_null public $deleted; // datetime() not_null - /* the code above is auto generated do not remove the tag below */ - ###END_AUTOCODE - public static function schemaDef() { return array( diff --git a/classes/Notice.php b/classes/Notice.php index 0fe1296bc3..37310af8ef 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -158,6 +158,14 @@ class Notice extends Managed_DataObject $this->_profile[$this->profile_id] = $profile; } + public function deleteAs(Profile $actor) + { + if ($this->getProfile()->sameAs($actor) || $actor->hasRight(Right::DELETEOTHERSNOTICE)) { + return $this->delete(); + } + throw new AuthorizationException('You are not allowed to delete other user\'s notices'); + } + function delete($useWhere=false) { // For auditing purposes, save a record that the notice diff --git a/classes/Profile.php b/classes/Profile.php index 09f9ca71d1..5ef77a9506 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -144,7 +144,7 @@ class Profile extends Managed_DataObject public function hasPassword() { try { - return !empty($this->getUser()->hasPassword()); + return $this->getUser()->hasPassword(); } catch (NoSuchUserException $e) { return false; } diff --git a/lib/activity.php b/lib/activity.php index 6b3ccf3519..49e7fdbcd4 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -245,7 +245,7 @@ class Activity if (!empty($targetEl)) { $this->target = new ActivityObject($targetEl); - } elseif (ActivityUtils::compareTypes($this->verb, array(ActivityVerb::FAVORITE))) { + } elseif (ActivityUtils::compareVerbs($this->verb, array(ActivityVerb::FAVORITE))) { // StatusNet didn't send a 'target' for their Favorite atom entries $this->target = clone($this->objects[0]); } diff --git a/lib/activityhandlerplugin.php b/lib/activityhandlerplugin.php index 529749cc1d..6639e27822 100644 --- a/lib/activityhandlerplugin.php +++ b/lib/activityhandlerplugin.php @@ -104,7 +104,7 @@ abstract class ActivityHandlerPlugin extends Plugin function isMyVerb($verb) { $verb = $verb ?: ActivityVerb::POST; // post is the default verb - return ActivityUtils::compareTypes($verb, $this->verbs()); + return ActivityUtils::compareVerbs($verb, $this->verbs()); } function isMyType($type) { @@ -389,7 +389,7 @@ abstract class ActivityHandlerPlugin extends Plugin } elseif ($target instanceof Profile && $target->isLocal()) { $original = null; // FIXME: Shouldn't favorites show up with a 'target' activityobject? - if (!ActivityUtils::compareTypes($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) { + if (!ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) { // If this is not a post, it's a verb targeted at something (such as a Favorite attached to a note) if (!empty($activity->objects[0]->id)) { $activity->context->replyToID = $activity->objects[0]->id; @@ -413,7 +413,7 @@ abstract class ActivityHandlerPlugin extends Plugin $actor = $oactor->localProfile(); // FIXME: will this work in all cases? I made it work for Favorite... - if (ActivityUtils::compareTypes($activity->verb, array(ActivityVerb::POST))) { + if (ActivityUtils::compareVerbs($activity->verb, array(ActivityVerb::POST))) { $object = $activity->objects[0]; } else { $object = $activity; diff --git a/lib/activityobject.php b/lib/activityobject.php index 2fe52eefcc..87eea13727 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -298,7 +298,7 @@ class ActivityObject if (!empty($guidEl)) { $this->id = $guidEl->textContent; - if ($guidEl->hasAttribute('isPermaLink')) { + if ($guidEl->hasAttribute('isPermaLink') && $guidEl->getAttribute('isPermaLink') != 'false') { // overwrites $this->link = $this->id; } diff --git a/lib/activitystreamjsondocument.php b/lib/activitystreamjsondocument.php index 0466045fef..12c3882c25 100644 --- a/lib/activitystreamjsondocument.php +++ b/lib/activitystreamjsondocument.php @@ -175,121 +175,3 @@ class ActivityStreamJSONDocument extends JSONActivityCollection } } - -/** - * A class for representing MediaLinks in JSON Activities - * - * @category Feed - * @package StatusNet - * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -class ActivityStreamsMediaLink extends ActivityStreamsLink -{ - private $linkDict; - - function __construct( - $url = null, - $width = null, - $height = null, - $mediaType = null, // extension - $rel = null, // extension - $duration = null - ) - { - parent::__construct($url, $rel, $mediaType); - $this->linkDict = array( - 'width' => intval($width), - 'height' => intval($height), - 'duration' => intval($duration) - ); - } - - function asArray() - { - return array_merge( - parent::asArray(), - array_filter($this->linkDict) - ); - } -} - -/* - * Collection primarily as the root of an Activity Streams doc but can be used as the value - * of extension properties in a variety of situations. - * - * A valid Collection object serialization MUST contain at least the url or items properties. - */ -class JSONActivityCollection { - - /* Non-negative integer specifying the total number of activities within the stream */ - protected $totalItems; - - /* An array containing a listing of Objects of any object type */ - protected $items; - - /* IRI referencing a JSON document containing the full listing of objects in the collection */ - protected $url; - - /** - * Constructor - * - * @param array $items array of activity items - * @param string $url url of a doc list all the objs in the collection - * @param int $totalItems total number of items in the collection - */ - function __construct($items = null, $url = null) - { - $this->items = empty($items) ? array() : $items; - $this->totalItems = count($items); - $this->url = $url; - } - - /** - * Get the total number of items in the collection - * - * @return int total the total - */ - public function getTotalItems() - { - $this->totalItems = count($items); - return $this->totalItems; - } -} - -/** - * A class for representing links in JSON Activities - * - * @category Feed - * @package StatusNet - * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -class ActivityStreamsLink -{ - private $linkDict; - - function __construct($url = null, $rel = null, $mediaType = null) - { - // links MUST have a URL - if (empty($url)) { - throw new Exception('Links must have a URL.'); - } - - $this->linkDict = array( - 'url' => $url, - 'rel' => $rel, // extension - 'type' => $mediaType // extension - ); - } - - function asArray() - { - return array_filter($this->linkDict); - } -} - diff --git a/lib/activitystreamslink.php b/lib/activitystreamslink.php new file mode 100644 index 0000000000..2c91deda0e --- /dev/null +++ b/lib/activitystreamslink.php @@ -0,0 +1,35 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ActivityStreamsLink +{ + private $linkDict; + + function __construct($url = null, $rel = null, $mediaType = null) + { + // links MUST have a URL + if (empty($url)) { + throw new Exception('Links must have a URL.'); + } + + $this->linkDict = array( + 'url' => $url, + 'rel' => $rel, // extension + 'type' => $mediaType // extension + ); + } + + function asArray() + { + return array_filter($this->linkDict); + } +} diff --git a/lib/activitystreamsmedialink.php b/lib/activitystreamsmedialink.php new file mode 100644 index 0000000000..c8612afc85 --- /dev/null +++ b/lib/activitystreamsmedialink.php @@ -0,0 +1,41 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ActivityStreamsMediaLink extends ActivityStreamsLink +{ + private $linkDict; + + function __construct( + $url = null, + $width = null, + $height = null, + $mediaType = null, // extension + $rel = null, // extension + $duration = null + ) + { + parent::__construct($url, $rel, $mediaType); + $this->linkDict = array( + 'width' => intval($width), + 'height' => intval($height), + 'duration' => intval($duration) + ); + } + + function asArray() + { + return array_merge( + parent::asArray(), + array_filter($this->linkDict) + ); + } +} diff --git a/lib/activityutils.php b/lib/activityutils.php index ac7eb94213..bb430c6f78 100644 --- a/lib/activityutils.php +++ b/lib/activityutils.php @@ -348,7 +348,7 @@ class ActivityUtils return null; } - static function compareTypes($type, array $objects) // this does verbs too! + static function compareTypes($type, array $objects) { $type = self::resolveUri($type); foreach ($objects as $object) { @@ -359,6 +359,11 @@ class ActivityUtils return false; } + static function compareVerbs($type, $objects) + { + return self::compareTypes($type, $objects); + } + static function resolveUri($uri, $make_relative=false) { if (empty($uri)) { diff --git a/lib/activityverb.php b/lib/activityverb.php index dc6f9c93f4..187962d617 100644 --- a/lib/activityverb.php +++ b/lib/activityverb.php @@ -54,9 +54,9 @@ class ActivityVerb const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; const JOIN = 'http://activitystrea.ms/schema/1.0/join'; const TAG = 'http://activitystrea.ms/schema/1.0/tag'; + const DELETE = 'delete'; // the url part is not used anymore, and this feature is new enough to avoid problems with legacy nodes if used without http://... // Custom OStatus verbs for the flipside until they're standardized - const DELETE = 'http://ostatus.org/schema/1.0/unfollow'; const UNFAVORITE = 'http://activitystrea.ms/schema/1.0/unfavorite'; const UNLIKE = 'http://activitystrea.ms/schema/1.0/unlike'; // This is a synonym of unfavorite const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow'; diff --git a/lib/apiaction.php b/lib/apiaction.php index fae8f33d0e..3564709e5b 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -795,7 +795,12 @@ class ApiAction extends Action 'xmlns:statusnet' => 'http://status.net/schema/api/1/')); if (is_array($notice)) { - $notice = new ArrayWrapper($notice); + //FIXME: make everything calling showJsonTimeline use only Notice objects + $ids = array(); + foreach ($notice as $n) { + $ids[] = $n->getID(); + } + $notice = Notice::multiGet('id', $ids); } while ($notice->fetch()) { @@ -851,7 +856,12 @@ class ApiAction extends Action $this->element('ttl', null, '40'); if (is_array($notice)) { - $notice = new ArrayWrapper($notice); + //FIXME: make everything calling showJsonTimeline use only Notice objects + $ids = array(); + foreach ($notice as $n) { + $ids[] = $n->getID(); + } + $notice = Notice::multiGet('id', $ids); } while ($notice->fetch()) { @@ -895,7 +905,12 @@ class ApiAction extends Action $this->element('subtitle', null, $subtitle); if (is_array($notice)) { - $notice = new ArrayWrapper($notice); + //FIXME: make everything calling showJsonTimeline use only Notice objects + $ids = array(); + foreach ($notice as $n) { + $ids[] = $n->getID(); + } + $notice = Notice::multiGet('id', $ids); } while ($notice->fetch()) { @@ -1000,7 +1015,6 @@ class ApiAction extends Action if (is_array($notice)) { //FIXME: make everything calling showJsonTimeline use only Notice objects - common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!'); $ids = array(); foreach ($notice as $n) { $ids[] = $n->getID(); diff --git a/lib/jsonactivitycollection.php b/lib/jsonactivitycollection.php new file mode 100644 index 0000000000..1d1b482e04 --- /dev/null +++ b/lib/jsonactivitycollection.php @@ -0,0 +1,44 @@ +items = empty($items) ? array() : $items; + $this->totalItems = count($items); + $this->url = $url; + } + + /** + * Get the total number of items in the collection + * + * @return int total the total + */ + public function getTotalItems() + { + $this->totalItems = count($items); + return $this->totalItems; + } +} diff --git a/lib/noticelistitem.php b/lib/noticelistitem.php index 36668a8f77..88bb8bf927 100644 --- a/lib/noticelistitem.php +++ b/lib/noticelistitem.php @@ -64,6 +64,7 @@ class NoticeListItem extends Widget protected $options = true; protected $maxchars = 0; // if <= 0 it means use full posts protected $item_tag = 'li'; + protected $pa = null; /** * constructor @@ -150,7 +151,13 @@ class NoticeListItem extends Widget $this->elementStart('section', array('class'=>'notice-headers')); $this->showNoticeTitle(); $this->showAuthor(); - if ($this->addressees) { $this->showAddressees(); } + + if (!empty($this->notice->reply_to) || count($this->getProfileAddressees()) > 0) { + $this->elementStart('div', array('class' => 'parents')); + if (!empty($this->notice->reply_to)) { $this->showParent(); } + if ($this->addressees) { $this->showAddressees(); } + $this->elementEnd('div'); + } $this->elementEnd('section'); } @@ -237,8 +244,9 @@ class NoticeListItem extends Widget function showAuthor() { $attrs = array('href' => $this->profile->profileurl, - 'class' => 'h-card p-author', + 'class' => 'h-card', 'title' => $this->profile->getNickname()); + if(empty($this->repeat)) { $attrs['class'] .= ' p-author'; } if (Event::handle('StartShowNoticeItemAuthor', array($this->profile, $this->out, &$attrs))) { $this->out->elementStart('a', $attrs); @@ -249,6 +257,19 @@ class NoticeListItem extends Widget } } + function showParent() + { + $this->out->element( + 'a', + array( + 'href' => $this->notice->getParent()->getUrl(), + 'class' => 'u-in-reply-to', + 'rel' => 'in-reply-to' + ), + 'in reply to' + ); + } + function showAddressees() { $pa = $this->getProfileAddressees(); @@ -269,19 +290,20 @@ class NoticeListItem extends Widget function getProfileAddressees() { - $pa = array(); + if($this->pa) { return $this->pa; } + $this->pa = array(); $attentions = $this->getReplyProfiles(); foreach ($attentions as $attn) { $class = $attn->isGroup() ? 'group' : 'account'; - $pa[] = array('href' => $attn->profileurl, - 'title' => $attn->getNickname(), - 'class' => "addressee {$class}", - 'text' => $attn->getStreamName()); + $this->pa[] = array('href' => $attn->profileurl, + 'title' => $attn->getNickname(), + 'class' => "addressee {$class}", + 'text' => $attn->getStreamName()); } - return $pa; + return $this->pa; } function getReplyProfiles() diff --git a/plugins/Favorite/FavoritePlugin.php b/plugins/Favorite/FavoritePlugin.php index eb73bbe43c..afe905a109 100644 --- a/plugins/Favorite/FavoritePlugin.php +++ b/plugins/Favorite/FavoritePlugin.php @@ -194,7 +194,7 @@ class FavoritePlugin extends ActivityVerbHandlerPlugin $actobj = $act->objects[0]; $object = Fave::saveActivityObject($actobj, $stored); - $stored->object_type = ActivityUtils::resolveUri($object->getObjectType(), true); + $stored->object_type = $object->getObjectType(); return $object; } @@ -527,8 +527,8 @@ class FavoritePlugin extends ActivityVerbHandlerPlugin $expected_verb = $exists ? ActivityVerb::UNFAVORITE : ActivityVerb::FAVORITE; switch (true) { - case $exists && ActivityUtils::compareTypes($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)): - case !$exists && ActivityUtils::compareTypes($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)): + case $exists && ActivityUtils::compareVerbs($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)): + case !$exists && ActivityUtils::compareVerbs($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)): common_redirect(common_local_url('activityverb', array('id' => $target->getID(), 'verb' => ActivityUtils::resolveUri($expected_verb, true)))); @@ -543,10 +543,10 @@ class FavoritePlugin extends ActivityVerbHandlerPlugin protected function doActionPost(ManagedAction $action, $verb, Notice $target, Profile $scoped) { switch (true) { - case ActivityUtils::compareTypes($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)): + case ActivityUtils::compareVerbs($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)): Fave::addNew($scoped, $target); break; - case ActivityUtils::compareTypes($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)): + case ActivityUtils::compareVerbs($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)): Fave::removeEntry($scoped, $target); break; default: diff --git a/plugins/Favorite/classes/Fave.php b/plugins/Favorite/classes/Fave.php index 310679ef63..51e11153db 100644 --- a/plugins/Favorite/classes/Fave.php +++ b/plugins/Favorite/classes/Fave.php @@ -357,7 +357,7 @@ class Fave extends Managed_DataObject $target = self::getTargetFromStored($stored); // The following logic was copied from StatusNet's Activity plugin - if (ActivityUtils::compareTypes($target->verb, array(ActivityVerb::POST))) { + if (ActivityUtils::compareVerbs($target->verb, array(ActivityVerb::POST))) { // "I like the thing you posted" $act->objects = $target->asActivity()->objects; } else { diff --git a/plugins/Linkback/LinkbackPlugin.php b/plugins/Linkback/LinkbackPlugin.php index a710abd7bf..e3519dac9e 100644 --- a/plugins/Linkback/LinkbackPlugin.php +++ b/plugins/Linkback/LinkbackPlugin.php @@ -65,9 +65,27 @@ class LinkbackPlugin extends Plugin // notice content $c = $notice->content; $this->notice = $notice; - // Ignoring results - common_replace_urls_callback($c, - array($this, 'linkbackUrl')); + + if(!$notice->getProfile()-> + getPref("linkbackplugin", "disable_linkbacks") + ) { + // Ignoring results + common_replace_urls_callback($c, + array($this, 'linkbackUrl')); + } + + if($notice->isRepeat()) { + $repeat = Notice::getByID($notice->repeat_of); + $this->linkbackUrl($repeat->getUrl()); + } else if(!empty($notice->reply_to)) { + $parent = $notice->getParent(); + $this->linkbackUrl($parent->getUrl()); + } + + $replyProfiles = Profile::multiGet('id', $notice->getReplies()); + foreach($replyProfiles->fetchAll('profileurl') as $profileurl) { + $this->linkbackUrl($profileurl); + } } return true; } @@ -95,32 +113,89 @@ class LinkbackPlugin extends Plugin return $orig; } - $pb = null; - $tb = null; + // XXX: Should handle relative-URI resolution in these detections - if (array_key_exists('X-Pingback', $result->headers)) { - $pb = $result->headers['X-Pingback']; - } else if (preg_match('//', - $result->body, - $match)) { - $pb = $match[1]; - } - - if (!empty($pb)) { - $this->pingback($result->final_url, $pb); + $wm = $this->getWebmention($result); + if(!empty($wm)) { + // It is the webmention receiver's job to resolve source + // Ref: https://github.com/converspace/webmention/issues/43 + $this->webmention($url, $wm); } else { - $tb = $this->getTrackback($result->body, $result->final_url); - if (!empty($tb)) { - $this->trackback($result->final_url, $tb); + $pb = $this->getPingback($result); + if (!empty($pb)) { + // Pingback still looks for exact URL in our source, so we + // must send what we have + $this->pingback($url, $pb); + } else { + $tb = $this->getTrackback($result); + if (!empty($tb)) { + $this->trackback($result->final_url, $tb); + } } } return $orig; } + // Based on https://github.com/indieweb/mention-client-php + // which is licensed Apache 2.0 + function getWebmention($result) { + // XXX: the fetcher only gives back one of each header, so this may fail on multiple Link headers + if(preg_match('~<((?:https?://)?[^>]+)>; rel="webmention"~', $result->headers['Link'], $match)) { + return $match[1]; + } elseif(preg_match('~<((?:https?://)?[^>]+)>; rel="http://webmention.org/?"~', $result->headers['Link'], $match)) { + return $match[1]; + } + + if(preg_match('/<(?:link|a)[ ]+href="([^"]+)"[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]*\/?>/i', $result->body, $match) + || preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]+href="([^"]+)"[ ]*\/?>/i', $result->body, $match)) { + return $match[1]; + } elseif(preg_match('/<(?:link|a)[ ]+href="([^"]+)"[ ]+rel="http:\/\/webmention\.org\/?"[ ]*\/?>/i', $result->body, $match) + || preg_match('/<(?:link|a)[ ]+rel="http:\/\/webmention\.org\/?"[ ]+href="([^"]+)"[ ]*\/?>/i', $result->body, $match)) { + return $match[1]; + } + } + + function webmention($url, $endpoint) { + $source = $this->notice->getUrl(); + + $payload = array( + 'source' => $source, + 'target' => $url + ); + + $request = HTTPClient::start(); + try { + $response = $request->post($endpoint, + array( + 'Content-type: application/x-www-form-urlencoded', + 'Accept: application/json' + ), + $payload + ); + + if(!in_array($response->getStatus(), array(200,202))) { + common_log(LOG_WARNING, + "Webmention request failed for '$url' ($endpoint)"); + } + } catch (HTTP_Request2_Exception $e) { + common_log(LOG_WARNING, + "Webmention request failed for '$url' ($endpoint)"); + } + } + + function getPingback($result) { + if (array_key_exists('X-Pingback', $result->headers)) { + return $result->headers['X-Pingback']; + } else if(preg_match('/<(?:link|a)[ ]+href="([^"]+)"[ ]+rel="[^" ]* ?pingback ?[^" ]*"[ ]*\/?>/i', $result->body, $match) + || preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?pingback ?[^" ]*"[ ]+href="([^"]+)"[ ]*\/?>/i', $result->body, $match)) { + return $match[1]; + } + } + function pingback($url, $endpoint) { - $args = array($this->notice->uri, $url); + $args = array($this->notice->getUrl(), $url); if (!extension_loaded('xmlrpc')) { if (!dl('xmlrpc.so')) { @@ -131,9 +206,10 @@ class LinkbackPlugin extends Plugin $request = HTTPClient::start(); try { + $request->setBody(xmlrpc_encode_request('pingback.ping', $args)); $response = $request->post($endpoint, array('Content-Type: text/xml'), - xmlrpc_encode_request('pingback.ping', $args)); + false); $response = xmlrpc_decode($response->getBody()); if (xmlrpc_is_fault($response)) { common_log(LOG_WARNING, @@ -153,8 +229,11 @@ class LinkbackPlugin extends Plugin // Largely cadged from trackback_cls.php by // Ran Aroussi , GPL2 or any later version // http://phptrackback.sourceforge.net/ - function getTrackback($text, $url) + function getTrackback($result) { + $text = $result->body; + $url = $result->final_url; + if (preg_match_all('/()/sm', $text, $match, PREG_SET_ORDER)) { for ($i = 0; $i < count($match); $i++) { if (preg_match('|dc:identifier="' . preg_quote($url) . '"|ms', $match[$i][1])) { @@ -246,4 +325,23 @@ class LinkbackPlugin extends Plugin 'or Trackback protocols.')); return true; } + + public function onStartInitializeRouter(URLMapper $m) + { + $m->connect('settings/linkback', array('action' => 'linkbacksettings')); + return true; + } + + function onEndAccountSettingsNav($action) + { + $action_name = $action->trimmed('action'); + + $action->menuItem(common_local_url('linkbacksettings'), + // TRANS: OpenID plugin menu item on user settings page. + _m('MENU', 'Send Linkbacks'), + // TRANS: OpenID plugin tooltip for user settings menu item. + _m('Opt-out of sending linkbacks.'), + $action_name === 'linkbacksettings'); + return true; + } } diff --git a/plugins/Linkback/actions/linkbacksettings.php b/plugins/Linkback/actions/linkbacksettings.php new file mode 100644 index 0000000000..261e2979dc --- /dev/null +++ b/plugins/Linkback/actions/linkbacksettings.php @@ -0,0 +1,91 @@ +. + * + * @category Settings + * @package StatusNet + * @author Stephen Paul Weber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Settings for Linkback + * + * Lets users opt out of sending linkbacks + * + * @category Settings + * @author Stephen Paul Weber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + */ +class LinkbacksettingsAction extends SettingsAction +{ + /** + * Title of the page + * + * @return string Page title + */ + function title() + { + // TRANS: Title of Linkback settings page for a user. + return _m('TITLE','Linkback settings'); + } + + /** + * Instructions for use + * + * @return string Instructions for use + */ + function getInstructions() + { + // TRANS: Form instructions for Linkback settings. + return _m('Linkbacks inform post authors when you link to them. ' . + 'You can disable this feature here.'); + } + + function showContent() + { + $this->elementStart('form', array('method' => 'post', + 'class' => 'form_settings', + 'action' => + common_local_url('linkbacksettings'))); + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset'); + $this->element('legend', null, _m('LEGEND','Preferences')); + $this->checkbox('disable_linkbacks', "Opt out of sending linkbacks for URLs you post", $this->scoped->getPref("linkbackplugin", "disable_linkbacks")); + // TRANS: Button text to save OpenID prefs + $this->submit('settings_linkback_prefs_save', _m('BUTTON','Save'), 'submit', 'save_prefs'); + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + } + + /** + * Handle a POST request + * + * @return void + */ + protected function doPost() + { + $x = $this->scoped->setPref("linkbackplugin", "disable_linkbacks", $this->boolean('disable_linkbacks')); + + return _m('Linkback preferences saved.'); + } +} diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 41f9abfcdf..8f729bef6f 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -104,6 +104,9 @@ class OStatusPlugin extends Plugin // Incoming from a foreign PuSH hub $qm->connect('pushin', 'PushInQueueHandler'); + + // Re-subscribe feeds that need renewal + $qm->connect('pushrenew', 'PushRenewQueueHandler'); return true; } @@ -1351,4 +1354,20 @@ class OStatusPlugin extends Plugin } return true; } + + public function onCronDaily() + { + try { + $sub = FeedSub::renewalCheck(); + } catch (NoResultException $e) { + common_log(LOG_INFO, "There were no expiring feeds."); + return; + } + + $qm = QueueManager::get(); + while ($sub->fetch()) { + $item = array('feedsub_id' => $sub->id); + $qm->enqueue($item, 'pushrenew'); + } + } } diff --git a/plugins/OStatus/classes/FeedSub.php b/plugins/OStatus/classes/FeedSub.php index 149d57f63c..b0875c2988 100644 --- a/plugins/OStatus/classes/FeedSub.php +++ b/plugins/OStatus/classes/FeedSub.php @@ -295,7 +295,7 @@ class FeedSub extends Managed_DataObject { $fs = new FeedSub(); // the "" empty string check is because we historically haven't saved unsubscribed feeds as NULL - $fs->whereAdd('sub_end IS NOT NULL AND sub_end!="" AND sub_end < NOW() - INTERVAL 1 day'); + $fs->whereAdd('sub_end IS NOT NULL AND sub_end!="" AND sub_end < NOW() + INTERVAL 1 day'); if (!$fs->find()) { // find can be both false and 0, depending on why nothing was found throw new NoResultException($fs); } @@ -355,7 +355,7 @@ class FeedSub extends Managed_DataObject $response = $client->post($hub, $headers, $post); $status = $response->getStatus(); // PuSH specificed response status code - if ($status == 202) { + if ($status == 202 || $status == 204) { common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback'); return; } else if ($status >= 200 && $status < 300) { diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index be8a6f66a7..ddd1a2a38d 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1004,11 +1004,13 @@ class Ostatus_profile extends Managed_DataObject } } + $obj = ActivityUtils::getFeedAuthor($feedEl); + // @todo FIXME: We should check whether this feed has elements // with different or elements, and... I dunno. // Do something about that. - $obj = ActivityObject::fromRssChannel($feedEl); + if(empty($obj)) { $obj = ActivityObject::fromRssChannel($feedEl); } return self::ensureActivityObjectProfile($obj, $hints); } diff --git a/plugins/OStatus/lib/pushrenewqueuehandler.php b/plugins/OStatus/lib/pushrenewqueuehandler.php new file mode 100644 index 0000000000..d79cbe503f --- /dev/null +++ b/plugins/OStatus/lib/pushrenewqueuehandler.php @@ -0,0 +1,49 @@ +. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Renew an expiring feedsub + * @package FeedSub + * @author Stephen Paul Weber + */ +class PushRenewQueueHandler extends QueueHandler +{ + function transport() + { + return 'pushrenew'; + } + + function handle($data) + { + $feedsub_id = $data['feedsub_id']; + $feedsub = FeedSub::getKV('id', $feedsub_id); + if ($feedsub instanceof FeedSub) { + try { + common_log(LOG_INFO, "Renewing feed subscription\n\tExp.: {$feedsub->sub_end}\n\tFeed: {$feedsub->uri}\n\tHub: {$feedsub->huburi}"); + $feedsub->renew(); + } catch(Exception $e) { + common_log(LOG_ERR, "Exception during PuSH renew processing for $feedsub->uri: " . $e->getMessage()); + } + } else { + common_log(LOG_ERR, "Discarding renew for unknown feed subscription id $feedsub_id"); + } + return true; + } +} diff --git a/plugins/Share/SharePlugin.php b/plugins/Share/SharePlugin.php index c337efbaec..c31686e445 100644 --- a/plugins/Share/SharePlugin.php +++ b/plugins/Share/SharePlugin.php @@ -204,7 +204,7 @@ class SharePlugin extends ActivityVerbHandlerPlugin 'class' => 'h-card p-author', 'title' => $repeater->getFancyName()); - $nli->out->elementStart('span', 'repeat h-entry'); + $nli->out->elementStart('span', 'repeat'); // TRANS: Addition in notice list item if notice was repeated. Followed by a span with a nickname. $nli->out->raw(_('Repeated by').' '); diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index 28dc1bf079..1edc3d8971 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -148,7 +148,7 @@ class WebFingerPlugin extends Plugin $url = common_local_url('webfinger') . '?resource='.$acct; foreach (array(Discovery::JRD_MIMETYPE, Discovery::XRD_MIMETYPE) as $type) { - header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="'.$type.'"'); + header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="'.$type.'"', false); } } } diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 20f9aa775f..0d3515ba33 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -648,12 +648,12 @@ address .poweredby { width:100%; } -.notice .p-author { - margin-right: 8px; +.notice .parents { + display: inline; } -.notice .addressees::before { - content: '\25B8'; +.notice .parents::before { + content: '\25B8 '; } .notice .addressees, .notice .addressees li { display: inline;