From 67dfc0a0469a221ad8cdcefb45609764158be4ad Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 00:04:14 +0100 Subject: [PATCH 01/55] application/xml allowed in uploads --- lib/default.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/default.php b/lib/default.php index f90892e169..d9f377ecd7 100644 --- a/lib/default.php +++ b/lib/default.php @@ -233,6 +233,7 @@ $default = 'application/vnd.oasis.opendocument.text-web' => 'oth', 'application/pdf' => 'pdf', 'application/zip' => 'zip', + 'application/xml' => 'xml', 'image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', From 7fdcbd56d5c4a3d01165c1369a60ec4a1bcc1556 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 21:18:44 +0100 Subject: [PATCH 02/55] XMPP URI scheme for HTMLPurifier --- .../lib/htmlpurifier/urischeme/xmpp.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 plugins/HTMLPurifierSchemes/lib/htmlpurifier/urischeme/xmpp.php diff --git a/plugins/HTMLPurifierSchemes/lib/htmlpurifier/urischeme/xmpp.php b/plugins/HTMLPurifierSchemes/lib/htmlpurifier/urischeme/xmpp.php new file mode 100644 index 0000000000..6d8dcc2bcf --- /dev/null +++ b/plugins/HTMLPurifierSchemes/lib/htmlpurifier/urischeme/xmpp.php @@ -0,0 +1,35 @@ +userinfo = null; + $uri->host = null; + $uri->port = null; + return true; + } +} + +// vim: et sw=4 sts=4 From 38a187b93ebe1069f105ccd7570b7bb6ab38fc50 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 22:19:56 +0100 Subject: [PATCH 03/55] Delete orphan files maintenance script When deleting a profile it'll delete its notices and the coupling to file entries, but not the file entries themselves (and thus not the files). So if one to delete a person uploading offending images, then the images are left behind and can be hotlinked. This will remove it. --- scripts/delete_orphan_files.php | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100755 scripts/delete_orphan_files.php diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php new file mode 100755 index 0000000000..ad575187bd --- /dev/null +++ b/scripts/delete_orphan_files.php @@ -0,0 +1,66 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$shortoptions = 'y'; +$longoptions = array('yes'); + +$helptext = <<query($sql) !== false) { + print " Deleting {$file->N} entries: "; + while ($file->fetch()) { + try { + $file->getPath(); + $file->delete(); + print 'x'; + } catch (Exception $e) { + // either FileNotFound exception or ClientException + $file->delete(); + print '.'; + } + } +} +print "\nDONE.\n"; From 6f2fbd448d97cd5b14e19905c89918ecad79babb Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 22:43:26 +0100 Subject: [PATCH 04/55] Fixed the delete orphan script to include deleted notices The file_to_post table sometimes had post_id with values that did not exist in the notice table. --- scripts/delete_orphan_files.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php index ad575187bd..5e6c2633a8 100755 --- a/scripts/delete_orphan_files.php +++ b/scripts/delete_orphan_files.php @@ -37,20 +37,28 @@ END_OF_HELP; require_once INSTALLDIR.'/scripts/commandline.inc'; +print "Finding File entries that are not related to a Notice (or the notice has been deleted)..."; +$file = new File(); +$sql = 'SELECT id FROM file'. + ' JOIN file_to_post ON file_to_post.file_id=file.id'. + ' WHERE'. + ' NOT EXISTS (SELECT file_to_post.file_id FROM file_to_post WHERE file.id=file_id)'. + ' OR NOT EXISTS (SELECT notice.id FROM notice WHERE notice.id=post_id);'; + +print " {$file->N} found.\n"; +if ($file->N == 0) { + exit(0); +} if (!have_option('y', 'yes')) { - print "About to delete local files that are not related to a Notice. Are you sure? [y/N] "; + print "About to delete the entries along with locally stored files. Are you sure? [y/N] "; $response = fgets(STDIN); if (strtolower(trim($response)) != 'y') { print "Aborting.\n"; exit(0); } } - -print "Finding File entries..."; -$file = new File(); -$sql = 'SELECT * FROM file WHERE NOT EXISTS (SELECT id FROM file_to_post WHERE file.id=file_id);'; if ($file->query($sql) !== false) { - print " Deleting {$file->N} entries: "; + print "\nDeleting: "; while ($file->fetch()) { try { $file->getPath(); From 2198f3959705f3d9342b4c8bd3301cdbda189f41 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 22:49:45 +0100 Subject: [PATCH 05/55] Haha, it essentially became a NOOP with the last commit --- scripts/delete_orphan_files.php | 41 ++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php index 5e6c2633a8..aa3046855f 100755 --- a/scripts/delete_orphan_files.php +++ b/scripts/delete_orphan_files.php @@ -39,16 +39,22 @@ require_once INSTALLDIR.'/scripts/commandline.inc'; print "Finding File entries that are not related to a Notice (or the notice has been deleted)..."; $file = new File(); -$sql = 'SELECT id FROM file'. +$sql = 'SELECT file.id FROM file'. ' JOIN file_to_post ON file_to_post.file_id=file.id'. ' WHERE'. - ' NOT EXISTS (SELECT file_to_post.file_id FROM file_to_post WHERE file.id=file_id)'. - ' OR NOT EXISTS (SELECT notice.id FROM notice WHERE notice.id=post_id);'; + ' NOT EXISTS (SELECT file_to_post.file_id FROM file_to_post WHERE file.id=file_to_post.file_id)'. + ' OR NOT EXISTS (SELECT notice.id FROM notice WHERE notice.id=file_to_post.post_id);'; -print " {$file->N} found.\n"; -if ($file->N == 0) { - exit(0); +if ($file->query($sql) !== false) { + print " {$file->N} found.\n"; + if ($file->N == 0) { + exit(0); + } +} else { + print "FAILED"; + exit(1); } + if (!have_option('y', 'yes')) { print "About to delete the entries along with locally stored files. Are you sure? [y/N] "; $response = fgets(STDIN); @@ -57,18 +63,17 @@ if (!have_option('y', 'yes')) { exit(0); } } -if ($file->query($sql) !== false) { - print "\nDeleting: "; - while ($file->fetch()) { - try { - $file->getPath(); - $file->delete(); - print 'x'; - } catch (Exception $e) { - // either FileNotFound exception or ClientException - $file->delete(); - print '.'; - } + +print "\nDeleting: "; +while ($file->fetch()) { + try { + $file->getPath(); + $file->delete(); + print 'x'; + } catch (Exception $e) { + // either FileNotFound exception or ClientException + $file->delete(); + print '.'; } } print "\nDONE.\n"; From 05fea4cdc6488e0b56cb781bed3a52204a11cd40 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 22:54:29 +0100 Subject: [PATCH 06/55] Aurhg, and get all the properties, not just id --- scripts/delete_orphan_files.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php index aa3046855f..e11220b373 100755 --- a/scripts/delete_orphan_files.php +++ b/scripts/delete_orphan_files.php @@ -39,7 +39,7 @@ require_once INSTALLDIR.'/scripts/commandline.inc'; print "Finding File entries that are not related to a Notice (or the notice has been deleted)..."; $file = new File(); -$sql = 'SELECT file.id FROM file'. +$sql = 'SELECT file.* FROM file'. ' JOIN file_to_post ON file_to_post.file_id=file.id'. ' WHERE'. ' NOT EXISTS (SELECT file_to_post.file_id FROM file_to_post WHERE file.id=file_to_post.file_id)'. From 1471defff322bdf39554412f12738a46424b174d Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 11 Feb 2016 23:38:12 +0100 Subject: [PATCH 07/55] ...and avoid duplicate results... --- scripts/delete_orphan_files.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php index e11220b373..33293cfb8b 100755 --- a/scripts/delete_orphan_files.php +++ b/scripts/delete_orphan_files.php @@ -43,7 +43,8 @@ $sql = 'SELECT file.* FROM file'. ' JOIN file_to_post ON file_to_post.file_id=file.id'. ' WHERE'. ' NOT EXISTS (SELECT file_to_post.file_id FROM file_to_post WHERE file.id=file_to_post.file_id)'. - ' OR NOT EXISTS (SELECT notice.id FROM notice WHERE notice.id=file_to_post.post_id);'; + ' OR NOT EXISTS (SELECT notice.id FROM notice WHERE notice.id=file_to_post.post_id)'. + ' GROUP BY file.id;'; if ($file->query($sql) !== false) { print " {$file->N} found.\n"; From 913595780f0217282754b46b6c24f08676821372 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 00:05:36 +0100 Subject: [PATCH 08/55] And LEFT JOIN to actually get all results --- scripts/delete_orphan_files.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php index 33293cfb8b..cae35a819f 100755 --- a/scripts/delete_orphan_files.php +++ b/scripts/delete_orphan_files.php @@ -40,7 +40,7 @@ require_once INSTALLDIR.'/scripts/commandline.inc'; print "Finding File entries that are not related to a Notice (or the notice has been deleted)..."; $file = new File(); $sql = 'SELECT file.* FROM file'. - ' JOIN file_to_post ON file_to_post.file_id=file.id'. + ' LEFT JOIN file_to_post ON file_to_post.file_id=file.id'. ' WHERE'. ' NOT EXISTS (SELECT file_to_post.file_id FROM file_to_post WHERE file.id=file_to_post.file_id)'. ' OR NOT EXISTS (SELECT notice.id FROM notice WHERE notice.id=file_to_post.post_id)'. From c8753353ed4eeeb5c72c874f591c37f9c54b6e4a Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 01:45:47 +0100 Subject: [PATCH 09/55] Do not delete_orphan_files on an instance with Qvitter --- scripts/delete_orphan_files.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php index cae35a819f..2c49c10a50 100755 --- a/scripts/delete_orphan_files.php +++ b/scripts/delete_orphan_files.php @@ -33,6 +33,9 @@ attached files weren't removed as well. Will print '.' for each deleted File entry and 'x' if it also had a locally stored file. +WARNING WARNING WARNING, this will also delete Qvitter files such as background etc. since +they are not linked to notices (yet anyway). + END_OF_HELP; require_once INSTALLDIR.'/scripts/commandline.inc'; From 338df7e35b252fff50da404a71ed82b095b9b8c1 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 02:21:11 +0100 Subject: [PATCH 10/55] Fix Nickname::isSystemPath() work properly for routes --- lib/nickname.php | 10 +++++----- lib/urlmapper.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/nickname.php b/lib/nickname.php index 1ed0abbe78..c49ce20f5e 100644 --- a/lib/nickname.php +++ b/lib/nickname.php @@ -180,18 +180,18 @@ class Nickname // All directory and file names in site root should be blacklisted $d = dir(INSTALLDIR); while (false !== ($entry = $d->read())) { - $paths[] = $entry; + $paths[$entry] = true; } $d->close(); // All top level names in the router should be blacklisted $router = Router::get(); - foreach (array_keys($router->m->getPaths()) as $path) { - if (preg_match('/^\/(.*?)[\/\?]/',$path,$matches)) { - $paths[] = $matches[1]; + foreach ($router->m->getPaths() as $path) { + if (preg_match('/^([^\/\?]+)[\/\?]/',$path,$matches) && isset($matches[1])) { + $paths[$matches[1]] = true; } } - return in_array($str, $paths); + return in_array($str, array_keys($paths)); } /** diff --git a/lib/urlmapper.php b/lib/urlmapper.php index ae31147203..931b5c3c2a 100644 --- a/lib/urlmapper.php +++ b/lib/urlmapper.php @@ -66,7 +66,7 @@ class URLMapper throw new Exception(sprintf("Can't connect %s; path has no action.", $path)); } - $allpaths[] = $path; + $this->allpaths[] = $path; $action = $args[self::ACTION]; From f10625f8bc59e3ae52da07fdba910329fad540a3 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 02:29:33 +0100 Subject: [PATCH 11/55] file and avatar dirs on instances with no such dirs in filesystem --- lib/nickname.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/nickname.php b/lib/nickname.php index c49ce20f5e..2dd08efc3f 100644 --- a/lib/nickname.php +++ b/lib/nickname.php @@ -191,6 +191,12 @@ class Nickname $paths[$matches[1]] = true; } } + + // FIXME: this assumes the 'path' is in the first-level directory, though common it's not certain + foreach (['avatar', 'attachments'] as $cat) { + $paths[basename(common_config($cat, 'path'))] = true; + } + return in_array($str, array_keys($paths)); } From 5dce08d068b13353ad7bb4de339f4a4996710aff Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 13:52:48 +0100 Subject: [PATCH 12/55] Add Profile::ensureCurrent() to verify we _certainly_ got a Profile. --- classes/Profile.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/classes/Profile.php b/classes/Profile.php index 875ad9ade1..e4ab508c06 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1628,6 +1628,15 @@ class Profile extends Managed_DataObject return $profile; } + static function ensureCurrent() + { + $profile = self::current(); + if (!$profile instanceof Profile) { + throw new AuthorizationException('A currently scoped profile is required.'); + } + return $profile; + } + /** * Magic function called at serialize() time. * From e5ad98e60150ec04686f6e58449e6ea914ca271c Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 14:22:25 +0100 Subject: [PATCH 13/55] Silence action can only be used on non-priviliged users --- actions/silence.php | 46 ++++--------------------------------- actions/unsilence.php | 48 +++++---------------------------------- classes/Profile.php | 29 +++++++++++++++++++++++ lib/profileformaction.php | 6 ++++- 4 files changed, 45 insertions(+), 84 deletions(-) diff --git a/actions/silence.php b/actions/silence.php index 6a4f84deb9..dccaf70a37 100644 --- a/actions/silence.php +++ b/actions/silence.php @@ -27,9 +27,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Silence a user. @@ -42,45 +40,11 @@ if (!defined('STATUSNET')) { */ class SilenceAction extends ProfileFormAction { - /** - * Check parameters - * - * @param array $args action arguments (URL, GET, POST) - * - * @return boolean success flag - */ - function prepare($args) - { - if (!parent::prepare($args)) { - return false; - } - - $cur = common_current_user(); - - assert(!empty($cur)); // checked by parent - - if (!$cur->hasRight(Right::SILENCEUSER)) { - // TRANS: Client error displayed trying to silence a user on a site where the feature is not enabled. - $this->clientError(_('You cannot silence users on this site.')); - } - - assert(!empty($this->profile)); // checked by parent - - if ($this->profile->isSilenced()) { - // TRANS: Client error displayed trying to silence an already silenced user. - $this->clientError(_('User is already silenced.')); - } - - return true; - } - - /** - * Silence a user. - * - * @return void - */ function handlePost() { - $this->profile->silence(); + assert($this->scoped instanceof Profile); + assert($this->profile instanceof Profile); + + $this->profile->silenceAs($this->scoped); } } diff --git a/actions/unsilence.php b/actions/unsilence.php index c01c141b1c..f1305373df 100644 --- a/actions/unsilence.php +++ b/actions/unsilence.php @@ -27,12 +27,10 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** - * Silence a user. + * Unsilence a user. * * @category Action * @package StatusNet @@ -42,45 +40,11 @@ if (!defined('STATUSNET')) { */ class UnsilenceAction extends ProfileFormAction { - /** - * Check parameters - * - * @param array $args action arguments (URL, GET, POST) - * - * @return boolean success flag - */ - function prepare($args) - { - if (!parent::prepare($args)) { - return false; - } - - $cur = common_current_user(); - - assert(!empty($cur)); // checked by parent - - if (!$cur->hasRight(Right::SILENCEUSER)) { - // TRANS: Client error on page to unsilence a user when the feature is not enabled. - $this->clientError(_('You cannot silence users on this site.')); - } - - assert(!empty($this->profile)); // checked by parent - - if (!$this->profile->isSilenced()) { - // TRANS: Client error on page to unsilence a user when the to be unsilenced user has not been silenced. - $this->clientError(_('User is not silenced.')); - } - - return true; - } - - /** - * Silence a user. - * - * @return void - */ function handlePost() { - $this->profile->unsilence(); + assert($this->scoped instanceof Profile); + assert($this->profile instanceof Profile); + + $this->profile->unsilenceAs($this->scoped); } } diff --git a/classes/Profile.php b/classes/Profile.php index e4ab508c06..0eaa06120a 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1174,6 +1174,22 @@ class Profile extends Managed_DataObject } } + function silenceAs(Profile $actor) + { + if (!$actor->hasRight(Right::SILENCEUSER)) { + throw new AuthorizationException(_('You cannot silence users on this site.')); + } + // Only administrators can silence other priviliged users (those who have the right to silence as well). + if ($this->hasRight(Right::SILENCEUSER) && !$actor->hasRole(Profile_role::ADMINISTRATOR)) { + throw new AuthorizationException(_('You cannot silence other priviliged users.')); + } + if ($this->isSilenced()) { + // TRANS: Client error displayed trying to silence an already silenced user. + throw new AlreadyFulfilledException(_('User is already silenced.')); + } + return $this->silence(); + } + function unsilence() { $this->revokeRole(Profile_role::SILENCED); @@ -1182,6 +1198,19 @@ class Profile extends Managed_DataObject } } + function unsilenceAs(Profile $actor) + { + if (!$actor->hasRight(Right::SILENCEUSER)) { + // TRANS: Client error displayed trying to unsilence a user when the user does not have the right. + throw new AuthorizationException(_('You cannot unsilence users on this site.')); + } + if (!$this->isSilenced()) { + // TRANS: Client error displayed trying to unsilence a user when the target user has not been silenced. + throw new AlreadyFulfilledException(_('User is not silenced.')); + } + return $this->unsilence(); + } + function flushVisibility() { // Get all notices diff --git a/lib/profileformaction.php b/lib/profileformaction.php index 9ace6676c3..1e00e6f12b 100644 --- a/lib/profileformaction.php +++ b/lib/profileformaction.php @@ -101,7 +101,11 @@ class ProfileFormAction extends RedirectingAction parent::handle($args); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost(); + try { + $this->handlePost(); + } catch (AlreadyFulfilledException $e) { + // 'tis alright + } $this->returnToPrevious(); } } From 3cef75bcac999487fe88b5bd3250abb408c4c4a9 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 14:38:03 +0100 Subject: [PATCH 14/55] Update the comment on silencing privileged users in ModHelper --- plugins/ModHelper/ModHelperPlugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/ModHelper/ModHelperPlugin.php b/plugins/ModHelper/ModHelperPlugin.php index 2752a21539..88f2f2a731 100644 --- a/plugins/ModHelper/ModHelperPlugin.php +++ b/plugins/ModHelper/ModHelperPlugin.php @@ -17,9 +17,7 @@ * along with this program. If not, see . */ -if (!defined('STATUSNET')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * @package ModHelperPlugin @@ -45,7 +43,9 @@ class ModHelperPlugin extends Plugin function onUserRightsCheck($profile, $right, &$result) { if (in_array($right, self::$rights)) { - // Hrm.... really we should confirm that the *other* user isn't privleged. :) + // To silence a profile without accidentally silencing other + // privileged users, always call Profile->silenceAs($actor) + // since it checks target's privileges too. if ($profile->hasRole('modhelper')) { $result = true; return false; From 83f679fb577fd8da2496e3672721a801b50f58d4 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 14:47:49 +0100 Subject: [PATCH 15/55] Profile->isPrivileged() to check if users have more rights than to post etc. --- classes/Profile.php | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index 0eaa06120a..7aae98fb5f 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1179,9 +1179,9 @@ class Profile extends Managed_DataObject if (!$actor->hasRight(Right::SILENCEUSER)) { throw new AuthorizationException(_('You cannot silence users on this site.')); } - // Only administrators can silence other priviliged users (those who have the right to silence as well). - if ($this->hasRight(Right::SILENCEUSER) && !$actor->hasRole(Profile_role::ADMINISTRATOR)) { - throw new AuthorizationException(_('You cannot silence other priviliged users.')); + // Only administrators can silence other privileged users (such as others who have the right to silence). + if ($this->isPrivileged() && !$actor->hasRole(Profile_role::ADMINISTRATOR)) { + throw new AuthorizationException(_('You cannot silence other privileged users.')); } if ($this->isSilenced()) { // TRANS: Client error displayed trying to silence an already silenced user. @@ -1221,6 +1221,22 @@ class Profile extends Managed_DataObject } } + public function isPrivileged() + { + // TODO: An Event::handle so plugins can report if users are privileged. + // The ModHelper is the only one I care about when coding this, and that + // can be tested with Right::SILENCEUSER which I do below: + switch (true) { + case $this->hasRight(Right::SILENCEUSER): + case $this->hasRole(Profile_role::MODERATOR): + case $this->hasRole(Profile_role::ADMINISTRATOR): + case $this->hasRole(Profile_role::OWNER): + return true; + } + + return false; + } + /** * Does this user have the right to do X? * From c7c34ec05a4435371b9a4e35c0890908ef0e3af1 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 12 Feb 2016 15:00:18 +0100 Subject: [PATCH 16/55] Only administrators can delete other privileged users. --- actions/deleteuser.php | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/actions/deleteuser.php b/actions/deleteuser.php index 6b74575ab4..6e0c6ebf7f 100644 --- a/actions/deleteuser.php +++ b/actions/deleteuser.php @@ -27,9 +27,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Delete a user @@ -44,33 +42,30 @@ class DeleteuserAction extends ProfileFormAction { var $user = null; - /** - * Take arguments for running - * - * @param array $args $_REQUEST args - * - * @return boolean success flag - */ - function prepare($args) + function prepare(array $args=array()) { if (!parent::prepare($args)) { return false; } - $cur = common_current_user(); + assert($this->scoped instanceof Profile); - assert(!empty($cur)); // checked by parent - - if (!$cur->hasRight(Right::DELETEUSER)) { + if (!$this->scoped->hasRight(Right::DELETEUSER)) { // TRANS: Client error displayed when trying to delete a user without having the right to delete users. - $this->clientError(_('You cannot delete users.')); + throw new AuthorizationException(_('You cannot delete users.')); } - $this->user = User::getKV('id', $this->profile->id); - - if (empty($this->user)) { + try { + $this->user = $this->profile->getUser(); + } catch (NoSuchUserException $e) { // TRANS: Client error displayed when trying to delete a non-local user. - $this->clientError(_('You can only delete local users.')); + throw new ClientException(_('You can only delete local users.')); + } + + // Only administrators can delete other privileged users (such as others who have the right to silence). + if ($this->profile->isPrivileged() && !$this->scoped->hasRole(Profile_role::ADMINISTRATOR)) { + // TRANS: Client error displayed when trying to delete a user that has been granted moderation privileges + throw new AuthorizationException(_('You cannot delete other privileged users.')); } return true; From 557ad2d1fd70d41a52f2805f8f8e7fbfd9303f99 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 00:51:43 +0100 Subject: [PATCH 17/55] Show user registration IP to users who can see ModLog --- .../RegisterThrottlePlugin.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php index 9d3be3b8a2..b1e352474f 100644 --- a/plugins/RegisterThrottle/RegisterThrottlePlugin.php +++ b/plugins/RegisterThrottle/RegisterThrottlePlugin.php @@ -134,6 +134,37 @@ class RegisterThrottlePlugin extends Plugin return true; } + function onEndShowSections(Action $action) + { + if (!$action instanceof ShowstreamAction) { + // early return for actions we're not interested in + return true; + } + + $scoped = $action->getScoped(); + if (!$scoped instanceof Profile || !$scoped->hasRight(self::VIEWMODLOG)) { + // only continue if we are allowed to VIEWMODLOG + return true; + } + + $ri = Registration_ip::getKV('user_id', $profile->id); + $ipaddress = null; + if ($ri instanceof Registration_ip) { + $ipaddress = $ri->ipaddress; + unset($ri); + } + + $action->elementStart('div', array('id' => 'entity_mod_log', + 'class' => 'section')); + + $action->element('h2', null, _('Registration IP')); + + $action->element('strong', null, _('Registered from:')); + $action->element('span', ['class'=>'ipaddress'], $ipaddress ?: 'unknown'); + + $action->elementEnd('div'); + } + /** * Called after someone registers, by any means. * From be35975b12f348753c7e2da66bf1eec0e34b2784 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 01:02:18 +0100 Subject: [PATCH 18/55] RegisterThrottle list-profiles-by-ip --- .../RegisterThrottlePlugin.php | 23 +++++++++-- .../actions/ipregistrations.php | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 plugins/RegisterThrottle/actions/ipregistrations.php diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php index b1e352474f..1150cd0112 100644 --- a/plugins/RegisterThrottle/RegisterThrottlePlugin.php +++ b/plugins/RegisterThrottle/RegisterThrottlePlugin.php @@ -81,6 +81,13 @@ class RegisterThrottlePlugin extends Plugin return true; } + public function onRouterInitialized(URLMapper $m) + { + $m->connect('main/ipregistrations/:ipaddress', + array('action' => 'ipregistrations'), + array('ipaddress' => '[0-9a-f\.\:]+')); + } + /** * Called when someone tries to register. * @@ -141,6 +148,12 @@ class RegisterThrottlePlugin extends Plugin return true; } + $target = $action->getTarget(); + if (!$target->isSilenced()) { + // Only show the IP of users who are not silenced. + return true; + } + $scoped = $action->getScoped(); if (!$scoped instanceof Profile || !$scoped->hasRight(self::VIEWMODLOG)) { // only continue if we are allowed to VIEWMODLOG @@ -160,7 +173,11 @@ class RegisterThrottlePlugin extends Plugin $action->element('h2', null, _('Registration IP')); $action->element('strong', null, _('Registered from:')); - $action->element('span', ['class'=>'ipaddress'], $ipaddress ?: 'unknown'); + $action->element('a', + [ 'class'=>'ipaddress', + 'href'=>common_local_url('ipregistrations', array('ipaddress'=>$ipaddress)), + ], + $ipaddress ?: 'unknown'); $action->elementEnd('div'); } @@ -185,8 +202,8 @@ class RegisterThrottlePlugin extends Plugin $reg = new Registration_ip(); - $reg->user_id = $profile->id; - $reg->ipaddress = $ipaddress; + $reg->user_id = $profile->getID(); + $reg->ipaddress = mb_strtolower($ipaddress); $reg->created = common_sql_now(); $result = $reg->insert(); diff --git a/plugins/RegisterThrottle/actions/ipregistrations.php b/plugins/RegisterThrottle/actions/ipregistrations.php new file mode 100644 index 0000000000..31217483b5 --- /dev/null +++ b/plugins/RegisterThrottle/actions/ipregistrations.php @@ -0,0 +1,40 @@ +ipaddress); + } + + protected function doPreparation() + { + if (!$scoped->hasRight(self::VIEWMODLOG) && !$scoped->hasRole(Profile_role::ADMINISTRATOR)) { + throw new AuthorizationException(_('You do not have privileges to see this page')); + } + + $this->ipaddress = $this->trimmed('ipaddress'); + $this->profile_ids = Registration_ip::usersByIP($this->ipaddress); + } + + public function showContent() + { + $this->elementStart('ul'); + foreach (Profile::listGet('id', $this->profile_ids) as $profile) { + $this->elementStart('li'); + try { + $this->element('a', ['href'=>$profile->getUrl()], $profile->getFancyName()); + } catch (InvalidUrlException $e) { + $this->element('span', null, $profile->getFancyName()); + } + $this->elementEnd('li'); + } + $this->elementEnd('ul'); + } +} From 799c2e47fe208f6fe111e035cf7824447d790b0b Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 01:10:01 +0100 Subject: [PATCH 19/55] Don't depend on ModLog --- plugins/RegisterThrottle/RegisterThrottlePlugin.php | 4 ++-- plugins/RegisterThrottle/actions/ipregistrations.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php index 1150cd0112..88f2a96771 100644 --- a/plugins/RegisterThrottle/RegisterThrottlePlugin.php +++ b/plugins/RegisterThrottle/RegisterThrottlePlugin.php @@ -155,8 +155,8 @@ class RegisterThrottlePlugin extends Plugin } $scoped = $action->getScoped(); - if (!$scoped instanceof Profile || !$scoped->hasRight(self::VIEWMODLOG)) { - // only continue if we are allowed to VIEWMODLOG + if (!$scoped->hasRight(Right::SILENCEUSER)) { + // only show registration IP if we have the right to silence users return true; } diff --git a/plugins/RegisterThrottle/actions/ipregistrations.php b/plugins/RegisterThrottle/actions/ipregistrations.php index 31217483b5..a748a9d0b2 100644 --- a/plugins/RegisterThrottle/actions/ipregistrations.php +++ b/plugins/RegisterThrottle/actions/ipregistrations.php @@ -15,8 +15,8 @@ class IpregistrationsAction extends ManagedAction protected function doPreparation() { - if (!$scoped->hasRight(self::VIEWMODLOG) && !$scoped->hasRole(Profile_role::ADMINISTRATOR)) { - throw new AuthorizationException(_('You do not have privileges to see this page')); + if (!$scoped->hasRight(Right::SILENCEUSER) && !$scoped->hasRole(Profile_role::ADMINISTRATOR)) { + throw new AuthorizationException(_('You are not authorized to view this page.')); } $this->ipaddress = $this->trimmed('ipaddress'); From 8ef2abf30bc5abcbd9558cd63fb309161f5e223c Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 01:16:34 +0100 Subject: [PATCH 20/55] Render RegiserThrottle extra profile data properly --- .../RegisterThrottle/RegisterThrottlePlugin.php | 17 +++++++++++------ .../actions/ipregistrations.php | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php index 88f2a96771..552420d8f6 100644 --- a/plugins/RegisterThrottle/RegisterThrottlePlugin.php +++ b/plugins/RegisterThrottle/RegisterThrottlePlugin.php @@ -160,7 +160,7 @@ class RegisterThrottlePlugin extends Plugin return true; } - $ri = Registration_ip::getKV('user_id', $profile->id); + $ri = Registration_ip::getKV('user_id', $target->getID()); $ipaddress = null; if ($ri instanceof Registration_ip) { $ipaddress = $ri->ipaddress; @@ -172,12 +172,17 @@ class RegisterThrottlePlugin extends Plugin $action->element('h2', null, _('Registration IP')); + // TRANS: Label for the information about which IP a users registered from. $action->element('strong', null, _('Registered from:')); - $action->element('a', - [ 'class'=>'ipaddress', - 'href'=>common_local_url('ipregistrations', array('ipaddress'=>$ipaddress)), - ], - $ipaddress ?: 'unknown'); + $el = 'span'; + $attrs = ['class'=>'ipaddress']; + if (!is_null($ipaddress)) { + $el = 'a'; + $attrs['href'] = common_local_url('ipregistrations', array('ipaddress'=>$ipaddress)); + } + $action->element($el, $attrs, + // TRANS: Unknown IP address. + $ipaddress ?: _('unknown')); $action->elementEnd('div'); } diff --git a/plugins/RegisterThrottle/actions/ipregistrations.php b/plugins/RegisterThrottle/actions/ipregistrations.php index a748a9d0b2..8d74803ae2 100644 --- a/plugins/RegisterThrottle/actions/ipregistrations.php +++ b/plugins/RegisterThrottle/actions/ipregistrations.php @@ -15,7 +15,7 @@ class IpregistrationsAction extends ManagedAction protected function doPreparation() { - if (!$scoped->hasRight(Right::SILENCEUSER) && !$scoped->hasRole(Profile_role::ADMINISTRATOR)) { + if (!$this->scoped->hasRight(Right::SILENCEUSER) && !$this->scoped->hasRole(Profile_role::ADMINISTRATOR)) { throw new AuthorizationException(_('You are not authorized to view this page.')); } From fbcca62ae16019b446938b400199737b46907eb8 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 01:19:47 +0100 Subject: [PATCH 21/55] listGet was not meant for that really --- plugins/RegisterThrottle/actions/ipregistrations.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/RegisterThrottle/actions/ipregistrations.php b/plugins/RegisterThrottle/actions/ipregistrations.php index 8d74803ae2..46f1ed854f 100644 --- a/plugins/RegisterThrottle/actions/ipregistrations.php +++ b/plugins/RegisterThrottle/actions/ipregistrations.php @@ -26,7 +26,8 @@ class IpregistrationsAction extends ManagedAction public function showContent() { $this->elementStart('ul'); - foreach (Profile::listGet('id', $this->profile_ids) as $profile) { + $profile = Profile::multiGet('id', $this->profile_ids); + while ($profile->fetch()) { $this->elementStart('li'); try { $this->element('a', ['href'=>$profile->getUrl()], $profile->getFancyName()); From be14e15dac7d6dc53d2cfa83fda3fdaa86ee10ca Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 13:17:39 +0100 Subject: [PATCH 22/55] Hide attachments in notices by silenced profiles --- lib/attachmentlist.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php index 4d4b451167..0ce19b0b1e 100644 --- a/lib/attachmentlist.php +++ b/lib/attachmentlist.php @@ -85,6 +85,12 @@ class AttachmentList extends Widget return 0; } + if ($this->notice->getProfile()->isSilenced()) { + // TRANS: Message for inline attachments list in notices when the author has been silenced. + $this->element('div', ['class'=>'error'], _('Attachments are hidden because this profile has been silenced.')); + return 0; + } + $this->showListStart(); foreach ($attachments as $att) { From 4bf26eff4ccf96624f47ac27be51c6de221f7f01 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 13:57:15 +0100 Subject: [PATCH 23/55] socialfy-your-domain updated for webfinger (not tested) --- socialfy-your-domain/README.txt | 59 +++++++------------ socialfy-your-domain/dot-well-known/host-meta | 9 +-- .../webfinger}/example@example.com.xml | 22 +++---- .../webfinger}/index.php | 31 +++++----- 4 files changed, 49 insertions(+), 72 deletions(-) rename socialfy-your-domain/{xrd => dot-well-known/webfinger}/example@example.com.xml (54%) rename socialfy-your-domain/{xrd => dot-well-known/webfinger}/index.php (65%) diff --git a/socialfy-your-domain/README.txt b/socialfy-your-domain/README.txt index 3e2688f86b..b7691abe8d 100644 --- a/socialfy-your-domain/README.txt +++ b/socialfy-your-domain/README.txt @@ -4,65 +4,46 @@ Initial simple way to Webfinger enable your domain -- needs PHP. Step 1 ====== -First, put the folders 'xrd' and 'dot-well-known' on your website, so -they load at: +Put the 'dot-well-known' on your website, so it loads at: - http://yourname.com/xrd/ + https://example.com/.well-known/ - and - - http://yourname.com/.well-known/ - - (Remember the . at the beginning of this one) - -NOTE: If you're using https, make sure each instance of http:// for - your own domain ("example.com") is replaced with https:// +(Remember the . at the beginning of this one, which is common practice +for "hidden" files and why we have renamed it "dot-") Step 2 ====== -Next, edit xrd/index.php and enter a secret in this line: - -$s = ""; - -This can be anything you like... - -$s = "johnny5"; - -or - -$s = "12345"; - -It really doesn't matter too much. +Edit the .well-known/host-meta file and replace "example.com" with the +domain name you're hosting the .well-known directory on. +Using vim you can do this as a quick method: + $ vim .well-known/host-meta [ENTER] + :%s/example.com/domain.com/ [ENTER] + :wq [ENTER] Step 3 ====== -Edit the .well-known/host-meta file and replace all occurrences of -"example.com" with your domain name. - -Step 4 -====== - For each user on your site, and this might only be you... -In the xrd directory, make a copy of the example@example.com.xml file -so that it's called... +In the webfinger directory, make a copy of the example@example.com.xml file +so that it's called (replace username and example.com with appropriate +values, the domain name should be the same as you're "socialifying"): - yoursecretusername@domain.com.xml + username@example.com.xml -So, if your secret from step 2 is 'johnny5' and your name is 'ben' and -your domain is 'titanictoycorp.biz', your file should be called -johnny5ben@titanictoycorp.biz.xml - -Then edit the file, replacing "social.example.com" with your GNU -social instance's base path, and change the user ID number (and +Then edit the file contents, replacing "social.example.com" with your +GNU social instance's base path, and change the user ID number (and nickname for the FOAF link) to that of your account on your social site. If you don't know your user ID number, you can see this on your GNU social profile page by looking at the destination URLs in the Feeds links. +PROTIP: You can get the bulk of the contents (note the element though) + from curling down your real webfinger data: +$ curl https://social.example.com/.well-known/webfinger?resource=acct:username@social.example.com + Finally ======= diff --git a/socialfy-your-domain/dot-well-known/host-meta b/socialfy-your-domain/dot-well-known/host-meta index 1929b2eb8e..bba942f673 100644 --- a/socialfy-your-domain/dot-well-known/host-meta +++ b/socialfy-your-domain/dot-well-known/host-meta @@ -1,8 +1,5 @@ - - example.com - - WebFinger resource descriptor - + + diff --git a/socialfy-your-domain/xrd/example@example.com.xml b/socialfy-your-domain/dot-well-known/webfinger/example@example.com.xml similarity index 54% rename from socialfy-your-domain/xrd/example@example.com.xml rename to socialfy-your-domain/dot-well-known/webfinger/example@example.com.xml index b713efe95b..e95662b6fe 100644 --- a/socialfy-your-domain/xrd/example@example.com.xml +++ b/socialfy-your-domain/dot-well-known/webfinger/example@example.com.xml @@ -1,35 +1,35 @@ - acct:example@example.com - acct:example@social.example.com - http://social.example.com/user/1 + acct:username@example.com + acct:username@social.example.com + https://social.example.com/user/1 + href="https://social.example.com/user/1"/> + href="https://social.example.com/api/statuses/user_timeline/1.atom"/> + href="https://social.example.com/hcard"/> --> + href="https://social.example.com/user/1"/> + href="https://social.example.com/username/foaf"/> + href="https://social.example.com/main/salmon/user/1"/> + href="https://social.example.com/main/salmon/user/1"/> + template="https://social.example.com/main/ostatussub?profile={uri}"/> diff --git a/socialfy-your-domain/xrd/index.php b/socialfy-your-domain/dot-well-known/webfinger/index.php similarity index 65% rename from socialfy-your-domain/xrd/index.php rename to socialfy-your-domain/dot-well-known/webfinger/index.php index 25f1d8bf3c..989b3203be 100644 --- a/socialfy-your-domain/xrd/index.php +++ b/socialfy-your-domain/dot-well-known/webfinger/index.php @@ -19,23 +19,22 @@ */ -$s = ""; +// basename should make sure we can't escape this directory +$u = basename($_GET['resource']); -/* this should be a secret */ - -$u = $_GET['uri']; - -$u = substr($u, 5); - -$f = $s . $u . ".xml"; - -if (file_exists($f)) { - $fh = fopen($f, 'r'); - $c = fread($fh, filesize($f)); - fclose($fh); - header('Content-type: text/xml'); - echo $c; +if (!strpos($u, '@')) { + throw new Exception('Bad resource'); + exit(1); } +if (mb_strpos($u, 'acct:')===0) { + $u = substr($u, 5); +} -?> \ No newline at end of file +$f = $u . ".xml"; + +if (file_exists($f)) { + header('Content-Disposition: attachment; filename="'.urlencode($f).'"'); + header('Content-type: application/xrd+xml'); + echo file_get_contents($f); +} From c23c3a4f537a13b638dd77399063376b7b1386b8 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sat, 13 Feb 2016 14:06:05 +0100 Subject: [PATCH 24/55] Might as well put a FILTER_SANITIZE_EMAIL there Not that I think we could break out of the directory since we use basename, but you never know... maybe there's a unicode bug in PHP or something. --- socialfy-your-domain/dot-well-known/webfinger/index.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/socialfy-your-domain/dot-well-known/webfinger/index.php b/socialfy-your-domain/dot-well-known/webfinger/index.php index 989b3203be..91071bc4c3 100644 --- a/socialfy-your-domain/dot-well-known/webfinger/index.php +++ b/socialfy-your-domain/dot-well-known/webfinger/index.php @@ -31,6 +31,9 @@ if (mb_strpos($u, 'acct:')===0) { $u = substr($u, 5); } +// Just to be a little bit safer, you know, with all the unicode stuff going on +$u = filter_var($u, FILTER_SANITIZE_EMAIL); + $f = $u . ".xml"; if (file_exists($f)) { From e2a090c9cc796bc5116972a5e3aae2af9e391993 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 14 Feb 2016 20:46:13 +0100 Subject: [PATCH 25/55] Use NoticeStream::filterVerbs for filtering in noticestreams --- lib/conversationnoticestream.php | 10 ++----- lib/inboxnoticestream.php | 15 +++++----- lib/networkpublicnoticestream.php | 4 +-- lib/noticestream.php | 36 +++++++++++++++-------- lib/profilenoticestream.php | 13 ++------ lib/publicnoticestream.php | 10 ++----- lib/replynoticestream.php | 20 +++++++++---- lib/tagnoticestream.php | 23 +++++++++------ plugins/Favorite/lib/favenoticestream.php | 10 ++----- 9 files changed, 69 insertions(+), 72 deletions(-) diff --git a/lib/conversationnoticestream.php b/lib/conversationnoticestream.php index 9c32159d42..21b2d7f0be 100644 --- a/lib/conversationnoticestream.php +++ b/lib/conversationnoticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Notice stream for a conversation @@ -96,9 +92,7 @@ class RawConversationNoticeStream extends NoticeStream $notice->limit($offset, $limit); } - if (!empty($this->selectVerbs)) { - $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); - } + self::filterVerbs($notice, $this->selectVerbs); // ORDER BY // currently imitates the previously used "_reverseChron" sorting diff --git a/lib/inboxnoticestream.php b/lib/inboxnoticestream.php index 496fe0d05d..3609f81ed3 100644 --- a/lib/inboxnoticestream.php +++ b/lib/inboxnoticestream.php @@ -30,7 +30,7 @@ * @link http://status.net/ */ -if (!defined('GNUSOCIAL') && !defined('STATUSNET')) { exit(1); } +if (!defined('GNUSOCIAL')) { exit(1); } /** * Stream of notices for a profile's "all" feed @@ -77,6 +77,8 @@ class RawInboxNoticeStream extends NoticeStream protected $target = null; protected $inbox = null; + protected $selectVerbs = array(); + /** * Constructor * @@ -84,8 +86,8 @@ class RawInboxNoticeStream extends NoticeStream */ function __construct(Profile $target) { + parent::__construct(); $this->target = $target; - $this->unselectVerbs = array(ActivityVerb::DELETE); } /** @@ -119,12 +121,9 @@ class RawInboxNoticeStream extends NoticeStream if (!empty($max_id)) { $notice->whereAdd(sprintf('notice.id <= %d', $max_id)); } - if (!empty($this->selectVerbs)) { - $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); - } - if (!empty($this->unselectVerbs)) { - $notice->whereAddIn('!verb', $this->unselectVerbs, $notice->columnType('verb')); - } + + self::filterVerbs($notice, $this->selectVerbs); + $notice->limit($offset, $limit); // notice.id will give us even really old posts, which were // recently imported. For example if a remote instance had diff --git a/lib/networkpublicnoticestream.php b/lib/networkpublicnoticestream.php index 3320b7cd5a..9a10c28988 100644 --- a/lib/networkpublicnoticestream.php +++ b/lib/networkpublicnoticestream.php @@ -46,9 +46,7 @@ class RawNetworkPublicNoticeStream extends NoticeStream Notice::addWhereSinceId($notice, $since_id); Notice::addWhereMaxId($notice, $max_id); - if (!empty($this->selectVerbs)) { - $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); - } + self::filterVerbs($notice, $this->selectVerbs); $ids = array(); diff --git a/lib/noticestream.php b/lib/noticestream.php index 01c5ee4a72..02b2a2da86 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Class for notice streams @@ -46,16 +42,15 @@ if (!defined('STATUSNET')) { */ abstract class NoticeStream { - protected $selectVerbs = null; // must be set to array - protected $unselectVerbs = null; // must be set to array + protected $selectVerbs = array(ActivityVerb::POST => true, + ActivityVerb::DELETE => false); public function __construct() { - if ($this->selectVerbs === null) { - $this->selectVerbs = array(ActivityVerb::POST, ActivityUtils::resolveUri(ActivityVerb::POST, true)); - } - if ($this->unselectVerbs === null) { - $this->unselectVerbs = array(ActivityVerb::DELETE); + foreach ($this->selectVerbs as $key=>$val) { + // to avoid database inconsistency issues we select both relative and absolute verbs + $this->selectVerbs[ActivityUtils::resolveUri($key)] = $val; + $this->selectVerbs[ActivityUtils::resolveUri($key, true)] = $val; } } @@ -74,4 +69,21 @@ abstract class NoticeStream { return Notice::multiGet('id', $ids); } + + static function filterVerbs(Notice $notice, array $selectVerbs) + { + $filter = array_keys(array_filter($selectVerbs)); + if (!empty($filter)) { + // include verbs in selectVerbs with values that equate to true + $notice->whereAddIn('verb', $filter, $notice->columnType('verb')); + } + + $filter = array_keys(array_filter($selectVerbs, function ($v) { return !$v; })); + if (!empty($filter)) { + // exclude verbs in selectVerbs with values that equate to false + $notice->whereAddIn('!verb', $filter, $notice->columnType('verb')); + } + + unset($filter); + } } diff --git a/lib/profilenoticestream.php b/lib/profilenoticestream.php index a31fb585d1..7ff4163fcb 100644 --- a/lib/profilenoticestream.php +++ b/lib/profilenoticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Stream of notices by a profile @@ -134,12 +130,7 @@ class RawProfileNoticeStream extends NoticeStream Notice::addWhereSinceId($notice, $since_id); Notice::addWhereMaxId($notice, $max_id); - if (!empty($this->selectVerbs)) { - $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); - } - if (!empty($this->unselectVerbs)) { - $notice->whereAddIn('!verb', $this->unselectVerbs, $notice->columnType('verb')); - } + self::filterVerbs($notice, $this->selectVerbs); $notice->orderBy('created DESC, id DESC'); diff --git a/lib/publicnoticestream.php b/lib/publicnoticestream.php index 757c2164c0..0137814ba4 100644 --- a/lib/publicnoticestream.php +++ b/lib/publicnoticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Public stream @@ -87,9 +83,7 @@ class RawPublicNoticeStream extends NoticeStream Notice::addWhereSinceId($notice, $since_id); Notice::addWhereMaxId($notice, $max_id); - if (!empty($this->selectVerbs)) { - $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); - } + self::filterVerbs($notice, $this->selectVerbs); $ids = array(); diff --git a/lib/replynoticestream.php b/lib/replynoticestream.php index 9fea5cac1e..9eb188d54d 100644 --- a/lib/replynoticestream.php +++ b/lib/replynoticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Stream of mentions of me @@ -92,8 +88,20 @@ class RawReplyNoticeStream extends NoticeStream Notice::addWhereMaxId($reply, $max_id, 'notice_id', 'reply.modified'); if (!empty($this->selectVerbs)) { + // this is a little special since we have to join in Notice $reply->joinAdd(array('notice_id', 'notice:id')); - $reply->whereAddIn('notice.verb', $this->selectVerbs, 'string'); + + $filter = array_keys(array_filter($this->selectVerbs)); + if (!empty($filter)) { + // include verbs in selectVerbs with values that equate to true + $reply->whereAddIn('notice.verb', $filter, 'string'); + } + + $filter = array_keys(array_filter($this->selectVerbs, function ($v) { return !$v; })); + if (!empty($filter)) { + // exclude verbs in selectVerbs with values that equate to false + $reply->whereAddIn('!notice.verb', $filter, 'string'); + } } $reply->orderBy('reply.modified DESC, reply.notice_id DESC'); diff --git a/lib/tagnoticestream.php b/lib/tagnoticestream.php index d24907fa7e..3d81f7415a 100644 --- a/lib/tagnoticestream.php +++ b/lib/tagnoticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Stream of notices with a given tag @@ -90,10 +86,19 @@ class RawTagNoticeStream extends NoticeStream Notice::addWhereMaxId($nt, $max_id, 'notice_id'); if (!empty($this->selectVerbs)) { - $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); - } - if (!empty($this->unselectVerbs)) { - $notice->whereAddIn('!verb', $this->unselectVerbs, $notice->columnType('verb')); + $nt->joinAdd(array('notice_id', 'notice:id')); + + $filter = array_keys(array_filter($this->selectVerbs)); + if (!empty($filter)) { + // include verbs in selectVerbs with values that equate to true + $nt->whereAddIn('notice.verb', $filter, 'string'); + } + + $filter = array_keys(array_filter($this->selectVerbs, function ($v) { return !$v; })); + if (!empty($filter)) { + // exclude verbs in selectVerbs with values that equate to false + $nt->whereAddIn('!notice.verb', $filter, 'string'); + } } $nt->orderBy('created DESC, notice_id DESC'); diff --git a/plugins/Favorite/lib/favenoticestream.php b/plugins/Favorite/lib/favenoticestream.php index 6294c8cdda..d10272ac91 100644 --- a/plugins/Favorite/lib/favenoticestream.php +++ b/plugins/Favorite/lib/favenoticestream.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Notice stream for favorites @@ -77,14 +73,14 @@ class RawFaveNoticeStream extends NoticeStream protected $user_id; protected $own; + protected $selectVerbs = array(); + function __construct($user_id, $own) { parent::__construct(); $this->user_id = $user_id; $this->own = $own; - - $this->selectVerbs = array(); } /** From dcb7ce36d8e4e1fe34d99cc52b4e1dc5d866fada Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 14 Feb 2016 20:53:26 +0100 Subject: [PATCH 26/55] Show shares in public timeline Also, the unselect rule for DELETE was useless anyway since it would already have been filtered out by not having true. (the => false stuff are for when you want ALL _except_ that) --- lib/noticestream.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/noticestream.php b/lib/noticestream.php index 02b2a2da86..2b04a89ca4 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -43,7 +43,7 @@ if (!defined('GNUSOCIAL')) { exit(1); } abstract class NoticeStream { protected $selectVerbs = array(ActivityVerb::POST => true, - ActivityVerb::DELETE => false); + ActivityVerb::SHARE => true); public function __construct() { From 2301862ae674ab41e77fe913f61c59eb2e6fffff Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Mon, 15 Feb 2016 09:59:18 +0100 Subject: [PATCH 27/55] We only want POST and SHARE in the inbox/home timeline right? --- lib/inboxnoticestream.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/inboxnoticestream.php b/lib/inboxnoticestream.php index 3609f81ed3..d90c525bf8 100644 --- a/lib/inboxnoticestream.php +++ b/lib/inboxnoticestream.php @@ -77,8 +77,6 @@ class RawInboxNoticeStream extends NoticeStream protected $target = null; protected $inbox = null; - protected $selectVerbs = array(); - /** * Constructor * From 2d1b70c94d0f74c44d9dab3d77e9ac0a5c220adb Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Mon, 15 Feb 2016 09:59:34 +0100 Subject: [PATCH 28/55] created column was ambigououuuouuus --- lib/tagnoticestream.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tagnoticestream.php b/lib/tagnoticestream.php index 3d81f7415a..28f5d0e824 100644 --- a/lib/tagnoticestream.php +++ b/lib/tagnoticestream.php @@ -101,7 +101,7 @@ class RawTagNoticeStream extends NoticeStream } } - $nt->orderBy('created DESC, notice_id DESC'); + $nt->orderBy('notice.created DESC, notice_id DESC'); if (!is_null($offset)) { $nt->limit($offset, $limit); From 46829c6d3c25224b23261ffedc5263326668773a Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Tue, 16 Feb 2016 02:21:39 +0100 Subject: [PATCH 29/55] FullNoticeStream selects all verbs. --- lib/fullnoticestream.php | 11 +++++++++++ lib/inboxnoticestream.php | 2 +- lib/networkpublicnoticestream.php | 2 +- lib/publicnoticestream.php | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 lib/fullnoticestream.php diff --git a/lib/fullnoticestream.php b/lib/fullnoticestream.php new file mode 100644 index 0000000000..8433c18c9c --- /dev/null +++ b/lib/fullnoticestream.php @@ -0,0 +1,11 @@ + Date: Tue, 16 Feb 2016 02:24:38 +0100 Subject: [PATCH 30/55] Gotta declare FullNoticeStream as abstract class --- lib/fullnoticestream.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fullnoticestream.php b/lib/fullnoticestream.php index 8433c18c9c..2f83007469 100644 --- a/lib/fullnoticestream.php +++ b/lib/fullnoticestream.php @@ -5,7 +5,7 @@ if (!defined('GNUSOCIAL')) { exit(1); } /** * Class for notice streams that does not filter anything out. */ -class FullNoticeStream extends NoticeStream +abstract class FullNoticeStream extends NoticeStream { protected $selectVerbs = []; } From 422d475e441e9fb00b844353ad8cc5cb14cb3035 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 21:57:52 +0100 Subject: [PATCH 31/55] Differentiate two similar log warning messages --- plugins/OStatus/classes/Ostatus_profile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index ec2b8351ea..8c7be80a60 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1297,7 +1297,7 @@ class Ostatus_profile extends Managed_DataObject try { $this->updateAvatar($avatar); } catch (Exception $ex) { - common_log(LOG_WARNING, "Exception saving OStatus profile avatar: " . $ex->getMessage()); + common_log(LOG_WARNING, "Exception updating OStatus profile avatar: " . $ex->getMessage()); } } } From ade4518ae47b94daded408fc921b439e209d9da4 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 22:36:33 +0100 Subject: [PATCH 32/55] Make the Link header give URI for WebFinger lookup --- plugins/WebFinger/WebFingerPlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index ce8c847aa7..1677501a20 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -144,8 +144,8 @@ class WebFingerPlugin extends Plugin public function onStartShowHTML($action) { if ($action instanceof ShowstreamAction) { - $acct = 'acct:'. $action->getTarget()->getNickname() .'@'. common_config('site', 'server'); - $url = common_local_url('webfinger') . '?resource='.$acct; + $resource = $action->getTarget()->getUri(); + $url = common_local_url('webfinger') . '?resource='.urlencode($resource); foreach (array(Discovery::JRD_MIMETYPE, Discovery::XRD_MIMETYPE) as $type) { header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="'.$type.'"', false); From d6bf90cfb7ba2613be5994d0090272c74fa7abbc Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 22:43:45 +0100 Subject: [PATCH 33/55] If profile fullname is 0 chars use nickname --- actions/profilesettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 5804f21ca5..a20615b019 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -345,7 +345,7 @@ class ProfilesettingsAction extends SettingsAction $this->scoped->nickname = $nickname; $this->scoped->profileurl = common_profile_url($this->scoped->getNickname()); } - $this->scoped->fullname = $fullname; + $this->scoped->fullname = (mb_strlen($fullname)>0 ? $fullname : $this->scoped->nickname); $this->scoped->homepage = $homepage; $this->scoped->bio = $bio; $this->scoped->location = $location; From 47dc15c9f6b5592fede970f1f27445f6435f4d0b Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 22:48:16 +0100 Subject: [PATCH 34/55] Describe that we don't allow empty fullnames. --- actions/profilesettings.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/actions/profilesettings.php b/actions/profilesettings.php index a20615b019..21de0579b7 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -110,7 +110,10 @@ class ProfilesettingsAction extends SettingsAction $this->elementStart('li'); // TRANS: Field label in form for profile settings. $this->input('fullname', _('Full name'), - $this->trimmed('fullname') ?: $this->scoped->getFullname()); + $this->trimmed('fullname') ?: $this->scoped->getFullname(), + // TRANS: Instructions for full name text field on profile settings + _('A full name is required, if empty it will be set to your nickname.'), + null, true); $this->elementEnd('li'); $this->elementStart('li'); // TRANS: Field label in form for profile settings. From 5fbb01130a4d455c7f657f3b68ad0317ffa90276 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 22:58:31 +0100 Subject: [PATCH 35/55] By default, disallow users to set private_stream --- actions/profilesettings.php | 26 ++++++++++++++++++-------- lib/default.php | 1 + 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 21de0579b7..a1d947530c 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -207,13 +207,15 @@ class ProfilesettingsAction extends SettingsAction (empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy); $this->elementEnd('li'); } - $this->elementStart('li'); - $this->checkbox('private_stream', - // TRANS: Checkbox label in profile settings. - _('Make updates visible only to my followers'), - ($this->arg('private_stream')) ? - $this->boolean('private_stream') : $user->private_stream); - $this->elementEnd('li'); + if (common_config('profile', 'allowprivate') || $user->private_stream) { + $this->elementStart('li'); + $this->checkbox('private_stream', + // TRANS: Checkbox label in profile settings. + _('Make updates visible only to my followers'), + ($this->arg('private_stream')) ? + $this->boolean('private_stream') : $user->private_stream); + $this->elementEnd('li'); + } $this->elementEnd('ul'); // TRANS: Button to save input in profile settings. $this->submit('save', _m('BUTTON','Save')); @@ -255,7 +257,6 @@ class ProfilesettingsAction extends SettingsAction $location = $this->trimmed('location'); $autosubscribe = $this->booleanintstring('autosubscribe'); $subscribe_policy = $this->trimmed('subscribe_policy'); - $private_stream = $this->booleanintstring('private_stream'); $language = $this->trimmed('language'); $timezone = $this->trimmed('timezone'); $tagstring = $this->trimmed('tags'); @@ -310,6 +311,15 @@ class ProfilesettingsAction extends SettingsAction $user = $this->scoped->getUser(); $user->query('BEGIN'); + // Only allow setting private_stream if site policy allows it + // (or user already _has_ a private stream, then you can unset it) + if (common_config('profile', 'allowprivate') || $user->private_stream) { + $private_stream = $this->booleanintstring('private_stream'); + } else { + // if not allowed, we set to the existing value + $private_stream = $user->private_stream; + } + // $user->nickname is updated through Profile->update(); // XXX: XOR diff --git a/lib/default.php b/lib/default.php index d9f377ecd7..79480483ce 100644 --- a/lib/default.php +++ b/lib/default.php @@ -129,6 +129,7 @@ $default = array('banned' => array(), 'biolimit' => null, 'changenick' => false, + 'allowprivate' => false, // whether to allow setting stream to private ("only followers can read") 'backup' => false, // can cause DoS, so should be done via CLI 'restore' => false, 'delete' => false, From d2c11925bfafa5a2842267cd90616aa7e1f4882b Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 23:05:44 +0100 Subject: [PATCH 36/55] To-selector padlock only shown if site config notice/allowprivate is true --- lib/default.php | 1 + lib/toselector.php | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/default.php b/lib/default.php index 79480483ce..3518bb4cb9 100644 --- a/lib/default.php +++ b/lib/default.php @@ -291,6 +291,7 @@ $default = ), 'notice' => array('contentlimit' => null, + 'allowprivate' => false, // whether to allow users to "check the padlock" to publish notices available for their subscribers. 'defaultscope' => null, // null means 1 if site/private, 0 otherwise 'hidespam' => true), // Whether to hide silenced users from timelines 'message' => diff --git a/lib/toselector.php b/lib/toselector.php index 153d9001b5..aed38e1cf6 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -127,10 +127,12 @@ class ToSelector extends Widget $default); $this->out->elementStart('span', 'checkbox-wrapper'); - $this->out->checkbox('notice_private', - // TRANS: Checkbox label in widget for selecting potential addressees to mark the notice private. - _('Private?'), - $this->private); + if (common_config('notice', 'allowprivate')) { + $this->out->checkbox('notice_private', + // TRANS: Checkbox label in widget for selecting potential addressees to mark the notice private. + _('Private?'), + $this->private); + } $this->out->elementEnd('span'); } @@ -138,7 +140,7 @@ class ToSelector extends Widget { // XXX: make arg name selectable $toArg = $action->trimmed('notice_to'); - $private = $action->boolean('notice_private'); + $private = common_config('notice', 'allowprivate') ? $action->boolean('notice_private') : false; if (empty($toArg)) { return; From d9b649642d6e7cf365f1b62d65d2628b9cf049d9 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 23:32:56 +0100 Subject: [PATCH 37/55] Show notice feed URLs (and author) --- actions/shownotice.php | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/actions/shownotice.php b/actions/shownotice.php index 64cf38afa7..b2385ec1d7 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -74,6 +74,7 @@ class ShownoticeAction extends ManagedAction } $this->notice = $this->getNotice(); + $this->target = $this->notice; if (!$this->notice->inScope($this->scoped)) { // TRANS: Client exception thrown when trying a view a notice the user has no access to. @@ -213,12 +214,24 @@ class ShownoticeAction extends ManagedAction { } - /** - * Don't show aside - * - * @return void - */ - function showAside() { + function getFeeds() + { + return array(new Feed(Feed::JSON, + common_local_url('ApiStatusesShow', + array( + 'id' => $this->target->getID(), + 'format' => 'json')), + // TRANS: Title for link to single notice representation. + // TRANS: %s is a user nickname. + sprintf(_('Single notice (JSON)'))), + new Feed(Feed::ATOM, + common_local_url('ApiStatusesShow', + array( + 'id' => $this->target->getID(), + 'format' => 'atom')), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. + sprintf(_('Single notice (Atom)')))); } /** From 73dbc5ca1b203758693f73d6423fea71ef6b6fb6 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Wed, 17 Feb 2016 23:44:15 +0100 Subject: [PATCH 38/55] Use ToSelector choice again. --- actions/newnotice.php | 3 +++ lib/toselector.php | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/actions/newnotice.php b/actions/newnotice.php index 298361d600..6ee2092061 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -190,6 +190,9 @@ class NewnoticeAction extends FormAction // and maybe even directly save whether they're local or not! $act->context->attention = common_get_attentions($content, $this->scoped, $parent); + // $options gets filled with possible scoping settings + ToSelector::fillActivity($this, $act, $options); + $actobj = new ActivityObject(); $actobj->type = ActivityObject::NOTE; $actobj->content = common_render_content($content, $this->scoped, $parent); diff --git a/lib/toselector.php b/lib/toselector.php index aed38e1cf6..85747b70e9 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -136,6 +136,26 @@ class ToSelector extends Widget $this->out->elementEnd('span'); } + static function fillActivity(Action $action, Activity $act, array &$options) + { + if (!$act->context instanceof ActivityContext) { + $act->context = new ActivityContext(); + } + self::fillOptions($action, $options); + if (isset($options['groups'])) { + foreach ($options['groups'] as $group_id) { + $group = User_group::getByID($group_id); + $act->context->attention[$group->getUri()] = $group->getObjectType(); + } + } + if (isset($options['replies'])) { + foreach ($options['replies'] as $profile_uri) { + $profile = Profile::fromUri($profile_uri); + $act->context->attention[$profile->getUri()] = $profile->getObjectType(); + } + } + } + static function fillOptions($action, &$options) { // XXX: make arg name selectable From a361fdbd77a8bcaa179cc7adc93877b1e90f0ec5 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 18 Feb 2016 00:05:09 +0100 Subject: [PATCH 39/55] Sort ToSelector by AcctUri --- lib/toselector.php | 25 ++++++++++++++++--------- theme/base/css/display.css | 4 ++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/toselector.php b/lib/toselector.php index 85747b70e9..f29451ad93 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -94,30 +94,37 @@ class ToSelector extends Widget $groups = $this->user->getGroups(); while ($groups instanceof User_group && $groups->fetch()) { - $value = 'group:'.$groups->id; + $value = 'group:'.$groups->getID(); if (($this->to instanceof User_group) && $this->to->id == $groups->id) { $default = $value; } - $choices[$value] = $groups->getBestName(); + $choices[$value] = "!{$groups->getNickname()} [{$groups->getBestName()}]"; } // Add subscribed users to dropdown menu $users = $this->user->getSubscribed(); while ($users->fetch()) { - $value = 'profile:'.$users->id; - if ($this->user->streamNicknames()) { - $choices[$value] = $users->getNickname(); - } else { - $choices[$value] = $users->getBestName(); + $value = 'profile:'.$users->getID(); + try { + $choices[$value] = substr($users->getAcctUri(), 5) . " [{$users->getBestName()}]"; + } catch (ProfileNoAcctUriException $e) { + $choices[$value] = "[?@?] " . $e->getBestName(); } } if ($this->to instanceof Profile) { - $value = 'profile:'.$this->to->id; + $value = 'profile:'.$this->to->getID(); $default = $value; - $choices[$value] = $this->to->getBestName(); + try { + $choices[$value] = substr($this->to->getAcctUri(), 5) . " [{$this->to->getBestName()}]"; + } catch (ProfileNoAcctUriException $e) { + $choices[$value] = "[?@?] " . $e->getBestName(); + } } + // alphabetical order + asort($choices); + $this->out->dropdown($this->id, // TRANS: Label for drop-down of potential addressees. _m('LABEL','To:'), diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 61696e6f11..295916d78e 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -503,6 +503,10 @@ address .poweredby { z-index: 99; } +.form_notice .to-selector > select { + max-width: 300px; +} + .form_settings label[for=notice_to] { left: 5px; margin-left: 0px; From 543d968b81b97c9ebd46de063d8d70621c12015b Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 18 Feb 2016 00:13:59 +0100 Subject: [PATCH 40/55] NoAcctUriException->profile not $e directly --- lib/toselector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/toselector.php b/lib/toselector.php index f29451ad93..a295dcd8ab 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -108,7 +108,7 @@ class ToSelector extends Widget try { $choices[$value] = substr($users->getAcctUri(), 5) . " [{$users->getBestName()}]"; } catch (ProfileNoAcctUriException $e) { - $choices[$value] = "[?@?] " . $e->getBestName(); + $choices[$value] = "[?@?] " . $e->profile->getBestName(); } } @@ -118,7 +118,7 @@ class ToSelector extends Widget try { $choices[$value] = substr($this->to->getAcctUri(), 5) . " [{$this->to->getBestName()}]"; } catch (ProfileNoAcctUriException $e) { - $choices[$value] = "[?@?] " . $e->getBestName(); + $choices[$value] = "[?@?] " . $e->profile->getBestName(); } } From f68d1ade3f0a06d94e8f3059b8b2bc6049c8404a Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 18 Feb 2016 00:32:09 +0100 Subject: [PATCH 41/55] Put "Everyone" and "Everyone at [local instance]" at the top of ToSelector --- lib/toselector.php | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/toselector.php b/lib/toselector.php index a295dcd8ab..2026c1dfdd 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -80,16 +80,7 @@ class ToSelector extends Widget function show() { $choices = array(); - $default = 'public:site'; - - if (!common_config('site', 'private')) { - // TRANS: Option in drop-down of potential addressees. - $choices['public:everyone'] = _m('SENDTO','Everyone'); - $default = 'public:everyone'; - } - // TRANS: Option in drop-down of potential addressees. - // TRANS: %s is a StatusNet sitename. - $choices['public:site'] = sprintf(_('Everyone at %s'), common_config('site', 'name')); + $default = common_config('site', 'private') ? 'public:site' : 'public:everyone'; $groups = $this->user->getGroups(); @@ -125,6 +116,21 @@ class ToSelector extends Widget // alphabetical order asort($choices); + // Reverse so we can add entries at the end (can't unshift with a key) + $choices = array_reverse($choices); + + // TRANS: Option in drop-down of potential addressees. + // TRANS: %s is a StatusNet sitename. + $choices['public:site'] = sprintf(_('Everyone at %s'), common_config('site', 'name')); + + if (!common_config('site', 'private')) { + // TRANS: Option in drop-down of potential addressees. + $choices['public:everyone'] = _m('SENDTO','Everyone'); + } + + // Return the order + $choices = array_reverse($choices); + $this->out->dropdown($this->id, // TRANS: Label for drop-down of potential addressees. _m('LABEL','To:'), From a838c909512591e12597f1a421be871399e1d097 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Thu, 18 Feb 2016 00:33:16 +0100 Subject: [PATCH 42/55] Only show "public:site" in ToSelector if notice/allowprivate is true --- lib/toselector.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/toselector.php b/lib/toselector.php index 2026c1dfdd..7a959ff8b5 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -119,9 +119,11 @@ class ToSelector extends Widget // Reverse so we can add entries at the end (can't unshift with a key) $choices = array_reverse($choices); - // TRANS: Option in drop-down of potential addressees. - // TRANS: %s is a StatusNet sitename. - $choices['public:site'] = sprintf(_('Everyone at %s'), common_config('site', 'name')); + if (common_config('notice', 'allowprivate')) { + // TRANS: Option in drop-down of potential addressees. + // TRANS: %s is a StatusNet sitename. + $choices['public:site'] = sprintf(_('Everyone at %s'), common_config('site', 'name')); + } if (!common_config('site', 'private')) { // TRANS: Option in drop-down of potential addressees. From afbdcf8938c503323e448b6bb35e7b3b812b2e86 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Fri, 19 Feb 2016 00:10:05 +0100 Subject: [PATCH 43/55] Don't publish mbox_sha1sum in FOAF by default. We say the email is private data, so reasonably we shouldn't reveal it indirectly through a hash sum: http://xmlns.com/foaf/spec/#term_mbox_sha1sum --- actions/foaf.php | 2 +- lib/default.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/actions/foaf.php b/actions/foaf.php index 260388ba44..bf9cf1b957 100644 --- a/actions/foaf.php +++ b/actions/foaf.php @@ -90,7 +90,7 @@ class FoafAction extends ManagedAction // Would be nice to tell if they were a Person or not (e.g. a #person usertag?) $this->elementStart('Agent', array('rdf:about' => $this->user->getUri())); - if ($this->user->email) { + if (common_config('foaf', 'mbox_sha1sum') && $this->user->email) { $this->element('mbox_sha1sum', null, sha1('mailto:' . $this->user->email)); } if ($this->profile->fullname) { diff --git a/lib/default.php b/lib/default.php index 3518bb4cb9..1b420684b6 100644 --- a/lib/default.php +++ b/lib/default.php @@ -142,6 +142,10 @@ $default = 'path' => $_path . '/avatar/', 'ssl' => null, 'maxsize' => 300), + 'foaf' => + array( + 'mbox_sha1sum' => false, + ), 'public' => array('localonly' => false, 'blacklist' => array(), From b23cc7465f823ee1199ad60cf36357873e5118e2 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 18:47:32 +0100 Subject: [PATCH 44/55] Keep a unique set of WebFingerResource aliases --- plugins/WebFinger/lib/webfingerresource.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index 61b2cc09ad..90b818add1 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -35,19 +35,20 @@ abstract class WebFingerResource // Add the URI as an identity, this is _not_ necessarily an HTTP url $uri = $this->object->getUri(); - $aliases[] = $uri; + $aliases[$uri] = true; if (common_config('webfinger', 'http_alias') && strtolower(parse_url($uri, PHP_URL_SCHEME)) === 'https') { - $aliases[] = preg_replace('/^https:/', 'http:', $uri, 1); + $aliases[preg_replace('/^https:/', 'http:', $uri, 1)] = true; } try { - $aliases[] = $this->object->getUrl(); + $aliases[$this->object->getUrl()] = true; } catch (InvalidUrlException $e) { // getUrl failed because no valid URL could be returned, just ignore it } - return $aliases; + // return a unique set of aliases by extracting only the keys + return array_keys($aliases); } abstract public function updateXRD(XML_XRD $xrd); From d16a883e17db66a8bb5c647725d319e3c0edb564 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 18:47:47 +0100 Subject: [PATCH 45/55] Allow lookup of User->getByUri (throws NoResultException) --- classes/User.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/classes/User.php b/classes/User.php index c232b2b12f..40e1a1b644 100644 --- a/classes/User.php +++ b/classes/User.php @@ -140,6 +140,16 @@ class User extends Managed_DataObject return $this->uri; } + static function getByUri($uri) + { + $user = new User(); + $user->uri = $uri; + if (!$user->find(true)) { + throw new NoResultException($user); + } + return $user; + } + public function getNickname() { return $this->getProfile()->getNickname(); From 23e66bef64fc2e6b23941984e5eb540ccbb28b84 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 18:48:18 +0100 Subject: [PATCH 46/55] common_fake_local_fancy_url to remove index.php/ from a local URL --- lib/util.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/util.php b/lib/util.php index 6a5c310193..e07b17ced5 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1391,6 +1391,42 @@ function common_path($relative, $ssl=false, $addSession=true) return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } +function common_fake_local_fancy_url($url) +{ + /** + * This is a hacky fix to make URIs generated with "index.php/" match against + * locally stored URIs without that. So for example if the remote site is looking + * up the webfinger for some user and for some reason knows about https://some.example/user/1 + * but we locally store and report only https://some.example/index.php/user/1 then they would + * dismiss the profile for not having an identified alias. + * + * There are various live instances where these issues occur, for various reasons. + * Most of them being users fiddling with configuration while already having + * started federating (distributing the URI to other servers) or maybe manually + * editing the local database. + */ + if (!preg_match( + // [1] protocol part, we can only rewrite http/https anyway. + '/^(https?:\/\/)' . + // [2] site name. + // FIXME: Dunno how this acts if we're aliasing ourselves with a .onion domain etc. + '('.preg_quote(common_config('site', 'server'), '/').')' . + // [3] site path, or if that is empty just '/' (to retain the /) + '('.preg_quote(common_config('site', 'path') ?: '/', '/').')' . + // [4] + [5] extract index.php (+ possible leading double /) and the rest of the URL separately. + '(\/?index\.php\/)(.*)$/', $url, $matches)) { + // if preg_match failed to match + throw Exception('No known change could be made to the URL.'); + } + + // now reconstruct the URL with everything except the "index.php/" part + $fancy_url = ''; + foreach ([1,2,3,5] as $idx) { + $fancy_url .= $matches[$idx]; + } + return $fancy_url; +} + function common_inject_session($url, $serverpart = null) { if (!common_have_session()) { From 0c17c322676d98d3a8bb6698818e0eefa556b917 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 18:48:48 +0100 Subject: [PATCH 47/55] Let the WebFingerPlugin lookup profile resources with index.php/ too --- plugins/WebFinger/WebFingerPlugin.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index 1677501a20..84d2f63b6c 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -100,12 +100,21 @@ class WebFingerPlugin extends Plugin } } } else { - $user = User::getKV('uri', $resource); - if ($user instanceof User) { + try { + $user = User::getByUri($resource); $profile = $user->getProfile(); - } else { - // try and get it by profile url - $profile = Profile::getKV('profileurl', $resource); + } catch (NoResultException $e) { + try { + // common_fake_local_fancy_url can throw an exception + $fancy_url = common_fake_local_fancy_url($resource); + + // and this will throw a NoResultException if not found + $user = User::getByUri($fancy_url); + $profile = $user->getProfile(); + } catch (Exception $e) { + // if our rewrite hack didn't work, try to get something by profile URL + $profile = Profile::getKV('profileurl', $resource); + } } } From 893d11730908a7b59b6f28b743ff8391b578a8b9 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 19:01:37 +0100 Subject: [PATCH 48/55] throw new, not just throw --- lib/util.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/util.php b/lib/util.php index e07b17ced5..64af25f059 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1391,6 +1391,7 @@ function common_path($relative, $ssl=false, $addSession=true) return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } +// FIXME: Maybe this should also be able to handle non-fancy URLs with index.php?p=... function common_fake_local_fancy_url($url) { /** @@ -1416,7 +1417,7 @@ function common_fake_local_fancy_url($url) // [4] + [5] extract index.php (+ possible leading double /) and the rest of the URL separately. '(\/?index\.php\/)(.*)$/', $url, $matches)) { // if preg_match failed to match - throw Exception('No known change could be made to the URL.'); + throw new Exception('No known change could be made to the URL.'); } // now reconstruct the URL with everything except the "index.php/" part From 1edb1bbc174669aa8e8eb444aba60d86f99cdb84 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 19:09:39 +0100 Subject: [PATCH 49/55] Claim that we are the URL without index.php/ in webfinger response --- plugins/WebFinger/lib/webfingerresource.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index 90b818add1..0445b9a0a2 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -47,6 +47,20 @@ abstract class WebFingerResource // getUrl failed because no valid URL could be returned, just ignore it } + // We claim that we are for example https://site.example/user/1 even if the client + // requests https://site.example/index.php/user/1 due to behaviour seen in the wild. + foreach(array_keys($aliases) as $alias) { + try { + // get a "fancy url" version of the alias, even without index.php/ + $fancy_url = common_fake_local_fancy_url($alias); + // store this as well so remote sites can be sure we really are the same profile + $aliases[$fancy_url] = true; + } catch (Exception $e) { + // in case we couldn't make a "fake local fancy URL", just continue the foreach-loop + continue; + } + } + // return a unique set of aliases by extracting only the keys return array_keys($aliases); } From ce803f6d0618451aefac97ecd68b7b3d0e8d3b32 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 20:00:07 +0100 Subject: [PATCH 50/55] WebFinger aliases with 'index.php/' --- lib/util.php | 31 +++++++++++++++++++++ plugins/WebFinger/WebFingerPlugin.php | 11 ++++++-- plugins/WebFinger/lib/webfingerresource.php | 28 +++++++++++++++---- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/lib/util.php b/lib/util.php index 64af25f059..bef56502a0 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1428,6 +1428,37 @@ function common_fake_local_fancy_url($url) return $fancy_url; } +// FIXME: Maybe this should also be able to handle non-fancy URLs with index.php?p=... +function common_fake_local_nonfancy_url($url) +{ + /** + * This is a hacky fix to make URIs NOT generated with "index.php/" match against + * locally stored URIs WITH that. The reverse from the above. + * + * It will also "repair" index.php URLs with multiple / prepended. Like https://some.example///index.php/user/1 + */ + if (!preg_match( + // [1] protocol part, we can only rewrite http/https anyway. + '/^(https?:\/\/)' . + // [2] site name. + // FIXME: Dunno how this acts if we're aliasing ourselves with a .onion domain etc. + '('.preg_quote(common_config('site', 'server'), '/').')' . + // [3] site path, or if that is empty just '/' (to retain the /) + '('.preg_quote(common_config('site', 'path') ?: '/', '/').')' . + // [4] should be empty (might contain one or more / and then maybe also index.php). Will be overwritten. + // [5] will have the extracted actual URL part (besides site path) + '((?!index.php\/)\/*(?:index.php\/)?)(.*)$/', $url, $matches)) { + // if preg_match failed to match + throw new Exception('No known change could be made to the URL.'); + } + + $matches[4] = 'index.php/'; // inject the index.php/ rewritethingy + + // remove the first element, which is the full matching string + array_shift($matches); + return implode($matches); +} + function common_inject_session($url, $serverpart = null) { if (!common_have_session()) { diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index 84d2f63b6c..5eec98ea69 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -105,11 +105,16 @@ class WebFingerPlugin extends Plugin $profile = $user->getProfile(); } catch (NoResultException $e) { try { - // common_fake_local_fancy_url can throw an exception - $fancy_url = common_fake_local_fancy_url($resource); + try { // if it's a /index.php/ url + // common_fake_local_fancy_url can throw an exception + $alt_url = common_fake_local_fancy_url($resource); + } catch (Exception $e) { // let's try to create a fake local /index.php/ url + // this too if it can't do anything about the URL + $alt_url = common_fake_local_nonfancy_url($resource); + } // and this will throw a NoResultException if not found - $user = User::getByUri($fancy_url); + $user = User::getByUri($alt_url); $profile = $user->getProfile(); } catch (Exception $e) { // if our rewrite hack didn't work, try to get something by profile URL diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index 0445b9a0a2..51308c32d4 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -47,17 +47,33 @@ abstract class WebFingerResource // getUrl failed because no valid URL could be returned, just ignore it } - // We claim that we are for example https://site.example/user/1 even if the client - // requests https://site.example/index.php/user/1 due to behaviour seen in the wild. + /** + * Here we add some hacky hotfixes for remote lookups that have been taught the + * (at least now) wrong URI but it's still obviously the same user. Such as: + * - https://site.example/user/1 even if the client requests https://site.example/index.php/user/1 + * - https://site.example/user/1 even if the client requests https://site.example//index.php/user/1 + * - https://site.example/index.php/user/1 even if the client requests https://site.example/user/1 + * - https://site.example/index.php/user/1 even if the client requests https://site.example///index.php/user/1 + */ + + foreach(array_keys($aliases) as $alias) { try { // get a "fancy url" version of the alias, even without index.php/ - $fancy_url = common_fake_local_fancy_url($alias); + $alt_url = common_fake_local_fancy_url($alias); // store this as well so remote sites can be sure we really are the same profile - $aliases[$fancy_url] = true; + $aliases[$alt_url] = true; } catch (Exception $e) { - // in case we couldn't make a "fake local fancy URL", just continue the foreach-loop - continue; + // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + } + + try { + // get a non-"fancy url" version of the alias, i.e. add index.php/ + $alt_url = common_fake_local_nonfancy_url($alias); + // store this as well so remote sites can be sure we really are the same profile + $aliases[$alt_url] = true; + } catch (Exception $e) { + // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be } } From c67b89e56bf0f90730a9e22beca7e1bd41fc26c3 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 21 Feb 2016 20:05:32 +0100 Subject: [PATCH 51/55] Make WebFinger fancyurlfix configurable --- plugins/WebFinger/WebFingerPlugin.php | 37 ++++++++------ plugins/WebFinger/lib/webfingerresource.php | 54 ++++++++++----------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index 5eec98ea69..fd25482c7a 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -36,10 +36,12 @@ class WebFingerPlugin extends Plugin const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize'; public $http_alias = false; + public $fancyurlfix = true; // adds + interprets some extra aliases related to 'index.php/' URLs public function initialize() { common_config_set('webfinger', 'http_alias', $this->http_alias); + common_config_set('webfinger', 'fancyurlfix', $this->fancyurlfix); } public function onRouterInitialized($m) @@ -104,25 +106,32 @@ class WebFingerPlugin extends Plugin $user = User::getByUri($resource); $profile = $user->getProfile(); } catch (NoResultException $e) { - try { - try { // if it's a /index.php/ url - // common_fake_local_fancy_url can throw an exception - $alt_url = common_fake_local_fancy_url($resource); - } catch (Exception $e) { // let's try to create a fake local /index.php/ url - // this too if it can't do anything about the URL - $alt_url = common_fake_local_nonfancy_url($resource); - } + if (common_config('webfinger', 'fancyurlfix')) { + try { + try { // if it's a /index.php/ url + // common_fake_local_fancy_url can throw an exception + $alt_url = common_fake_local_fancy_url($resource); + } catch (Exception $e) { // let's try to create a fake local /index.php/ url + // this too if it can't do anything about the URL + $alt_url = common_fake_local_nonfancy_url($resource); + } - // and this will throw a NoResultException if not found - $user = User::getByUri($alt_url); - $profile = $user->getProfile(); - } catch (Exception $e) { - // if our rewrite hack didn't work, try to get something by profile URL - $profile = Profile::getKV('profileurl', $resource); + // and this will throw a NoResultException if not found + $user = User::getByUri($alt_url); + $profile = $user->getProfile(); + } catch (Exception $e) { + // apparently we didn't get any matches with that, so continue... + } } } } + // if we still haven't found a match... + if (!$profile instanceof Profile) { + // if our rewrite hack didn't work, try to get something by profile URL + $profile = Profile::getKV('profileurl', $resource); + } + if ($profile instanceof Profile) { $target = new WebFingerResource_Profile($profile); return false; // We got our target, stop handler execution diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index 51308c32d4..e04d3b407f 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -47,33 +47,33 @@ abstract class WebFingerResource // getUrl failed because no valid URL could be returned, just ignore it } - /** - * Here we add some hacky hotfixes for remote lookups that have been taught the - * (at least now) wrong URI but it's still obviously the same user. Such as: - * - https://site.example/user/1 even if the client requests https://site.example/index.php/user/1 - * - https://site.example/user/1 even if the client requests https://site.example//index.php/user/1 - * - https://site.example/index.php/user/1 even if the client requests https://site.example/user/1 - * - https://site.example/index.php/user/1 even if the client requests https://site.example///index.php/user/1 - */ - - - foreach(array_keys($aliases) as $alias) { - try { - // get a "fancy url" version of the alias, even without index.php/ - $alt_url = common_fake_local_fancy_url($alias); - // store this as well so remote sites can be sure we really are the same profile - $aliases[$alt_url] = true; - } catch (Exception $e) { - // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be - } - - try { - // get a non-"fancy url" version of the alias, i.e. add index.php/ - $alt_url = common_fake_local_nonfancy_url($alias); - // store this as well so remote sites can be sure we really are the same profile - $aliases[$alt_url] = true; - } catch (Exception $e) { - // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + if (common_config('webfinger', 'fancyurlfix')) { + /** + * Here we add some hacky hotfixes for remote lookups that have been taught the + * (at least now) wrong URI but it's still obviously the same user. Such as: + * - https://site.example/user/1 even if the client requests https://site.example/index.php/user/1 + * - https://site.example/user/1 even if the client requests https://site.example//index.php/user/1 + * - https://site.example/index.php/user/1 even if the client requests https://site.example/user/1 + * - https://site.example/index.php/user/1 even if the client requests https://site.example///index.php/user/1 + */ + foreach(array_keys($aliases) as $alias) { + try { + // get a "fancy url" version of the alias, even without index.php/ + $alt_url = common_fake_local_fancy_url($alias); + // store this as well so remote sites can be sure we really are the same profile + $aliases[$alt_url] = true; + } catch (Exception $e) { + // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + } + + try { + // get a non-"fancy url" version of the alias, i.e. add index.php/ + $alt_url = common_fake_local_nonfancy_url($alias); + // store this as well so remote sites can be sure we really are the same profile + $aliases[$alt_url] = true; + } catch (Exception $e) { + // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + } } } From 5f7032dfee1fd202c14e76a9f8b37af35d584901 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Mon, 22 Feb 2016 15:19:10 +0100 Subject: [PATCH 52/55] Verify that authenticated API calls are made from our domain name. Evil forms on other websites could otherwise potentially be configured to have action="https://gnusocial.example/api/statuses/update.json" or whatever. XHR is already blocked with CORS stuff. Really, why do browsers allow cross domain POSTs at all? Sigh. The web. --- lib/apiauthaction.php | 6 ++++-- lib/util.php | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/apiauthaction.php b/lib/apiauthaction.php index 0e81082c35..a3deccd3da 100644 --- a/lib/apiauthaction.php +++ b/lib/apiauthaction.php @@ -85,8 +85,10 @@ class ApiAuthAction extends ApiAction // NOTE: $this->scoped and $this->auth_user has to get set in // prepare(), not handle(), as subclasses use them in prepares. - // Allow regular login session - if (common_logged_in()) { + // Allow regular login session, but we have to double-check the + // HTTP_REFERER value to avoid cross domain POSTing since the API + // doesn't use the "token" form field. + if (common_logged_in() && common_local_referer()) { $this->scoped = Profile::current(); $this->auth_user = $this->scoped->getUser(); if (!$this->auth_user->hasRight(Right::API)) { diff --git a/lib/util.php b/lib/util.php index bef56502a0..c87b0f1bf6 100644 --- a/lib/util.php +++ b/lib/util.php @@ -264,6 +264,11 @@ function common_logged_in() return (!is_null(common_current_user())); } +function common_local_referer() +{ + return parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) === common_config('site', 'server'); +} + function common_have_session() { return (0 != strcmp(session_id(), '')); From b59dacb806c9246668db7a004b931513f4e4076b Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Tue, 23 Feb 2016 14:00:59 +0100 Subject: [PATCH 53/55] getAliases for Profile and Notice Also move fancyurlfix into site-wide $config['fix']['fancyurls'] TODO: getByUri should make use of this directly I guess? --- classes/Managed_DataObject.php | 49 ++++++++++++++++++++ lib/default.php | 3 ++ plugins/WebFinger/WebFingerPlugin.php | 4 +- plugins/WebFinger/lib/webfingerresource.php | 51 ++++----------------- 4 files changed, 63 insertions(+), 44 deletions(-) diff --git a/classes/Managed_DataObject.php b/classes/Managed_DataObject.php index 31ae6614fb..cab7edd5c7 100644 --- a/classes/Managed_DataObject.php +++ b/classes/Managed_DataObject.php @@ -412,6 +412,55 @@ abstract class Managed_DataObject extends Memcached_DataObject return intval($this->id); } + /** + * WARNING: Only use this on Profile and Notice. We should probably do + * this with traits/"implements" or whatever, but that's over the top + * right now, I'm just throwing this in here to avoid code duplication + * in Profile and Notice classes. + */ + public function getAliases() + { + $aliases = array(); + $aliases[$this->getUri()] = $this->getID(); + + try { + $aliases[$this->getUrl()] = $this->getID(); + } catch (InvalidUrlException $e) { + // getUrl failed because no valid URL could be returned, just ignore it + } + + if (common_config('fix', 'fancyurls')) { + /** + * Here we add some hacky hotfixes for remote lookups that have been taught the + * (at least now) wrong URI but it's still obviously the same user. Such as: + * - https://site.example/user/1 even if the client requests https://site.example/index.php/user/1 + * - https://site.example/user/1 even if the client requests https://site.example//index.php/user/1 + * - https://site.example/index.php/user/1 even if the client requests https://site.example/user/1 + * - https://site.example/index.php/user/1 even if the client requests https://site.example///index.php/user/1 + */ + foreach ($aliases as $alias=>$id) { + try { + // get a "fancy url" version of the alias, even without index.php/ + $alt_url = common_fake_local_fancy_url($alias); + // store this as well so remote sites can be sure we really are the same profile + $aliases[$alt_url] = $id; + } catch (Exception $e) { + // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + } + + try { + // get a non-"fancy url" version of the alias, i.e. add index.php/ + $alt_url = common_fake_local_nonfancy_url($alias); + // store this as well so remote sites can be sure we really are the same profile + $aliases[$alt_url] = $id; + } catch (Exception $e) { + // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + } + } + } + return $aliases; + } + // 'update' won't write key columns, so we have to do it ourselves. // This also automatically calls "update" _before_ it sets the keys. // FIXME: This only works with single-column primary keys so far! Beware! diff --git a/lib/default.php b/lib/default.php index 1b420684b6..f8ce3bd4fe 100644 --- a/lib/default.php +++ b/lib/default.php @@ -81,6 +81,9 @@ $default = 'log_queries' => false, // true to log all DB queries 'log_slow_queries' => 0, // if set, log queries taking over N seconds 'mysql_foreign_keys' => false), // if set, enables experimental foreign key support on MySQL + 'fix' => + array('fancyurls' => true, // makes sure aliases in WebFinger etc. are not f'd by index.php/ URLs + ), 'syslog' => array('appname' => 'statusnet', # for syslog 'priority' => 'debug', # XXX: currently ignored diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index fd25482c7a..d902947d93 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -36,12 +36,10 @@ class WebFingerPlugin extends Plugin const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize'; public $http_alias = false; - public $fancyurlfix = true; // adds + interprets some extra aliases related to 'index.php/' URLs public function initialize() { common_config_set('webfinger', 'http_alias', $this->http_alias); - common_config_set('webfinger', 'fancyurlfix', $this->fancyurlfix); } public function onRouterInitialized($m) @@ -106,7 +104,7 @@ class WebFingerPlugin extends Plugin $user = User::getByUri($resource); $profile = $user->getProfile(); } catch (NoResultException $e) { - if (common_config('webfinger', 'fancyurlfix')) { + if (common_config('fix', 'fancyurls')) { try { try { // if it's a /index.php/ url // common_fake_local_fancy_url can throw an exception diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index e04d3b407f..3afbd41713 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -31,49 +31,18 @@ abstract class WebFingerResource public function getAliases() { - $aliases = array(); + $aliases = $this->object->getAliases(); - // Add the URI as an identity, this is _not_ necessarily an HTTP url - $uri = $this->object->getUri(); - $aliases[$uri] = true; - if (common_config('webfinger', 'http_alias') - && strtolower(parse_url($uri, PHP_URL_SCHEME)) === 'https') { - $aliases[preg_replace('/^https:/', 'http:', $uri, 1)] = true; - } - - try { - $aliases[$this->object->getUrl()] = true; - } catch (InvalidUrlException $e) { - // getUrl failed because no valid URL could be returned, just ignore it - } - - if (common_config('webfinger', 'fancyurlfix')) { - /** - * Here we add some hacky hotfixes for remote lookups that have been taught the - * (at least now) wrong URI but it's still obviously the same user. Such as: - * - https://site.example/user/1 even if the client requests https://site.example/index.php/user/1 - * - https://site.example/user/1 even if the client requests https://site.example//index.php/user/1 - * - https://site.example/index.php/user/1 even if the client requests https://site.example/user/1 - * - https://site.example/index.php/user/1 even if the client requests https://site.example///index.php/user/1 - */ - foreach(array_keys($aliases) as $alias) { - try { - // get a "fancy url" version of the alias, even without index.php/ - $alt_url = common_fake_local_fancy_url($alias); - // store this as well so remote sites can be sure we really are the same profile - $aliases[$alt_url] = true; - } catch (Exception $e) { - // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be - } - - try { - // get a non-"fancy url" version of the alias, i.e. add index.php/ - $alt_url = common_fake_local_nonfancy_url($alias); - // store this as well so remote sites can be sure we really are the same profile - $aliases[$alt_url] = true; - } catch (Exception $e) { - // Apparently we couldn't rewrite that, the $alias was as the function wanted it to be + // Some sites have changed from http to https and still want + // (because remote sites look for it) verify that they are still + // the same identity as they were on HTTP. Should NOT be used if + // you've run HTTPS all the time! + if (common_config('webfinger', 'http_alias')) { + foreach ($aliases as $alias=>$id) { + if (!strtolower(parse_url($alias, PHP_URL_SCHEME)) === 'https') { + continue; } + $aliases[preg_replace('/^https:/', 'http:', $alias, 1)] = $id; } } From e16f7d04a84ac657560a3469910141b1c47d1877 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Tue, 23 Feb 2016 14:15:08 +0100 Subject: [PATCH 54/55] Let OpenID match against aliases (fix fancyurl stuff etc.) --- plugins/OpenID/actions/openidserver.php | 28 ++++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/plugins/OpenID/actions/openidserver.php b/plugins/OpenID/actions/openidserver.php index b50a9129d7..d4bb6e25f4 100644 --- a/plugins/OpenID/actions/openidserver.php +++ b/plugins/OpenID/actions/openidserver.php @@ -63,8 +63,7 @@ class OpenidserverAction extends Action $request = $this->oserver->decodeRequest(); if (in_array($request->mode, array('checkid_immediate', 'checkid_setup'))) { - $user = common_current_user(); - if(!$user){ + if (!$this->scoped instanceof Profile) { if($request->immediate){ //cannot prompt the user to login in immediate mode, so answer false $response = $this->generateDenyResponse($request); @@ -77,9 +76,9 @@ class OpenidserverAction extends Action common_set_returnto($_SERVER['REQUEST_URI']); common_redirect(common_local_url('login'), 303); } - }else if(common_profile_url($user->nickname) == $request->identity || $request->idSelect()){ + } elseif (in_array($request->identity, $this->scoped->getAliases()) || $request->idSelect()) { $user_openid_trustroot = User_openid_trustroot::pkeyGet( - array('user_id'=>$user->id, 'trustroot'=>$request->trust_root)); + array('user_id'=>$this->scoped->getID(), 'trustroot'=>$request->trust_root)); if(empty($user_openid_trustroot)){ if($request->immediate){ //cannot prompt the user to trust this trust root in immediate mode, so answer false @@ -87,7 +86,7 @@ class OpenidserverAction extends Action }else{ common_ensure_session(); $_SESSION['openid_trust_root'] = $request->trust_root; - $allowResponse = $this->generateAllowResponse($request, $user); + $allowResponse = $this->generateAllowResponse($request, $this->scoped); $this->oserver->encodeResponse($allowResponse); //sign the response $denyResponse = $this->generateDenyResponse($request); $this->oserver->encodeResponse($denyResponse); //sign the response @@ -101,12 +100,11 @@ class OpenidserverAction extends Action // were POSTed here. common_redirect(common_local_url('openidtrust'), 303); } - }else{ + } else { //user has previously authorized this trust root - $response = $this->generateAllowResponse($request, $user); - //$response = $request->answer(true, null, common_profile_url($user->nickname)); + $response = $this->generateAllowResponse($request, $this->scoped); } - } else if ($request->immediate) { + } elseif ($request->immediate) { $response = $this->generateDenyResponse($request); } else { //invalid @@ -137,14 +135,14 @@ class OpenidserverAction extends Action } } - function generateAllowResponse($request, $user){ - $response = $request->answer(true, null, common_profile_url($user->nickname)); + function generateAllowResponse($request, Profile $profile){ + $response = $request->answer(true, null, $profile->getUrl()); + $user = $profile->getUser(); - $profile = $user->getProfile(); $sreg_data = array( - 'fullname' => $profile->fullname, - 'nickname' => $user->nickname, - 'email' => $user->email, + 'fullname' => $profile->getFullname(), + 'nickname' => $profile->getNickname(), + 'email' => $user->email, // FIXME: Should we make the email optional? 'language' => $user->language, 'timezone' => $user->timezone); $sreg_request = Auth_OpenID_SRegRequest::fromOpenIDRequest($request); From d67254711264bbe82c80a9d2513a26f0ddf2c79d Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Tue, 23 Feb 2016 14:33:09 +0100 Subject: [PATCH 55/55] getAliases should be only a list (numeric array) --- classes/Managed_DataObject.php | 5 +++++ plugins/WebFinger/lib/webfingerresource.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/classes/Managed_DataObject.php b/classes/Managed_DataObject.php index cab7edd5c7..0857bb11f6 100644 --- a/classes/Managed_DataObject.php +++ b/classes/Managed_DataObject.php @@ -419,6 +419,11 @@ abstract class Managed_DataObject extends Memcached_DataObject * in Profile and Notice classes. */ public function getAliases() + { + return array_keys($this->getAliasesWithIDs()); + } + + public function getAliasesWithIDs() { $aliases = array(); $aliases[$this->getUri()] = $this->getID(); diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index 3afbd41713..b7bace36d2 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -31,7 +31,7 @@ abstract class WebFingerResource public function getAliases() { - $aliases = $this->object->getAliases(); + $aliases = $this->object->getAliasesWithIDs(); // Some sites have changed from http to https and still want // (because remote sites look for it) verify that they are still