diff --git a/INSTALL b/INSTALL index aad21756fe..90fa84923b 100644 --- a/INSTALL +++ b/INSTALL @@ -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 diff --git a/actions/conversation.php b/actions/conversation.php index 3b6f48c853..5a6e4b5c7a 100644 --- a/actions/conversation.php +++ b/actions/conversation.php @@ -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)'))); } } diff --git a/actions/emailsettings.php b/actions/emailsettings.php index dfdbe1bad0..a0f111c0d5 100644 --- a/actions/emailsettings.php +++ b/actions/emailsettings.php @@ -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)); diff --git a/actions/networkpublic.php b/actions/networkpublic.php index 7baa313bee..41c4e37e3c 100644 --- a/actions/networkpublic.php +++ b/actions/networkpublic.php @@ -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 diff --git a/actions/public.php b/actions/public.php index 06ee75b8d1..000f82cb93 100644 --- a/actions/public.php +++ b/actions/public.php @@ -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,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 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() { // Show invite button, as long as site isn't closed, and @@ -239,23 +96,30 @@ class PublicAction extends ManagedAction $feat->show(); } - function showAnonymousMessage() + /** + * Output elements for RSS and Atom feeds + * + * @return void + */ + function getFeeds() { - 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'); + 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)'))); } } diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index 4839a036c0..060ba83510 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -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; diff --git a/classes/File.php b/classes/File.php index 1e296242b7..d4abbfddee 100644 --- a/classes/File.php +++ b/classes/File.php @@ -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 ( diff --git a/classes/File_redirection.php b/classes/File_redirection.php index 12619b0394..ea9db8e891 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -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); diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php index a2e633249f..fb2515f9f5 100644 --- a/classes/File_thumbnail.php +++ b/classes/File_thumbnail.php @@ -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); } } diff --git a/classes/File_to_post.php b/classes/File_to_post.php index 4c751ae4f3..e06e34aa46 100644 --- a/classes/File_to_post.php +++ b/classes/File_to_post.php @@ -17,9 +17,7 @@ * along with this program. If not, see . */ -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); } } diff --git a/classes/Managed_DataObject.php b/classes/Managed_DataObject.php index b324984b7f..a69a957bcc 100644 --- a/classes/Managed_DataObject.php +++ b/classes/Managed_DataObject.php @@ -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 * diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 3f1945205a..91b986891c 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -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(); diff --git a/classes/Notice.php b/classes/Notice.php index 38e31cb274..9246c26919 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -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); } } @@ -1126,11 +1116,7 @@ class Notice extends Managed_DataObject if (common_config('attachments', 'process_links')) { // @fixme validation? foreach (array_unique($urls) as $url) { - try { - File::processNew($url, $this->id); - } catch (ServerException $e) { - // Could not save URL. Log it? - } + $this->saveUrl($url, $this); } } } @@ -1138,9 +1124,9 @@ class Notice extends Managed_DataObject /** * @private callback */ - function saveUrl($url, $notice_id) { + function saveUrl($url, Notice $notice) { try { - File::processNew($url, $notice_id); + File::processNew($url, $notice); } catch (ServerException $e) { // Could not save URL. Log it? } @@ -1311,7 +1297,7 @@ class Notice extends Managed_DataObject $last = $parent; continue; } - } catch (Exception $e) { + } catch (NoParentNoticeException $e) { // Latest notice has no parent } // No parent, or parent out of scope @@ -1617,7 +1603,7 @@ class Notice extends Managed_DataObject $this->saveReply($parentauthor->id); $replied[$parentauthor->id] = 1; self::blow('reply:stream:%d', $parentauthor->id); - } catch (Exception $e) { + } catch (NoParentNoticeException $e) { // Not a reply, since it has no parent! } @@ -1634,8 +1620,7 @@ class Notice extends Managed_DataObject foreach ($mention['mentioned'] as $mentioned) { // skip if they're already covered - - if (!empty($replied[$mentioned->id])) { + if (array_key_exists($mentioned->id, $replied)) { continue; } @@ -1852,8 +1837,8 @@ class Notice extends Managed_DataObject try { $reply = $this->getParent(); $ctx->replyToID = $reply->getUri(); - $ctx->replyToUrl = $reply->getUrl(); - } catch (Exception $e) { + $ctx->replyToUrl = $reply->getUrl(true); // true for fallback to local URL, less messy + } catch (NoParentNoticeException $e) { // This is not a reply to something } @@ -2763,13 +2748,10 @@ class Notice extends Managed_DataObject public function getParent() { - $parent = Notice::getKV('id', $this->reply_to); - - if (!$parent instanceof Notice) { - throw new ServerException('Notice has no parent'); + if (empty($this->reply_to)) { + throw new NoParentNoticeException($this); } - - return $parent; + return self::getByID($this->reply_to); } /** diff --git a/classes/Profile.php b/classes/Profile.php index 6eb09782b1..b5ba00caa9 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -48,12 +48,12 @@ class Profile extends Managed_DataObject 'description' => 'local and remote users have profiles', 'fields' => array( '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'), - 'fullname' => array('type' => 'varchar', 'length' => 191, 'description' => 'display name', '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' => 'utf8mb4_general_ci'), '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'), - 'bio' => array('type' => 'text', 'description' => 'descriptive biography', 'collate' => 'utf8_general_ci'), - 'location' => array('type' => 'varchar', 'length' => 191, 'description' => 'physical location', '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' => 'utf8mb4_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'), 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'), 'location_id' => array('type' => 'int', 'description' => 'location id if possible'), @@ -880,6 +880,11 @@ class Profile extends Managed_DataObject $inst->delete(); } + $localuser = User::getKV('id', $this->id); + if ($localuser instanceof User) { + $localuser->delete(); + } + return parent::delete($useWhere); } diff --git a/classes/Profile_prefs.php b/classes/Profile_prefs.php index 27034390f8..72a707cae8 100644 --- a/classes/Profile_prefs.php +++ b/classes/Profile_prefs.php @@ -62,11 +62,11 @@ class Profile_prefs extends Managed_DataObject { if (empty($topic)) { $prefs = new Profile_prefs(); - $prefs->profile_id = $profile->id; + $prefs->profile_id = $profile->getID(); $prefs->namespace = $namespace; $prefs->find(); } 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)) { @@ -85,7 +85,7 @@ class Profile_prefs extends Managed_DataObject static function getAll(Profile $profile) { try { - $prefs = self::listFind('profile_id', $profile->id); + $prefs = self::listFind('profile_id', $profile->getID()); } catch (NoResultException $e) { return array(); } @@ -101,15 +101,9 @@ class Profile_prefs extends Managed_DataObject } static function getTopic(Profile $profile, $namespace, $topic) { - $pref = new Profile_prefs; - $pref->profile_id = $profile->id; - $pref->namespace = $namespace; - $pref->topic = $topic; - - if (!$pref->find(true)) { - throw new NoResultException($pref); - } - return $pref; + return Profile_prefs::getByPK(array('profile_id' => $profile->getID(), + 'namespace' => $namespace, + 'topic' => $topic)); } static function getData(Profile $profile, $namespace, $topic, $def=null) { @@ -164,7 +158,7 @@ class Profile_prefs extends Managed_DataObject } $pref = new Profile_prefs(); - $pref->profile_id = $profile->id; + $pref->profile_id = $profile->getID(); $pref->namespace = $namespace; $pref->topic = $topic; $pref->data = $data; diff --git a/classes/Queue_item.php b/classes/Queue_item.php index 0d6fd56af2..3a7d05adef 100644 --- a/classes/Queue_item.php +++ b/classes/Queue_item.php @@ -63,7 +63,7 @@ class Queue_item extends Managed_DataObject // XXX: potential race condition // can we force it to only update if claimed is still null // (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); $orig = clone($qi); $qi->claimed = common_sql_now(); @@ -85,7 +85,7 @@ class Queue_item extends Managed_DataObject function releaseClaim() { // 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->claimed = null; diff --git a/classes/User.php b/classes/User.php index f543a75528..3efaa5e721 100644 --- a/classes/User.php +++ b/classes/User.php @@ -853,57 +853,59 @@ class User extends Managed_DataObject 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) { - try { - $user = User::getKV('nickname', common_canonical_nickname($nore)); - } catch (NicknameException $e) { - // invalid + if (common_is_email($nore)) { + $user = User::getKV('email', common_canonical_email($nore)); + + // See if it's an unconfirmed email address + 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 - - if (!$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'; - $confirm_email->find(); - if ($confirm_email->fetch()) { - $user = User::getKV($confirm_email->user_id); - } else { - $confirm_email = null; + // No luck finding anyone by that email address. + if (!$user instanceof User) { + if (common_config('site', 'fakeaddressrecovery')) { + // Return without actually doing anything! We fake address recovery + // to avoid revealing which email addresses are registered with the site. + return; + } + // TRANS: Information on password recovery form if no known e-mail address was specified. + throw new ClientException(_('No user with that email address exists here.')); } } else { - $confirm_email = null; - } - - if (!$user) { - // TRANS: Information on password recovery form if no known username or e-mail address was specified. - throw new ClientException(_('No user with that email address or username.')); - return; + // This might throw a NicknameException on bad nicknames + $user = User::getKV('nickname', common_canonical_nickname($nore)); + if (!$user instanceof User) { + // TRANS: Information on password recovery form if no known username was specified. + throw new ClientException(_('No user with that nickname exists here.')); + } } // Try to get an unconfirmed email address if they used a user name - - if (!$user->email && !$confirm_email) { + if (empty($user->email) && $confirm_email === null) { $confirm_email = new Confirm_address(); $confirm_email->user_id = $user->id; $confirm_email->address_type = 'email'; $confirm_email->find(); if (!$confirm_email->fetch()) { + // Nothing found, so let's reset it to 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. throw new ClientException(_('No registered email address for that user.')); - return; } // 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->address_type = 'recover'; $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()) { 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. throw new ServerException(_('Error saving address confirmation.')); - return; } // @todo FIXME: needs i18n. diff --git a/js/extlib/jquery.js b/js/extlib/jquery.js index 79d631ff46..eed17778c6 100644 --- a/js/extlib/jquery.js +++ b/js/extlib/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v2.1.3 + * jQuery JavaScript Library v2.1.4 * http://jquery.com/ * * Includes Sizzle.js @@ -9,7 +9,7 @@ * Released under the MIT license * http://jquery.org/license * - * Date: 2014-12-18T15:11Z + * Date: 2015-04-28T16:01Z */ (function( global, factory ) { @@ -67,7 +67,7 @@ var // Use the correct document accordingly with window argument (sandbox) document = window.document, - version = "2.1.3", + version = "2.1.4", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -531,7 +531,12 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli }); 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 ); if ( type === "function" || jQuery.isWindow( obj ) ) { diff --git a/lib/apiaction.php b/lib/apiaction.php index 0eea08bed6..724447f120 100755 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -328,7 +328,7 @@ class ApiAction extends Action // different story for parenting. $parent = $notice->getParent(); $in_reply_to = $parent->id; - } catch (Exception $e) { + } catch (NoParentNoticeException $e) { $in_reply_to = null; } $twitter_status['in_reply_to_status_id'] = $in_reply_to; diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index d2b744e93d..c546cf0fca 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -36,6 +36,7 @@ class CommandInterpreter // StatusNet $cmd = strtolower($cmd); + $result = false; if (Event::handle('StartInterpretCommand', array($cmd, $arg, $user, &$result))) { switch($cmd) { @@ -297,8 +298,6 @@ class CommandInterpreter $result = new TrackingCommand($user); } break; - default: - $result = false; } Event::handle('EndInterpretCommand', array($cmd, $arg, $user, &$result)); diff --git a/lib/default.php b/lib/default.php index 6369fbddc6..0ec9fc4e14 100644 --- a/lib/default.php +++ b/lib/default.php @@ -48,6 +48,7 @@ $default = 'languages' => get_all_languages(), 'email' => array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null, + 'fakeaddressrecovery' => true, 'broughtby' => null, 'timezone' => 'UTC', 'broughtbyurl' => null, diff --git a/lib/framework.php b/lib/framework.php index 4ec8b083e0..da43297d10 100644 --- a/lib/framework.php +++ b/lib/framework.php @@ -23,7 +23,7 @@ define('GNUSOCIAL_ENGINE', 'GNU social'); define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/social/'); 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); @@ -38,6 +38,9 @@ define('PROFILES_PER_PAGE', 20); define('MESSAGES_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_RECV', 2); define('FOREIGN_NOTICE_SEND_REPLY', 4); diff --git a/lib/groupminilist.php b/lib/groupminilist.php index ca7f8775c6..8212b81b17 100644 --- a/lib/groupminilist.php +++ b/lib/groupminilist.php @@ -33,8 +33,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { require_once INSTALLDIR.'/lib/grouplist.php'; -define('GROUPS_PER_MINILIST', 8); - /** * Widget to show a list of groups, good for sidebar * diff --git a/lib/httpclient.php b/lib/httpclient.php index 6016f89314..865fc9029e 100644 --- a/lib/httpclient.php +++ b/lib/httpclient.php @@ -103,7 +103,7 @@ class GNUsocial_HTTPResponse extends HTTP_Request2_Response * * This extends the PEAR HTTP_Request2 package: * - 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 * - extended response class adds getRedirectCount() and getUrl() methods * - 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. * + * 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 * @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 } /** diff --git a/lib/implugin.php b/lib/implugin.php index 5b0f3dbe09..2da4fa961a 100644 --- a/lib/implugin.php +++ b/lib/implugin.php @@ -380,7 +380,7 @@ abstract class ImPlugin extends Plugin $parent = $notice->getParent(); $orig_profile = $parent->getProfile(); $nicknames = sprintf('%1$s => %2$s', $profile->nickname, $orig_profile->nickname); - } catch (Exception $e) { + } catch (NoParentNoticeException $e) { $nicknames = $profile->nickname; } @@ -402,9 +402,8 @@ abstract class ImPlugin extends Plugin $chan = new IMChannel($this); $cmd->execute($chan); return true; - } else { - return false; } + return false; } /** diff --git a/lib/mediafile.php b/lib/mediafile.php index 546239ed7d..2b8f324df2 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -61,7 +61,7 @@ class MediaFile public function attachToNotice(Notice $notice) { - File_to_post::processNew($this->fileRecord->id, $notice->id); + File_to_post::processNew($this->fileRecord, $notice); } public function getPath() diff --git a/lib/noparentnoticeexception.php b/lib/noparentnoticeexception.php new file mode 100644 index 0000000000..fea179c409 --- /dev/null +++ b/lib/noparentnoticeexception.php @@ -0,0 +1,41 @@ +. + * + * @category Exception + * @package GNUsocial + * @author Mikael Nordfeldth + * @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)); + } +} diff --git a/lib/profileminilist.php b/lib/profileminilist.php index 4f47487220..d2997d12de 100644 --- a/lib/profileminilist.php +++ b/lib/profileminilist.php @@ -29,8 +29,6 @@ if (!defined('GNUSOCIAL')) { exit(1); } -define('PROFILES_PER_MINILIST', 8); - /** * Widget to show a list of profiles, good for sidebar * diff --git a/lib/publicgroupnav.php b/lib/publicgroupnav.php index 69347bc0d4..620a61ddd9 100644 --- a/lib/publicgroupnav.php +++ b/lib/publicgroupnav.php @@ -64,6 +64,13 @@ class PublicGroupNav extends Menu // TRANS: Menu item title in search group navigation panel. _('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. $this->out->menuItem(common_local_url('groups'), _m('MENU','Groups'), diff --git a/lib/schema.php b/lib/schema.php index 0421bcb810..f536f01645 100644 --- a/lib/schema.php +++ b/lib/schema.php @@ -535,6 +535,7 @@ class Schema $res = $this->conn->query($sql); if ($_PEAR->isError($res)) { + common_debug('PEAR exception on query: '.$sql); PEAR_ErrorToPEAR_Exception($res); } } diff --git a/lib/sitestreamaction.php b/lib/sitestreamaction.php new file mode 100644 index 0000000000..d462c499a5 --- /dev/null +++ b/lib/sitestreamaction.php @@ -0,0 +1,182 @@ +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 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'); + } +} diff --git a/lib/util.php b/lib/util.php index dbc036c461..f29d9559b9 100644 --- a/lib/util.php +++ b/lib/util.php @@ -628,7 +628,7 @@ function common_render_content($text, Notice $notice) * @param Notice $notice in-progress or complete Notice object for context * @return string partially-rendered HTML */ -function common_linkify_mentions($text, $notice) +function common_linkify_mentions($text, Notice $notice) { $mentions = common_find_mentions($text, $notice); @@ -655,7 +655,7 @@ function common_linkify_mentions($text, $notice) return $text; } -function common_linkify_mention($mention) +function common_linkify_mention(array $mention) { $output = null; @@ -695,13 +695,10 @@ function common_linkify_mention($mention) * * @access private */ -function common_find_mentions($text, $notice) +function common_find_mentions($text, Notice $notice) { - try { - $sender = Profile::getKV('id', $notice->profile_id); - } catch (NoProfileException $e) { - return array(); - } + // The getProfile call throws NoProfileException on failure + $sender = $notice->getProfile(); $mentions = array(); @@ -728,8 +725,8 @@ function common_find_mentions($text, $notice) } } catch (NoProfileException $e) { common_log(LOG_WARNING, sprintf('Notice %d author profile id %d does not exist', $origNotice->id, $origNotice->profile_id)); - } catch (ServerException $e) { - // Probably just no parent. Should get a specific NoParentException + } catch (NoParentNoticeException $e) { + // This notice is not in reply to anything } catch (Exception $e) { common_log(LOG_WARNING, __METHOD__ . ' got exception ' . get_class($e) . ' : ' . $e->getMessage()); } diff --git a/plugins/ActivityVerb/actions/activityverb.php b/plugins/ActivityVerb/actions/activityverb.php index 0abfacd645..45bb18be46 100644 --- a/plugins/ActivityVerb/actions/activityverb.php +++ b/plugins/ActivityVerb/actions/activityverb.php @@ -50,7 +50,7 @@ class ActivityverbAction extends ManagedAction 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)) { // TRANS: %1$s is a user nickname, %2$d is a notice ID (number). diff --git a/plugins/AntiBrute/AntiBrutePlugin.php b/plugins/AntiBrute/AntiBrutePlugin.php index 365937fedf..625180d23d 100755 --- a/plugins/AntiBrute/AntiBrutePlugin.php +++ b/plugins/AntiBrute/AntiBrutePlugin.php @@ -9,6 +9,13 @@ class AntiBrutePlugin extends Plugin { 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) { if (common_is_email($nickname)) { @@ -22,9 +29,6 @@ class AntiBrutePlugin extends Plugin { 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); switch (true) { case $this->failed_attempts >= 5: diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 07c9d1c182..4d1b95e2b7 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -691,8 +691,8 @@ class Ostatus_profile extends Managed_DataObject $options); if ($saved instanceof Notice) { Ostatus_source::saveNew($saved, $this, $method); - if (!empty($attachment)) { - File_to_post::processNew($attachment->id, $saved->id); + if ($attachment instanceof File) { + File_to_post::processNew($attachment, $saved); } } } catch (Exception $e) { diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index 0d093f2868..3ba2f4e5ab 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -154,7 +154,7 @@ class OpenIDPlugin extends Plugin * * @return boolean hook return */ - function onEndPublicXRDS($action, &$xrdsOutputter) + function onEndPublicXRDS(Action $action, &$xrdsOutputter) { $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)', 'xmlns:simple' => 'http://xrds-simple.net/core/1.0', @@ -174,37 +174,6 @@ class OpenIDPlugin extends Plugin $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. * @@ -415,7 +384,7 @@ class OpenIDPlugin extends Plugin * * @return void */ - function onEndShowHeadElements($action) + function onEndShowHeadElements(Action $action) { if ($action instanceof ShowstreamAction) { $action->element('link', array('rel' => 'openid2.provider', @@ -427,6 +396,11 @@ class OpenIDPlugin extends Plugin $action->element('link', array('rel' => 'openid.delegate', 'href' => $action->profile->profileurl)); } + + if ($action instanceof SitestreamAction) { + $action->element('meta', array('http-equiv' => 'X-XRDS-Location', + 'content' => common_local_url('publicxrds'))); + } return true; } diff --git a/actions/publicxrds.php b/plugins/OpenID/actions/publicxrds.php similarity index 88% rename from actions/publicxrds.php rename to plugins/OpenID/actions/publicxrds.php index aac6f423cf..25801e7861 100644 --- a/actions/publicxrds.php +++ b/plugins/OpenID/actions/publicxrds.php @@ -30,12 +30,9 @@ * along with this program. If not, see . */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } -require_once INSTALLDIR.'/plugins/OpenID/openid.php'; -require_once INSTALLDIR.'/lib/xrdsoutputter.php'; +require_once __DIR__.'/../openid.php'; /** * Public XRDS @@ -48,8 +45,6 @@ require_once INSTALLDIR.'/lib/xrdsoutputter.php'; * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ - * - * @todo factor out similarities with XrdsAction */ class PublicxrdsAction extends Action { @@ -70,9 +65,9 @@ class PublicxrdsAction extends Action * * @return nothing */ - function handle($args) + protected function handle() { - parent::handle($args); + parent::handle(); $xrdsOutputter = new XRDSOutputter(); $xrdsOutputter->startXRDS(); Event::handle('StartPublicXRDS', array($this,&$xrdsOutputter)); @@ -80,4 +75,3 @@ class PublicxrdsAction extends Action $xrdsOutputter->endXRDS(); } } - diff --git a/lib/xrdsoutputter.php b/plugins/OpenID/lib/xrdsoutputter.php similarity index 96% rename from lib/xrdsoutputter.php rename to plugins/OpenID/lib/xrdsoutputter.php index 95dc73300a..9841d9e871 100644 --- a/lib/xrdsoutputter.php +++ b/plugins/OpenID/lib/xrdsoutputter.php @@ -28,11 +28,7 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -require_once INSTALLDIR.'/lib/xmloutputter.php'; +if (!defined('GNUSOCIAL')) { exit(1); } /** * Low-level generator for XRDS XML diff --git a/plugins/Share/SharePlugin.php b/plugins/Share/SharePlugin.php index b5643c1d09..c337efbaec 100644 --- a/plugins/Share/SharePlugin.php +++ b/plugins/Share/SharePlugin.php @@ -161,7 +161,7 @@ class SharePlugin extends ActivityVerbHandlerPlugin public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null) { // 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 // and %2$s is the repeated user's nickname. $act->title = sprintf(_('%1$s repeated a notice by %2$s'), diff --git a/plugins/TwitterBridge/lib/twitterimport.php b/plugins/TwitterBridge/lib/twitterimport.php index 5258bfc2c9..45b7547ce2 100644 --- a/plugins/TwitterBridge/lib/twitterimport.php +++ b/plugins/TwitterBridge/lib/twitterimport.php @@ -564,13 +564,13 @@ class TwitterImport * @param Notice $notice * @param object $status */ - function saveStatusAttachments($notice, $status) + function saveStatusAttachments(Notice $notice, $status) { if (common_config('attachments', 'process_links')) { if (!empty($status->entities) && !empty($status->entities->urls)) { foreach ($status->entities->urls as $url) { try { - File::processNew($url->url, $notice->id); + File::processNew($url->url, $notice); } catch (ServerException $e) { // Could not process attached URL } diff --git a/plugins/WebFinger/WebFingerPlugin.php b/plugins/WebFinger/WebFingerPlugin.php index e5759e886c..6f8ec9397d 100644 --- a/plugins/WebFinger/WebFingerPlugin.php +++ b/plugins/WebFinger/WebFingerPlugin.php @@ -31,6 +31,10 @@ if (!defined('GNUSOCIAL')) { exit(1); } 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 function initialize() @@ -127,6 +131,11 @@ class WebFingerPlugin extends Plugin $type, 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')); } /** diff --git a/plugins/Xmpp/XmppPlugin.php b/plugins/Xmpp/XmppPlugin.php index 2974e8b2ab..d95ffcf0d6 100644 --- a/plugins/Xmpp/XmppPlugin.php +++ b/plugins/Xmpp/XmppPlugin.php @@ -354,7 +354,7 @@ class XmppPlugin extends ImPlugin $xs->text(": "); } catch (InvalidUrlException $e) { $xs->text(sprintf(' => %s', $orig_profile->nickname)); - } catch (Exception $e) { + } catch (NoParentNoticeException $e) { $xs->text(": "); } if (!empty($notice->rendered)) { diff --git a/scripts/console.php b/scripts/console.php index c260ffaa00..692cedf8d1 100755 --- a/scripts/console.php +++ b/scripts/console.php @@ -113,7 +113,7 @@ function readline_emulation($prompt) 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 "\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) { - print "StatusNet interactive PHP console... type ctrl+D or enter 'exit' to exit.\n"; - $prompt = common_config('site', 'name') . '> '; + print "GNU social interactive PHP console... type ctrl+D or enter 'exit' to exit.\n"; + $prompt = common_slugify(common_config('site', 'name')) . '> '; } else { $prompt = ''; } diff --git a/scripts/nukefile.php b/scripts/nukefile.php new file mode 100755 index 0000000000..1381676483 --- /dev/null +++ b/scripts/nukefile.php @@ -0,0 +1,78 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$shortoptions = 'i::yv'; +$longoptions = array('id=', 'yes', 'verbose'); + +$helptext = <<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";