Commit c00b2ecc authored by abjectio's avatar abjectio

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

parents 0200b1d7 94f5247f
......@@ -26,7 +26,7 @@ PHP modules
The following software packages are *required* for this software to
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
section on 'Queues and daemons' for more information.
- MariaDB 5+ GNU Social uses, by default, a MariaDB server for data
......
......@@ -128,7 +128,7 @@ class ConversationAction extends ManagedAction
'format' => 'atom')),
// TRANS: Title for link to notice feed.
// TRANS: %s is a user nickname.
_('Conversation feed (Activity Streams JSON)')));
_('Conversation feed (Atom)')));
}
}
......@@ -410,6 +410,7 @@ class EmailsettingsAction extends SettingsAction
$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);
Event::handle('EndAddEmailAddress', array($user, $email));
......
......@@ -2,7 +2,7 @@
if (!defined('GNUSOCIAL')) { exit(1); }
class NetworkpublicAction extends PublicAction
class NetworkpublicAction extends SitestreamAction
{
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()
{
// Show invite button, as long as site isn't closed, and
......
......@@ -29,10 +29,6 @@
if (!defined('GNUSOCIAL')) { exit(1); }
// Farther than any human will go
define('MAX_PUBLIC_PAGE', 100);
/**
* Action for displaying the public stream
*
......@@ -43,54 +39,9 @@ define('MAX_PUBLIC_PAGE', 100);
* @link http://status.net/
*
* @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()
{
if ($this->scoped instanceof Profile && $this->scoped->isLocal() && $this->scoped->getUser()->streamModeOnly()) {
......@@ -117,24 +68,32 @@ class PublicAction extends ManagedAction
}
}
function extraHead()
function showSections()
{
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
// Show invite button, as long as site isn't closed, and
// we have a logged in user.
if (common_config('invite', 'enabled') && !common_config('site', 'closed') && common_logged_in()) {
if (!common_config('site', 'private')) {
$ibs = new InviteButtonSection(
$this,
// TRANS: Button text for inviting more users to the StatusNet instance.
// TRANS: Less business/enterprise-oriented language for public sites.
_m('BUTTON', 'Send invite')
);
} else {
$ibs = new InviteButtonSection($this);
}
$ibs->show();
}
$this->element('link', array('rel' => 'EditURI',
'type' => 'application/rsd+xml',
'href' => $rsd));
$p = Profile::current();
if ($this->page != 1) {
$this->element('link', array('rel' => 'canonical',
'href' => common_local_url('public')));
if (!common_config('performance', 'high')) {
$cloud = new PublicTagCloudSection($this);
$cloud->show();
}
$feat = new FeaturedUsersSection($this);
$feat->show();
}
/**
......@@ -163,99 +122,4 @@ class PublicAction extends ManagedAction
// 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()
{
// Show invite button, as long as site isn't closed, and
// we have a logged in user.
if (common_config('invite', 'enabled') && !common_config('site', 'closed') && common_logged_in()) {
if (!common_config('site', 'private')) {
$ibs = new InviteButtonSection(
$this,
// TRANS: Button text for inviting more users to the StatusNet instance.
// TRANS: Less business/enterprise-oriented language for public sites.
_m('BUTTON', 'Send invite')
);
} else {
$ibs = new InviteButtonSection($this);
}
$ibs->show();
}
$p = Profile::current();
if (!common_config('performance', 'high')) {
$cloud = new PublicTagCloudSection($this);
$cloud->show();
}
$feat = new FeaturedUsersSection($this);
$feat->show();
}
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');
}
}
......@@ -272,10 +272,16 @@ class RecoverpasswordAction extends Action
try {
User::recoverPassword($nore);
$this->mode = 'sent';
// 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.');
if (common_is_email($nore) && common_config('site', 'fakeaddressrecovery')) {
// TRANS: User notification when recovering password by giving email address,
// regardless if the mail was sent or not (to hide registered email status).
$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;
} catch (Exception $e) {
$this->success = false;
......
......@@ -116,14 +116,14 @@ class File extends Managed_DataObject
*
* @fixme refactor this mess, it's gotten pretty scary.
* @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
*
* @return mixed File on success, -1 on some errors
*
* @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)) {
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
// 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);
}
......@@ -193,8 +193,8 @@ class File extends Managed_DataObject
}
}
if (!empty($notice_id)) {
File_to_post::processNew($file->id, $notice_id);
if ($notice instanceof Notice) {
File_to_post::processNew($file, $notice);
}
return $file;
}
......@@ -249,6 +249,15 @@ class File extends Managed_DataObject
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?
static function filename(Profile $profile, $origname, $mimetype)
......@@ -501,9 +510,9 @@ class File extends Managed_DataObject
function blowCache($last=false)
{
self::blow('file:notice-ids:%s', $this->urlhash);
self::blow('file:notice-ids:%s', $this->id);
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);
}
......@@ -610,12 +619,45 @@ class File extends Managed_DataObject
return;
}
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,
// transfer data and THEN run checkSchema
$schemadef['fields']['urlhash'] = array (
'type' => 'varchar',
'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)',
);
$schemadef['fields']['url'] = array (
......
......@@ -59,12 +59,7 @@ class File_redirection extends Managed_DataObject
static public function getByUrl($url)
{
$file = new File_redirection();
$file->urlhash = File::hashurl($url);
if (!$file->find(true)) {
throw new NoResultException($file);
}
return $file;
return self::getByPK(array('urlhash' => File::hashurl($url)));
}
static function _commonHttp($url, $redirs) {
......@@ -261,7 +256,7 @@ class File_redirection extends Managed_DataObject
// store it
$file = File::getKV('url', $long_url);
if ($file instanceof File) {
$file_id = $file->id;
$file_id = $file->getID();
} else {
// Check if the target URL is itself a redirect...
$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.
// Save file and embedding data about it!
$file = File::saveNew($redir_data, $long_url);
$file_id = $file->id;
$file_id = $file->getID();
} else if (is_string($redir_data)) {
// The file is a known redirect target.
$file = File::getKV('url', $redir_data);
......@@ -281,7 +276,7 @@ class File_redirection extends Managed_DataObject
// SSL sites with cert issues.
return null;
}
$file_id = $file->id;
$file_id = $file->getID();
}
}
$file_redir = File_redirection::getKV('url', $short_url);
......
......@@ -82,9 +82,9 @@ class File_thumbnail extends Managed_DataObject
* Fetch an entry by using a File's id
*/
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) {
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;
}
......@@ -167,11 +167,6 @@ class File_thumbnail extends Managed_DataObject
public function getFile()
{
$file = new File();
$file->id = $this->file_id;
if (!$file->find(true)) {
throw new NoResultException($file);
}
return $file;
return File::getByID($this->file_id);
}
}
......@@ -17,9 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* 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();
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_id));
if (empty($f2p)) {
$file_id = $file->getID();
$notice_id = $notice->getID();
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->file_id = $file_id;
$f2p->post_id = $notice_id;
$f2p->insert();
$f = File::getKV($file_id);
if (!empty($f)) {
$f->blowCache();
}
$file->blowCache();
}
if (empty($seen[$notice_id])) {
$seen[$notice_id] = array($file_id);
} else {
$seen[$notice_id][] = $file_id;
}
$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)
{
$f = File::getKV('id', $this->file_id);
if ($f instanceof File) {
try {
$f = File::getByID($this->file_id);
$f->blowCache();
} catch (NoResultException $e) {
// ...alright, that's weird, but no File to delete anyway.
}
return parent::delete($useWhere);
}
}
......@@ -64,6 +64,11 @@ abstract class Managed_DataObject extends Memcached_DataObject
return parent::pkeyGetClass(get_called_class(), $kv);
}
static function pkeyCols()
{
return parent::pkeyColsClass(get_called_class());
}
/**
* 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());
}
/**
* 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
*
......
......@@ -34,7 +34,7 @@ class Memcached_DataObject extends Safe_DataObject
{
if (is_null($v)) {
$v = $k;
$keys = self::pkeyCols($cls);
$keys = static::pkeyCols();
if (count($keys) > 1) {
// FIXME: maybe call pkeyGetClass() ourselves?
throw new Exception('Use pkeyGetClass() for compound primary keys');
......@@ -246,7 +246,7 @@ class Memcached_DataObject extends Safe_DataObject
return $query;
}
static function pkeyCols($cls)
static function pkeyColsClass($cls)
{
$i = new $cls;
$types = $i->keyTypes();
......@@ -279,7 +279,7 @@ class Memcached_DataObject extends Safe_DataObject
$pkeyMap = array_fill_keys($keyVals, array());
$result = array_fill_keys($keyVals, array());
$pkeyCols = self::pkeyCols($cls);
$pkeyCols = static::pkeyCols();
$toFetch = array();
$allPkeys = array();
......
......@@ -84,7 +84,7 @@ class Notice extends Managed_DataObject
'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
'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'),
'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'),
'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'),
......@@ -313,16 +313,6 @@ class Notice extends Managed_DataObject
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.
*/
......@@ -1109,7 +1099,7 @@ class Notice extends Managed_DataObject
*/
function saveUrls() {
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);