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; 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/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/actions/profilesettings.php b/actions/profilesettings.php index 5804f21ca5..a1d947530c 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. @@ -204,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')); @@ -252,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'); @@ -307,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 @@ -345,7 +358,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; 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)')))); } /** 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/Managed_DataObject.php b/classes/Managed_DataObject.php index 31ae6614fb..0857bb11f6 100644 --- a/classes/Managed_DataObject.php +++ b/classes/Managed_DataObject.php @@ -412,6 +412,60 @@ 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() + { + return array_keys($this->getAliasesWithIDs()); + } + + public function getAliasesWithIDs() + { + $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/classes/Profile.php b/classes/Profile.php index 875ad9ade1..7aae98fb5f 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 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. + 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 @@ -1192,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? * @@ -1628,6 +1673,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. * 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(); 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/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) { 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/default.php b/lib/default.php index f90892e169..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 @@ -129,6 +132,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, @@ -141,6 +145,10 @@ $default = 'path' => $_path . '/avatar/', 'ssl' => null, 'maxsize' => 300), + 'foaf' => + array( + 'mbox_sha1sum' => false, + ), 'public' => array('localonly' => false, 'blacklist' => array(), @@ -233,6 +241,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', @@ -289,6 +298,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/fullnoticestream.php b/lib/fullnoticestream.php new file mode 100644 index 0000000000..2f83007469 --- /dev/null +++ b/lib/fullnoticestream.php @@ -0,0 +1,11 @@ +target = $target; - $this->unselectVerbs = array(ActivityVerb::DELETE); } /** @@ -119,12 +119,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..bd4da5d075 100644 --- a/lib/networkpublicnoticestream.php +++ b/lib/networkpublicnoticestream.php @@ -23,7 +23,7 @@ class NetworkPublicNoticeStream extends ScopingNoticeStream * @link http://status.net/ */ -class RawNetworkPublicNoticeStream extends NoticeStream +class RawNetworkPublicNoticeStream extends FullNoticeStream { function getNoticeIds($offset, $limit, $since_id, $max_id) { @@ -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/nickname.php b/lib/nickname.php index 1ed0abbe78..2dd08efc3f 100644 --- a/lib/nickname.php +++ b/lib/nickname.php @@ -180,18 +180,24 @@ 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); + + // 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)); } /** diff --git a/lib/noticestream.php b/lib/noticestream.php index 01c5ee4a72..2b04a89ca4 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::SHARE => true); 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/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(); } } 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..4a16cbd235 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 @@ -66,7 +62,7 @@ class PublicNoticeStream extends ScopingNoticeStream * @link http://status.net/ */ -class RawPublicNoticeStream extends NoticeStream +class RawPublicNoticeStream extends FullNoticeStream { function getNoticeIds($offset, $limit, $since_id, $max_id) { @@ -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..28f5d0e824 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,13 +86,22 @@ 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'); + $nt->orderBy('notice.created DESC, notice_id DESC'); if (!is_null($offset)) { $nt->limit($offset, $limit); diff --git a/lib/toselector.php b/lib/toselector.php index 153d9001b5..7a959ff8b5 100644 --- a/lib/toselector.php +++ b/lib/toselector.php @@ -80,44 +80,59 @@ 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(); 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->profile->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->profile->getBestName(); + } } + // alphabetical order + asort($choices); + + // Reverse so we can add entries at the end (can't unshift with a key) + $choices = array_reverse($choices); + + 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. + $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:'), @@ -127,18 +142,40 @@ 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'); } + 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 $toArg = $action->trimmed('notice_to'); - $private = $action->boolean('notice_private'); + $private = common_config('notice', 'allowprivate') ? $action->boolean('notice_private') : false; if (empty($toArg)) { return; 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]; diff --git a/lib/util.php b/lib/util.php index 6a5c310193..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(), '')); @@ -1391,6 +1396,74 @@ 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) +{ + /** + * 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 new 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; +} + +// 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/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(); } /** 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 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; 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()); } } } 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); diff --git a/plugins/RegisterThrottle/RegisterThrottlePlugin.php b/plugins/RegisterThrottle/RegisterThrottlePlugin.php index 9d3be3b8a2..552420d8f6 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. * @@ -134,6 +141,52 @@ 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; + } + + $target = $action->getTarget(); + if (!$target->isSilenced()) { + // Only show the IP of users who are not silenced. + return true; + } + + $scoped = $action->getScoped(); + if (!$scoped->hasRight(Right::SILENCEUSER)) { + // only show registration IP if we have the right to silence users + return true; + } + + $ri = Registration_ip::getKV('user_id', $target->getID()); + $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')); + + // TRANS: Label for the information about which IP a users registered from. + $action->element('strong', null, _('Registered from:')); + $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'); + } + /** * Called after someone registers, by any means. * @@ -154,8 +207,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..46f1ed854f --- /dev/null +++ b/plugins/RegisterThrottle/actions/ipregistrations.php @@ -0,0 +1,41 @@ +ipaddress); + } + + protected function doPreparation() + { + if (!$this->scoped->hasRight(Right::SILENCEUSER) && !$this->scoped->hasRole(Profile_role::ADMINISTRATOR)) { + throw new AuthorizationException(_('You are not authorized to view this page.')); + } + + $this->ipaddress = $this->trimmed('ipaddress'); + $this->profile_ids = Registration_ip::usersByIP($this->ipaddress); + } + + public function showContent() + { + $this->elementStart('ul'); + $profile = Profile::multiGet('id', $this->profile_ids); + while ($profile->fetch()) { + $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'); + } +} diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index ce8c847aa7..d902947d93 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -100,15 +100,36 @@ 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) { + if (common_config('fix', 'fancyurls')) { + 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) { + // 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 @@ -144,8 +165,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); diff --git a/plugins/WebFinger/lib/webfingerresource.php b/plugins/WebFinger/lib/webfingerresource.php index 61b2cc09ad..b7bace36d2 100644 --- a/plugins/WebFinger/lib/webfingerresource.php +++ b/plugins/WebFinger/lib/webfingerresource.php @@ -31,23 +31,23 @@ abstract class WebFingerResource public function getAliases() { - $aliases = array(); + $aliases = $this->object->getAliasesWithIDs(); - // Add the URI as an identity, this is _not_ necessarily an HTTP url - $uri = $this->object->getUri(); - $aliases[] = $uri; - if (common_config('webfinger', 'http_alias') - && strtolower(parse_url($uri, PHP_URL_SCHEME)) === 'https') { - $aliases[] = preg_replace('/^https:/', 'http:', $uri, 1); + // 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; + } } - try { - $aliases[] = $this->object->getUrl(); - } 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); diff --git a/scripts/delete_orphan_files.php b/scripts/delete_orphan_files.php new file mode 100755 index 0000000000..2c49c10a50 --- /dev/null +++ b/scripts/delete_orphan_files.php @@ -0,0 +1,83 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$shortoptions = 'y'; +$longoptions = array('yes'); + +$helptext = <<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); + if (strtolower(trim($response)) != 'y') { + print "Aborting.\n"; + exit(0); + } +} + +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"; 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 59% rename from socialfy-your-domain/xrd/index.php rename to socialfy-your-domain/dot-well-known/webfinger/index.php index 25f1d8bf3c..91071bc4c3 100644 --- a/socialfy-your-domain/xrd/index.php +++ b/socialfy-your-domain/dot-well-known/webfinger/index.php @@ -19,23 +19,25 @@ */ -$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 +// 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)) { + header('Content-Disposition: attachment; filename="'.urlencode($f).'"'); + header('Content-type: application/xrd+xml'); + echo file_get_contents($f); +} 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;