Merge branch 'nightly' of git.gnu.io:gnu/gnu-social into nightly

This commit is contained in:
abjectio 2016-02-23 21:13:58 +01:00
commit 6bcfc73175
42 changed files with 773 additions and 344 deletions

View File

@ -27,9 +27,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET') && !defined('LACONICA')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
/** /**
* Delete a user * Delete a user
@ -44,33 +42,30 @@ class DeleteuserAction extends ProfileFormAction
{ {
var $user = null; var $user = null;
/** function prepare(array $args=array())
* Take arguments for running
*
* @param array $args $_REQUEST args
*
* @return boolean success flag
*/
function prepare($args)
{ {
if (!parent::prepare($args)) { if (!parent::prepare($args)) {
return false; return false;
} }
$cur = common_current_user(); assert($this->scoped instanceof Profile);
assert(!empty($cur)); // checked by parent if (!$this->scoped->hasRight(Right::DELETEUSER)) {
if (!$cur->hasRight(Right::DELETEUSER)) {
// TRANS: Client error displayed when trying to delete a user without having the right to delete users. // 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); try {
$this->user = $this->profile->getUser();
if (empty($this->user)) { } catch (NoSuchUserException $e) {
// TRANS: Client error displayed when trying to delete a non-local user. // 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; return true;

View File

@ -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?) // 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())); $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)); $this->element('mbox_sha1sum', null, sha1('mailto:' . $this->user->email));
} }
if ($this->profile->fullname) { if ($this->profile->fullname) {

View File

@ -190,6 +190,9 @@ class NewnoticeAction extends FormAction
// and maybe even directly save whether they're local or not! // and maybe even directly save whether they're local or not!
$act->context->attention = common_get_attentions($content, $this->scoped, $parent); $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 = new ActivityObject();
$actobj->type = ActivityObject::NOTE; $actobj->type = ActivityObject::NOTE;
$actobj->content = common_render_content($content, $this->scoped, $parent); $actobj->content = common_render_content($content, $this->scoped, $parent);

View File

@ -110,7 +110,10 @@ class ProfilesettingsAction extends SettingsAction
$this->elementStart('li'); $this->elementStart('li');
// TRANS: Field label in form for profile settings. // TRANS: Field label in form for profile settings.
$this->input('fullname', _('Full name'), $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->elementEnd('li');
$this->elementStart('li'); $this->elementStart('li');
// TRANS: Field label in form for profile settings. // TRANS: Field label in form for profile settings.
@ -204,6 +207,7 @@ class ProfilesettingsAction extends SettingsAction
(empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy); (empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy);
$this->elementEnd('li'); $this->elementEnd('li');
} }
if (common_config('profile', 'allowprivate') || $user->private_stream) {
$this->elementStart('li'); $this->elementStart('li');
$this->checkbox('private_stream', $this->checkbox('private_stream',
// TRANS: Checkbox label in profile settings. // TRANS: Checkbox label in profile settings.
@ -211,6 +215,7 @@ class ProfilesettingsAction extends SettingsAction
($this->arg('private_stream')) ? ($this->arg('private_stream')) ?
$this->boolean('private_stream') : $user->private_stream); $this->boolean('private_stream') : $user->private_stream);
$this->elementEnd('li'); $this->elementEnd('li');
}
$this->elementEnd('ul'); $this->elementEnd('ul');
// TRANS: Button to save input in profile settings. // TRANS: Button to save input in profile settings.
$this->submit('save', _m('BUTTON','Save')); $this->submit('save', _m('BUTTON','Save'));
@ -252,7 +257,6 @@ class ProfilesettingsAction extends SettingsAction
$location = $this->trimmed('location'); $location = $this->trimmed('location');
$autosubscribe = $this->booleanintstring('autosubscribe'); $autosubscribe = $this->booleanintstring('autosubscribe');
$subscribe_policy = $this->trimmed('subscribe_policy'); $subscribe_policy = $this->trimmed('subscribe_policy');
$private_stream = $this->booleanintstring('private_stream');
$language = $this->trimmed('language'); $language = $this->trimmed('language');
$timezone = $this->trimmed('timezone'); $timezone = $this->trimmed('timezone');
$tagstring = $this->trimmed('tags'); $tagstring = $this->trimmed('tags');
@ -307,6 +311,15 @@ class ProfilesettingsAction extends SettingsAction
$user = $this->scoped->getUser(); $user = $this->scoped->getUser();
$user->query('BEGIN'); $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(); // $user->nickname is updated through Profile->update();
// XXX: XOR // XXX: XOR
@ -345,7 +358,7 @@ class ProfilesettingsAction extends SettingsAction
$this->scoped->nickname = $nickname; $this->scoped->nickname = $nickname;
$this->scoped->profileurl = common_profile_url($this->scoped->getNickname()); $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->homepage = $homepage;
$this->scoped->bio = $bio; $this->scoped->bio = $bio;
$this->scoped->location = $location; $this->scoped->location = $location;

View File

@ -74,6 +74,7 @@ class ShownoticeAction extends ManagedAction
} }
$this->notice = $this->getNotice(); $this->notice = $this->getNotice();
$this->target = $this->notice;
if (!$this->notice->inScope($this->scoped)) { if (!$this->notice->inScope($this->scoped)) {
// TRANS: Client exception thrown when trying a view a notice the user has no access to. // TRANS: Client exception thrown when trying a view a notice the user has no access to.
@ -213,12 +214,24 @@ class ShownoticeAction extends ManagedAction
{ {
} }
/** function getFeeds()
* Don't show aside {
* return array(new Feed(Feed::JSON,
* @return void common_local_url('ApiStatusesShow',
*/ array(
function showAside() { '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)'))));
} }
/** /**

View File

@ -27,9 +27,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
/** /**
* Silence a user. * Silence a user.
@ -42,45 +40,11 @@ if (!defined('STATUSNET')) {
*/ */
class SilenceAction extends ProfileFormAction 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() function handlePost()
{ {
$this->profile->silence(); assert($this->scoped instanceof Profile);
assert($this->profile instanceof Profile);
$this->profile->silenceAs($this->scoped);
} }
} }

View File

@ -27,12 +27,10 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
/** /**
* Silence a user. * Unsilence a user.
* *
* @category Action * @category Action
* @package StatusNet * @package StatusNet
@ -42,45 +40,11 @@ if (!defined('STATUSNET')) {
*/ */
class UnsilenceAction extends ProfileFormAction 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() function handlePost()
{ {
$this->profile->unsilence(); assert($this->scoped instanceof Profile);
assert($this->profile instanceof Profile);
$this->profile->unsilenceAs($this->scoped);
} }
} }

View File

@ -412,6 +412,60 @@ abstract class Managed_DataObject extends Memcached_DataObject
return intval($this->id); 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. // 'update' won't write key columns, so we have to do it ourselves.
// This also automatically calls "update" _before_ it sets the keys. // This also automatically calls "update" _before_ it sets the keys.
// FIXME: This only works with single-column primary keys so far! Beware! // FIXME: This only works with single-column primary keys so far! Beware!

View File

@ -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() function unsilence()
{ {
$this->revokeRole(Profile_role::SILENCED); $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() function flushVisibility()
{ {
// Get all notices // 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? * Does this user have the right to do X?
* *
@ -1628,6 +1673,15 @@ class Profile extends Managed_DataObject
return $profile; 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. * Magic function called at serialize() time.
* *

View File

@ -140,6 +140,16 @@ class User extends Managed_DataObject
return $this->uri; 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() public function getNickname()
{ {
return $this->getProfile()->getNickname(); return $this->getProfile()->getNickname();

View File

@ -85,8 +85,10 @@ class ApiAuthAction extends ApiAction
// NOTE: $this->scoped and $this->auth_user has to get set in // NOTE: $this->scoped and $this->auth_user has to get set in
// prepare(), not handle(), as subclasses use them in prepares. // prepare(), not handle(), as subclasses use them in prepares.
// Allow regular login session // Allow regular login session, but we have to double-check the
if (common_logged_in()) { // 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->scoped = Profile::current();
$this->auth_user = $this->scoped->getUser(); $this->auth_user = $this->scoped->getUser();
if (!$this->auth_user->hasRight(Right::API)) { if (!$this->auth_user->hasRight(Right::API)) {

View File

@ -85,6 +85,12 @@ class AttachmentList extends Widget
return 0; 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(); $this->showListStart();
foreach ($attachments as $att) { foreach ($attachments as $att) {

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Notice stream for a conversation * Notice stream for a conversation
@ -96,9 +92,7 @@ class RawConversationNoticeStream extends NoticeStream
$notice->limit($offset, $limit); $notice->limit($offset, $limit);
} }
if (!empty($this->selectVerbs)) { self::filterVerbs($notice, $this->selectVerbs);
$notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb'));
}
// ORDER BY // ORDER BY
// currently imitates the previously used "_reverseChron" sorting // currently imitates the previously used "_reverseChron" sorting

View File

@ -81,6 +81,9 @@ $default =
'log_queries' => false, // true to log all DB queries 'log_queries' => false, // true to log all DB queries
'log_slow_queries' => 0, // if set, log queries taking over N seconds '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 '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' => 'syslog' =>
array('appname' => 'statusnet', # for syslog array('appname' => 'statusnet', # for syslog
'priority' => 'debug', # XXX: currently ignored 'priority' => 'debug', # XXX: currently ignored
@ -129,6 +132,7 @@ $default =
array('banned' => array(), array('banned' => array(),
'biolimit' => null, 'biolimit' => null,
'changenick' => false, '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 'backup' => false, // can cause DoS, so should be done via CLI
'restore' => false, 'restore' => false,
'delete' => false, 'delete' => false,
@ -141,6 +145,10 @@ $default =
'path' => $_path . '/avatar/', 'path' => $_path . '/avatar/',
'ssl' => null, 'ssl' => null,
'maxsize' => 300), 'maxsize' => 300),
'foaf' =>
array(
'mbox_sha1sum' => false,
),
'public' => 'public' =>
array('localonly' => false, array('localonly' => false,
'blacklist' => array(), 'blacklist' => array(),
@ -233,6 +241,7 @@ $default =
'application/vnd.oasis.opendocument.text-web' => 'oth', 'application/vnd.oasis.opendocument.text-web' => 'oth',
'application/pdf' => 'pdf', 'application/pdf' => 'pdf',
'application/zip' => 'zip', 'application/zip' => 'zip',
'application/xml' => 'xml',
'image/png' => 'png', 'image/png' => 'png',
'image/jpeg' => 'jpg', 'image/jpeg' => 'jpg',
'image/gif' => 'gif', 'image/gif' => 'gif',
@ -289,6 +298,7 @@ $default =
), ),
'notice' => 'notice' =>
array('contentlimit' => null, 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 'defaultscope' => null, // null means 1 if site/private, 0 otherwise
'hidespam' => true), // Whether to hide silenced users from timelines 'hidespam' => true), // Whether to hide silenced users from timelines
'message' => 'message' =>

11
lib/fullnoticestream.php Normal file
View File

@ -0,0 +1,11 @@
<?php
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Class for notice streams that does not filter anything out.
*/
abstract class FullNoticeStream extends NoticeStream
{
protected $selectVerbs = [];
}

View File

@ -30,7 +30,7 @@
* @link http://status.net/ * @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 * Stream of notices for a profile's "all" feed
@ -72,7 +72,7 @@ class InboxNoticeStream extends ScopingNoticeStream
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/ * @link http://status.net/
*/ */
class RawInboxNoticeStream extends NoticeStream class RawInboxNoticeStream extends FullNoticeStream
{ {
protected $target = null; protected $target = null;
protected $inbox = null; protected $inbox = null;
@ -84,8 +84,8 @@ class RawInboxNoticeStream extends NoticeStream
*/ */
function __construct(Profile $target) function __construct(Profile $target)
{ {
parent::__construct();
$this->target = $target; $this->target = $target;
$this->unselectVerbs = array(ActivityVerb::DELETE);
} }
/** /**
@ -119,12 +119,9 @@ class RawInboxNoticeStream extends NoticeStream
if (!empty($max_id)) { if (!empty($max_id)) {
$notice->whereAdd(sprintf('notice.id <= %d', $max_id)); $notice->whereAdd(sprintf('notice.id <= %d', $max_id));
} }
if (!empty($this->selectVerbs)) {
$notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); self::filterVerbs($notice, $this->selectVerbs);
}
if (!empty($this->unselectVerbs)) {
$notice->whereAddIn('!verb', $this->unselectVerbs, $notice->columnType('verb'));
}
$notice->limit($offset, $limit); $notice->limit($offset, $limit);
// notice.id will give us even really old posts, which were // notice.id will give us even really old posts, which were
// recently imported. For example if a remote instance had // recently imported. For example if a remote instance had

View File

@ -23,7 +23,7 @@ class NetworkPublicNoticeStream extends ScopingNoticeStream
* @link http://status.net/ * @link http://status.net/
*/ */
class RawNetworkPublicNoticeStream extends NoticeStream class RawNetworkPublicNoticeStream extends FullNoticeStream
{ {
function getNoticeIds($offset, $limit, $since_id, $max_id) function getNoticeIds($offset, $limit, $since_id, $max_id)
{ {
@ -46,9 +46,7 @@ class RawNetworkPublicNoticeStream extends NoticeStream
Notice::addWhereSinceId($notice, $since_id); Notice::addWhereSinceId($notice, $since_id);
Notice::addWhereMaxId($notice, $max_id); Notice::addWhereMaxId($notice, $max_id);
if (!empty($this->selectVerbs)) { self::filterVerbs($notice, $this->selectVerbs);
$notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb'));
}
$ids = array(); $ids = array();

View File

@ -180,18 +180,24 @@ class Nickname
// All directory and file names in site root should be blacklisted // All directory and file names in site root should be blacklisted
$d = dir(INSTALLDIR); $d = dir(INSTALLDIR);
while (false !== ($entry = $d->read())) { while (false !== ($entry = $d->read())) {
$paths[] = $entry; $paths[$entry] = true;
} }
$d->close(); $d->close();
// All top level names in the router should be blacklisted // All top level names in the router should be blacklisted
$router = Router::get(); $router = Router::get();
foreach (array_keys($router->m->getPaths()) as $path) { foreach ($router->m->getPaths() as $path) {
if (preg_match('/^\/(.*?)[\/\?]/',$path,$matches)) { if (preg_match('/^([^\/\?]+)[\/\?]/',$path,$matches) && isset($matches[1])) {
$paths[] = $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));
} }
/** /**

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Class for notice streams * Class for notice streams
@ -46,16 +42,15 @@ if (!defined('STATUSNET')) {
*/ */
abstract class NoticeStream abstract class NoticeStream
{ {
protected $selectVerbs = null; // must be set to array protected $selectVerbs = array(ActivityVerb::POST => true,
protected $unselectVerbs = null; // must be set to array ActivityVerb::SHARE => true);
public function __construct() public function __construct()
{ {
if ($this->selectVerbs === null) { foreach ($this->selectVerbs as $key=>$val) {
$this->selectVerbs = array(ActivityVerb::POST, ActivityUtils::resolveUri(ActivityVerb::POST, true)); // to avoid database inconsistency issues we select both relative and absolute verbs
} $this->selectVerbs[ActivityUtils::resolveUri($key)] = $val;
if ($this->unselectVerbs === null) { $this->selectVerbs[ActivityUtils::resolveUri($key, true)] = $val;
$this->unselectVerbs = array(ActivityVerb::DELETE);
} }
} }
@ -74,4 +69,21 @@ abstract class NoticeStream
{ {
return Notice::multiGet('id', $ids); 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);
}
} }

View File

@ -101,7 +101,11 @@ class ProfileFormAction extends RedirectingAction
parent::handle($args); parent::handle($args);
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
try {
$this->handlePost(); $this->handlePost();
} catch (AlreadyFulfilledException $e) {
// 'tis alright
}
$this->returnToPrevious(); $this->returnToPrevious();
} }
} }

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Stream of notices by a profile * Stream of notices by a profile
@ -134,12 +130,7 @@ class RawProfileNoticeStream extends NoticeStream
Notice::addWhereSinceId($notice, $since_id); Notice::addWhereSinceId($notice, $since_id);
Notice::addWhereMaxId($notice, $max_id); Notice::addWhereMaxId($notice, $max_id);
if (!empty($this->selectVerbs)) { self::filterVerbs($notice, $this->selectVerbs);
$notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb'));
}
if (!empty($this->unselectVerbs)) {
$notice->whereAddIn('!verb', $this->unselectVerbs, $notice->columnType('verb'));
}
$notice->orderBy('created DESC, id DESC'); $notice->orderBy('created DESC, id DESC');

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Public stream * Public stream
@ -66,7 +62,7 @@ class PublicNoticeStream extends ScopingNoticeStream
* @link http://status.net/ * @link http://status.net/
*/ */
class RawPublicNoticeStream extends NoticeStream class RawPublicNoticeStream extends FullNoticeStream
{ {
function getNoticeIds($offset, $limit, $since_id, $max_id) function getNoticeIds($offset, $limit, $since_id, $max_id)
{ {
@ -87,9 +83,7 @@ class RawPublicNoticeStream extends NoticeStream
Notice::addWhereSinceId($notice, $since_id); Notice::addWhereSinceId($notice, $since_id);
Notice::addWhereMaxId($notice, $max_id); Notice::addWhereMaxId($notice, $max_id);
if (!empty($this->selectVerbs)) { self::filterVerbs($notice, $this->selectVerbs);
$notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb'));
}
$ids = array(); $ids = array();

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Stream of mentions of me * Stream of mentions of me
@ -92,8 +88,20 @@ class RawReplyNoticeStream extends NoticeStream
Notice::addWhereMaxId($reply, $max_id, 'notice_id', 'reply.modified'); Notice::addWhereMaxId($reply, $max_id, 'notice_id', 'reply.modified');
if (!empty($this->selectVerbs)) { if (!empty($this->selectVerbs)) {
// this is a little special since we have to join in Notice
$reply->joinAdd(array('notice_id', 'notice:id')); $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'); $reply->orderBy('reply.modified DESC, reply.notice_id DESC');

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Stream of notices with a given tag * Stream of notices with a given tag
@ -90,13 +86,22 @@ class RawTagNoticeStream extends NoticeStream
Notice::addWhereMaxId($nt, $max_id, 'notice_id'); Notice::addWhereMaxId($nt, $max_id, 'notice_id');
if (!empty($this->selectVerbs)) { if (!empty($this->selectVerbs)) {
$notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb')); $nt->joinAdd(array('notice_id', 'notice:id'));
}
if (!empty($this->unselectVerbs)) { $filter = array_keys(array_filter($this->selectVerbs));
$notice->whereAddIn('!verb', $this->unselectVerbs, $notice->columnType('verb')); if (!empty($filter)) {
// include verbs in selectVerbs with values that equate to true
$nt->whereAddIn('notice.verb', $filter, 'string');
} }
$nt->orderBy('created DESC, notice_id DESC'); $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('notice.created DESC, notice_id DESC');
if (!is_null($offset)) { if (!is_null($offset)) {
$nt->limit($offset, $limit); $nt->limit($offset, $limit);

View File

@ -80,43 +80,58 @@ class ToSelector extends Widget
function show() function show()
{ {
$choices = array(); $choices = array();
$default = 'public:site'; $default = common_config('site', 'private') ? 'public:site' : 'public:everyone';
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'));
$groups = $this->user->getGroups(); $groups = $this->user->getGroups();
while ($groups instanceof User_group && $groups->fetch()) { 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) { if (($this->to instanceof User_group) && $this->to->id == $groups->id) {
$default = $value; $default = $value;
} }
$choices[$value] = $groups->getBestName(); $choices[$value] = "!{$groups->getNickname()} [{$groups->getBestName()}]";
} }
// Add subscribed users to dropdown menu // Add subscribed users to dropdown menu
$users = $this->user->getSubscribed(); $users = $this->user->getSubscribed();
while ($users->fetch()) { while ($users->fetch()) {
$value = 'profile:'.$users->id; $value = 'profile:'.$users->getID();
if ($this->user->streamNicknames()) { try {
$choices[$value] = $users->getNickname(); $choices[$value] = substr($users->getAcctUri(), 5) . " [{$users->getBestName()}]";
} else { } catch (ProfileNoAcctUriException $e) {
$choices[$value] = $users->getBestName(); $choices[$value] = "[?@?] " . $e->profile->getBestName();
} }
} }
if ($this->to instanceof Profile) { if ($this->to instanceof Profile) {
$value = 'profile:'.$this->to->id; $value = 'profile:'.$this->to->getID();
$default = $value; $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, $this->out->dropdown($this->id,
// TRANS: Label for drop-down of potential addressees. // TRANS: Label for drop-down of potential addressees.
@ -127,18 +142,40 @@ class ToSelector extends Widget
$default); $default);
$this->out->elementStart('span', 'checkbox-wrapper'); $this->out->elementStart('span', 'checkbox-wrapper');
if (common_config('notice', 'allowprivate')) {
$this->out->checkbox('notice_private', $this->out->checkbox('notice_private',
// TRANS: Checkbox label in widget for selecting potential addressees to mark the notice private. // TRANS: Checkbox label in widget for selecting potential addressees to mark the notice private.
_('Private?'), _('Private?'),
$this->private); $this->private);
}
$this->out->elementEnd('span'); $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) static function fillOptions($action, &$options)
{ {
// XXX: make arg name selectable // XXX: make arg name selectable
$toArg = $action->trimmed('notice_to'); $toArg = $action->trimmed('notice_to');
$private = $action->boolean('notice_private'); $private = common_config('notice', 'allowprivate') ? $action->boolean('notice_private') : false;
if (empty($toArg)) { if (empty($toArg)) {
return; return;

View File

@ -66,7 +66,7 @@ class URLMapper
throw new Exception(sprintf("Can't connect %s; path has no action.", $path)); throw new Exception(sprintf("Can't connect %s; path has no action.", $path));
} }
$allpaths[] = $path; $this->allpaths[] = $path;
$action = $args[self::ACTION]; $action = $args[self::ACTION];

View File

@ -264,6 +264,11 @@ function common_logged_in()
return (!is_null(common_current_user())); 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() function common_have_session()
{ {
return (0 != strcmp(session_id(), '')); return (0 != strcmp(session_id(), ''));
@ -1391,6 +1396,74 @@ function common_path($relative, $ssl=false, $addSession=true)
return $proto.'://'.$serverpart.'/'.$pathpart.$relative; 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) function common_inject_session($url, $serverpart = null)
{ {
if (!common_have_session()) { if (!common_have_session()) {

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/** /**
* Notice stream for favorites * Notice stream for favorites
@ -77,14 +73,14 @@ class RawFaveNoticeStream extends NoticeStream
protected $user_id; protected $user_id;
protected $own; protected $own;
protected $selectVerbs = array();
function __construct($user_id, $own) function __construct($user_id, $own)
{ {
parent::__construct(); parent::__construct();
$this->user_id = $user_id; $this->user_id = $user_id;
$this->own = $own; $this->own = $own;
$this->selectVerbs = array();
} }
/** /**

View File

@ -0,0 +1,35 @@
<?php
/**
* Validates xmpp (for XMPP, so called JIDs)
* @todo Validate the xmpp address
*/
class HTMLPurifier_URIScheme_xmpp extends HTMLPurifier_URIScheme
{
/**
* @type bool
*/
public $browsable = false;
/**
* @type bool
*/
public $may_omit_host = true;
/**
* @param HTMLPurifier_URI $uri
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool
*/
public function doValidate(&$uri, $config, $context)
{
$uri->userinfo = null;
$uri->host = null;
$uri->port = null;
return true;
}
}
// vim: et sw=4 sts=4

View File

@ -17,9 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
if (!defined('STATUSNET')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
/** /**
* @package ModHelperPlugin * @package ModHelperPlugin
@ -45,7 +43,9 @@ class ModHelperPlugin extends Plugin
function onUserRightsCheck($profile, $right, &$result) function onUserRightsCheck($profile, $right, &$result)
{ {
if (in_array($right, self::$rights)) { 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')) { if ($profile->hasRole('modhelper')) {
$result = true; $result = true;
return false; return false;

View File

@ -1297,7 +1297,7 @@ class Ostatus_profile extends Managed_DataObject
try { try {
$this->updateAvatar($avatar); $this->updateAvatar($avatar);
} catch (Exception $ex) { } 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());
} }
} }
} }

View File

@ -63,8 +63,7 @@ class OpenidserverAction extends Action
$request = $this->oserver->decodeRequest(); $request = $this->oserver->decodeRequest();
if (in_array($request->mode, array('checkid_immediate', if (in_array($request->mode, array('checkid_immediate',
'checkid_setup'))) { 'checkid_setup'))) {
$user = common_current_user(); if (!$this->scoped instanceof Profile) {
if(!$user){
if($request->immediate){ if($request->immediate){
//cannot prompt the user to login in immediate mode, so answer false //cannot prompt the user to login in immediate mode, so answer false
$response = $this->generateDenyResponse($request); $response = $this->generateDenyResponse($request);
@ -77,9 +76,9 @@ class OpenidserverAction extends Action
common_set_returnto($_SERVER['REQUEST_URI']); common_set_returnto($_SERVER['REQUEST_URI']);
common_redirect(common_local_url('login'), 303); 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( $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(empty($user_openid_trustroot)){
if($request->immediate){ if($request->immediate){
//cannot prompt the user to trust this trust root in immediate mode, so answer false //cannot prompt the user to trust this trust root in immediate mode, so answer false
@ -87,7 +86,7 @@ class OpenidserverAction extends Action
}else{ }else{
common_ensure_session(); common_ensure_session();
$_SESSION['openid_trust_root'] = $request->trust_root; $_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 $this->oserver->encodeResponse($allowResponse); //sign the response
$denyResponse = $this->generateDenyResponse($request); $denyResponse = $this->generateDenyResponse($request);
$this->oserver->encodeResponse($denyResponse); //sign the response $this->oserver->encodeResponse($denyResponse); //sign the response
@ -101,12 +100,11 @@ class OpenidserverAction extends Action
// were POSTed here. // were POSTed here.
common_redirect(common_local_url('openidtrust'), 303); common_redirect(common_local_url('openidtrust'), 303);
} }
}else{ } else {
//user has previously authorized this trust root //user has previously authorized this trust root
$response = $this->generateAllowResponse($request, $user); $response = $this->generateAllowResponse($request, $this->scoped);
//$response = $request->answer(true, null, common_profile_url($user->nickname));
} }
} else if ($request->immediate) { } elseif ($request->immediate) {
$response = $this->generateDenyResponse($request); $response = $this->generateDenyResponse($request);
} else { } else {
//invalid //invalid
@ -137,14 +135,14 @@ class OpenidserverAction extends Action
} }
} }
function generateAllowResponse($request, $user){ function generateAllowResponse($request, Profile $profile){
$response = $request->answer(true, null, common_profile_url($user->nickname)); $response = $request->answer(true, null, $profile->getUrl());
$user = $profile->getUser();
$profile = $user->getProfile();
$sreg_data = array( $sreg_data = array(
'fullname' => $profile->fullname, 'fullname' => $profile->getFullname(),
'nickname' => $user->nickname, 'nickname' => $profile->getNickname(),
'email' => $user->email, 'email' => $user->email, // FIXME: Should we make the email optional?
'language' => $user->language, 'language' => $user->language,
'timezone' => $user->timezone); 'timezone' => $user->timezone);
$sreg_request = Auth_OpenID_SRegRequest::fromOpenIDRequest($request); $sreg_request = Auth_OpenID_SRegRequest::fromOpenIDRequest($request);

View File

@ -81,6 +81,13 @@ class RegisterThrottlePlugin extends Plugin
return true; 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. * Called when someone tries to register.
* *
@ -134,6 +141,52 @@ class RegisterThrottlePlugin extends Plugin
return true; 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. * Called after someone registers, by any means.
* *
@ -154,8 +207,8 @@ class RegisterThrottlePlugin extends Plugin
$reg = new Registration_ip(); $reg = new Registration_ip();
$reg->user_id = $profile->id; $reg->user_id = $profile->getID();
$reg->ipaddress = $ipaddress; $reg->ipaddress = mb_strtolower($ipaddress);
$reg->created = common_sql_now(); $reg->created = common_sql_now();
$result = $reg->insert(); $result = $reg->insert();

View File

@ -0,0 +1,41 @@
<?php
if (!defined('GNUSOCIAL')) { exit(1); }
class IpregistrationsAction extends ManagedAction
{
protected $needLogin = true;
protected $ipaddress = null;
function title()
{
return sprintf(_('Registrations from IP %s'), $this->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');
}
}

View File

@ -100,13 +100,34 @@ class WebFingerPlugin extends Plugin
} }
} }
} else { } else {
$user = User::getKV('uri', $resource); try {
if ($user instanceof User) { $user = User::getByUri($resource);
$profile = $user->getProfile(); $profile = $user->getProfile();
} else { } catch (NoResultException $e) {
// try and get it by profile url if (common_config('fix', 'fancyurls')) {
$profile = Profile::getKV('profileurl', $resource); 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) { if ($profile instanceof Profile) {
@ -144,8 +165,8 @@ class WebFingerPlugin extends Plugin
public function onStartShowHTML($action) public function onStartShowHTML($action)
{ {
if ($action instanceof ShowstreamAction) { if ($action instanceof ShowstreamAction) {
$acct = 'acct:'. $action->getTarget()->getNickname() .'@'. common_config('site', 'server'); $resource = $action->getTarget()->getUri();
$url = common_local_url('webfinger') . '?resource='.$acct; $url = common_local_url('webfinger') . '?resource='.urlencode($resource);
foreach (array(Discovery::JRD_MIMETYPE, Discovery::XRD_MIMETYPE) as $type) { foreach (array(Discovery::JRD_MIMETYPE, Discovery::XRD_MIMETYPE) as $type) {
header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="'.$type.'"', false); header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="'.$type.'"', false);

View File

@ -31,23 +31,23 @@ abstract class WebFingerResource
public function getAliases() public function getAliases()
{ {
$aliases = array(); $aliases = $this->object->getAliasesWithIDs();
// Add the URI as an identity, this is _not_ necessarily an HTTP url // Some sites have changed from http to https and still want
$uri = $this->object->getUri(); // (because remote sites look for it) verify that they are still
$aliases[] = $uri; // the same identity as they were on HTTP. Should NOT be used if
if (common_config('webfinger', 'http_alias') // you've run HTTPS all the time!
&& strtolower(parse_url($uri, PHP_URL_SCHEME)) === 'https') { if (common_config('webfinger', 'http_alias')) {
$aliases[] = preg_replace('/^https:/', 'http:', $uri, 1); foreach ($aliases as $alias=>$id) {
if (!strtolower(parse_url($alias, PHP_URL_SCHEME)) === 'https') {
continue;
}
$aliases[preg_replace('/^https:/', 'http:', $alias, 1)] = $id;
}
} }
try { // return a unique set of aliases by extracting only the keys
$aliases[] = $this->object->getUrl(); return array_keys($aliases);
} catch (InvalidUrlException $e) {
// getUrl failed because no valid URL could be returned, just ignore it
}
return $aliases;
} }
abstract public function updateXRD(XML_XRD $xrd); abstract public function updateXRD(XML_XRD $xrd);

83
scripts/delete_orphan_files.php Executable file
View File

@ -0,0 +1,83 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$shortoptions = 'y';
$longoptions = array('yes');
$helptext = <<<END_OF_HELP
delete_orphan_files.php [options]
Deletes all files and their File entries where there is no link to a
Notice entry. Good for cleaning up after user deletion or so where the
attached files weren't removed as well.
-y --yes do not wait for confirmation
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';
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'.
' 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)'.
' GROUP BY file.id;';
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);
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";

View File

@ -4,65 +4,46 @@ Initial simple way to Webfinger enable your domain -- needs PHP.
Step 1 Step 1
====== ======
First, put the folders 'xrd' and 'dot-well-known' on your website, so Put the 'dot-well-known' on your website, so it loads at:
they load at:
http://yourname.com/xrd/ https://example.com/.well-known/
and (Remember the . at the beginning of this one, which is common practice
for "hidden" files and why we have renamed it "dot-")
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://
Step 2 Step 2
====== ======
Next, edit xrd/index.php and enter a secret in this line: Edit the .well-known/host-meta file and replace "example.com" with the
domain name you're hosting the .well-known directory on.
$s = "";
This can be anything you like...
$s = "johnny5";
or
$s = "12345";
It really doesn't matter too much.
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 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... 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 In the webfinger directory, make a copy of the example@example.com.xml file
so that it's called... 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 Then edit the file contents, replacing "social.example.com" with your
your domain is 'titanictoycorp.biz', your file should be called GNU social instance's base path, and change the user ID number (and
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
nickname for the FOAF link) to that of your account on your social 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 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 GNU social profile page by looking at the destination URLs in the
Feeds links. Feeds links.
PROTIP: You can get the bulk of the contents (note the <Subject> element though)
from curling down your real webfinger data:
$ curl https://social.example.com/.well-known/webfinger?resource=acct:username@social.example.com
Finally Finally
======= =======

View File

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0" <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0" xmlns:hm="http://host-meta.net/xrd/1.0">
xmlns:hm="http://host-meta.net/xrd/1.0"> <Link rel="lrdd" type="application/xrd+xml"
<hm:Host>example.com</hm:Host> template="https://example.com/.well-known/webfinger?resource={uri}"/>
<Link rel="lrdd" template="http://example.com/.well-known/xrd?uri={uri}">
<Title>WebFinger resource descriptor</Title>
</Link>
</XRD> </XRD>

View File

@ -1,35 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Subject>acct:example@example.com</Subject> <Subject>acct:username@example.com</Subject>
<Alias>acct:example@social.example.com</Alias> <Alias>acct:username@social.example.com</Alias>
<Alias>http://social.example.com/user/1</Alias> <Alias>https://social.example.com/user/1</Alias>
<Link rel="http://webfinger.net/rel/profile-page" <Link rel="http://webfinger.net/rel/profile-page"
type="text/html" type="text/html"
href="http://social.example.com/user/1"/> href="https://social.example.com/user/1"/>
<Link rel="http://schemas.google.com/g/2010#updates-from" <Link rel="http://schemas.google.com/g/2010#updates-from"
type="application/atom+xml" type="application/atom+xml"
href="http://social.example.com/api/statuses/user_timeline/1.atom"/> href="https://social.example.com/api/statuses/user_timeline/1.atom"/>
<!-- Is this/was this ever supported? <!-- Is this/was this ever supported?
<Link rel="http://microformats.org/profile/hcard" <Link rel="http://microformats.org/profile/hcard"
type="text/html" type="text/html"
href="http://social.example.com/hcard"/> --> href="https://social.example.com/hcard"/> -->
<Link rel="http://gmpg.org/xfn/11" <Link rel="http://gmpg.org/xfn/11"
type="text/html" type="text/html"
href="http://social.example.com/user/1"/> href="https://social.example.com/user/1"/>
<Link rel="describedby" <Link rel="describedby"
type="application/rdf+xml" type="application/rdf+xml"
href="http://social.example.com/username/foaf"/> href="https://social.example.com/username/foaf"/>
<Link rel="http://salmon-protocol.org/ns/salmon-replies" <Link rel="http://salmon-protocol.org/ns/salmon-replies"
href="http://social.example.com/main/salmon/user/1"/> href="https://social.example.com/main/salmon/user/1"/>
<Link rel="http://salmon-protocol.org/ns/salmon-mention" <Link rel="http://salmon-protocol.org/ns/salmon-mention"
href="http://social.example.com/main/salmon/user/1"/> href="https://social.example.com/main/salmon/user/1"/>
<Link rel="http://ostatus.org/schema/1.0/subscribe" <Link rel="http://ostatus.org/schema/1.0/subscribe"
template="http://social.example.com/main/ostatussub?profile={uri}"/> template="https://social.example.com/main/ostatussub?profile={uri}"/>
</XRD> </XRD>

View File

@ -19,23 +19,25 @@
*/ */
$s = ""; // basename should make sure we can't escape this directory
$u = basename($_GET['resource']);
/* this should be a secret */ if (!strpos($u, '@')) {
throw new Exception('Bad resource');
$u = $_GET['uri']; exit(1);
$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 (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)) {
header('Content-Disposition: attachment; filename="'.urlencode($f).'"');
header('Content-type: application/xrd+xml');
echo file_get_contents($f);
}

View File

@ -503,6 +503,10 @@ address .poweredby {
z-index: 99; z-index: 99;
} }
.form_notice .to-selector > select {
max-width: 300px;
}
.form_settings label[for=notice_to] { .form_settings label[for=notice_to] {
left: 5px; left: 5px;
margin-left: 0px; margin-left: 0px;