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

This commit is contained in:
abjectio 2015-06-05 20:57:28 +02:00
commit c00b2ecc4b
44 changed files with 648 additions and 398 deletions

View File

@ -26,7 +26,7 @@ PHP modules
The following software packages are *required* for this software to The following software packages are *required* for this software to
run correctly. run correctly.
- PHP 5.4+ For newer versions, some functions that are used may be - PHP 5.5+ For newer versions, some functions that are used may be
disabled by default, such as the pcntl_* family. See the disabled by default, such as the pcntl_* family. See the
section on 'Queues and daemons' for more information. section on 'Queues and daemons' for more information.
- MariaDB 5+ GNU Social uses, by default, a MariaDB server for data - MariaDB 5+ GNU Social uses, by default, a MariaDB server for data

View File

@ -128,7 +128,7 @@ class ConversationAction extends ManagedAction
'format' => 'atom')), 'format' => 'atom')),
// TRANS: Title for link to notice feed. // TRANS: Title for link to notice feed.
// TRANS: %s is a user nickname. // TRANS: %s is a user nickname.
_('Conversation feed (Activity Streams JSON)'))); _('Conversation feed (Atom)')));
} }
} }

View File

@ -410,6 +410,7 @@ class EmailsettingsAction extends SettingsAction
$this->serverError(_('Could not insert confirmation code.')); $this->serverError(_('Could not insert confirmation code.'));
} }
common_debug('Sending confirmation address for user '.$user->id.' to email '.$email);
mail_confirm_address($user, $confirm->code, $user->nickname, $email); mail_confirm_address($user, $confirm->code, $user->nickname, $email);
Event::handle('EndAddEmailAddress', array($user, $email)); Event::handle('EndAddEmailAddress', array($user, $email));

View File

@ -2,7 +2,7 @@
if (!defined('GNUSOCIAL')) { exit(1); } if (!defined('GNUSOCIAL')) { exit(1); }
class NetworkpublicAction extends PublicAction class NetworkpublicAction extends SitestreamAction
{ {
protected function streamPrepare() protected function streamPrepare()
{ {
@ -28,13 +28,6 @@ class NetworkpublicAction extends PublicAction
} }
} }
function extraHead()
{
// the PublicAction has some XRDS stuff that might be unique to the non-network public feed
// FIXME: Solve this with a call that doesn't rely on parent:: and is unique for each class.
ManagedAction::extraHead();
}
function showSections() function showSections()
{ {
// Show invite button, as long as site isn't closed, and // Show invite button, as long as site isn't closed, and

View File

@ -29,10 +29,6 @@
if (!defined('GNUSOCIAL')) { exit(1); } if (!defined('GNUSOCIAL')) { exit(1); }
// Farther than any human will go
define('MAX_PUBLIC_PAGE', 100);
/** /**
* Action for displaying the public stream * Action for displaying the public stream
* *
@ -43,54 +39,9 @@ define('MAX_PUBLIC_PAGE', 100);
* @link http://status.net/ * @link http://status.net/
* *
* @see PublicrssAction * @see PublicrssAction
* @see PublicxrdsAction
*/ */
class PublicAction extends ManagedAction class PublicAction extends SitestreamAction
{ {
/**
* page of the stream we're on; default = 1
*/
var $page = null;
var $notice;
protected $stream = null;
function isReadOnly($args)
{
return true;
}
protected function doPreparation()
{
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
if ($this->page > MAX_PUBLIC_PAGE) {
// TRANS: Client error displayed when requesting a public timeline page beyond the page limit.
// TRANS: %s is the page limit.
$this->clientError(sprintf(_('Beyond the page limit (%s).'), MAX_PUBLIC_PAGE));
}
common_set_returnto($this->selfUrl());
$this->streamPrepare();
$this->notice = $this->stream->getNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
if (!$this->notice) {
// TRANS: Server error displayed when a public timeline cannot be retrieved.
$this->serverError(_('Could not retrieve public timeline.'));
}
if ($this->page > 1 && $this->notice->N == 0){
// TRANS: Client error when page not found (404).
$this->clientError(_('No such page.'), 404);
}
return true;
}
protected function streamPrepare() protected function streamPrepare()
{ {
if ($this->scoped instanceof Profile && $this->scoped->isLocal() && $this->scoped->getUser()->streamModeOnly()) { if ($this->scoped instanceof Profile && $this->scoped->isLocal() && $this->scoped->getUser()->streamModeOnly()) {
@ -117,100 +68,6 @@ class PublicAction extends ManagedAction
} }
} }
function extraHead()
{
parent::extraHead();
$this->element('meta', array('http-equiv' => 'X-XRDS-Location',
'content' => common_local_url('publicxrds')));
$rsd = common_local_url('rsd');
// RSD, http://tales.phrasewise.com/rfc/rsd
$this->element('link', array('rel' => 'EditURI',
'type' => 'application/rsd+xml',
'href' => $rsd));
if ($this->page != 1) {
$this->element('link', array('rel' => 'canonical',
'href' => common_local_url('public')));
}
}
/**
* Output <head> elements for RSS and Atom feeds
*
* @return void
*/
function getFeeds()
{
return array(new Feed(Feed::JSON,
common_local_url('ApiTimelinePublic',
array('format' => 'as')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (Activity Streams JSON)')),
new Feed(Feed::RSS1, common_local_url('publicrss'),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (RSS 1.0)')),
new Feed(Feed::RSS2,
common_local_url('ApiTimelinePublic',
array('format' => 'rss')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (RSS 2.0)')),
new Feed(Feed::ATOM,
common_local_url('ApiTimelinePublic',
array('format' => 'atom')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (Atom)')));
}
function showEmptyList()
{
// TRANS: Text displayed for public feed when there are no public notices.
$message = _('This is the public timeline for %%site.name%% but no one has posted anything yet.') . ' ';
if (common_logged_in()) {
// TRANS: Additional text displayed for public feed when there are no public notices for a logged in user.
$message .= _('Be the first to post!');
}
else {
if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
// TRANS: Additional text displayed for public feed when there are no public notices for a not logged in user.
$message .= _('Why not [register an account](%%action.register%%) and be the first to post!');
}
}
$this->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message));
$this->elementEnd('div');
}
/**
* Fill the content area
*
* Shows a list of the notices in the public stream, with some pagination
* controls.
*
* @return void
*/
function showContent()
{
if ($this->scoped instanceof Profile && $this->scoped->isLocal() && $this->scoped->getUser()->streamModeOnly()) {
$nl = new PrimaryNoticeList($this->notice, $this, array('show_n'=>NOTICES_PER_PAGE));
} else {
$nl = new ThreadedNoticeList($this->notice, $this, $this->scoped);
}
$cnt = $nl->show();
if ($cnt == 0) {
$this->showEmptyList();
}
$this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
$this->page, $this->action);
}
function showSections() function showSections()
{ {
// Show invite button, as long as site isn't closed, and // Show invite button, as long as site isn't closed, and
@ -239,23 +96,30 @@ class PublicAction extends ManagedAction
$feat->show(); $feat->show();
} }
function showAnonymousMessage() /**
* Output <head> elements for RSS and Atom feeds
*
* @return void
*/
function getFeeds()
{ {
if (! (common_config('site','closed') || common_config('site','inviteonly'))) { return array(new Feed(Feed::JSON,
// TRANS: Message for not logged in users at an invite-only site trying to view the public feed of notices. common_local_url('ApiTimelinePublic',
// TRANS: This message contains Markdown links. Please mind the formatting. array('format' => 'as')),
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . // TRANS: Link description for public timeline feed.
'based on the Free Software [StatusNet](http://status.net/) tool. ' . _('Public Timeline Feed (Activity Streams JSON)')),
'[Join now](%%action.register%%) to share notices about yourself with friends, family, and colleagues! ' . new Feed(Feed::RSS1, common_local_url('publicrss'),
'([Read more](%%doc.help%%))'); // TRANS: Link description for public timeline feed.
} else { _('Public Timeline Feed (RSS 1.0)')),
// TRANS: Message for not logged in users at a closed site trying to view the public feed of notices. new Feed(Feed::RSS2,
// TRANS: This message contains Markdown links. Please mind the formatting. common_local_url('ApiTimelinePublic',
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . array('format' => 'rss')),
'based on the Free Software [StatusNet](http://status.net/) tool.'); // TRANS: Link description for public timeline feed.
} _('Public Timeline Feed (RSS 2.0)')),
$this->elementStart('div', array('id' => 'anon_notice')); new Feed(Feed::ATOM,
$this->raw(common_markup_to_html($m)); common_local_url('ApiTimelinePublic',
$this->elementEnd('div'); array('format' => 'atom')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (Atom)')));
} }
} }

View File

@ -272,10 +272,16 @@ class RecoverpasswordAction extends Action
try { try {
User::recoverPassword($nore); User::recoverPassword($nore);
$this->mode = 'sent'; $this->mode = 'sent';
// TRANS: User notification after an e-mail with instructions was sent from the password recovery form. if (common_is_email($nore) && common_config('site', 'fakeaddressrecovery')) {
$this->msg = _('Instructions for recovering your password ' . // TRANS: User notification when recovering password by giving email address,
'have been sent to the email address registered to your ' . // regardless if the mail was sent or not (to hide registered email status).
'account.'); $this->msg = _('If the email address you provided was found in the database, a recovery mail with instructions has been sent there.');
} else {
// TRANS: User notification after an e-mail with instructions was sent from the password recovery form.
$this->msg = _('Instructions for recovering your password ' .
'have been sent to the email address registered to your ' .
'account.');
}
$this->success = true; $this->success = true;
} catch (Exception $e) { } catch (Exception $e) {
$this->success = false; $this->success = false;

View File

@ -116,14 +116,14 @@ class File extends Managed_DataObject
* *
* @fixme refactor this mess, it's gotten pretty scary. * @fixme refactor this mess, it's gotten pretty scary.
* @param string $given_url the URL we're looking at * @param string $given_url the URL we're looking at
* @param int $notice_id (optional) * @param Notice $notice (optional)
* @param bool $followRedirects defaults to true * @param bool $followRedirects defaults to true
* *
* @return mixed File on success, -1 on some errors * @return mixed File on success, -1 on some errors
* *
* @throws ServerException on failure * @throws ServerException on failure
*/ */
public static function processNew($given_url, $notice_id=null, $followRedirects=true) { public static function processNew($given_url, Notice $notice=null, $followRedirects=true) {
if (empty($given_url)) { if (empty($given_url)) {
throw new ServerException('No given URL to process'); throw new ServerException('No given URL to process');
} }
@ -181,7 +181,7 @@ class File extends Managed_DataObject
// //
// Seen in the wild with clojure.org, which redirects through // Seen in the wild with clojure.org, which redirects through
// wikispaces for auth and appends session data in the URL params. // wikispaces for auth and appends session data in the URL params.
$file = self::processNew($redir_url, $notice_id, /*followRedirects*/false); $file = self::processNew($redir_url, $notice, /*followRedirects*/false);
File_redirection::saveNew($redir_data, $file->id, $given_url); File_redirection::saveNew($redir_data, $file->id, $given_url);
} }
@ -193,8 +193,8 @@ class File extends Managed_DataObject
} }
} }
if (!empty($notice_id)) { if ($notice instanceof Notice) {
File_to_post::processNew($file->id, $notice_id); File_to_post::processNew($file, $notice);
} }
return $file; return $file;
} }
@ -249,6 +249,15 @@ class File extends Managed_DataObject
return true; return true;
} }
public function getFilename()
{
if (!self::validFilename($this->filename)) {
// TRANS: Client exception thrown if a file upload does not have a valid name.
throw new ClientException(_("Invalid filename."));
}
return $this->filename;
}
// where should the file go? // where should the file go?
static function filename(Profile $profile, $origname, $mimetype) static function filename(Profile $profile, $origname, $mimetype)
@ -501,9 +510,9 @@ class File extends Managed_DataObject
function blowCache($last=false) function blowCache($last=false)
{ {
self::blow('file:notice-ids:%s', $this->urlhash); self::blow('file:notice-ids:%s', $this->id);
if ($last) { if ($last) {
self::blow('file:notice-ids:%s;last', $this->urlhash); self::blow('file:notice-ids:%s;last', $this->id);
} }
self::blow('file:notice-count:%d', $this->id); self::blow('file:notice-count:%d', $this->id);
} }
@ -610,12 +619,45 @@ class File extends Managed_DataObject
return; return;
} }
echo "\nFound old $table table, upgrading it to contain 'urlhash' field..."; echo "\nFound old $table table, upgrading it to contain 'urlhash' field...";
$file = new File();
$file->query(sprintf('SELECT id, LEFT(url, 191) AS shortenedurl, COUNT(*) AS c FROM %1$s WHERE LENGTH(url)>191 GROUP BY shortenedurl HAVING c > 1', $schema->quoteIdentifier($table)));
print "\nFound {$file->N} URLs with too long entries in file table\n";
while ($file->fetch()) {
// We've got a URL that is too long for our future file table
// so we'll cut it. We could save the original URL, but there is
// no guarantee it is complete anyway since the previous max was 255 chars.
$dupfile = new File();
// First we find file entries that would be duplicates of this when shortened
// ... and we'll just throw the dupes out the window for now! It's already so borken.
$dupfile->query(sprintf('SELECT * FROM file WHERE LEFT(url, 191) = "%1$s"', $file->shortenedurl));
// Leave one of the URLs in the database by using ->find(true) (fetches first entry)
if ($dupfile->find(true)) {
print "\nShortening url entry for $table id: {$file->id} [";
$orig = clone($dupfile);
$dupfile->url = $file->shortenedurl; // make sure it's only 191 chars from now on
$dupfile->update($orig);
print "\nDeleting duplicate entries of too long URL on $table id: {$file->id} [";
// only start deleting with this fetch.
while($dupfile->fetch()) {
print ".";
$dupfile->delete();
}
print "]\n";
} else {
print "\nWarning! URL suddenly disappeared from database: {$file->url}\n";
}
}
echo "...and now all the non-duplicates which are longer than 191 characters...\n";
$file->query('UPDATE file SET url=LEFT(url, 191) WHERE LENGTH(url)>191');
echo "\n...now running hacky pre-schemaupdate change for $table:";
// We have to create a urlhash that is _not_ the primary key, // We have to create a urlhash that is _not_ the primary key,
// transfer data and THEN run checkSchema // transfer data and THEN run checkSchema
$schemadef['fields']['urlhash'] = array ( $schemadef['fields']['urlhash'] = array (
'type' => 'varchar', 'type' => 'varchar',
'length' => 64, 'length' => 64,
'not null' => true, 'not null' => false, // this is because when adding column, all entries will _be_ NULL!
'description' => 'sha256 of destination URL (url field)', 'description' => 'sha256 of destination URL (url field)',
); );
$schemadef['fields']['url'] = array ( $schemadef['fields']['url'] = array (

View File

@ -59,12 +59,7 @@ class File_redirection extends Managed_DataObject
static public function getByUrl($url) static public function getByUrl($url)
{ {
$file = new File_redirection(); return self::getByPK(array('urlhash' => File::hashurl($url)));
$file->urlhash = File::hashurl($url);
if (!$file->find(true)) {
throw new NoResultException($file);
}
return $file;
} }
static function _commonHttp($url, $redirs) { static function _commonHttp($url, $redirs) {
@ -261,7 +256,7 @@ class File_redirection extends Managed_DataObject
// store it // store it
$file = File::getKV('url', $long_url); $file = File::getKV('url', $long_url);
if ($file instanceof File) { if ($file instanceof File) {
$file_id = $file->id; $file_id = $file->getID();
} else { } else {
// Check if the target URL is itself a redirect... // Check if the target URL is itself a redirect...
$redir_data = File_redirection::where($long_url); $redir_data = File_redirection::where($long_url);
@ -269,7 +264,7 @@ class File_redirection extends Managed_DataObject
// We haven't seen the target URL before. // We haven't seen the target URL before.
// Save file and embedding data about it! // Save file and embedding data about it!
$file = File::saveNew($redir_data, $long_url); $file = File::saveNew($redir_data, $long_url);
$file_id = $file->id; $file_id = $file->getID();
} else if (is_string($redir_data)) { } else if (is_string($redir_data)) {
// The file is a known redirect target. // The file is a known redirect target.
$file = File::getKV('url', $redir_data); $file = File::getKV('url', $redir_data);
@ -281,7 +276,7 @@ class File_redirection extends Managed_DataObject
// SSL sites with cert issues. // SSL sites with cert issues.
return null; return null;
} }
$file_id = $file->id; $file_id = $file->getID();
} }
} }
$file_redir = File_redirection::getKV('url', $short_url); $file_redir = File_redirection::getKV('url', $short_url);

View File

@ -82,9 +82,9 @@ class File_thumbnail extends Managed_DataObject
* Fetch an entry by using a File's id * Fetch an entry by using a File's id
*/ */
static function byFile(File $file) { static function byFile(File $file) {
$file_thumbnail = self::getKV('file_id', $file->id); $file_thumbnail = self::getKV('file_id', $file->getID());
if (!$file_thumbnail instanceof File_thumbnail) { if (!$file_thumbnail instanceof File_thumbnail) {
throw new ServerException(sprintf('No File_thumbnail entry for File id==%u', $file->id)); throw new ServerException(sprintf('No File_thumbnail entry for File id==%u', $file->getID()));
} }
return $file_thumbnail; return $file_thumbnail;
} }
@ -167,11 +167,6 @@ class File_thumbnail extends Managed_DataObject
public function getFile() public function getFile()
{ {
$file = new File(); return File::getByID($this->file_id);
$file->id = $this->file_id;
if (!$file->find(true)) {
throw new NoResultException($file);
}
return $file;
} }
} }

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') && !defined('LACONICA')) { exit(1); } if (!defined('GNUSOCIAL')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
/** /**
* Table Definition for file_to_post * Table Definition for file_to_post
@ -58,39 +56,59 @@ class File_to_post extends Managed_DataObject
); );
} }
function processNew($file_id, $notice_id) { function processNew(File $file, Notice $notice) {
static $seen = array(); static $seen = array();
if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) {
$f2p = File_to_post::pkeyGet(array('post_id' => $notice_id, $file_id = $file->getID();
'file_id' => $file_id)); $notice_id = $notice->getID();
if (empty($f2p)) { if (!array_key_exists($notice_id, $seen)) {
$seen[$notice_id] = array();
}
if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) {
try {
$f2p = File_to_post::getByPK(array('post_id' => $notice_id,
'file_id' => $file_id));
} catch (NoResultException $e) {
$f2p = new File_to_post; $f2p = new File_to_post;
$f2p->file_id = $file_id; $f2p->file_id = $file_id;
$f2p->post_id = $notice_id; $f2p->post_id = $notice_id;
$f2p->insert(); $f2p->insert();
$f = File::getKV($file_id); $file->blowCache();
if (!empty($f)) {
$f->blowCache();
}
} }
if (empty($seen[$notice_id])) { $seen[$notice_id][] = $file_id;
$seen[$notice_id] = array($file_id);
} else {
$seen[$notice_id][] = $file_id;
}
} }
} }
static function getNoticeIDsByFile(File $file)
{
$f2p = new File_to_post();
$f2p->selectAdd();
$f2p->selectAdd('post_id');
$f2p->file_id = $file->getID();
$ids = array();
if (!$f2p->find()) {
throw new NoResultException($f2p);
}
return $f2p->fetchAll('post_id');
}
function delete($useWhere=false) function delete($useWhere=false)
{ {
$f = File::getKV('id', $this->file_id); try {
if ($f instanceof File) { $f = File::getByID($this->file_id);
$f->blowCache(); $f->blowCache();
} catch (NoResultException $e) {
// ...alright, that's weird, but no File to delete anyway.
} }
return parent::delete($useWhere); return parent::delete($useWhere);
} }
} }

View File

@ -64,6 +64,11 @@ abstract class Managed_DataObject extends Memcached_DataObject
return parent::pkeyGetClass(get_called_class(), $kv); return parent::pkeyGetClass(get_called_class(), $kv);
} }
static function pkeyCols()
{
return parent::pkeyColsClass(get_called_class());
}
/** /**
* Get multiple items from the database by key * Get multiple items from the database by key
* *
@ -304,6 +309,53 @@ abstract class Managed_DataObject extends Memcached_DataObject
return common_database_tablename($this->tableName()); return common_database_tablename($this->tableName());
} }
/**
* Returns an object by looking at the primary key column(s).
*
* Will require all primary key columns to be defined in an associative array
* and ignore any keys which are not part of the primary key.
*
* Will NOT accept NULL values as part of primary key.
*
* @param array $vals Must match all primary key columns for the dataobject.
*
* @return Managed_DataObject of the get_called_class() type
* @throws NoResultException if no object with that primary key
*/
static function getByPK(array $vals)
{
$classname = get_called_class();
$pkey = static::pkeyCols();
if (is_null($pkey)) {
throw new ServerException("Failed to get primary key columns for class '{$classname}'");
}
$object = new $classname();
foreach ($pkey as $col) {
if (!array_key_exists($col, $vals)) {
throw new ServerException("Missing primary key column '{$col}'");
} elseif (is_null($vals[$col])) {
throw new ServerException("NULL values not allowed in getByPK for column '{$col}'");
}
$object->$col = $vals[$col];
}
if (!$object->find(true)) {
throw new NoResultException($object);
}
return $object;
}
static function getByID($id)
{
if (empty($id)) {
throw new ServerException('Empty ID on lookup');
}
// getByPK throws exception if id is null
// or if the class does not have a single 'id' column as primary key
return static::getByPK(array('id' => $id));
}
/** /**
* Returns an ID, checked that it is set and reasonably valid * Returns an ID, checked that it is set and reasonably valid
* *

View File

@ -34,7 +34,7 @@ class Memcached_DataObject extends Safe_DataObject
{ {
if (is_null($v)) { if (is_null($v)) {
$v = $k; $v = $k;
$keys = self::pkeyCols($cls); $keys = static::pkeyCols();
if (count($keys) > 1) { if (count($keys) > 1) {
// FIXME: maybe call pkeyGetClass() ourselves? // FIXME: maybe call pkeyGetClass() ourselves?
throw new Exception('Use pkeyGetClass() for compound primary keys'); throw new Exception('Use pkeyGetClass() for compound primary keys');
@ -246,7 +246,7 @@ class Memcached_DataObject extends Safe_DataObject
return $query; return $query;
} }
static function pkeyCols($cls) static function pkeyColsClass($cls)
{ {
$i = new $cls; $i = new $cls;
$types = $i->keyTypes(); $types = $i->keyTypes();
@ -279,7 +279,7 @@ class Memcached_DataObject extends Safe_DataObject
$pkeyMap = array_fill_keys($keyVals, array()); $pkeyMap = array_fill_keys($keyVals, array());
$result = array_fill_keys($keyVals, array()); $result = array_fill_keys($keyVals, array());
$pkeyCols = self::pkeyCols($cls); $pkeyCols = static::pkeyCols();
$toFetch = array(); $toFetch = array();
$allPkeys = array(); $allPkeys = array();

View File

@ -84,7 +84,7 @@ class Notice extends Managed_DataObject
'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'), 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'),
'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'), 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8_general_ci'), 'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8mb4_general_ci'),
'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'),
'url' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), 'url' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'),
'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
@ -313,16 +313,6 @@ class Notice extends Managed_DataObject
return $notice; return $notice;
} }
public static function getById($id)
{
$notice = new Notice();
$notice->id = $id;
if (!$notice->find(true)) {
throw new NoResultException($notice);
}
return $notice;
}
/** /**
* Extract #hashtags from this notice's content and save them to the database. * Extract #hashtags from this notice's content and save them to the database.
*/ */
@ -1109,7 +1099,7 @@ class Notice extends Managed_DataObject
*/ */
function saveUrls() { function saveUrls() {
if (common_config('attachments', 'process_links')) { if (common_config('attachments', 'process_links')) {
common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this);
} }
} }
@ -1126,11 +1116,7 @@ class Notice extends Managed_DataObject
if (common_config('attachments', 'process_links')) { if (common_config('attachments', 'process_links')) {
// @fixme validation? // @fixme validation?
foreach (array_unique($urls) as $url) { foreach (array_unique($urls) as $url) {
try { $this->saveUrl($url, $this);
File::processNew($url, $this->id);
} catch (ServerException $e) {
// Could not save URL. Log it?
}
} }
} }
} }
@ -1138,9 +1124,9 @@ class Notice extends Managed_DataObject
/** /**
* @private callback * @private callback
*/ */
function saveUrl($url, $notice_id) { function saveUrl($url, Notice $notice) {
try { try {
File::processNew($url, $notice_id); File::processNew($url, $notice);
} catch (ServerException $e) { } catch (ServerException $e) {
// Could not save URL. Log it? // Could not save URL. Log it?
} }
@ -1311,7 +1297,7 @@ class Notice extends Managed_DataObject
$last = $parent; $last = $parent;
continue; continue;
} }
} catch (Exception $e) { } catch (NoParentNoticeException $e) {
// Latest notice has no parent // Latest notice has no parent
} }
// No parent, or parent out of scope // No parent, or parent out of scope
@ -1617,7 +1603,7 @@ class Notice extends Managed_DataObject
$this->saveReply($parentauthor->id); $this->saveReply($parentauthor->id);
$replied[$parentauthor->id] = 1; $replied[$parentauthor->id] = 1;
self::blow('reply:stream:%d', $parentauthor->id); self::blow('reply:stream:%d', $parentauthor->id);
} catch (Exception $e) { } catch (NoParentNoticeException $e) {
// Not a reply, since it has no parent! // Not a reply, since it has no parent!
} }
@ -1634,8 +1620,7 @@ class Notice extends Managed_DataObject
foreach ($mention['mentioned'] as $mentioned) { foreach ($mention['mentioned'] as $mentioned) {
// skip if they're already covered // skip if they're already covered
if (array_key_exists($mentioned->id, $replied)) {
if (!empty($replied[$mentioned->id])) {
continue; continue;
} }
@ -1852,8 +1837,8 @@ class Notice extends Managed_DataObject
try { try {
$reply = $this->getParent(); $reply = $this->getParent();
$ctx->replyToID = $reply->getUri(); $ctx->replyToID = $reply->getUri();
$ctx->replyToUrl = $reply->getUrl(); $ctx->replyToUrl = $reply->getUrl(true); // true for fallback to local URL, less messy
} catch (Exception $e) { } catch (NoParentNoticeException $e) {
// This is not a reply to something // This is not a reply to something
} }
@ -2763,13 +2748,10 @@ class Notice extends Managed_DataObject
public function getParent() public function getParent()
{ {
$parent = Notice::getKV('id', $this->reply_to); if (empty($this->reply_to)) {
throw new NoParentNoticeException($this);
if (!$parent instanceof Notice) {
throw new ServerException('Notice has no parent');
} }
return self::getByID($this->reply_to);
return $parent;
} }
/** /**

View File

@ -48,12 +48,12 @@ class Profile extends Managed_DataObject
'description' => 'local and remote users have profiles', 'description' => 'local and remote users have profiles',
'fields' => array( 'fields' => array(
'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
'nickname' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username', 'collate' => 'utf8_general_ci'), 'nickname' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username', 'collate' => 'utf8mb4_general_ci'),
'fullname' => array('type' => 'varchar', 'length' => 191, 'description' => 'display name', 'collate' => 'utf8_general_ci'), 'fullname' => array('type' => 'varchar', 'length' => 191, 'description' => 'display name', 'collate' => 'utf8mb4_general_ci'),
'profileurl' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL, cached so we dont regenerate'), 'profileurl' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL, cached so we dont regenerate'),
'homepage' => array('type' => 'varchar', 'length' => 191, 'description' => 'identifying URL', 'collate' => 'utf8_general_ci'), 'homepage' => array('type' => 'varchar', 'length' => 191, 'description' => 'identifying URL', 'collate' => 'utf8mb4_general_ci'),
'bio' => array('type' => 'text', 'description' => 'descriptive biography', 'collate' => 'utf8_general_ci'), 'bio' => array('type' => 'text', 'description' => 'descriptive biography', 'collate' => 'utf8mb4_general_ci'),
'location' => array('type' => 'varchar', 'length' => 191, 'description' => 'physical location', 'collate' => 'utf8_general_ci'), 'location' => array('type' => 'varchar', 'length' => 191, 'description' => 'physical location', 'collate' => 'utf8mb4_general_ci'),
'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'), 'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'),
'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'), 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'),
'location_id' => array('type' => 'int', 'description' => 'location id if possible'), 'location_id' => array('type' => 'int', 'description' => 'location id if possible'),
@ -880,6 +880,11 @@ class Profile extends Managed_DataObject
$inst->delete(); $inst->delete();
} }
$localuser = User::getKV('id', $this->id);
if ($localuser instanceof User) {
$localuser->delete();
}
return parent::delete($useWhere); return parent::delete($useWhere);
} }

View File

@ -62,11 +62,11 @@ class Profile_prefs extends Managed_DataObject
{ {
if (empty($topic)) { if (empty($topic)) {
$prefs = new Profile_prefs(); $prefs = new Profile_prefs();
$prefs->profile_id = $profile->id; $prefs->profile_id = $profile->getID();
$prefs->namespace = $namespace; $prefs->namespace = $namespace;
$prefs->find(); $prefs->find();
} else { } else {
$prefs = self::pivotGet('profile_id', $profile->id, array('namespace'=>$namespace, 'topic'=>$topic)); $prefs = self::pivotGet('profile_id', $profile->getID(), array('namespace'=>$namespace, 'topic'=>$topic));
} }
if (empty($prefs->N)) { if (empty($prefs->N)) {
@ -85,7 +85,7 @@ class Profile_prefs extends Managed_DataObject
static function getAll(Profile $profile) static function getAll(Profile $profile)
{ {
try { try {
$prefs = self::listFind('profile_id', $profile->id); $prefs = self::listFind('profile_id', $profile->getID());
} catch (NoResultException $e) { } catch (NoResultException $e) {
return array(); return array();
} }
@ -101,15 +101,9 @@ class Profile_prefs extends Managed_DataObject
} }
static function getTopic(Profile $profile, $namespace, $topic) { static function getTopic(Profile $profile, $namespace, $topic) {
$pref = new Profile_prefs; return Profile_prefs::getByPK(array('profile_id' => $profile->getID(),
$pref->profile_id = $profile->id; 'namespace' => $namespace,
$pref->namespace = $namespace; 'topic' => $topic));
$pref->topic = $topic;
if (!$pref->find(true)) {
throw new NoResultException($pref);
}
return $pref;
} }
static function getData(Profile $profile, $namespace, $topic, $def=null) { static function getData(Profile $profile, $namespace, $topic, $def=null) {
@ -164,7 +158,7 @@ class Profile_prefs extends Managed_DataObject
} }
$pref = new Profile_prefs(); $pref = new Profile_prefs();
$pref->profile_id = $profile->id; $pref->profile_id = $profile->getID();
$pref->namespace = $namespace; $pref->namespace = $namespace;
$pref->topic = $topic; $pref->topic = $topic;
$pref->data = $data; $pref->data = $data;

View File

@ -63,7 +63,7 @@ class Queue_item extends Managed_DataObject
// XXX: potential race condition // XXX: potential race condition
// can we force it to only update if claimed is still null // can we force it to only update if claimed is still null
// (or old)? // (or old)?
common_log(LOG_INFO, 'claiming queue item id = ' . $qi->id . common_log(LOG_INFO, 'claiming queue item id = ' . $qi->getID() .
' for transport ' . $qi->transport); ' for transport ' . $qi->transport);
$orig = clone($qi); $orig = clone($qi);
$qi->claimed = common_sql_now(); $qi->claimed = common_sql_now();
@ -85,7 +85,7 @@ class Queue_item extends Managed_DataObject
function releaseClaim() function releaseClaim()
{ {
// DB_DataObject doesn't let us save nulls right now // DB_DataObject doesn't let us save nulls right now
$sql = sprintf("UPDATE queue_item SET claimed=NULL WHERE id=%d", $this->id); $sql = sprintf("UPDATE queue_item SET claimed=NULL WHERE id=%d", $this->getID());
$this->query($sql); $this->query($sql);
$this->claimed = null; $this->claimed = null;

View File

@ -853,57 +853,59 @@ class User extends Managed_DataObject
static function recoverPassword($nore) static function recoverPassword($nore)
{ {
$user = User::getKV('email', common_canonical_email($nore)); // $confirm_email will be used as a fallback if our user doesn't have a confirmed email
$confirm_email = null;
if (!$user) { if (common_is_email($nore)) {
try { $user = User::getKV('email', common_canonical_email($nore));
$user = User::getKV('nickname', common_canonical_nickname($nore));
} catch (NicknameException $e) { // See if it's an unconfirmed email address
// invalid if (!$user instanceof User) {
// Warning: it may actually be legit to have multiple folks
// who have claimed, but not yet confirmed, the same address.
// We'll only send to the first one that comes up.
$confirm_email = new Confirm_address();
$confirm_email->address = common_canonical_email($nore);
$confirm_email->address_type = 'email';
if ($confirm_email->find(true)) {
$user = User::getKV('id', $confirm_email->user_id);
}
} }
}
// See if it's an unconfirmed email address // No luck finding anyone by that email address.
if (!$user instanceof User) {
if (!$user) { if (common_config('site', 'fakeaddressrecovery')) {
// Warning: it may actually be legit to have multiple folks // Return without actually doing anything! We fake address recovery
// who have claimed, but not yet confirmed, the same address. // to avoid revealing which email addresses are registered with the site.
// We'll only send to the first one that comes up. return;
$confirm_email = new Confirm_address(); }
$confirm_email->address = common_canonical_email($nore); // TRANS: Information on password recovery form if no known e-mail address was specified.
$confirm_email->address_type = 'email'; throw new ClientException(_('No user with that email address exists here.'));
$confirm_email->find();
if ($confirm_email->fetch()) {
$user = User::getKV($confirm_email->user_id);
} else {
$confirm_email = null;
} }
} else { } else {
$confirm_email = null; // This might throw a NicknameException on bad nicknames
} $user = User::getKV('nickname', common_canonical_nickname($nore));
if (!$user instanceof User) {
if (!$user) { // TRANS: Information on password recovery form if no known username was specified.
// TRANS: Information on password recovery form if no known username or e-mail address was specified. throw new ClientException(_('No user with that nickname exists here.'));
throw new ClientException(_('No user with that email address or username.')); }
return;
} }
// Try to get an unconfirmed email address if they used a user name // Try to get an unconfirmed email address if they used a user name
if (empty($user->email) && $confirm_email === null) {
if (!$user->email && !$confirm_email) {
$confirm_email = new Confirm_address(); $confirm_email = new Confirm_address();
$confirm_email->user_id = $user->id; $confirm_email->user_id = $user->id;
$confirm_email->address_type = 'email'; $confirm_email->address_type = 'email';
$confirm_email->find(); $confirm_email->find();
if (!$confirm_email->fetch()) { if (!$confirm_email->fetch()) {
// Nothing found, so let's reset it to null
$confirm_email = null; $confirm_email = null;
} }
} }
if (!$user->email && !$confirm_email) { if (empty($user->email) && !$confirm_email instanceof Confirm_address) {
// TRANS: Client error displayed on password recovery form if a user does not have a registered e-mail address. // TRANS: Client error displayed on password recovery form if a user does not have a registered e-mail address.
throw new ClientException(_('No registered email address for that user.')); throw new ClientException(_('No registered email address for that user.'));
return;
} }
// Success! We have a valid user and a confirmed or unconfirmed email address // Success! We have a valid user and a confirmed or unconfirmed email address
@ -912,13 +914,12 @@ class User extends Managed_DataObject
$confirm->code = common_confirmation_code(128); $confirm->code = common_confirmation_code(128);
$confirm->address_type = 'recover'; $confirm->address_type = 'recover';
$confirm->user_id = $user->id; $confirm->user_id = $user->id;
$confirm->address = (!empty($user->email)) ? $user->email : $confirm_email->address; $confirm->address = $user->email ?: $confirm_email->address;
if (!$confirm->insert()) { if (!$confirm->insert()) {
common_log_db_error($confirm, 'INSERT', __FILE__); common_log_db_error($confirm, 'INSERT', __FILE__);
// TRANS: Server error displayed if e-mail address confirmation fails in the database on the password recovery form. // TRANS: Server error displayed if e-mail address confirmation fails in the database on the password recovery form.
throw new ServerException(_('Error saving address confirmation.')); throw new ServerException(_('Error saving address confirmation.'));
return;
} }
// @todo FIXME: needs i18n. // @todo FIXME: needs i18n.

13
js/extlib/jquery.js vendored
View File

@ -1,5 +1,5 @@
/*! /*!
* jQuery JavaScript Library v2.1.3 * jQuery JavaScript Library v2.1.4
* http://jquery.com/ * http://jquery.com/
* *
* Includes Sizzle.js * Includes Sizzle.js
@ -9,7 +9,7 @@
* Released under the MIT license * Released under the MIT license
* http://jquery.org/license * http://jquery.org/license
* *
* Date: 2014-12-18T15:11Z * Date: 2015-04-28T16:01Z
*/ */
(function( global, factory ) { (function( global, factory ) {
@ -67,7 +67,7 @@ var
// Use the correct document accordingly with window argument (sandbox) // Use the correct document accordingly with window argument (sandbox)
document = window.document, document = window.document,
version = "2.1.3", version = "2.1.4",
// Define a local copy of jQuery // Define a local copy of jQuery
jQuery = function( selector, context ) { jQuery = function( selector, context ) {
@ -531,7 +531,12 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli
}); });
function isArraylike( obj ) { function isArraylike( obj ) {
var length = obj.length,
// Support: iOS 8.2 (not reproducible in simulator)
// `in` check used to prevent JIT error (gh-2145)
// hasOwn isn't used here due to false negatives
// regarding Nodelist length in IE
var length = "length" in obj && obj.length,
type = jQuery.type( obj ); type = jQuery.type( obj );
if ( type === "function" || jQuery.isWindow( obj ) ) { if ( type === "function" || jQuery.isWindow( obj ) ) {

View File

@ -328,7 +328,7 @@ class ApiAction extends Action
// different story for parenting. // different story for parenting.
$parent = $notice->getParent(); $parent = $notice->getParent();
$in_reply_to = $parent->id; $in_reply_to = $parent->id;
} catch (Exception $e) { } catch (NoParentNoticeException $e) {
$in_reply_to = null; $in_reply_to = null;
} }
$twitter_status['in_reply_to_status_id'] = $in_reply_to; $twitter_status['in_reply_to_status_id'] = $in_reply_to;

View File

@ -36,6 +36,7 @@ class CommandInterpreter
// StatusNet // StatusNet
$cmd = strtolower($cmd); $cmd = strtolower($cmd);
$result = false;
if (Event::handle('StartInterpretCommand', array($cmd, $arg, $user, &$result))) { if (Event::handle('StartInterpretCommand', array($cmd, $arg, $user, &$result))) {
switch($cmd) { switch($cmd) {
@ -297,8 +298,6 @@ class CommandInterpreter
$result = new TrackingCommand($user); $result = new TrackingCommand($user);
} }
break; break;
default:
$result = false;
} }
Event::handle('EndInterpretCommand', array($cmd, $arg, $user, &$result)); Event::handle('EndInterpretCommand', array($cmd, $arg, $user, &$result));

View File

@ -48,6 +48,7 @@ $default =
'languages' => get_all_languages(), 'languages' => get_all_languages(),
'email' => 'email' =>
array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null, array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null,
'fakeaddressrecovery' => true,
'broughtby' => null, 'broughtby' => null,
'timezone' => 'UTC', 'timezone' => 'UTC',
'broughtbyurl' => null, 'broughtbyurl' => null,

View File

@ -23,7 +23,7 @@ define('GNUSOCIAL_ENGINE', 'GNU social');
define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/social/'); define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/social/');
define('GNUSOCIAL_BASE_VERSION', '1.2.0'); define('GNUSOCIAL_BASE_VERSION', '1.2.0');
define('GNUSOCIAL_LIFECYCLE', 'dev'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release' define('GNUSOCIAL_LIFECYCLE', 'alpha1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE); define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE);
@ -38,6 +38,9 @@ define('PROFILES_PER_PAGE', 20);
define('MESSAGES_PER_PAGE', 20); define('MESSAGES_PER_PAGE', 20);
define('GROUPS_PER_PAGE', 20); define('GROUPS_PER_PAGE', 20);
define('GROUPS_PER_MINILIST', 8);
define('PROFILES_PER_MINILIST', 8);
define('FOREIGN_NOTICE_SEND', 1); define('FOREIGN_NOTICE_SEND', 1);
define('FOREIGN_NOTICE_RECV', 2); define('FOREIGN_NOTICE_RECV', 2);
define('FOREIGN_NOTICE_SEND_REPLY', 4); define('FOREIGN_NOTICE_SEND_REPLY', 4);

View File

@ -33,8 +33,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
require_once INSTALLDIR.'/lib/grouplist.php'; require_once INSTALLDIR.'/lib/grouplist.php';
define('GROUPS_PER_MINILIST', 8);
/** /**
* Widget to show a list of groups, good for sidebar * Widget to show a list of groups, good for sidebar
* *

View File

@ -103,7 +103,7 @@ class GNUsocial_HTTPResponse extends HTTP_Request2_Response
* *
* This extends the PEAR HTTP_Request2 package: * This extends the PEAR HTTP_Request2 package:
* - sends StatusNet-specific User-Agent header * - sends StatusNet-specific User-Agent header
* - 'follow_redirects' config option, defaulting off * - 'follow_redirects' config option, defaulting on
* - 'max_redirs' config option, defaulting to 10 * - 'max_redirs' config option, defaulting to 10
* - extended response class adds getRedirectCount() and getUrl() methods * - extended response class adds getRedirectCount() and getUrl() methods
* - get() and post() convenience functions return body content directly * - get() and post() convenience functions return body content directly
@ -205,12 +205,28 @@ class HTTPClient extends HTTP_Request2
/** /**
* Convenience function to run a HEAD request. * Convenience function to run a HEAD request.
* *
* NOTE: Will probably turn into a GET request if you let it follow redirects!
* That option is only there to be flexible and may be removed in the future!
*
* @return GNUsocial_HTTPResponse * @return GNUsocial_HTTPResponse
* @throws HTTP_Request2_Exception * @throws HTTP_Request2_Exception
*/ */
public function head($url, $headers=array()) public function head($url, $headers=array(), $follow_redirects=false)
{ {
return $this->doRequest($url, self::METHOD_HEAD, $headers); // Save the configured value for follow_redirects
$old_follow = $this->config['follow_redirects'];
try {
// Temporarily (possibly) override the follow_redirects setting
$this->config['follow_redirects'] = $follow_redirects;
return $this->doRequest($url, self::METHOD_HEAD, $headers);
} catch (Exception $e) {
// Let the exception go on its merry way.
throw $e;
} finally {
// reset to the old value
$this->config['follow_redirects'] = $old_follow;
}
//we've either returned or thrown exception here
} }
/** /**

View File

@ -380,7 +380,7 @@ abstract class ImPlugin extends Plugin
$parent = $notice->getParent(); $parent = $notice->getParent();
$orig_profile = $parent->getProfile(); $orig_profile = $parent->getProfile();
$nicknames = sprintf('%1$s => %2$s', $profile->nickname, $orig_profile->nickname); $nicknames = sprintf('%1$s => %2$s', $profile->nickname, $orig_profile->nickname);
} catch (Exception $e) { } catch (NoParentNoticeException $e) {
$nicknames = $profile->nickname; $nicknames = $profile->nickname;
} }
@ -402,9 +402,8 @@ abstract class ImPlugin extends Plugin
$chan = new IMChannel($this); $chan = new IMChannel($this);
$cmd->execute($chan); $cmd->execute($chan);
return true; return true;
} else {
return false;
} }
return false;
} }
/** /**

View File

@ -61,7 +61,7 @@ class MediaFile
public function attachToNotice(Notice $notice) public function attachToNotice(Notice $notice)
{ {
File_to_post::processNew($this->fileRecord->id, $notice->id); File_to_post::processNew($this->fileRecord, $notice);
} }
public function getPath() public function getPath()

View File

@ -0,0 +1,41 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Class for an exception when a database lookup returns no results
*
* PHP version 5
*
* LICENCE: 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/>.
*
* @category Exception
* @package GNUsocial
* @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2013 Free Software Foundation, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://www.gnu.org/software/social/
*/
if (!defined('GNUSOCIAL')) { exit(1); }
class NoParentNoticeException extends ServerException
{
public $notice; // The notice which has no parent
public function __construct(Notice $notice)
{
$this->notice = $notice;
parent::__construct(sprintf(_('No parent for notice with ID "%s".'), $this->notice->id));
}
}

View File

@ -29,8 +29,6 @@
if (!defined('GNUSOCIAL')) { exit(1); } if (!defined('GNUSOCIAL')) { exit(1); }
define('PROFILES_PER_MINILIST', 8);
/** /**
* Widget to show a list of profiles, good for sidebar * Widget to show a list of profiles, good for sidebar
* *

View File

@ -64,6 +64,13 @@ class PublicGroupNav extends Menu
// TRANS: Menu item title in search group navigation panel. // TRANS: Menu item title in search group navigation panel.
_('Public timeline'), $this->actionName == 'public', 'nav_timeline_public'); _('Public timeline'), $this->actionName == 'public', 'nav_timeline_public');
} }
if (!common_config('public', 'localonly') || $this->action->getScoped() instanceof Profile) {
// Allow network wide view if you're logged in
// TRANS: Menu item in search group navigation panel.
$this->out->menuItem(common_local_url('networkpublic'), _m('MENU','Network'),
// TRANS: Menu item title in search group navigation panel.
_('Network public timeline'), $this->actionName == 'networkpublic', 'nav_timeline_networkpublic');
}
// TRANS: Menu item in search group navigation panel. // TRANS: Menu item in search group navigation panel.
$this->out->menuItem(common_local_url('groups'), _m('MENU','Groups'), $this->out->menuItem(common_local_url('groups'), _m('MENU','Groups'),

View File

@ -535,6 +535,7 @@ class Schema
$res = $this->conn->query($sql); $res = $this->conn->query($sql);
if ($_PEAR->isError($res)) { if ($_PEAR->isError($res)) {
common_debug('PEAR exception on query: '.$sql);
PEAR_ErrorToPEAR_Exception($res); PEAR_ErrorToPEAR_Exception($res);
} }
} }

182
lib/sitestreamaction.php Normal file
View File

@ -0,0 +1,182 @@
<?php
if (!defined('GNUSOCIAL')) { exit(1); }
// Farther than any human will go
define('MAX_PUBLIC_PAGE', 100);
class SitestreamAction extends ManagedAction
{
/**
* page of the stream we're on; default = 1
*/
var $page = null;
var $notice;
protected $stream = null;
function isReadOnly($args)
{
return true;
}
protected function doPreparation()
{
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
if ($this->page > MAX_PUBLIC_PAGE) {
// TRANS: Client error displayed when requesting a public timeline page beyond the page limit.
// TRANS: %s is the page limit.
$this->clientError(sprintf(_('Beyond the page limit (%s).'), MAX_PUBLIC_PAGE));
}
common_set_returnto($this->selfUrl());
$this->streamPrepare();
$this->notice = $this->stream->getNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
if (!$this->notice) {
// TRANS: Server error displayed when a public timeline cannot be retrieved.
$this->serverError(_('Could not retrieve public timeline.'));
}
if ($this->page > 1 && $this->notice->N == 0){
// TRANS: Client error when page not found (404).
$this->clientError(_('No such page.'), 404);
}
return true;
}
/**
* Title of the page
*
* @return page title, including page number if over 1
*/
function title()
{
if ($this->page > 1) {
// TRANS: Title for all public timeline pages but the first.
// TRANS: %d is the page number.
return sprintf(_('Public timeline, page %d'), $this->page);
} else {
// TRANS: Title for the first public timeline page.
return _('Public timeline');
}
}
function extraHead()
{
parent::extraHead();
$rsd = common_local_url('rsd');
// RSD, http://tales.phrasewise.com/rfc/rsd
$this->element('link', array('rel' => 'EditURI',
'type' => 'application/rsd+xml',
'href' => $rsd));
if ($this->page != 1) {
$this->element('link', array('rel' => 'canonical',
'href' => common_local_url('public')));
}
}
/**
* Output <head> elements for RSS and Atom feeds
*
* @return void
*/
function getFeeds()
{
return array(new Feed(Feed::JSON,
common_local_url('ApiTimelinePublic',
array('format' => 'as')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (Activity Streams JSON)')),
new Feed(Feed::RSS1, common_local_url('publicrss'),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (RSS 1.0)')),
new Feed(Feed::RSS2,
common_local_url('ApiTimelinePublic',
array('format' => 'rss')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (RSS 2.0)')),
new Feed(Feed::ATOM,
common_local_url('ApiTimelinePublic',
array('format' => 'atom')),
// TRANS: Link description for public timeline feed.
_('Public Timeline Feed (Atom)')));
}
function showEmptyList()
{
// TRANS: Text displayed for public feed when there are no public notices.
$message = _('This is the public timeline for %%site.name%% but no one has posted anything yet.') . ' ';
if (common_logged_in()) {
// TRANS: Additional text displayed for public feed when there are no public notices for a logged in user.
$message .= _('Be the first to post!');
}
else {
if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
// TRANS: Additional text displayed for public feed when there are no public notices for a not logged in user.
$message .= _('Why not [register an account](%%action.register%%) and be the first to post!');
}
}
$this->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message));
$this->elementEnd('div');
}
/**
* Fill the content area
*
* Shows a list of the notices in the public stream, with some pagination
* controls.
*
* @return void
*/
function showContent()
{
if ($this->scoped instanceof Profile && $this->scoped->isLocal() && $this->scoped->getUser()->streamModeOnly()) {
$nl = new PrimaryNoticeList($this->notice, $this, array('show_n'=>NOTICES_PER_PAGE));
} else {
$nl = new ThreadedNoticeList($this->notice, $this, $this->scoped);
}
$cnt = $nl->show();
if ($cnt == 0) {
$this->showEmptyList();
}
$this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
$this->page, $this->action);
}
function showAnonymousMessage()
{
if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
// TRANS: Message for not logged in users at an invite-only site trying to view the public feed of notices.
// TRANS: This message contains Markdown links. Please mind the formatting.
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. ' .
'[Join now](%%action.register%%) to share notices about yourself with friends, family, and colleagues! ' .
'([Read more](%%doc.help%%))');
} else {
// TRANS: Message for not logged in users at a closed site trying to view the public feed of notices.
// TRANS: This message contains Markdown links. Please mind the formatting.
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool.');
}
$this->elementStart('div', array('id' => 'anon_notice'));
$this->raw(common_markup_to_html($m));
$this->elementEnd('div');
}
}

View File

@ -628,7 +628,7 @@ function common_render_content($text, Notice $notice)
* @param Notice $notice in-progress or complete Notice object for context * @param Notice $notice in-progress or complete Notice object for context
* @return string partially-rendered HTML * @return string partially-rendered HTML
*/ */
function common_linkify_mentions($text, $notice) function common_linkify_mentions($text, Notice $notice)
{ {
$mentions = common_find_mentions($text, $notice); $mentions = common_find_mentions($text, $notice);
@ -655,7 +655,7 @@ function common_linkify_mentions($text, $notice)
return $text; return $text;
} }
function common_linkify_mention($mention) function common_linkify_mention(array $mention)
{ {
$output = null; $output = null;
@ -695,13 +695,10 @@ function common_linkify_mention($mention)
* *
* @access private * @access private
*/ */
function common_find_mentions($text, $notice) function common_find_mentions($text, Notice $notice)
{ {
try { // The getProfile call throws NoProfileException on failure
$sender = Profile::getKV('id', $notice->profile_id); $sender = $notice->getProfile();
} catch (NoProfileException $e) {
return array();
}
$mentions = array(); $mentions = array();
@ -728,8 +725,8 @@ function common_find_mentions($text, $notice)
} }
} catch (NoProfileException $e) { } catch (NoProfileException $e) {
common_log(LOG_WARNING, sprintf('Notice %d author profile id %d does not exist', $origNotice->id, $origNotice->profile_id)); common_log(LOG_WARNING, sprintf('Notice %d author profile id %d does not exist', $origNotice->id, $origNotice->profile_id));
} catch (ServerException $e) { } catch (NoParentNoticeException $e) {
// Probably just no parent. Should get a specific NoParentException // This notice is not in reply to anything
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_WARNING, __METHOD__ . ' got exception ' . get_class($e) . ' : ' . $e->getMessage()); common_log(LOG_WARNING, __METHOD__ . ' got exception ' . get_class($e) . ' : ' . $e->getMessage());
} }

View File

@ -50,7 +50,7 @@ class ActivityverbAction extends ManagedAction
throw new ServerException('A verb has not been specified.'); throw new ServerException('A verb has not been specified.');
} }
$this->notice = Notice::getById($this->trimmed('id')); $this->notice = Notice::getByID($this->trimmed('id'));
if (!$this->notice->inScope($this->scoped)) { if (!$this->notice->inScope($this->scoped)) {
// TRANS: %1$s is a user nickname, %2$d is a notice ID (number). // TRANS: %1$s is a user nickname, %2$d is a notice ID (number).

View File

@ -9,6 +9,13 @@ class AntiBrutePlugin extends Plugin {
const FAILED_LOGIN_IP_SECTION = 'failed_login_ip'; const FAILED_LOGIN_IP_SECTION = 'failed_login_ip';
public function initialize()
{
// This probably needs some work. For example with IPv6 you can easily generate new IPs...
$client_ip = common_client_ip();
$this->client_ip = $client_ip[0] ?: $client_ip[1]; // [0] is proxy, [1] should be the real IP
}
public function onStartCheckPassword($nickname, $password, &$authenticatedUser) public function onStartCheckPassword($nickname, $password, &$authenticatedUser)
{ {
if (common_is_email($nickname)) { if (common_is_email($nickname)) {
@ -22,9 +29,6 @@ class AntiBrutePlugin extends Plugin {
return true; return true;
} }
// This probably needs some work. For example with IPv6 you can easily generate new IPs...
$client_ip = common_client_ip();
$this->client_ip = $client_ip[0] ?: $client_ip[1]; // [0] is proxy, [1] should be the real IP
$this->failed_attempts = (int)$this->unauthed_user->getPref(self::FAILED_LOGIN_IP_SECTION, $this->client_ip); $this->failed_attempts = (int)$this->unauthed_user->getPref(self::FAILED_LOGIN_IP_SECTION, $this->client_ip);
switch (true) { switch (true) {
case $this->failed_attempts >= 5: case $this->failed_attempts >= 5:

View File

@ -691,8 +691,8 @@ class Ostatus_profile extends Managed_DataObject
$options); $options);
if ($saved instanceof Notice) { if ($saved instanceof Notice) {
Ostatus_source::saveNew($saved, $this, $method); Ostatus_source::saveNew($saved, $this, $method);
if (!empty($attachment)) { if ($attachment instanceof File) {
File_to_post::processNew($attachment->id, $saved->id); File_to_post::processNew($attachment, $saved);
} }
} }
} catch (Exception $e) { } catch (Exception $e) {

View File

@ -154,7 +154,7 @@ class OpenIDPlugin extends Plugin
* *
* @return boolean hook return * @return boolean hook return
*/ */
function onEndPublicXRDS($action, &$xrdsOutputter) function onEndPublicXRDS(Action $action, &$xrdsOutputter)
{ {
$xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
'xmlns:simple' => 'http://xrds-simple.net/core/1.0', 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
@ -174,37 +174,6 @@ class OpenIDPlugin extends Plugin
$xrdsOutputter->elementEnd('XRD'); $xrdsOutputter->elementEnd('XRD');
} }
/**
* User XRDS output hook
*
* Puts the bits of code needed to discover OpenID endpoints.
*
* @param Action $action Action being executed
* @param XMLOutputter &$xrdsOutputter Output channel
*
* @return boolean hook return
*/
function onEndUserXRDS($action, &$xrdsOutputter)
{
$xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
'xml:id' => 'openid',
'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
'version' => '2.0'));
$xrdsOutputter->element('Type', null, 'xri://$xrds*simple');
//consumer
$xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/return_to',
common_local_url('finishopenidlogin'));
//provider
$xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/signon',
common_local_url('openidserver'),
null,
null,
common_profile_url($action->user->nickname));
$xrdsOutputter->elementEnd('XRD');
}
/** /**
* If we're in OpenID-only mode, hide all the main menu except OpenID login. * If we're in OpenID-only mode, hide all the main menu except OpenID login.
* *
@ -415,7 +384,7 @@ class OpenIDPlugin extends Plugin
* *
* @return void * @return void
*/ */
function onEndShowHeadElements($action) function onEndShowHeadElements(Action $action)
{ {
if ($action instanceof ShowstreamAction) { if ($action instanceof ShowstreamAction) {
$action->element('link', array('rel' => 'openid2.provider', $action->element('link', array('rel' => 'openid2.provider',
@ -427,6 +396,11 @@ class OpenIDPlugin extends Plugin
$action->element('link', array('rel' => 'openid.delegate', $action->element('link', array('rel' => 'openid.delegate',
'href' => $action->profile->profileurl)); 'href' => $action->profile->profileurl));
} }
if ($action instanceof SitestreamAction) {
$action->element('meta', array('http-equiv' => 'X-XRDS-Location',
'content' => common_local_url('publicxrds')));
}
return true; return true;
} }

View File

@ -30,12 +30,9 @@
* 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') && !defined('LACONICA')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
require_once INSTALLDIR.'/plugins/OpenID/openid.php'; require_once __DIR__.'/../openid.php';
require_once INSTALLDIR.'/lib/xrdsoutputter.php';
/** /**
* Public XRDS * Public XRDS
@ -48,8 +45,6 @@ require_once INSTALLDIR.'/lib/xrdsoutputter.php';
* @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/ * @link http://status.net/
*
* @todo factor out similarities with XrdsAction
*/ */
class PublicxrdsAction extends Action class PublicxrdsAction extends Action
{ {
@ -70,9 +65,9 @@ class PublicxrdsAction extends Action
* *
* @return nothing * @return nothing
*/ */
function handle($args) protected function handle()
{ {
parent::handle($args); parent::handle();
$xrdsOutputter = new XRDSOutputter(); $xrdsOutputter = new XRDSOutputter();
$xrdsOutputter->startXRDS(); $xrdsOutputter->startXRDS();
Event::handle('StartPublicXRDS', array($this,&$xrdsOutputter)); Event::handle('StartPublicXRDS', array($this,&$xrdsOutputter));
@ -80,4 +75,3 @@ class PublicxrdsAction extends Action
$xrdsOutputter->endXRDS(); $xrdsOutputter->endXRDS();
} }
} }

View File

@ -28,11 +28,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET') && !defined('LACONICA')) { if (!defined('GNUSOCIAL')) { exit(1); }
exit(1);
}
require_once INSTALLDIR.'/lib/xmloutputter.php';
/** /**
* Low-level generator for XRDS XML * Low-level generator for XRDS XML

View File

@ -161,7 +161,7 @@ class SharePlugin extends ActivityVerbHandlerPlugin
public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null) public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null)
{ {
// TODO: How to handle repeats of deleted notices? // TODO: How to handle repeats of deleted notices?
$target = Notice::getById($stored->repeat_of); $target = Notice::getByID($stored->repeat_of);
// TRANS: A repeat activity's title. %1$s is repeater's nickname // TRANS: A repeat activity's title. %1$s is repeater's nickname
// and %2$s is the repeated user's nickname. // and %2$s is the repeated user's nickname.
$act->title = sprintf(_('%1$s repeated a notice by %2$s'), $act->title = sprintf(_('%1$s repeated a notice by %2$s'),

View File

@ -564,13 +564,13 @@ class TwitterImport
* @param Notice $notice * @param Notice $notice
* @param object $status * @param object $status
*/ */
function saveStatusAttachments($notice, $status) function saveStatusAttachments(Notice $notice, $status)
{ {
if (common_config('attachments', 'process_links')) { if (common_config('attachments', 'process_links')) {
if (!empty($status->entities) && !empty($status->entities->urls)) { if (!empty($status->entities) && !empty($status->entities->urls)) {
foreach ($status->entities->urls as $url) { foreach ($status->entities->urls as $url) {
try { try {
File::processNew($url->url, $notice->id); File::processNew($url->url, $notice);
} catch (ServerException $e) { } catch (ServerException $e) {
// Could not process attached URL // Could not process attached URL
} }

View File

@ -31,6 +31,10 @@ if (!defined('GNUSOCIAL')) { exit(1); }
class WebFingerPlugin extends Plugin class WebFingerPlugin extends Plugin
{ {
const OAUTH_ACCESS_TOKEN_REL = 'http://apinamespace.org/oauth/access_token';
const OAUTH_REQUEST_TOKEN_REL = 'http://apinamespace.org/oauth/request_token';
const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize';
public $http_alias = false; public $http_alias = false;
public function initialize() public function initialize()
@ -127,6 +131,11 @@ class WebFingerPlugin extends Plugin
$type, $type,
true); // isTemplate true); // isTemplate
} }
// OAuth connections
$links[] = new XML_XRD_Element_link(self::OAUTH_ACCESS_TOKEN_REL, common_local_url('ApiOAuthAccessToken'));
$links[] = new XML_XRD_Element_link(self::OAUTH_REQUEST_TOKEN_REL, common_local_url('ApiOAuthRequestToken'));
$links[] = new XML_XRD_Element_link(self::OAUTH_AUTHORIZE_REL, common_local_url('ApiOAuthAuthorize'));
} }
/** /**

View File

@ -354,7 +354,7 @@ class XmppPlugin extends ImPlugin
$xs->text(": "); $xs->text(": ");
} catch (InvalidUrlException $e) { } catch (InvalidUrlException $e) {
$xs->text(sprintf(' => %s', $orig_profile->nickname)); $xs->text(sprintf(' => %s', $orig_profile->nickname));
} catch (Exception $e) { } catch (NoParentNoticeException $e) {
$xs->text(": "); $xs->text(": ");
} }
if (!empty($notice->rendered)) { if (!empty($notice->rendered)) {

View File

@ -113,7 +113,7 @@ function readline_emulation($prompt)
function console_help() function console_help()
{ {
print "Welcome to StatusNet's interactive PHP console!\n"; print "Welcome to GNU social's interactive PHP console!\n";
print "Type some PHP code and it'll execute...\n"; print "Type some PHP code and it'll execute...\n";
print "\n"; print "\n";
print "Hint: return a value of any type to output it via var_export():\n"; print "Hint: return a value of any type to output it via var_export():\n";
@ -128,8 +128,8 @@ function console_help()
} }
if (CONSOLE_INTERACTIVE) { if (CONSOLE_INTERACTIVE) {
print "StatusNet interactive PHP console... type ctrl+D or enter 'exit' to exit.\n"; print "GNU social interactive PHP console... type ctrl+D or enter 'exit' to exit.\n";
$prompt = common_config('site', 'name') . '> '; $prompt = common_slugify(common_config('site', 'name')) . '> ';
} else { } else {
$prompt = ''; $prompt = '';
} }

78
scripts/nukefile.php Executable file
View File

@ -0,0 +1,78 @@
#!/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 = 'i::yv';
$longoptions = array('id=', 'yes', 'verbose');
$helptext = <<<END_OF_HELP
nukefile.php [options]
deletes a file and related notices from the database
-i --id ID of the file
-v --verbose Be verbose (print the contents of the notices deleted).
END_OF_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
if (have_option('i', 'id')) {
$id = get_option_value('i', 'id');
$file = File::getKV('id', $id);
if (!$file instanceof File) {
print "Can't find file with ID $id\n";
exit(1);
}
} else {
print "You must provide a file ID.\n";
exit(1);
}
$verbose = have_option('v', 'verbose');
if (!have_option('y', 'yes')) {
try {
$filename = $file->getFilename();
} catch (Exception $e) {
$filename = '(remote file or no filename)';
}
print "About to PERMANENTLY delete file ($filename) ({$file->id}). Are you sure? [y/N] ";
$response = fgets(STDIN);
if (strtolower(trim($response)) != 'y') {
print "Aborting.\n";
exit(0);
}
}
print "Finding notices...\n";
try {
$ids = File_to_post::getNoticeIDsByFile($file);
$notice = Notice::multiGet('id', $ids);
while ($notice->fetch()) {
print "Deleting notice {$notice->id}".($verbose ? ": $notice->content\n" : "\n");
$notice->delete();
}
} catch (NoResultException $e) {
print "No notices found with this File attached.\n";
}
print "Deleting File object together with possibly locally stored copy.\n";
$file->delete();
print "DONE.\n";