Merge branch 'nightly' into singpolyma/gnu-social-events-saveObjectFromActivity

Conflicts:
	plugins/Event/EventPlugin.php
	plugins/Event/classes/RSVP.php

I just fixed 'em with magic!
This commit is contained in:
Mikael Nordfeldth 2016-01-03 13:08:34 +01:00
commit 95d415257a
7438 changed files with 145830 additions and 16117 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
avatar/*
background/*
files/*
file/*
local/*

View File

@ -632,20 +632,6 @@ notify third-party servers of updates.
notify: an array of URLs for ping endpoints. Default is the empty
array (no notification).
design
------
Default design (colors and background) for the site. Actual appearance
depends on the theme. Null values mean to use the theme defaults.
backgroundcolor: Hex color of the site background.
contentcolor: Hex color of the content area background.
sidebarcolor: Hex color of the sidebar background.
textcolor: Hex color of all non-link text.
linkcolor: Hex color of all links.
backgroundimage: Image to use for the background.
disposition: Flags for whether or not to tile the background image.
notice
------

10
UPGRADE
View File

@ -27,13 +27,13 @@ and follow this procedure:
The upgrade script will likely take a long time because it will
upgrade the tables to another character encoding and make other
automated upgrades. Make sure it ends without errors. If you get
errors, create a new task on https://bugz.foocorp.net/
errors, create a new task on https://git.gnu.io/gnu/gnu-social/issues
4. Start your queue daemons again (you can run this command even if you
do not use the queue daemons):
$ bash scripts/startdaemons.sh
5. Report any issues at https://bugz.foocorp.net/ (tag GNU social)
5. Report any issues at https://git.gnu.io/gnu/gnu-social/issues
If you are using ssh keys to log in to your server, you can make this
procedure pretty painless (assuming you have automated backups already).
@ -69,7 +69,7 @@ variant of this command (you will be prompted for the database password):
2. Unpack your GNU social code to a fresh directory. You can do this
by cloning our git repository:
$ git clone https://gitorious.org/social/mainline.git gnusocial
$ git clone https://git.gnu.io/gnu/gnu-social.git gnusocial
3. Synchronize your local files to the GNU social directory. These
will be the local files such as avatars, config and files:
@ -91,8 +91,8 @@ variant of this command (you will be prompted for the database password):
The upgrade script will likely take a long time because it will
upgrade the tables to another character encoding and make other
automated upgrades. Make sure it ends without errors. If you get
errors, create a new task on https://bugz.foocorp.net/
errors, create a new task on https://git.gnu.io/gnu/gnu-social/issues
6. Start your queue daemons: 'bash scripts/startdaemons.sh'
7. Report any issues at https://bugz.foocorp.net/ (tag GNU social)
7. Report any issues at https://git.gnu.io/gnu/gnu-social/issues

View File

@ -30,7 +30,7 @@ if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Upload an image via the API. Returns a shortened URL for the image
* to the user.
* to the user. Apparently modelled after a former Twitpic API.
*
* @category API
* @package StatusNet
@ -91,9 +91,14 @@ class ApiMediaUploadAction extends ApiAuthAction
function showResponse(MediaFile $upload)
{
$this->initDocument();
$this->elementStart('rsp', array('stat' => 'ok'));
$this->elementStart('rsp', array('stat' => 'ok', 'xmlns:atom'=>Activity::ATOM));
$this->element('mediaid', null, $upload->fileRecord->id);
$this->element('mediaurl', null, $upload->shortUrl());
$enclosure = $upload->fileRecord->getEnclosure();
$this->element('atom:link', array('rel' => 'enclosure',
'href' => $enclosure->url,
'type' => $enclosure->mimetype));
$this->elementEnd('rsp');
$this->endDocument();
}
@ -103,7 +108,7 @@ class ApiMediaUploadAction extends ApiAuthAction
*
* @param String $msg an error message
*/
function clientError($msg)
function clientError($msg, $code=400, $format=null)
{
$this->initDocument();
$this->elementStart('rsp', array('stat' => 'fail'));
@ -114,5 +119,6 @@ class ApiMediaUploadAction extends ApiAuthAction
$this->element('err', $errAttr, null);
$this->elementEnd('rsp');
$this->endDocument();
exit;
}
}

View File

@ -124,7 +124,7 @@ class ApiStatusesDestroyAction extends ApiAuthAction
if ($this->user->id == $this->notice->profile_id) {
if (Event::handle('StartDeleteOwnNotice', array($this->user, $this->notice))) {
$this->notice->delete();
$this->notice->deleteAs($this->scoped);
Event::handle('EndDeleteOwnNotice', array($this->user, $this->notice));
}
$this->showNotice();

View File

@ -236,7 +236,7 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
}
if (Event::handle('StartDeleteOwnNotice', array($this->auth_user, $this->notice))) {
$this->notice->delete();
$this->notice->deleteAs($this->scoped);
Event::handle('EndDeleteOwnNotice', array($this->auth_user, $this->notice));
}

View File

@ -34,9 +34,7 @@
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Returns the most recent notices (default 20) posted by the authenticating
@ -115,11 +113,11 @@ class ApiTimelineUserAction extends ApiBareAuthAction
{
// We'll use the shared params from the Atom stub
// for other feed types.
$atom = new AtomUserNoticeFeed($this->target->getUser(), $this->auth_user);
$atom = new AtomUserNoticeFeed($this->target->getUser(), $this->scoped);
$link = common_local_url(
'showstream',
array('nickname' => $this->target->nickname)
array('nickname' => $this->target->getNickname())
);
$self = $this->getSelfUri();
@ -127,7 +125,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
// FriendFeed's SUP protocol
// Also added RSS and Atom feeds
$suplink = common_local_url('sup', null, null, $this->target->id);
$suplink = common_local_url('sup', null, null, $this->target->getID());
header('X-SUP-ID: ' . $suplink);
@ -135,7 +133,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$nextUrl = !empty($this->next_id)
? common_local_url('ApiTimelineUser',
array('format' => $this->format,
'id' => $this->target->id),
'id' => $this->target->getID()),
array('max_id' => $this->next_id))
: null;
@ -147,11 +145,11 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$prevUrl = common_local_url('ApiTimelineUser',
array('format' => $this->format,
'id' => $this->target->id),
'id' => $this->target->getID()),
$prevExtra);
$firstUrl = common_local_url('ApiTimelineUser',
array('format' => $this->format,
'id' => $this->target->id));
'id' => $this->target->getID()));
switch($this->format) {
case 'xml':
@ -206,7 +204,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
break;
case 'as':
header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE);
$doc = new ActivityStreamJSONDocument($this->auth_user);
$doc = new ActivityStreamJSONDocument($this->scoped);
$doc->setTitle($atom->title);
$doc->addLink($link, 'alternate', 'text/html');
$doc->addItemsFromNotices($this->notices);
@ -307,9 +305,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction
return '"' . implode(
':',
array($this->arg('action'),
common_user_cache_hash($this->auth_user),
common_user_cache_hash($this->scoped),
common_language(),
$this->target->id,
$this->target->getID(),
strtotime($this->notices[0]->created),
strtotime($this->notices[$last]->created))
)
@ -321,10 +319,10 @@ class ApiTimelineUserAction extends ApiBareAuthAction
function handlePost()
{
if (empty($this->auth_user) ||
$this->auth_user->id != $this->target->id) {
if (!$this->scoped instanceof Profile ||
!$this->target->sameAs($this->scoped)) {
// TRANS: Client error displayed trying to add a notice to another user's timeline.
$this->clientError(_('Only the user can add to their own timeline.'));
$this->clientError(_('Only the user can add to their own timeline.'), 403);
}
// Only handle posts for Atom
@ -356,156 +354,28 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$activity = new Activity($dom->documentElement);
$saved = null;
common_debug('AtomPub: Ignoring right now, but this POST was made to collection: '.$activity->id);
if (Event::handle('StartAtomPubNewActivity', array(&$activity, $this->target->getUser(), &$saved))) {
if ($activity->verb != ActivityVerb::POST) {
// TRANS: Client error displayed when not using the POST verb. Do not translate POST.
$this->clientError(_('Can only handle POST activities.'));
}
// Reset activity data so we can handle it in the same functions as with OStatus
// because we don't let clients set their own UUIDs... Not sure what AtomPub thinks
// about that though.
$activity->id = null;
$activity->actor = null; // not used anyway, we use $this->target
$activity->objects[0]->id = null;
$note = $activity->objects[0];
if (!in_array($note->type, array(ActivityObject::NOTE,
ActivityObject::BLOGENTRY,
ActivityObject::STATUS))) {
// TRANS: Client error displayed when using an unsupported activity object type.
// TRANS: %s is the unsupported activity object type.
$this->clientError(sprintf(_('Cannot handle activity object type "%s".'),
$note->type));
}
$saved = $this->postNote($activity);
Event::handle('EndAtomPubNewActivity', array($activity, $this->target->getUser(), $saved));
$stored = null;
if (Event::handle('StartAtomPubNewActivity', array($activity, $this->target, &$stored))) {
// TRANS: Client error displayed when not using the POST verb. Do not translate POST.
throw new ClientException(_('Could not handle this Atom Activity.'));
}
if (!empty($saved)) {
header('HTTP/1.1 201 Created');
header("Location: " . common_local_url('ApiStatusesShow', array('id' => $saved->id,
'format' => 'atom')));
$this->showSingleAtomStatus($saved);
if (!$stored instanceof Notice) {
throw new ServerException('Server did not create a Notice object from handled AtomPub activity.');
}
}
Event::handle('EndAtomPubNewActivity', array($activity, $this->target, $stored));
function postNote($activity)
{
$note = $activity->objects[0];
// Use summary as fallback for content
if (!empty($note->content)) {
$sourceContent = $note->content;
} else if (!empty($note->summary)) {
$sourceContent = $note->summary;
} else if (!empty($note->title)) {
$sourceContent = $note->title;
} else {
// @fixme fetch from $sourceUrl?
// TRANS: Client error displayed when posting a notice without content through the API.
// TRANS: %d is the notice ID (number).
$this->clientError(sprintf(_('No content for notice %d.'), $note->id));
}
// Get (safe!) HTML and text versions of the content
$rendered = common_purify($sourceContent);
$content = common_strip_html($rendered);
$shortened = $this->auth_user->shortenLinks($content);
$options = array('is_local' => Notice::LOCAL_PUBLIC,
'rendered' => $rendered,
'replies' => array(),
'groups' => array(),
'tags' => array(),
'urls' => array());
// accept remote URI (not necessarily a good idea)
common_debug("Note ID is {$note->id}");
if (!empty($note->id)) {
$notice = Notice::getKV('uri', trim($note->id));
if (!empty($notice)) {
// TRANS: Client error displayed when using another format than AtomPub.
// TRANS: %s is the notice URI.
$this->clientError(sprintf(_('Notice with URI "%s" already exists.'), $note->id));
}
common_log(LOG_NOTICE, "Saving client-supplied notice URI '$note->id'");
$options['uri'] = $note->id;
}
// accept remote create time (also maybe not such a good idea)
if (!empty($activity->time)) {
common_log(LOG_NOTICE, "Saving client-supplied create time {$activity->time}");
$options['created'] = common_sql_date($activity->time);
}
// Check for optional attributes...
if ($activity->context instanceof ActivityContext) {
foreach ($activity->context->attention as $uri=>$type) {
try {
$profile = Profile::fromUri($uri);
if ($profile->isGroup()) {
$options['groups'][] = $profile->id;
} else {
$options['replies'][] = $uri;
}
} catch (UnknownUriException $e) {
common_log(LOG_WARNING, sprintf('AtomPub post with unknown attention URI %s', $uri));
}
}
// Maintain direct reply associations
// @fixme what about conversation ID?
if (!empty($activity->context->replyToID)) {
$orig = Notice::getKV('uri',
$activity->context->replyToID);
if (!empty($orig)) {
$options['reply_to'] = $orig->id;
}
}
$location = $activity->context->location;
if ($location) {
$options['lat'] = $location->lat;
$options['lon'] = $location->lon;
if ($location->location_id) {
$options['location_ns'] = $location->location_ns;
$options['location_id'] = $location->location_id;
}
}
}
// Atom categories <-> hashtags
foreach ($activity->categories as $cat) {
if ($cat->term) {
$term = common_canonical_tag($cat->term);
if ($term) {
$options['tags'][] = $term;
}
}
}
// Atom enclosures -> attachment URLs
foreach ($activity->enclosures as $href) {
// @fixme save these locally or....?
$options['urls'][] = $href;
}
$saved = Notice::saveNew($this->target->id,
$content,
'atompub', // TODO: deal with this
$options);
return $saved;
header('HTTP/1.1 201 Created');
header("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(),
'format' => 'atom')));
$this->showSingleAtomStatus($stored);
}
}

View File

@ -65,7 +65,7 @@ class DeletenoticeAction extends FormAction
{
if ($this->arg('yes')) {
if (Event::handle('StartDeleteOwnNotice', array($this->scoped->getUser(), $this->notice))) {
$this->notice->delete();
$this->notice->deleteAs($this->scoped);
Event::handle('EndDeleteOwnNotice', array($this->scoped->getUser(), $this->notice));
}
} else {

View File

@ -96,7 +96,7 @@ class NewnoticeAction extends FormAction
assert($this->scoped instanceof Profile); // XXX: maybe an error instead...
$user = $this->scoped->getUser();
$content = $this->trimmed('status_textarea');
$options = array();
$options = array('source' => 'web');
Event::handle('StartSaveNewNoticeWeb', array($this, $user, &$content, &$options));
if (empty($content)) {
@ -117,31 +117,30 @@ class NewnoticeAction extends FormAction
return;
}
$content_shortened = $user->shortenLinks($content);
if (Notice::contentTooLong($content_shortened)) {
// TRANS: Client error displayed when the parameter "status" is missing.
// TRANS: %d is the maximum number of character for a notice.
$this->clientError(sprintf(_m('That\'s too long. Maximum notice size is %d character.',
'That\'s too long. Maximum notice size is %d characters.',
Notice::maxContent()),
Notice::maxContent()));
if ($this->int('inreplyto')) {
// Throws exception if the inreplyto Notice is given but not found.
$parent = Notice::getByID($this->int('inreplyto'));
} else {
$parent = null;
}
$replyto = $this->int('inreplyto');
if ($replyto) {
$options['reply_to'] = $replyto;
}
$act = new Activity();
$act->verb = ActivityVerb::POST;
$act->time = time();
$act->actor = $this->scoped->asActivityObject();
$content = $this->scoped->shortenLinks($content);
$upload = null;
try {
// throws exception on failure
$upload = MediaFile::fromUpload('attach', $this->scoped);
if (Event::handle('StartSaveNewNoticeAppendAttachment', array($this, $upload, &$content_shortened, &$options))) {
$content_shortened .= ' ' . $upload->shortUrl();
if (Event::handle('StartSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options))) {
$content .= ' ' . $upload->shortUrl();
}
Event::handle('EndSaveNewNoticeAppendAttachment', array($this, $upload, &$content_shortened, &$options));
Event::handle('EndSaveNewNoticeAppendAttachment', array($this, $upload, &$content, &$options));
if (Notice::contentTooLong($content_shortened)) {
if (Notice::contentTooLong($content)) {
$upload->delete();
// TRANS: Client error displayed exceeding the maximum notice length.
// TRANS: %d is the maximum length for a notice.
@ -150,10 +149,25 @@ class NewnoticeAction extends FormAction
Notice::maxContent()),
Notice::maxContent()));
}
$act->enclosures[] = $upload->getEnclosure();
} catch (NoUploadedMediaException $e) {
// simply no attached media to the new notice
}
$actobj = new ActivityObject();
$actobj->type = ActivityObject::NOTE;
$actobj->content = common_render_content($content, $this->scoped, $parent);
$act->objects[] = $actobj;
$act->context = new ActivityContext();
if ($parent instanceof Notice) {
$act->context->replyToID = $parent->getUri();
$act->context->replyToUrl = $parent->getUrl(true); // maybe we don't have to send true here to force a URL?
}
if ($this->scoped->shareLocation()) {
// use browser data if checked; otherwise profile data
@ -171,19 +185,20 @@ class NewnoticeAction extends FormAction
$this->scoped);
}
$options = array_merge($options, $locOptions);
$act->context->location = Location::fromOptions($locOptions);
}
$author_id = $this->scoped->id;
$text = $content_shortened;
$text = $content;
// Does the heavy-lifting for getting "To:" information
ToSelector::fillOptions($this, $options);
// FIXME: Make sure NoticeTitle plugin gets a change to add the title to our activityobject!
if (Event::handle('StartNoticeSaveWeb', array($this, &$author_id, &$text, &$options))) {
$this->stored = Notice::saveNew($this->scoped->id, $content_shortened, 'web', $options);
$this->stored = Notice::saveActivity($act, $this->scoped, $options);
if ($upload instanceof MediaFile) {
$upload->attachToNotice($this->stored);
@ -192,7 +207,7 @@ class NewnoticeAction extends FormAction
Event::handle('EndNoticeSaveWeb', array($this, $this->stored));
}
Event::handle('EndSaveNewNoticeWeb', array($this, $user, &$content_shortened, &$options));
Event::handle('EndSaveNewNoticeWeb', array($this, $user, &$content, &$options));
if (!GNUsocial::isAjax()) {
$url = common_local_url('shownotice', array('notice' => $this->stored->id));

View File

@ -203,14 +203,7 @@ class SearchNoticeListItem extends NoticeListItem {
{
// FIXME: URL, image, video, audio
$this->out->elementStart('p', array('class' => 'e-content'));
if ($this->notice->rendered) {
$this->out->raw($this->highlight($this->notice->rendered, $this->terms));
} else {
// XXX: may be some uncooked notices in the DB,
// we cook them right now. This should probably disappear in future
// versions (>> 0.4.x)
$this->out->raw($this->highlight(common_render_content($this->notice->content, $this->notice), $this->terms));
}
$this->out->raw($this->highlight($this->notice->getRendered(), $this->terms));
$this->out->elementEnd('p');
}

View File

@ -144,22 +144,8 @@ class PasswordsettingsAction extends SettingsAction
if (Event::handle('StartChangePassword', array($this->scoped, $oldpassword, $newpassword))) {
//no handler changed the password, so change the password internally
$user = $this->scoped->getUser();
$original = clone($user);
$user->setPassword($newpassword);
$user->password = common_munge_password($newpassword, $this->scoped);
$val = $user->validate();
if ($val !== true) {
// TRANS: Form validation error on page where to change password.
throw new ServerException(_('Error saving user; invalid.'));
}
if (!$user->update($original)) {
// TRANS: Server error displayed on page where to change password when password change
// TRANS: could not be made because of a server error.
throw new ServerException(_('Cannot save new password.'));
}
Event::handle('EndChangePassword', array($this->scoped));
}

View File

@ -322,16 +322,7 @@ class RecoverpasswordAction extends Action
}
// OK, we're ready to go
$original = clone($user);
$user->password = common_munge_password($newpassword, $user->getProfile());
if (!$user->update($original)) {
common_log_db_error($user, 'UPDATE', __FILE__);
// TRANS: Reset password form validation error message.
$this->serverError(_('Cannot save new password.'));
}
$user->setPassword($newpassword);
$this->clearTempUser();

View File

@ -151,7 +151,7 @@ class RsdAction extends Action
$this->elementStart('api', $apiAttrs);
$this->elementStart('settings');
$this->element('docs', null,
'http://status.net/wiki/TwitterCompatibleAPI');
common_local_url('doc', array('title' => 'api')));
$this->element('setting', array('name' => 'OAuth'),
'true');
$this->elementEnd('settings');

View File

@ -22,97 +22,31 @@
* @link http://status.net
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
if (!defined('GNUSOCIAL')) { exit(1); }
require_once INSTALLDIR.'/lib/profileminilist.php';
require_once INSTALLDIR.'/lib/peopletaglist.php';
require_once INSTALLDIR.'/lib/noticelist.php';
require_once INSTALLDIR.'/lib/feedlist.php';
class ShowprofiletagAction extends Action
class ShowprofiletagAction extends ShowstreamAction
{
var $notice, $tagger, $peopletag, $userProfile;
var $notice, $peopletag;
function isReadOnly($args)
protected function doStreamPreparation()
{
return true;
}
function prepare($args)
{
parent::prepare($args);
if (common_config('singleuser', 'enabled')) {
$tagger_arg = User::singleUserNickname();
} else {
$tagger_arg = $this->arg('tagger');
}
$tag_arg = $this->arg('tag');
$tagger = common_canonical_nickname($tagger_arg);
$tag = common_canonical_tag($tag_arg);
// Permanent redirect on non-canonical nickname
if ($tagger_arg != $tagger || $tag_arg != $tag) {
$args = array('tagger' => $nickname, 'tag' => $tag);
if ($this->page != 1) {
$args['page'] = $this->page;
}
common_redirect(common_local_url('showprofiletag', $args), 301);
}
if (!$tagger) {
// TRANS: Client error displayed when a tagger is expected but not provided.
$this->clientError(_('No tagger.'), 404);
}
$user = User::getKV('nickname', $tagger);
if (!$user) {
// TRANS: Client error displayed trying to perform an action related to a non-existing user.
$this->clientError(_('No such user.'), 404);
}
$this->tagger = $user->getProfile();
$this->peopletag = Profile_list::pkeyGet(array('tagger' => $user->id, 'tag' => $tag));
$current = common_current_user();
$can_see = !empty($this->peopletag) && (!$this->peopletag->private ||
($this->peopletag->private && $this->peopletag->tagger === $current->id));
if (!$can_see) {
$tag = common_canonical_tag($this->arg('tag'));
try {
$this->peopletag = Profile_list::getByPK(array('tagger' => $this->target->getID(), 'tag' => $tag));
} catch (NoResultException $e) {
// TRANS: Client error displayed trying to reference a non-existing list.
$this->clientError(_('No such list.'), 404);
throw new ClientException('No such list.');
}
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
$this->userProfile = Profile::current();
$stream = new PeopletagNoticeStream($this->peopletag, $this->userProfile);
$this->notice = $stream->getNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
if ($this->page > 1 && $this->notice->N == 0) {
// TRANS: Client error when page not found (404).
$this->clientError(_('No such page.'), 404);
if ($this->peopletag->private && !$this->peopletag->getTagger()->sameAs($this->scoped)) {
// TRANS: Client error displayed trying to reference a non-existing list.
throw new AuthorizationException('You do not have permission to see this list.');
}
return true;
}
function handle($args)
public function getStream()
{
parent::handle($args);
if (!$this->peopletag) {
// TRANS: Client error displayed trying to perform an action related to a non-existing user.
$this->clientError(_('No such user.'));
}
$this->showPage();
return new PeopletagNoticeStream($this->peopletag, $this->scoped);
}
function title()
@ -137,7 +71,7 @@ class ShowprofiletagAction extends Action
// TRANS: %1$s is a list, %2$s is the tagger's nickname, %3$d is a page number.
return sprintf(_('Timeline for %1$s list by %2$s, page %3$d'),
$this->peopletag->tag,
$this->tagger->nickname,
$this->target->getNickname(),
$this->page
);
} else {
@ -160,7 +94,7 @@ class ShowprofiletagAction extends Action
// TRANS: %1$s is a list, %2$s is the tagger's nickname.
return sprintf(_('Timeline for %1$s list by %2$s'),
$this->peopletag->tag,
$this->tagger->nickname
$this->target->getNickname()
);
}
}
@ -171,29 +105,29 @@ class ShowprofiletagAction extends Action
return array(new Feed(Feed::JSON,
common_local_url(
'ApiTimelineList', array(
'user' => $this->tagger->id,
'user' => $this->target->id,
'id' => $this->peopletag->id,
'format' => 'as'
)
),
// TRANS: Feed title.
// TRANS: %s is tagger's nickname.
sprintf(_('Feed for friends of %s (Activity Streams JSON)'), $this->tagger->nickname)),
sprintf(_('Feed for friends of %s (Activity Streams JSON)'), $this->target->getNickname())),
new Feed(Feed::RSS2,
common_local_url(
'ApiTimelineList', array(
'user' => $this->tagger->id,
'user' => $this->target->id,
'id' => $this->peopletag->id,
'format' => 'rss'
)
),
// TRANS: Feed title.
// TRANS: %s is tagger's nickname.
sprintf(_('Feed for friends of %s (RSS 2.0)'), $this->tagger->nickname)),
sprintf(_('Feed for friends of %s (RSS 2.0)'), $this->target->getNickname())),
new Feed(Feed::ATOM,
common_local_url(
'ApiTimelineList', array(
'user' => $this->tagger->id,
'user' => $this->target->id,
'id' => $this->peopletag->id,
'format' => 'atom'
)
@ -201,7 +135,7 @@ class ShowprofiletagAction extends Action
// TRANS: Feed title.
// TRANS: %1$s is a list, %2$s is tagger's nickname.
sprintf(_('Feed for %1$s list by %2$s (Atom)'),
$this->peopletag->tag, $this->tagger->nickname
$this->peopletag->tag, $this->target->getNickname()
)
)
);
@ -219,11 +153,10 @@ class ShowprofiletagAction extends Action
// TRANS: %1$s is a list, %2$s is a tagger's nickname.
$message = sprintf(_('This is the timeline for %1$s list by %2$s but no one has posted anything yet.'),
$this->peopletag->tag,
$this->tagger->nickname) . ' ';
$this->target->getNickname()) . ' ';
if (common_logged_in()) {
$current_user = common_current_user();
if ($this->tagger->id == $current_user->id) {
if ($this->target->sameAs($this->scoped)) {
// TRANS: Additional empty list message for list timeline for currently logged in user tagged tags.
$message .= _('Try tagging more people.');
}
@ -238,16 +171,15 @@ class ShowprofiletagAction extends Action
$this->elementEnd('div');
}
function showContent()
protected function showContent()
{
$this->showPeopletag();
$this->showNotices();
parent::showContent();
}
function showPeopletag()
{
$cur = common_current_user();
$tag = new Peopletag($this->peopletag, $cur, $this);
$tag = new Peopletag($this->peopletag, $this->scoped, $this);
$tag->show();
}
@ -267,7 +199,7 @@ class ShowprofiletagAction extends Action
$this->page,
'showprofiletag',
array('tag' => $this->peopletag->tag,
'tagger' => $this->tagger->nickname)
'nickname' => $this->target->getNickname())
);
Event::handle('EndShowProfileTagContent', array($this));
@ -283,11 +215,6 @@ class ShowprofiletagAction extends Action
# $this->showStatistics();
}
function showPageTitle()
{
$this->element('h1', null, $this->title());
}
function showTagged()
{
$profile = $this->peopletag->getTagged(0, PROFILES_PER_MINILIST + 1);
@ -314,7 +241,7 @@ class ShowprofiletagAction extends Action
if ($cnt > PROFILES_PER_MINILIST) {
$this->elementStart('p');
$this->element('a', array('href' => common_local_url('taggedprofiles',
array('nickname' => $this->tagger->nickname,
array('nickname' => $this->target->getNickname(),
'profiletag' => $this->peopletag->tag)),
'class' => 'more'),
// TRANS: Link for more "People in list x by a user"
@ -356,20 +283,3 @@ class ShowprofiletagAction extends Action
$this->elementEnd('div');
}
}
class Peopletag extends PeopletagListItem
{
protected $avatarSize = AVATAR_PROFILE_SIZE;
function showStart()
{
$mode = $this->peopletag->private ? 'private' : 'public';
$this->out->elementStart('div', array('class' => 'h-entry peopletag peopletag-profile mode-'.$mode,
'id' => 'peopletag-' . $this->peopletag->id));
}
function showEnd()
{
$this->out->elementEnd('div');
}
}

View File

@ -58,7 +58,6 @@ class ShowstreamAction extends NoticestreamAction
return $stream;
}
function title()
{
$base = $this->target->getFancyName();
@ -74,7 +73,7 @@ class ShowstreamAction extends NoticestreamAction
}
} else {
if ($this->page == 1) {
return $base;
return sprintf(_('Notices by %s'), $base);
} else {
// TRANS: Extended page title showing tagged notices in one user's timeline.
// TRANS: %1$s is the username, %2$d is the page number.
@ -85,7 +84,7 @@ class ShowstreamAction extends NoticestreamAction
}
}
function showContent()
protected function showContent()
{
$this->showNotices();
}

View File

@ -62,21 +62,44 @@ class UsergroupsAction extends GalleryAction
}
}
function showPageNotice()
{
if ($this->scoped instanceof Profile && $this->scoped->sameAs($this->getTarget())) {
$this->element('p', 'instructions',
// TRANS: Page notice for page with an overview of all subscribed groups
// TRANS: of the logged in user's own profile.
_('These are the groups whose notices '.
'you listen to.'));
} else {
$this->element('p', 'instructions',
// TRANS: Page notice for page with an overview of all groups a user other
// TRANS: than the logged in user. %s is the user nickname.
sprintf(_('These are the groups whose '.
'notices %s listens to.'),
$this->target->getNickname()));
}
}
function showContent()
{
$this->elementStart('p', array('id' => 'new_group'));
$this->element('a', array('href' => common_local_url('newgroup'),
'class' => 'more'),
// TRANS: Link text on group page to create a new group.
_('Create a new group'));
$this->elementEnd('p');
$this->elementStart('p', array('id' => 'group_search'));
$this->element('a', array('href' => common_local_url('groupsearch'),
'class' => 'more'),
// TRANS: Link text on group page to search for groups.
_('Search for more groups'));
$this->elementEnd('p');
if ($this->scoped instanceof Profile && $this->scoped->sameAs($this->getTarget())) {
$notice =
// TRANS: Page notice of user's groups page.
// TRANS: %%%%action.groupsearch%%%% and %%%%action.newgroup%%%% are URLs. Do not change them.
// TRANS: This message contains Markdown links in the form [link text](link).
sprintf(_('Groups let you find and talk with ' .
'people of similar interests. ' .
'You can [search for groups](%%%%action.groups%%%%) in your instance or ' .
'[create a new group](%%%%action.newgroup%%%%). ' .
'You can also follow groups ' .
'from other GNU social instances: click on the remote button below ' .
'and copy the group\'s link. ' .
'You can find a list of GNU social groups [here](http://skilledtests.com/wiki/List_of_federated_GNU_social_groups)' .
''));
$this->elementStart('div', 'instructions');
$this->raw(common_markup_to_html($notice));
$this->elementEnd('div');
}
if (Event::handle('StartShowUserGroupsContent', array($this))) {
$offset = ($this->page-1) * GROUPS_PER_PAGE;
@ -87,11 +110,13 @@ class UsergroupsAction extends GalleryAction
if ($groups instanceof User_group) {
$gl = new GroupList($groups, $this->getTarget(), $this);
$cnt = $gl->show();
$this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE,
$this->page, 'usergroups',
array('nickname' => $this->getTarget()->getNickname()));
} else {
$this->showEmptyListMessage();
if (0 == $cnt) {
$this->showEmptyListMessage();
} else {
$this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE,
$this->page, 'usergroups',
array('nickname' => $this->getTarget()->getNickname()));
}
}
Event::handle('EndShowUserGroupsContent', array($this));
@ -103,15 +128,15 @@ class UsergroupsAction extends GalleryAction
// TRANS: Text on group page for a user that is not a member of any group.
// TRANS: %s is a user nickname.
$message = sprintf(_('%s is not a member of any group.'), $this->getTarget()->getNickname()) . ' ';
if (common_logged_in()) {
$current_user = common_current_user();
if ($this->scoped->sameAs($this->getTarget())) {
// TRANS: Text on group page for a user that is not a member of any group. This message contains
// TRANS: a Markdown link in the form [link text](link) and a variable that should not be changed.
$message .= _('Try [searching for groups](%%action.groupsearch%%) and joining them.');
$message = _('You are not member of any group yet. After you join a group ' .
'you can send messages to its members using the ' .
'syntax "!groupname".');
}
}
}
$this->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message));
$this->elementEnd('div');

View File

@ -17,16 +17,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Table Definition for config
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
class Config extends Managed_DataObject
{
###START_AUTOCODE
@ -35,7 +31,7 @@ class Config extends Managed_DataObject
public $__table = 'config'; // table name
public $section; // varchar(32) primary_key not_null
public $setting; // varchar(32) primary_key not_null
public $value; // varchar(191) not 255 because utf8mb4 takes more space
public $value; // text
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
@ -46,7 +42,7 @@ class Config extends Managed_DataObject
'fields' => array(
'section' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'default' => '', 'description' => 'configuration section'),
'setting' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'default' => '', 'description' => 'configuration setting'),
'value' => array('type' => 'varchar', 'length' => 191, 'description' => 'configuration value'),
'value' => array('type' => 'text', 'description' => 'configuration value'),
),
'primary key' => array('section', 'setting'),
);

View File

@ -2,13 +2,9 @@
/**
* Table Definition for confirm_address
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
class Confirm_address extends Managed_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'confirm_address'; // table name
public $code; // varchar(32) primary_key not_null
public $user_id; // int(4) not_null
@ -19,9 +15,6 @@ class Confirm_address extends Managed_DataObject
public $sent; // datetime()
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
public static function schemaDef()
{
return array(
@ -29,7 +22,7 @@ class Confirm_address extends Managed_DataObject
'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'good random code'),
'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who requested confirmation'),
'address' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'address (email, xmpp, SMS, etc.)'),
'address_extra' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'carrier ID, for SMS'),
'address_extra' => array('type' => 'varchar', 'length' => 191, 'description' => 'carrier ID, for SMS'),
'address_type' => array('type' => 'varchar', 'length' => 8, 'not null' => true, 'description' => 'address type ("email", "xmpp", "sms")'),
'claimed' => array('type' => 'datetime', 'description' => 'date this was claimed for queueing'),
'sent' => array('type' => 'datetime', 'description' => 'date this was sent for queueing'),

View File

@ -1,54 +0,0 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, Control Yourself, 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/>.
*/
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Table Definition for deleted_notice
*/
class Deleted_notice extends Managed_DataObject
{
public $__table = 'deleted_notice'; // table name
public $id; // int(4) primary_key not_null
public $profile_id; // int(4) not_null
public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
public $created; // datetime() not_null
public $deleted; // datetime() not_null
public static function schemaDef()
{
return array(
'fields' => array(
'id' => array('type' => 'int', 'not null' => true, 'description' => 'identity of notice'),
'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'author of the notice'),
'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'),
'deleted' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'),
),
'primary key' => array('id'),
'unique keys' => array(
'deleted_notice_uri_key' => array('uri'),
),
'indexes' => array(
'deleted_notice_profile_id_idx' => array('profile_id'),
),
);
}
}

View File

@ -31,10 +31,10 @@ class File extends Managed_DataObject
public $filehash; // varchar(64) indexed
public $mimetype; // varchar(50)
public $size; // int(4)
public $title; // varchar(191) not 255 because utf8mb4 takes more space
public $title; // text()
public $date; // int(4)
public $protected; // int(4)
public $filename; // varchar(191) not 255 because utf8mb4 takes more space
public $filename; // text()
public $width; // int(4)
public $height; // int(4)
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
@ -52,10 +52,10 @@ class File extends Managed_DataObject
'filehash' => array('type' => 'varchar', 'length' => 64, 'not null' => false, 'description' => 'sha256 of the file contents, only for locally stored files of course'),
'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'),
'size' => array('type' => 'int', 'description' => 'size of resource when available'),
'title' => array('type' => 'varchar', 'length' => 191, 'description' => 'title of resource when available'),
'title' => array('type' => 'text', 'description' => 'title of resource when available'),
'date' => array('type' => 'int', 'description' => 'date of resource according to http query'),
'protected' => array('type' => 'int', 'description' => 'true when URL is private (needs login)'),
'filename' => array('type' => 'varchar', 'length' => 191, 'description' => 'if a local file, name of the file'),
'filename' => array('type' => 'text', 'description' => 'if a local file, name of the file'),
'width' => array('type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'),
'height' => array('type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'),
@ -82,30 +82,45 @@ class File extends Managed_DataObject
* @param string $given_url
* @return File
*/
public static function saveNew(array $redir_data, $given_url) {
// I don't know why we have to keep doing this but I'm adding this last check to avoid
// uniqueness bugs.
$file = File::getKV('urlhash', self::hashurl($given_url));
if (!$file instanceof File) {
$file = new File;
$file->urlhash = self::hashurl($given_url);
$file->url = $given_url;
if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected'];
if (!empty($redir_data['title'])) $file->title = $redir_data['title'];
if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type'];
if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']);
if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']);
$file_id = $file->insert();
public static function saveNew(array $redir_data, $given_url)
{
$file = null;
try {
// I don't know why we have to keep doing this but we run a last check to avoid
// uniqueness bugs.
$file = File::getByUrl($given_url);
return $file;
} catch (NoResultException $e) {
// We don't have the file's URL since before, so let's continue.
}
Event::handle('EndFileSaveNew', array($file, $redir_data, $given_url));
assert ($file instanceof File);
$file = new File;
$file->url = $given_url;
if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected'];
if (!empty($redir_data['title'])) $file->title = $redir_data['title'];
if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type'];
if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']);
if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']);
$file->saveFile();
return $file;
}
public function saveFile() {
$this->urlhash = self::hashurl($this->url);
if (!Event::handle('StartFileSaveNew', array(&$this))) {
throw new ServerException('File not saved due to an aborted StartFileSaveNew event.');
}
$this->id = $this->insert();
if ($this->id === false) {
throw new ServerException('File/URL metadata could not be saved to the database.');
}
Event::handle('EndFileSaveNew', array($this));
}
/**
* Go look at a URL and possibly save data about it if it's new:
* - follow redirect chains and store them in file_redirection
@ -114,7 +129,6 @@ class File extends Managed_DataObject
* - optionally save a file_to_post record
* - return the File object with the full reference
*
* @fixme refactor this mess, it's gotten pretty scary.
* @param string $given_url the URL we're looking at
* @param Notice $notice (optional)
* @param bool $followRedirects defaults to true
@ -133,69 +147,30 @@ class File extends Managed_DataObject
throw new ServerException('No canonical URL from given URL to process');
}
$file = null;
$redir = File_redirection::where($given_url);
$file = $redir->getFile();
try {
$file = File::getByUrl($given_url);
} catch (NoResultException $e) {
// First check if we have a lookup trace for this URL already
try {
$file_redir = File_redirection::getByUrl($given_url);
$file = File::getKV('id', $file_redir->file_id);
if (!$file instanceof File) {
// File did not exist, let's clean up the File_redirection entry
$file_redir->delete();
}
} catch (NoResultException $e) {
// We just wanted to doublecheck whether a File_thumbnail we might've had
// actually referenced an existing File object.
// If we still don't have a File object, let's create one now!
if (empty($file->id)) {
if ($redir->url === $given_url || !$followRedirects) {
// Save the File object based on our lookup trace
$file->saveFile();
} else {
$file->saveFile();
$redir->file_id = $file->id;
$redir->insert();
}
}
// If we still don't have a File object, let's create one now!
if (!$file instanceof File) {
// @fixme for new URLs this also looks up non-redirect data
// such as target content type, size, etc, which we need
// for File::saveNew(); so we call it even if not following
// new redirects.
$redir_data = File_redirection::where($given_url);
if (is_array($redir_data)) {
$redir_url = $redir_data['url'];
} elseif (is_string($redir_data)) {
$redir_url = $redir_data;
$redir_data = array();
} else {
// TRANS: Server exception thrown when a URL cannot be processed.
throw new ServerException(sprintf(_("Cannot process URL '%s'"), $given_url));
}
if ($redir_url === $given_url || !$followRedirects) {
// Save the File object based on our lookup trace
$file = File::saveNew($redir_data, $given_url);
} else {
// This seems kind of messed up... for now skipping this part
// if we're already under a redirect, so we don't go into
// horrible infinite loops if we've been given an unstable
// redirect (where the final destination of the first request
// doesn't match what we get when we ask for it again).
//
// 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, /*followRedirects*/false);
File_redirection::saveNew($redir_data, $file->id, $given_url);
}
if (!$file instanceof File) {
// This should only happen if File::saveNew somehow did not return a File object,
// though we have an assert for that in case the event there might've gone wrong.
// If anything else goes wrong, there should've been an exception thrown.
throw new ServerException('URL processing failed without new File object');
}
if (!$file instanceof File || empty($file->id)) {
// This should not happen
throw new ServerException('URL processing failed without new File object');
}
if ($notice instanceof Notice) {
File_to_post::processNew($file, $notice);
}
return $file;
}
@ -449,7 +424,11 @@ class File extends Managed_DataObject
if (self::hashurl($url) !== $this->urlhash) {
// For indexing purposes, in case we do a lookup on the 'url' field.
// also we're fixing possible changes from http to https, or paths
$this->updateUrl($url);
try {
$this->updateUrl($url);
} catch (ServerException $e) {
//
}
}
return $url;
}
@ -471,7 +450,7 @@ class File extends Managed_DataObject
/**
* @param string $hashstr String of (preferrably lower case) hexadecimal characters, same as result of 'hash_file(...)'
*/
static public function getByHash($hashstr, $alg=File::FILEHASH_ALG)
static public function getByHash($hashstr)
{
$file = new File();
$file->filehash = strtolower($hashstr);
@ -681,4 +660,4 @@ class File extends Managed_DataObject
echo "DONE.\n";
echo "Resuming core schema upgrade...";
}
}
}

View File

@ -39,6 +39,8 @@ class File_redirection extends Managed_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
protected $file; /* Cache the associated file sometimes */
public static function schemaDef()
{
return array(
@ -155,41 +157,55 @@ class File_redirection extends Managed_DataObject
*
* @param string $in_url
* @param boolean $discover true to attempt dereferencing the redirect if we don't know it already
* @return mixed one of:
* string - target URL, if this is a direct link or a known redirect
* array - redirect info if this is an *unknown* redirect:
* associative array with the following elements:
* code: HTTP status code
* redirects: count of redirects followed
* url: URL string of final target
* type (optional): MIME type from Content-Type header
* size (optional): byte size from Content-Length header
* time (optional): timestamp from Last-Modified header
* @return File_redirection
*/
static function where($in_url, $discover=true) {
// let's see if we know this...
$redir = new File_redirection();
$redir->url = $in_url;
$redir->urlhash = File::hashurl($redir->url);
$redir->redirections = 0;
try {
$a = File::getByUrl($in_url);
// this is a direct link to $a->url
return $a->url;
$r = File_redirection::getByUrl($in_url);
if($r instanceof File_redirection) {
return $r;
}
} catch (NoResultException $e) {
try {
$b = File_redirection::getByUrl($in_url);
// this is a redirect to $b->file_id
$a = File::getByID($b->file_id);
return $a->url;
$f = File::getByUrl($in_url);
$redir->file_id = $f->id;
$redir->file = $f;
return $redir;
} catch (NoResultException $e) {
// Oh well, let's keep going
}
}
if ($discover) {
$ret = File_redirection::lookupWhere($in_url);
return $ret;
$redir_info = File_redirection::lookupWhere($in_url);
if(is_string($redir_info)) {
$redir_info = array('url' => $redir_info);
}
// Double check that we don't already have the resolved URL
$r = self::where($redir_info['url'], false);
if (!empty($r->file_id)) {
return $r;
}
$redir->httpcode = $redir_info['code'];
$redir->redirections = intval($redir_info['redirects']);
$redir->file = new File();
$redir->file->url = $redir_info['url'];
$redir->file->mimetype = $redir_info['type'];
$redir->file->size = isset($redir_info['size']) ? $redir_info['size'] : null;
$redir->file->date = isset($redir_info['time']) ? $redir_info['time'] : null;
if (isset($redir_info['protected']) && !empty($redir_info['protected'])) {
$redir->file->protected = true;
}
}
// No manual dereferencing; leave the unknown URL as is.
return $in_url;
return $redir;
}
/**
@ -246,37 +262,24 @@ class File_redirection extends Managed_DataObject
if (!empty($short_url) && $short_url != $long_url) {
$short_url = (string)$short_url;
// store it
$file = File::getKV('url', $long_url);
if ($file instanceof File) {
$file_id = $file->getID();
} else {
try {
$file = File::getByUrl($long_url);
} catch (NoResultException $e) {
// Check if the target URL is itself a redirect...
$redir_data = File_redirection::where($long_url);
if (is_array($redir_data)) {
// 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->getID();
} else if (is_string($redir_data)) {
// The file is a known redirect target.
$file = File::getKV('url', $redir_data);
if (empty($file)) {
// @fixme should we save a new one?
// this case was triggering sometimes for redirects
// with unresolvable targets; found while fixing
// "can't linkify" bugs with shortened links to
// SSL sites with cert issues.
return null;
}
$file_id = $file->getID();
$redir = File_redirection::where($long_url);
$file = $redir->getFile();
if (empty($file->id)) {
$file->saveFile();
}
}
$file_redir = File_redirection::getKV('url', $short_url);
if (!$file_redir instanceof File_redirection) {
$file_redir = new File_redirection;
// Now we definitely have a File object in $file
try {
$file_redir = File_redirection::getByUrl($short_url);
} catch (NoResultException $e) {
$file_redir = new File_redirection();
$file_redir->urlhash = File::hashurl($short_url);
$file_redir->url = $short_url;
$file_redir->file_id = $file_id;
$file_redir->file_id = $file->getID();
$file_redir->insert();
}
return $short_url;
@ -336,7 +339,7 @@ class File_redirection extends Managed_DataObject
static function saveNew($data, $file_id, $url) {
$file_redir = new File_redirection;
$file_redir->urlhash = File::hashurl($short_url);
$file_redir->urlhash = File::hashurl($url);
$file_redir->url = $url;
$file_redir->file_id = $file_id;
$file_redir->redirections = intval($data['redirects']);
@ -385,4 +388,12 @@ class File_redirection extends Managed_DataObject
echo "DONE.\n";
echo "Resuming core schema upgrade...";
}
public function getFile() {
if(empty($this->file) && $this->file_id) {
$this->file = File::getKV('id', $this->file_id);
}
return $this->file;
}
}

View File

@ -153,6 +153,26 @@ class File_thumbnail extends Managed_DataObject
return $this->url;
}
public function getHeight()
{
return $this->height;
}
public function getWidth()
{
return $this->width;
}
public function getHtmlAttrs(array $orig=array(), $overwrite=true)
{
$attrs = [
'height' => $this->getHeight(),
'width' => $this->getWidth(),
'src' => $this->getUrl(),
];
return $overwrite ? array_merge($orig, $attrs) : array_merge($attrs, $orig);
}
public function delete($useWhere=false)
{
if (!empty($this->filename) && file_exists(File_thumbnail::path($this->filename))) {

View File

@ -76,18 +76,15 @@ class Group_join_queue extends Managed_DataObject
/**
* Abort the pending group join...
*
* @param User_group $group
*/
function abort()
{
$profile = $this->getMember();
$group = $this->getGroup();
if ($request) {
if (Event::handle('StartCancelJoinGroup', array($profile, $group))) {
$this->delete();
Event::handle('EndCancelJoinGroup', array($profile, $group));
}
if (Event::handle('StartCancelJoinGroup', array($profile, $group))) {
$this->delete();
Event::handle('EndCancelJoinGroup', array($profile, $group));
}
}

View File

@ -65,7 +65,9 @@ class Group_member extends Managed_DataObject
$member->group_id = $group_id;
$member->profile_id = $profile_id;
$member->created = common_sql_now();
$member->uri = self::newURI($profile_id, $group_id, $member->created);
$member->uri = self::newUri(Profile::getByID($profile_id),
User_group::getByID($group_id),
$member->created);
$result = $member->insert();
@ -166,7 +168,7 @@ class Group_member extends Managed_DataObject
$act = new Activity();
$act->id = $this->getURI();
$act->id = $this->getUri();
$act->actor = $member->asActivityObject();
$act->verb = ActivityVerb::JOIN;
@ -201,20 +203,8 @@ class Group_member extends Managed_DataObject
mail_notify_group_join($this->getGroup(), $this->getMember());
}
function getURI()
function getUri()
{
if (!empty($this->uri)) {
return $this->uri;
} else {
return self::newURI($this->profile_id, $this->group_id, $this->created);
}
}
static function newURI($profile_id, $group_id, $created)
{
return TagURI::mint('join:%d:%d:%s',
$profile_id,
$group_id,
common_date_iso8601($created));
return $this->uri ?: self::newUri($this->getMember(), $this->getGroup()->getProfile(), $this->created);
}
}

View File

@ -334,7 +334,7 @@ abstract class Managed_DataObject extends Memcached_DataObject
$object = new $classname();
foreach ($pkey as $col) {
if (!array_key_exists($col, $vals)) {
throw new ServerException("Missing primary key column '{$col}'");
throw new ServerException("Missing primary key column '{$col}' for ".get_called_class()." among provided keys: ".implode(',', array_keys($vals)));
} elseif (is_null($vals[$col])) {
throw new ServerException("NULL values not allowed in getByPK for column '{$col}'");
}
@ -346,6 +346,41 @@ abstract class Managed_DataObject extends Memcached_DataObject
return $object;
}
/**
* Returns an object by looking at given unique key columns.
*
* Will NOT accept NULL values for a unique key column. Ignores non-key values.
*
* @param array $vals All array keys which are set must be non-null.
*
* @return Managed_DataObject of the get_called_class() type
* @throws NoResultException if no object with that primary key
*/
static function getByKeys(array $vals)
{
$classname = get_called_class();
$object = new $classname();
$keys = $object->keys();
if (is_null($keys)) {
throw new ServerException("Failed to get key columns for class '{$classname}'");
}
foreach ($keys as $col) {
if (!array_key_exists($col, $vals)) {
continue;
} elseif (is_null($vals[$col])) {
throw new ServerException("NULL values not allowed in getByKeys for column '{$col}'");
}
$object->$col = $vals[$col];
}
if (!$object->find(true)) {
throw new NoResultException($object);
}
return $object;
}
static function getByID($id)
{
if (empty($id)) {
@ -453,4 +488,16 @@ abstract class Managed_DataObject extends Memcached_DataObject
{
// NOOP
}
static function newUri(Profile $actor, Managed_DataObject $object, $created=null)
{
if (is_null($created)) {
$created = common_sql_now();
}
return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
$actor->getID(),
ActivityUtils::resolveUri($object->getObjectType(), true),
$object->getID(),
common_date_iso8601($created));
}
}

View File

@ -90,7 +90,7 @@ class Notice extends Managed_DataObject
'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'),
'conversation' => array('type' => 'int', 'description' => 'id of root notice in this conversation'),
'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'),
'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'),
'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => null),
'verb' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'),
'scope' => array('type' => 'int',
'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'),
@ -158,43 +158,14 @@ class Notice extends Managed_DataObject
$this->_profile[$this->profile_id] = $profile;
}
public function deleteAs(Profile $actor)
public function deleteAs(Profile $actor, $delete_event=true)
{
if ($this->getProfile()->sameAs($actor) || $actor->hasRight(Right::DELETEOTHERSNOTICE)) {
return $this->delete();
}
throw new AuthorizationException('You are not allowed to delete other user\'s notices');
}
function delete($useWhere=false)
{
// For auditing purposes, save a record that the notice
// was deleted.
// @fixme we have some cases where things get re-run and so the
// insert fails.
$deleted = Deleted_notice::getKV('id', $this->id);
if (!$deleted instanceof Deleted_notice) {
$deleted = Deleted_notice::getKV('uri', $this->uri);
}
if (!$deleted instanceof Deleted_notice) {
$deleted = new Deleted_notice();
$deleted->id = $this->id;
$deleted->profile_id = $this->profile_id;
$deleted->uri = $this->uri;
$deleted->created = $this->created;
$deleted->deleted = common_sql_now();
$deleted->insert();
if (!$this->getProfile()->sameAs($actor) && !$actor->hasRight(Right::DELETEOTHERSNOTICE)) {
throw new AuthorizationException(_('You are not allowed to delete another user\'s notice.'));
}
if (Event::handle('NoticeDeleteRelated', array($this))) {
// Clear related records
$this->clearReplies();
$this->clearLocation();
$this->clearRepeats();
@ -202,10 +173,21 @@ class Notice extends Managed_DataObject
$this->clearGroupInboxes();
$this->clearFiles();
$this->clearAttentions();
// NOTE: we don't clear queue items
}
$result = null;
if (!$delete_event || Event::handle('DeleteNoticeAsProfile', array($this, $actor, &$result))) {
// If $delete_event is true, we run the event. If the Event then
// returns false it is assumed everything was handled properly
// and the notice was deleted.
$result = $this->delete();
}
return $result;
}
public function delete($useWhere=false)
{
$result = parent::delete($useWhere);
$this->blowOnDelete();
@ -275,6 +257,11 @@ class Notice extends Managed_DataObject
return $this->content;
}
public function getRendered()
{
return $this->rendered;
}
/*
* Get the original representation URL of this notice.
*
@ -298,10 +285,8 @@ class Notice extends Managed_DataObject
}
}
public function get_object_type($canonical=false) {
return $canonical
? ActivityObject::canonicalType($this->object_type)
: $this->object_type;
public function getObjectType($canonical=false) {
return ActivityUtils::resolveUri($this->object_type, $canonical);
}
public static function getByUri($uri)
@ -636,7 +621,9 @@ class Notice extends Managed_DataObject
if (!empty($rendered)) {
$notice->rendered = $rendered;
} else {
$notice->rendered = common_render_content($final, $notice);
$notice->rendered = common_render_content($final,
$notice->getProfile(),
$notice->hasParent() ? $notice->getParent() : null);
}
if (empty($verb)) {
@ -697,33 +684,33 @@ class Notice extends Managed_DataObject
}
}
// Clear the cache for subscribed users, so they'll update at next request
// XXX: someone clever could prepend instead of clearing the cache
// Only save 'attention' and metadata stuff (URLs, tags...) stuff if
// the activityverb is a POST (since stuff like repeat, favorite etc.
// reasonably handle notifications themselves.
if (ActivityUtils::compareVerbs($notice->verb, array(ActivityVerb::POST))) {
if (isset($replies)) {
$notice->saveKnownReplies($replies);
} else {
$notice->saveReplies();
}
// Save per-notice metadata...
if (isset($tags)) {
$notice->saveKnownTags($tags);
} else {
$notice->saveTags();
}
if (isset($replies)) {
$notice->saveKnownReplies($replies);
} else {
$notice->saveReplies();
}
// Note: groups may save tags, so must be run after tags are saved
// to avoid errors on duplicates.
// Note: groups should always be set.
if (isset($tags)) {
$notice->saveKnownTags($tags);
} else {
$notice->saveTags();
}
$notice->saveKnownGroups($groups);
// Note: groups may save tags, so must be run after tags are saved
// to avoid errors on duplicates.
// Note: groups should always be set.
$notice->saveKnownGroups($groups);
if (isset($urls)) {
$notice->saveKnownUrls($urls);
} else {
$notice->saveUrls();
if (isset($urls)) {
$notice->saveKnownUrls($urls);
} else {
$notice->saveUrls();
}
}
if ($distribute) {
@ -751,6 +738,7 @@ class Notice extends Managed_DataObject
}
// Get ActivityObject properties
$actobj = null;
if (!empty($act->id)) {
// implied object
$options['uri'] = $act->id;
@ -769,7 +757,7 @@ class Notice extends Managed_DataObject
$defaults = array(
'groups' => array(),
'is_local' => self::LOCAL_PUBLIC,
'is_local' => $actor->isLocal() ? self::LOCAL_PUBLIC : self::REMOTE,
'mentions' => array(),
'reply_to' => null,
'repeat_of' => null,
@ -789,12 +777,36 @@ class Notice extends Managed_DataObject
}
extract($options, EXTR_SKIP);
// dupe check
$stored = new Notice();
if (!empty($uri)) {
if (!empty($uri) && !ActivityUtils::compareVerbs($act->verb, array(ActivityVerb::DELETE))) {
$stored->uri = $uri;
if ($stored->find()) {
common_debug('cannot create duplicate Notice URI: '.$stored->uri);
throw new Exception('Notice URI already exists');
// I _assume_ saving a Notice with a colliding URI means we're really trying to
// save the same notice again...
throw new AlreadyFulfilledException('Notice URI already exists');
}
}
$autosource = common_config('public', 'autosource');
// Sandboxed are non-false, but not 1, either
if (!$actor->hasRight(Right::PUBLICNOTICE) ||
($source && $autosource && in_array($source, $autosource))) {
// FIXME: ...what about remote nonpublic? Hmmm. That is, if we sandbox remote profiles...
$stored->is_local = Notice::LOCAL_NONPUBLIC;
} else {
$stored->is_local = intval($is_local);
}
if (!$stored->isLocal()) {
// Only do these checks for non-local notices. Local notices will generate these values later.
if (!common_valid_http_url($url)) {
common_debug('Bad notice URL: ['.$url.'], URI: ['.$uri.']. Cannot link back to original! This is normal for shared notices etc.');
}
if (empty($uri)) {
throw new ServerException('No URI for remote notice. Cannot accept that.');
}
}
@ -804,18 +816,24 @@ class Notice extends Managed_DataObject
$stored->url = $url;
$stored->verb = $act->verb;
// Use the local user's shortening preferences, if applicable.
$stored->rendered = $actor->isLocal()
? $actor->shortenLinks($act->content)
: $act->content;
// Notice content. We trust local users to provide HTML we like, but of course not remote users.
// FIXME: What about local users importing feeds? Mirror functions must filter out bad HTML first...
$content = $act->content ?: $act->summary;
if (is_null($content) && !is_null($actobj)) {
$content = $actobj->content ?: $actobj->summary;
}
$stored->rendered = $actor->isLocal() ? $content : common_purify($content);
$stored->content = common_strip_html($stored->rendered);
$autosource = common_config('public', 'autosource');
// Sandboxed are non-false, but not 1, either
if (!$actor->hasRight(Right::PUBLICNOTICE) ||
($source && $autosource && in_array($source, $autosource))) {
$stored->is_local = Notice::LOCAL_NONPUBLIC;
// Reject notice if it is too long (without the HTML)
// FIXME: Reject if too short (empty) too? But we have to pass the
if ($actor->isLocal() && Notice::contentTooLong($stored->content)) {
// TRANS: Client error displayed when the parameter "status" is missing.
// TRANS: %d is the maximum number of character for a notice.
throw new ClientException(sprintf(_m('That\'s too long. Maximum notice size is %d character.',
'That\'s too long. Maximum notice size is %d characters.',
Notice::maxContent()),
Notice::maxContent()));
}
// Maybe a missing act-time should be fatal if the actor is not local?
@ -846,7 +864,6 @@ class Notice extends Managed_DataObject
// If the original is private to a group, and notice has no group specified,
// make it to the same group(s)
if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) {
$groups = array();
$replyGroups = $reply->getGroups();
foreach ($replyGroups as $group) {
if ($actor->isMember($group)) {
@ -866,7 +883,7 @@ class Notice extends Managed_DataObject
$conv = Conversation::getKV('uri', $act->context->conversation);
if ($conv instanceof Conversation) {
common_debug('Conversation stitched together from (probably) a reply activity to unknown remote user. Activity creation time ('.$stored->created.') should maybe be compared to conversation creation time ('.$conv->created.').');
$stored->conversation = $conv->id;
$stored->conversation = $conv->getID();
} else {
// Conversation URI was not found, so we must create it. But we can't create it
// until we have a Notice ID because of the database layout...
@ -901,11 +918,24 @@ class Notice extends Managed_DataObject
$urls[] = $href;
}
if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) {
if (empty($act->objects[0]->type)) {
// Default type for the post verb is 'note', but we know it's
// a 'comment' if it is in reply to something.
$stored->object_type = empty($stored->reply_to) ? ActivityObject::NOTE : ActivityObject::COMMENT;
} else {
//TODO: Is it safe to always return a relative URI? The
// JSON version of ActivityStreams always use it, so we
// should definitely be able to handle it...
$stored->object_type = ActivityUtils::resolveUri($act->objects[0]->type, true);
}
}
if (Event::handle('StartNoticeSave', array(&$stored))) {
// XXX: some of these functions write to the DB
try {
$stored->insert(); // throws exception on error
$result = $stored->insert(); // throws exception on error
if ($notloc instanceof Notice_location) {
$notloc->notice_id = $stored->getID();
@ -917,7 +947,7 @@ class Notice extends Managed_DataObject
$object = null;
Event::handle('StoreActivityObject', array($act, $stored, $options, &$object));
if (empty($object)) {
throw new ServerException('Unsuccessful call to StoreActivityObject '.$stored->uri . ': '.$act->asString());
throw new ServerException('Unsuccessful call to StoreActivityObject '.$stored->getUri() . ': '.$act->asString());
}
// If it's not part of a conversation, it's the beginning
@ -926,7 +956,7 @@ class Notice extends Managed_DataObject
// $act->context->conversation will be null if it was not provided
common_debug('Creating a new conversation for stored notice ID='.$stored->getID().' with URI: '.$act->context->conversation);
$conv = Conversation::create($stored, $act->context->conversation);
$stored->conversation = $conv->id;
$stored->conversation = $conv->getID();
}
$stored->update($orig);
@ -946,34 +976,39 @@ class Notice extends Managed_DataObject
// Save per-notice metadata...
$mentions = array();
$groups = array();
$group_ids = array();
// This event lets plugins filter out non-local recipients (attentions we don't care about)
// Used primarily for OStatus (and if we don't federate, all attentions would be local anyway)
Event::handle('GetLocalAttentions', array($actor, $act->context->attention, &$mentions, &$groups));
Event::handle('GetLocalAttentions', array($actor, $act->context->attention, &$mentions, &$group_ids));
if (!empty($mentions)) {
$stored->saveKnownReplies($mentions);
} else {
$stored->saveReplies();
}
// Only save 'attention' and metadata stuff (URLs, tags...) stuff if
// the activityverb is a POST (since stuff like repeat, favorite etc.
// reasonably handle notifications themselves.
if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) {
if (!empty($mentions)) {
$stored->saveKnownReplies($mentions);
} else {
$stored->saveReplies();
}
if (!empty($tags)) {
$stored->saveKnownTags($tags);
} else {
$stored->saveTags();
}
if (!empty($tags)) {
$stored->saveKnownTags($tags);
} else {
$stored->saveTags();
}
// Note: groups may save tags, so must be run after tags are saved
// to avoid errors on duplicates.
// Note: groups should always be set.
// Note: groups may save tags, so must be run after tags are saved
// to avoid errors on duplicates.
// Note: groups should always be set.
$stored->saveKnownGroups($groups);
$stored->saveKnownGroups($group_ids);
if (!empty($urls)) {
$stored->saveKnownUrls($urls);
} else {
$stored->saveUrls();
if (!empty($urls)) {
$stored->saveKnownUrls($urls);
} else {
$stored->saveUrls();
}
}
if ($distribute) {
@ -985,15 +1020,13 @@ class Notice extends Managed_DataObject
}
static public function figureOutScope(Profile $actor, array $groups, $scope=null) {
if (is_null($scope)) {
$scope = self::defaultScope();
}
$scope = is_null($scope) ? self::defaultScope() : intval($scope);
// For private streams
try {
$user = $actor->getUser();
// FIXME: We can't do bit comparison with == (Legacy StatusNet thing. Let's keep it for now.)
if ($user->private_stream && ($scope == Notice::PUBLIC_SCOPE || $scope == Notice::SITE_SCOPE)) {
if ($user->private_stream && ($scope === Notice::PUBLIC_SCOPE || $scope === Notice::SITE_SCOPE)) {
$scope |= Notice::FOLLOWER_SCOPE;
}
} catch (NoSuchUserException $e) {
@ -1025,8 +1058,10 @@ class Notice extends Managed_DataObject
$this->blowStream('networkpublic');
}
self::blow('notice:list-ids:conversation:%s', $this->conversation);
self::blow('conversation:notice_count:%d', $this->conversation);
if ($this->conversation) {
self::blow('notice:list-ids:conversation:%s', $this->conversation);
self::blow('conversation:notice_count:%d', $this->conversation);
}
if ($this->isRepeat()) {
// XXX: we should probably only use one of these
@ -1320,6 +1355,10 @@ class Notice extends Managed_DataObject
}
} catch (NoParentNoticeException $e) {
// Latest notice has no parent
} catch (NoResultException $e) {
// Notice was not found, so we can't go further up in the tree.
// FIXME: Maybe we should do this in a more stable way where deleted
// notices won't break conversation chains?
}
// No parent, or parent out of scope
$root = $last;
@ -1491,13 +1530,8 @@ class Notice extends Managed_DataObject
* best with generalizations on user_group to support
* remote groups better.
*/
function saveKnownGroups($group_ids)
function saveKnownGroups(array $group_ids)
{
if (!is_array($group_ids)) {
// TRANS: Server exception thrown when no array is provided to the method saveKnownGroups().
throw new ServerException(_('Bad type provided to saveKnownGroups.'));
}
$groups = array();
foreach (array_unique($group_ids) as $id) {
$group = User_group::getKV('id', $id);
@ -1573,7 +1607,7 @@ class Notice extends Managed_DataObject
return;
}
$sender = Profile::getKV($this->profile_id);
$sender = $this->getProfile();
foreach (array_unique($uris) as $uri) {
try {
@ -1588,11 +1622,9 @@ class Notice extends Managed_DataObject
continue;
}
$this->saveReply($profile->id);
self::blow('reply:stream:%d', $profile->id);
$this->saveReply($profile->getID());
self::blow('reply:stream:%d', $profile->getID());
}
return;
}
/**
@ -1607,12 +1639,6 @@ class Notice extends Managed_DataObject
function saveReplies()
{
// Don't save reply data for repeats
if ($this->isRepeat()) {
return array();
}
$sender = $this->getProfile();
$replied = array();
@ -1621,17 +1647,21 @@ class Notice extends Managed_DataObject
try {
$parent = $this->getParent();
$parentauthor = $parent->getProfile();
$this->saveReply($parentauthor->id);
$replied[$parentauthor->id] = 1;
self::blow('reply:stream:%d', $parentauthor->id);
$this->saveReply($parentauthor->getID());
$replied[$parentauthor->getID()] = 1;
self::blow('reply:stream:%d', $parentauthor->getID());
} catch (NoParentNoticeException $e) {
// Not a reply, since it has no parent!
$parent = null;
} catch (NoResultException $e) {
// Parent notice was probably deleted
$parent = null;
}
// @todo ideally this parser information would only
// be calculated once.
$mentions = common_find_mentions($this->content, $this);
$mentions = common_find_mentions($this->content, $sender, $parent);
// store replied only for first @ (what user/notice what the reply directed,
// we assume first @ is it)
@ -1720,7 +1750,6 @@ class Notice extends Managed_DataObject
function sendReplyNotifications()
{
// Don't send reply notifications for repeats
if ($this->isRepeat()) {
return array();
}
@ -1730,9 +1759,11 @@ class Notice extends Managed_DataObject
require_once INSTALLDIR.'/lib/mail.php';
foreach ($recipientIds as $recipientId) {
$user = User::getKV('id', $recipientId);
if ($user instanceof User) {
try {
$user = User::getByID($recipientId);
mail_notify_attn($user, $this);
} catch (NoResultException $e) {
// No such user
}
}
Event::handle('EndNotifyMentioned', array($this, $recipientIds));
@ -1851,6 +1882,8 @@ class Notice extends Managed_DataObject
$ctx->replyToUrl = $reply->getUrl(true); // true for fallback to local URL, less messy
} catch (NoParentNoticeException $e) {
// This is not a reply to something
} catch (NoResultException $e) {
// Parent notice was probably deleted
}
try {
@ -2035,6 +2068,7 @@ class Notice extends Managed_DataObject
if (Event::handle('StartActivityObjectFromNotice', array($this, &$object))) {
$object->type = $this->object_type ?: ActivityObject::NOTE;
$object->id = $this->getUri();
//FIXME: = $object->title ?: sprintf(... because we might get a title from StartActivityObjectFromNotice
$object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $this->getProfile()->getNickname());
$object->content = $this->rendered;
$object->link = $this->getUrl();
@ -2407,7 +2441,7 @@ class Notice extends Managed_DataObject
$this->uri = sprintf('%s%s=%d:%s=%s',
TagURI::mint(),
'noticeId', $this->id,
'objectType', $this->get_object_type(true));
'objectType', $this->getObjectType(true));
$changed = true;
}
@ -2469,8 +2503,13 @@ class Notice extends Managed_DataObject
public function isLocal()
{
return ($this->is_local == Notice::LOCAL_PUBLIC ||
$this->is_local == Notice::LOCAL_NONPUBLIC);
$is_local = intval($this->is_local);
return ($is_local === self::LOCAL_PUBLIC || $is_local === self::LOCAL_NONPUBLIC);
}
public function getScope()
{
return intval($this->scope);
}
public function isRepeat()
@ -2663,13 +2702,9 @@ class Notice extends Managed_DataObject
protected function _inScope($profile)
{
if (!is_null($this->scope)) {
$scope = $this->scope;
} else {
$scope = self::defaultScope();
}
$scope = is_null($this->scope) ? self::defaultScope() : $this->getScope();
if ($scope == 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public.
if ($scope === 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public.
return !$this->isHiddenSpam($profile);
}
@ -2754,12 +2789,35 @@ class Notice extends Managed_DataObject
return false;
}
public function hasParent()
{
try {
$this->getParent();
} catch (NoParentNoticeException $e) {
return false;
}
return true;
}
public function getParent()
{
$reply_to_id = null;
if (empty($this->reply_to)) {
throw new NoParentNoticeException($this);
}
return self::getByID($this->reply_to);
// The reply_to ID in the table Notice could exist with a number
// however, the replied to notice might not exist in the database.
// Thus we need to catch the exception and throw the NoParentNoticeException else
// the timeline will not display correctly.
try {
$reply_to_id = self::getByID($this->reply_to);
} catch(Exception $e){
throw new NoParentNoticeException($this);
}
return $reply_to_id;
}
/**

View File

@ -166,7 +166,7 @@ class Oauth_application extends Managed_DataObject
'consumer_key' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'application consumer key'),
'name' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'name of the application'),
'description' => array('type' => 'varchar', 'length' => 191, 'description' => 'description of the application'),
'icon' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'application icon'),
'icon' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'default' => '/theme/base/default-avatar-stream.png', 'description' => 'application icon'),
'source_url' => array('type' => 'varchar', 'length' => 191, 'description' => 'application homepage - used for source link'),
'organization' => array('type' => 'varchar', 'length' => 191, 'description' => 'name of the organization running the application'),
'homepage' => array('type' => 'varchar', 'length' => 191, 'description' => 'homepage for the organization'),

View File

@ -17,24 +17,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Table Definition for profile
*/
class Profile extends Managed_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'profile'; // table name
public $id; // int(4) primary_key not_null
public $nickname; // varchar(64) multiple_key not_null
public $fullname; // varchar(191) multiple_key not 255 because utf8mb4 takes more space
public $profileurl; // varchar(191) not 255 because utf8mb4 takes more space
public $homepage; // varchar(191) multiple_key not 255 because utf8mb4 takes more space
public $fullname; // text()
public $profileurl; // text()
public $homepage; // text()
public $bio; // text() multiple_key
public $location; // varchar(191) multiple_key not 255 because utf8mb4 takes more space
public $location; // text()
public $lat; // decimal(10,7)
public $lon; // decimal(10,7)
public $location_id; // int(4)
@ -49,11 +46,11 @@ class Profile extends Managed_DataObject
'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' => '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' => 'utf8mb4_general_ci'),
'fullname' => array('type' => 'text', 'description' => 'display name', 'collate' => 'utf8mb4_general_ci'),
'profileurl' => array('type' => 'text', 'description' => 'URL, cached so we dont regenerate'),
'homepage' => array('type' => 'text', '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'),
'location' => array('type' => 'text', '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'),
@ -76,9 +73,6 @@ class Profile extends Managed_DataObject
return $def;
}
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
public static function getByEmail($email)
{
@ -1242,8 +1236,9 @@ class Profile extends Managed_DataObject
{
// XXX: not really a pkey, but should work
$notice = Notice::pkeyGet(array('profile_id' => $this->id,
'repeat_of' => $notice->id));
$notice = Notice::pkeyGet(array('profile_id' => $this->getID(),
'repeat_of' => $notice->getID(),
'verb' => ActivityVerb::SHARE));
return !empty($notice);
}
@ -1438,6 +1433,11 @@ class Profile extends Managed_DataObject
$user = User::getKV('id', $this->id);
if ($user instanceof User) {
$uri = $user->getUri();
} else {
$group = User_group::getKV('profile_id', $this->id);
if ($group instanceof User_group) {
$uri = $group->getUri();
}
}
Event::handle('EndGetProfileUri', array($this, &$uri));
@ -1593,8 +1593,20 @@ class Profile extends Managed_DataObject
return $this;
}
public function sameAs(Profile $other)
/**
* Test whether the given profile is the same as the current class,
* for testing identities.
*
* @param Profile $other The other profile, usually from Action's $this->scoped
*
* @return boolean
*/
public function sameAs(Profile $other=null)
{
if (is_null($other)) {
// In case $this->scoped is null or something, i.e. not a current/legitimate profile.
return false;
}
return $this->getID() === $other->getID();
}

View File

@ -21,20 +21,10 @@
* @license GNU Affero General Public License http://www.gnu.org/licenses/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
/**
* Table Definition for profile_list
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
if (!defined('GNUSOCIAL')) { exit(1); }
class Profile_list extends Managed_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'profile_list'; // table name
public $id; // int(4) primary_key not_null
public $tagger; // int(4)
@ -48,9 +38,6 @@ class Profile_list extends Managed_DataObject
public $tagged_count; // smallint
public $subscriber_count; // smallint
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
public static function schemaDef()
{
return array(
@ -94,7 +81,7 @@ class Profile_list extends Managed_DataObject
function getTagger()
{
return Profile::getKV('id', $this->tagger);
return Profile::getByID($this->tagger);
}
/**
@ -145,7 +132,7 @@ class Profile_list extends Managed_DataObject
$url = $this->mainpage;
} else {
$url = common_local_url('showprofiletag',
array('tagger' => $this->getTagger()->nickname,
array('nickname' => $this->getTagger()->nickname,
'tag' => $this->tag));
}
}
@ -659,7 +646,7 @@ class Profile_list extends Managed_DataObject
$orig = clone($ptag);
$user = User::getKV('id', $ptag->tagger);
if(!empty($user)) {
$ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname));
$ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'nickname' => $user->getNickname()));
} else {
$ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
}

View File

@ -85,17 +85,17 @@ class Profile_prefs extends Managed_DataObject
static function getAll(Profile $profile)
{
try {
$prefs = self::listFind('profile_id', $profile->getID());
$prefs = self::listFind('profile_id', array($profile->getID()));
} catch (NoResultException $e) {
return array();
}
$list = array();
while ($entry = $prefs->fetch()) {
if (!isset($list[$entry->namespace])) {
$list[$entry->namespace] = array();
while ($prefs->fetch()) {
if (!isset($list[$prefs->namespace])) {
$list[$prefs->namespace] = array();
}
$list[$entry->namespace][$entry->topic] = $entry->data;
$list[$prefs->namespace][$prefs->topic] = $prefs->data;
}
return $list;
}

View File

@ -68,16 +68,15 @@ 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->getID() .
' for transport ' . $qi->transport);
common_log(LOG_INFO, 'claiming queue item id = ' . $qi->getID() . ' for transport ' . $qi->transport);
$orig = clone($qi);
$qi->claimed = common_sql_now();
$result = $qi->update($orig);
if ($result) {
common_log(LOG_INFO, 'claim succeeded.');
common_log(LOG_DEBUG, 'claim succeeded.');
return $qi;
} else {
common_log(LOG_INFO, 'claim failed.');
common_log(LOG_ERR, 'claim of queue item id= ' . $qi->getID() . ' for transport ' . $qi->transport . ' failed.');
}
}
$qi = null;

View File

@ -159,8 +159,8 @@ class Subscription extends Managed_DataObject
$sub->jabber = 1;
$sub->sms = 1;
$sub->created = common_sql_now();
$sub->uri = self::newURI($sub->subscriber,
$sub->subscribed,
$sub->uri = self::newUri($subscriber,
$other,
$sub->created);
$result = $sub->insert();
@ -267,18 +267,20 @@ class Subscription extends Managed_DataObject
return $sub;
}
public function getSubscriber()
{
return Profile::getByID($this->subscriber);
}
public function getSubscribed()
{
return Profile::getByID($this->subscribed);
}
function asActivity()
{
$subscriber = Profile::getKV('id', $this->subscriber);
$subscribed = Profile::getKV('id', $this->subscribed);
if (!$subscriber instanceof Profile) {
throw new NoProfileException($this->subscriber);
}
if (!$subscribed instanceof Profile) {
throw new NoProfileException($this->subscribed);
}
$subscriber = $this->getSubscriber();
$subscribed = $this->getSubscribed();
$act = new Activity();
@ -286,7 +288,7 @@ class Subscription extends Managed_DataObject
// XXX: rationalize this with the URL
$act->id = $this->getURI();
$act->id = $this->getUri();
$act->time = strtotime($this->created);
// TRANS: Activity title when subscribing to another person.
@ -431,20 +433,8 @@ class Subscription extends Managed_DataObject
return parent::update($dataObject);
}
function getURI()
public function getUri()
{
if (!empty($this->uri)) {
return $this->uri;
} else {
return self::newURI($this->subscriber, $this->subscribed, $this->created);
}
}
static function newURI($subscriber_id, $subscribed_id, $created)
{
return TagURI::mint('follow:%d:%d:%s',
$subscriber_id,
$subscribed_id,
common_date_iso8601($created));
return $this->uri ?: self::newUri($this->getSubscriber(), $this->getSubscribed(), $this->created);
}
}

View File

@ -1012,6 +1012,24 @@ class User extends Managed_DataObject
return !empty($this->password);
}
public function setPassword($password)
{
$orig = clone($this);
$this->password = common_munge_password($password, $this->getProfile());
if ($this->validate() !== true) {
// TRANS: Form validation error on page where to change password.
throw new ServerException(_('Error saving user; invalid.'));
}
if (!$this->update($orig)) {
common_log_db_error($this, 'UPDATE', __FILE__);
// TRANS: Server error displayed on page where to change password when password change
// TRANS: could not be made because of a server error.
throw new ServerException(_('Cannot save new password.'));
}
}
public function delPref($namespace, $topic)
{
return $this->getProfile()->delPref($namespace, $topic);

View File

@ -33,6 +33,12 @@ class User_group extends Managed_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
public function getObjectType()
{
return ActivityObject::GROUP;
}
public static function schemaDef()
{
return array(
@ -214,24 +220,19 @@ class User_group extends Managed_DataObject
*/
function getRequests($offset=0, $limit=null)
{
$qry =
'SELECT profile.* ' .
'FROM profile JOIN group_join_queue '.
'ON profile.id = group_join_queue.profile_id ' .
'WHERE group_join_queue.group_id = %d ' .
'ORDER BY group_join_queue.created DESC ';
if ($limit != null) {
if (common_config('db','type') == 'pgsql') {
$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
} else {
$qry .= ' LIMIT ' . $offset . ', ' . $limit;
}
}
$rq = new Group_join_queue();
$rq->group_id = $this->id;
$members = new Profile();
$members->query(sprintf($qry, $this->id));
$members->joinAdd(['id', $rq, 'profile_id']);
if ($limit != null) {
$members->limit($offset, $limit);
}
$members->find();
return $members;
}
@ -818,7 +819,7 @@ class User_group extends Managed_DataObject
function isPrivate()
{
return ($this->join_policy == self::JOIN_POLICY_MODERATE &&
$this->force_scope == 1);
intval($this->force_scope) === 1);
}
public function isLocal()

View File

@ -72,7 +72,6 @@ $classes = array('Schema_version',
'Group_block',
'Group_alias',
'Session',
'Deleted_notice',
'Config',
'Profile_role',
'Location_namespace',

View File

@ -2,5 +2,12 @@
<!-- Document licensed under Creative Commons Attribution 3.0 Unported. See -->
<!-- https://creativecommons.org/licenses/by/3.0/ for details. -->
%%site.name%% provides an API that applications can use to interact with it.
More information about this API can be found on the [StatusNet Wiki](http://status.net/wiki/API).
%%site.name%% provides APIs that applications can use to interact with it:
* [AtomPub](atompub)
* [Twitter-compatible API](twitterapi)
## API discovery
The base URLs for the APIs can be obtained using [Really Simple Discovery](https://en.wikipedia.org/wiki/Really_Simple_Discovery).

229
doc-src/atompub Normal file
View File

@ -0,0 +1,229 @@
> The Atom Publishing Protocol (AtomPub) is an application-level
> protocol for publishing and editing Web resources. The protocol is
> based on HTTP transfer of Atom-formatted representations. The Atom
> format is documented in the Atom Syndication Format.
You can find more information about AtomPub in [RFC5023](https://tools.ietf.org/html/rfc5023).
> Activity Streams is an open format specification for activity stream protocols,
> which are used to syndicate activities taken in social web applications and
> services.
You can find more information about Activity Streams at [activitystrea.ms](http://activitystrea.ms/).
## Authentication
The API supports both
[HTTP Basic](https://en.wikipedia.org/wiki/Basic_access_authentication)
and [OAuth](https://en.wikipedia.org/wiki/OAuth).
## Service document
The service document for an account is found at
`/api/statusnet/app/service/<nickname>.xml`
Each service document has one workspace ('Main') and four collections:
* **notices**: notices generated by the user
* **subscriptions**: subscriptions by the user
* **favorites**: the user's favorites
* **memberships**: the user's group memberships
Collections are identified by the `<activity:verb>` element(s) in their
`<collection>` element.
## Notices
Notice feeds, in reverse-chronological order, are at
`/api/statuses/user_timeline/<id>.atom`.
This is a partial feed; navigation links are included in the feed to scroll forward
and back.
Notices are represented as Activity Streams events with the "Post" verb and "Note" object-type:
<entry>
<activity:object-type>
http://activitystrea.ms/schema/1.0/note
</activity:object-type>
[...]
<activity:verb>
http://activitystrea.ms/schema/1.0/post
</activity:verb>
[...]
</entry>
Repeats are be represented as Activity Streams events with the "Share" verb, and with the activity object being an entry representing a Notice:
<entry>
<activity:verb>
http://activitystrea.ms/schema/1.0/share
</activity:verb>
[...]
<activity:object>
<activity:object-type>
http://activitystrea.ms/schema/1.0/activity
</activity:object-type>
[...]
<activity:verb>
http://activitystrea.ms/schema/1.0/post
</activity:verb>
[...]
<activity:object>
<activity:object-type>
http://activitystrea.ms/schema/1.0/note
</activity:object-type>
[...]
</activity:object>
[...]
</activity:object>
[...]
</entry>
Posted files will be represented by the "Post" verb and "Image, File, Video" object-type.
### Single-notice URL
Single notices are be available as an Activity Streams event at `/api/statuses/show/<notice-id>.atom`.
<entry>
<activity:object-type>
http://activitystrea.ms/schema/1.0/note
</activity:object-type>
[...]
<activity:verb>
http://activitystrea.ms/schema/1.0/post
</activity:verb>
<author>
<activity:object-type>
http://activitystrea.ms/schema/1.0/person
</activity:object-type>
[...]
</author>
</entry>
### Posting a notice
A notice can be posted by sending a POST request containing a single `<entry>`
element to the URL of the notice feed. These should have a "Post" verb, and a "Note"
object-type, but since these are the default values, Atom entries that aren't
marked up as Activity Streams objects should be fine to post.
The resulting entry will be returned, per the APP, in Activity Streams format. The
location of the notice can be read from the Content-Location HTTP header of the
result or from the rel=self URL in the resulting entry.
### Editing a notice
Notices cannot be edited. PUT requests to a notice URL will fail.
### Deleting a notice
A single notice can be deleted by posting a DELETE HTTP request to the notice's
Atom representation.
Example with cURL:
curl -u username:password -X DELETE \
http://example.org/api/statuses/show/<notice-id>.atom
## Subscriptions
The subscriptions feed, in reverse-chronological order, is at
`/api/statusnet/app/subscriptions/<id>.atom`.
This is a partial feed; it includes the navigation links necessary to scroll forward
and back.
Subscriptions are represented as Activity Streams entries with the "Follow" verb and
"Person" object-type.
### Subscription URL
A subscription has an URL at
`/api/statusnet/app/subscriptions/<subscriber id>/<subscribed id>.atom`.
### Adding a new subscription
To add a new subscription, POST an Activity Streams `<entry>` with a "Follow" verb
and "Person" object-type.
The resulting entry will be returned, per the APP, in Activity Streams format. The
location of the subscription can be read from the Content-Location HTTP header of
the result or from the rel=self URL in the resulting entry.
### Editing a subscription
Subscriptions cannot be edited. PUT requests to the subscription URL will result in
an error.
### Deleting a subscription
To delete a subscription, send a DELETE HTTP request to the Subscription URL.
## Favorites
The feed of the user's favorites, in reverse-chronological order, is at
`/api/statusnet/app/favorites/<user id>.atom`.
This is a partial feed; it includes the navigation links necessary to scroll forward
and back.
Favorites are represented as Activity Streams entries with the "Favorite" verb and
"Note" object-type.
### Favorite URL
Favorite entries have a self URL at
`/api/statusnet/app/favorites/<user id>/<notice id>.atom`.
### Favoriting a notice
To favorite a notice, POST an Activity Streams `<entry>` with the "Favorite" verb and
"Note" object-type.
The resulting favorite will be returned, per the APP, in Activity Streams format.
The location of the favorite can be read from the Content-Location HTTP header of
the result or from the rel=self URL in the resulting entry.
### Editing a favorite
Favorites cannot be edited. PUT requests to a favorite URL will fail.
### Deleting a favorite
To "unfavorite" a notice, POST a DELETE request to the URL for the favorite.
## Groups
A feed of group memberships, in reverse-chron order, is available at
`/api/statusnet/app/memberships/<user id>.atom`.
This is a partial feed; it includes the navigation links necessary to scroll forward
and back.
Memberships are represented as Activity Streams entries with the "Join" ber and
"Group" object-type.
### Membership URL
Each membership has a representation at
`/api/statusnet/app/memberships/<user id>/<group id>.atom`.
### Joining a group
To join a group, POST an activity entry with a "Join" verb and "Group" object-type to
the memberships feed.
The resulting membership will be returned, per the APP, in Activity Streams format.
The location of the membership can be read from the Content-Location HTTP header of
the result or from the rel=self URL in the resulting entry.
### Editing group membership
Group memberships cannot be edited. PUT requests to a membership feed will fail.
### Leaving a group
To leave a group, send a DELETE request to the membership URL.

430
doc-src/twitterapi Normal file
View File

@ -0,0 +1,430 @@
## Authentication
### HTTP Basic authentication
The API uses [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
Note that this means that users with only an OpenID login cannot use the API; they have to add a
password to their account using the control panel on the site.
### OAuth authentication
OAuth 1.0a authentication for API resources is also supported. Generally, StatusNet's
UI and API are similar to Twitter's for OAuth applications (if you're new to OAuth
check out [Beginners Guide to OAuth](http://hueniverse.com/oauth/)).
To use OAuth, you'll need to register your client application via the web interface
and obtain a consumer key and secret. You can find the interface for application
registration at [http://%%site.server%%/%%site.path%%settings/oauthapps](http://%%site.server%%/%%site.path%%settings/oauthapps).
## JSONP callbacks
For API methods that return [JSON](https://en.wikipedia.org/wiki/JSON), an optional
JSONP-style callback parameter is supported. If supplied, the response will be in
JSONP format with a callback of the given name. To make it easier for clients to
handle error conditions, HTTP error codes are suppressed, and the errors will be
returned in the response body when using JSONP.
## Rate limiting
There is currently no rate-limiting.
## Gotchas
Some things to remember:
* %%site.name%% supports the
[OStatus federation protocol](https://en.wikipedia.org/wiki/OStatus) (as well as
[OpenMicroBlogging](https://en.wikipedia.org/wiki/OpenMicroBlogging) for backwards
compatibility), so many notices and friends' profiles may come from other servers.
* User nicknames are unique, but they are not globally unique. Use the ID number
instead.
* Private streams are not implemented yet.
* GNU social sites can be configured as private. In that case, all API methods
require authentication, including the public timeline (see the 'config' method
below).
* If "Fancy URLs" are not enabled, urls from above need to include "index.php" at
the root. ( e.g. http://example.org/statusnet/api becomes http://www.example.org/statusnet/index.php/api )
* The `since_id` parameter does not work as documented by Twitter. Twitter says of
`since_id`: "There are limits to the number of Tweets which can be accessed
through the API. If the limit of Tweets has occured since the `since_id`, the
`since_id` will be forced to the oldest ID available." However, GNU social will
return the newest notices (or the newest back from max_id, if present)! Also, a
`since_id` <= 0 will be ignored.
## Timeline resources
### statuses/public_timeline
Returns the 20 most recent notices, including repeats if they exist, from
non-protected users.
### statuses/home_timeline
Returns the 20 most recent notices, including repeats if they exist, posted by the
authenticating user and the users they follow. This is the same timeline seen by a
user when they login to their instance. This method is identical to
statuses/friends_timeline, except that this method always includes repeats.
### statuses/friends_timeline
Alias of statuses/home_timeline
### statuses/friends_timeline/:username
Alias of statuses/home_timeline for the specified username
### statuses/mentions
Returns the 20 most recent mentions (notices containing @username) for the
authenticating user.
This method will not include repeats in the XML and JSON responses unless the
include_rts parameter is set. The RSS and Atom responses will always include repeats
as notices prefixed with RT.
### statuses/replies
Alias of statuses/mentions
### statuses/replies/:username
Alias of statuses/mentions for the specified username
### statuses/user_timeline
Returns the 20 most recent notices posted by the authenticating user. It is also
possible to request another user's timeline by using the screen\_name or user_id
parameter. The other users timeline will only be visible if they are not protected,
or if the authenticating user's follow request was accepted by the protected user.
This method will not include repeats in the XML and JSON responses unless the
include_rts parameter is set. The RSS and Atom responses will always include
repeats as notices prefixed with RT, regardless of provided parameters.
### statuses/retweeted\_to_me
Not implemented.
### statuses/retweeted\_by_me
Not implemented.
### statuses/retweets\_of_me
Not implemented.
## Status resources
### statuses/show/:id
Returns a single notice, specified by the id parameter. The notice's author will be
returned inline.
### statuses/update
Post a new notice as the authenticating user.
Additional 'media' parameter allows binary multimedia uploads (images, etc.). Format
post data as multipart/form-data when using the 'media' parameter.
### statuses/destroy/:id
Destroys the notice specified by the required ID parameter. The authenticating user
must be the author of the specified notice. Returns the destroyed notice if successful.
### statuses/retweet/:id
Repeats a notice. Returns the original notice with repeat details embedded.
## User resources
### statuses/friends
Returns the user's subscriptions (friends) as an array of profiles.
### statuses/followers
Returns the user's subscribers (followers) as an array of profiles.
### users/show
Returns extended information of a given user, specified by ID or screen name as per
the required id parameter.
## Direct message resources
### direct_messages
Returns the 20 most recent direct messages sent to the authenticating user. The XML
and JSON versions include detailed information about the sender and recipient user.
### direct_messages/sent
Returns the 20 most recent direct messages sent by the authenticating user. The XML
and JSON versions include detailed information about the sender and recipient user.
### direct_messages/new
Sends a new direct message to the specified user from the authenticating user.
Requires both the user and text parameters and must be a POST. Returns the sent
message in the requested format if successful.
### direct_messages/destroy
Not implemented.
## Friendships resources
### friendships/create
Allows the authenticating users to follow the user specified in the ID parameter.
Returns the befriended user in the requested format when successful. Returns a
string describing the failure condition when unsuccessful.
If you are already friends with the user a HTTP 403 may be returned, though for
performance reasons you may get a 200 OK message even if the friendship already
exists.
Note that users cannot subscribe to remote profiles using this API.
### friendships/destroy
Allows the authenticating users to unfollow the user specified in the ID parameter.
Returns the unfollowed user in the requested format when successful. Returns a
string describing the failure condition when unsuccessful.
Users can unsubscribe to a remote profile using this API, but it's preferred to use
numeric IDs to nicknames.
### friendships/exists
Test for the existence of friendship between two users. Will return true if user\_a
follows user_b, otherwise will return false. Authentication is required if either
user A or user B are protected. Additionally the authenticating user must be a
follower of the protected user.
### friendships/show
Returns detailed information about the relationship between two users.
## Friends and subscribers resources
### friends/ids
Returns an array of numeric IDs for every user the specified user is subscribed to.
This method is powerful when used in conjunction with users/lookup.
### followers/ids
Returns an array of numeric IDs for every user subscsribed to the specified user.
This method is powerful when used in conjunction with users/lookup.
## Account resources
### account/verify_credentials
Returns an HTTP 200 OK response code and a representation of the requesting user if
authentication was successful; returns a 401 status code and an error message if
not. Use this method to test if supplied user credentials are valid.
### account/end_session
Not implemented.
### account/update\_delivery_device
Not implemented.
### account/rate\_limit_status
Returns the remaining number of API requests available to the requesting user before
the API limit is reached.
We have no rate limit, so this always returns 150 hits left.
### account/update\_profile\_background_image
Updates the authenticating user's profile background image. This method can also be
used to enable or disable the profile background image.
### account/update\_profile_image
Updates the authenticating user's profile image. Note that this method expects raw
multipart data, not a URL to an image.
## Favorite resources
### favorites
Returns the 20 most recent favorite statuses for the authenticating or specified
user in the requested format.
### favorites/create/:id
Favorites the status specified in the ID parameter as the authenticating user.
Returns the favorite status when successful.
### favorites/destroy/:id
Un-favorites the status specified in the ID parameter as the authenticating user.
Returns the un-favorited status in the requested format when successful.
## Notification resources
### notifications/follow
Not implemented.
### notifications/leave
Not implemented.
## Block resources
### blocks/create
Blocks the specified user from following the authenticating user. In addition the
blocked user will not show in the authenticating users mentions or timeline (unless
retweeted by another user). If a follow or friend relationship exists it is
destroyed.
### blocks/destroy
Un-blocks the user specified in the ID parameter for the authenticating user.
Returns the un-blocked user in the requested format when successful. If
relationships existed before the block was instated, they will not be restored.
### blocks/exists
Not implemented.
### blocks/blocking
Not implemented.
## Help resources
### help/test
Returns the string "ok" in the requested format with a 200 OK HTTP status code. This
method is great for sending a HEAD request to determine our servers current time.
## OAuth resources
It is strongly recommended you use HTTPS for all OAuth authorization steps.
### oauth/request_token
Allows a Consumer application to obtain an OAuth Request Token to request user
authorization. This method fulfills Section 6.1 of the OAuth 1.0 authentication
flow. It is strongly recommended you use HTTPS for all OAuth authorization steps.
### oauth/authorize
Allows a Consumer application to use an OAuth Request Token to request user
authorization. This method fulfills Section 6.2 of the OAuth 1.0 authentication
flow. Desktop applications must use this method (and cannot use GET oauth/authenticate).
### oauth/access_token
Allows a Consumer application to exchange the OAuth Request Token for an OAuth
Access Token. This method fulfills Section 6.3 of the OAuth 1.0 authentication flow.
The OAuth access token may also be used for xAuth operations.
## Search
The search method supports the following optional URL parameters:
* **callback**: if supplied when using the JSON format, the response will use the
JSONP format with a callback of the given name.
* **rpp**: the number of notices to return per page, up to a max of 100.
* **page**: the page number (starting at 1) to return.
* **since_id:**: returns notices with ids greater than the given id.
Note:
* The search does not support operators, such as "from:", "to:" and booleans.
* Notice content is HTML-encoded.
### search
Returns relevant notices that match a specified query.
### Atom
To request search results in Atom, append your URL-encoded query as a parameter to
the search method and specify the Atom format:
`%%site.server%%/%%site.path%%api/search.atom?q=<query>`
### JSON
To request search results in JSON, append your URL-encoded query as a parameter to
the search method and specify the JSON format:
`%%site.server%%/%%site.path%%api/search.json?q=<query>`
## Additional resources
These are extensions to the Twitter API that expose additional functionality.
### Group resources
#### statusnet/groups/timeline
Shows a group's timeline. Similar to other timeline resources.
#### statusnet/groups/show
Show a groups profile.
#### statusnet/groups/create
Create a new group.
#### statusnet/groups/join
Join a group.
#### statusnet/groups/leave
Leave a group.
#### statusnet/groups/list
Show the groups a given user is a member of.
#### statusnet/groups/list_all
List all local groups.
#### statusnet/groups/membership
List the members of a given group.
#### statusnet/groups/is_member
Determine whether a given user is a member of a given group.
### Tag resources
#### statusnet/tags/timeline
Shows a tag's timeline. Similar to other timeline resources.
### Media resources
#### statusnet/media/upload
Endpoint for uploading an image. Returns a URL that can be used in a status update.
Format post data as multipart/form-data.
### Configuration
#### statusnet/config
Show an instance's configuration information.
Of special note is the `<private>` element (config/site/private), which indicates
whether a site is private. When a site is configured as private every other API
method requires authentication, including the public timeline (`/api/statuses/public_timeline.format`).

View File

@ -771,7 +771,7 @@ var SN = { // StatusNet
form
.addClass('dialogbox')
.append('<button class="close">&#215;</button>')
.append('<button class="close" title="' + SN.msg('popup_close_button') + '">&#215;</button>')
.closest('.notice-options')
.addClass('opaque');

View File

@ -460,6 +460,7 @@ class Action extends HTMLOutputter // lawsuit
// TRANS: Localized tooltip for '...' expansion button on overlong remote messages.
$messages['showmore_tooltip'] = _m('TOOLTIP', 'Show more');
$messages['popup_close_button'] = _m('TOOLTIP', 'Close popup');
$messages = array_merge($messages, $this->getScriptMessages());
@ -1489,7 +1490,7 @@ class Action extends HTMLOutputter // lawsuit
}
$this->initDocument('json');
$error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
$this->text(json_encode($error_array));
print(json_encode($error_array));
$this->endDocument('json');
break;
case 'text':

View File

@ -579,8 +579,8 @@ class Activity
if (!empty($this->link)) {
$xs->element('link', array('rel' => 'alternate',
'type' => 'text/html'),
$this->link);
'type' => 'text/html',
'href' => $this->link));
}
}

View File

@ -233,6 +233,9 @@ abstract class ActivityHandlerPlugin extends Plugin
protected function notifyMentioned(Notice $stored, array &$mentioned_ids)
{
// pass through silently by default
// If we want to stop any other plugin from notifying based on this activity, return false instead.
return true;
}
/**
@ -281,10 +284,14 @@ abstract class ActivityHandlerPlugin extends Plugin
*
* @return boolean hook value
*/
function onNoticeDeleteRelated(Notice $notice)
public function onNoticeDeleteRelated(Notice $notice)
{
if ($this->isMyNotice($notice)) {
$this->deleteRelated($notice);
try {
$this->deleteRelated($notice);
} catch (AlreadyFulfilledException $e) {
// Nothing to see here, it's obviously already gone...
}
}
// Always continue this event in our activity handling plugins.
@ -301,10 +308,7 @@ abstract class ActivityHandlerPlugin extends Plugin
return true;
}
$this->notifyMentioned($stored, $mentioned_ids);
// If it was _our_ notice, only we should do anything with the mentions.
return false;
return $this->notifyMentioned($stored, $mentioned_ids);
}
/**
@ -436,14 +440,13 @@ abstract class ActivityHandlerPlugin extends Plugin
/**
* Handle object posted via AtomPub
*
* @param Activity &$activity Activity that was posted
* @param Activity $activity Activity that was posted
* @param Profile $scoped Profile of user posting
* @param Notice &$notice Resulting notice
*
* @return boolean hook value
*/
// FIXME: Make sure we can really do strong Notice typing with a $notice===null without having =null here
public function onStartAtomPubNewActivity(Activity &$activity, Profile $scoped, Notice &$notice)
public function onStartAtomPubNewActivity(Activity $activity, Profile $scoped, Notice &$notice=null)
{
if (!$this->isMyActivity($activity)) {
return true;
@ -453,8 +456,6 @@ abstract class ActivityHandlerPlugin extends Plugin
$notice = $this->saveNoticeFromActivity($activity, $scoped, $options);
Event::handle('EndAtomPubNewActivity', array($activity, $scoped, $notice));
return false;
}
@ -584,7 +585,8 @@ abstract class ActivityHandlerPlugin extends Plugin
try {
$this->showNoticeListItem($nli);
} catch (Exception $e) {
$nli->out->element('p', 'error', 'Error showing notice: '.htmlspecialchars($e->getMessage()));
common_log(LOG_ERR, 'Error showing notice: ' . $e->getMessage());
$nli->out->element('p', 'error', sprintf(_('Error showing notice: %s'), $e->getMessage()));
}
Event::handle('EndShowNoticeItem', array($nli));
@ -593,17 +595,9 @@ abstract class ActivityHandlerPlugin extends Plugin
protected function showNoticeListItem(NoticeListItem $nli)
{
$nli->showNotice();
$nli->showNoticeAttachments();
$nli->showNoticeInfo();
$nli->showNoticeOptions();
$nli->showNoticeLink();
$nli->showNoticeSource();
$nli->showNoticeLocation();
$nli->showPermalink();
$nli->showNoticeOptions();
$nli->showNoticeHeaders();
$nli->showContent();
$nli->showNoticeFooter();
}
public function onStartShowNoticeItemNotice(NoticeListItem $nli)
@ -632,7 +626,11 @@ abstract class ActivityHandlerPlugin extends Plugin
return true;
}
$this->showNoticeContent($stored, $out, $scoped);
try {
$this->showNoticeContent($stored, $out, $scoped);
} catch (Exception $e) {
$out->element('div', 'error', $e->getMessage());
}
return false;
}

View File

@ -114,7 +114,7 @@ class ActivityMover extends QueueHandler
$sink->postActivity($act);
$notice = Notice::getKV('uri', $act->objects[0]->id);
if (!empty($notice)) {
$notice->delete();
$notice->deleteAs($user->getProfile(), false);
}
break;
case ActivityVerb::JOIN:

View File

@ -298,7 +298,7 @@ class ActivityObject
if (!empty($guidEl)) {
$this->id = $guidEl->textContent;
if ($guidEl->hasAttribute('isPermaLink')) {
if ($guidEl->hasAttribute('isPermaLink') && $guidEl->getAttribute('isPermaLink') != 'false') {
// overwrites <link>
$this->link = $this->id;
}

View File

@ -274,11 +274,11 @@ class ApiAction extends Action
$sub = Subscription::getSubscription($this->scoped, $profile);
// Notifications on?
$twitter_user['following'] = true;
$twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
$twitter_user['notifications'] = ($sub->jabber || $sub->sms);
} catch (NoResultException $e) {
// well, the values are already false...
}
$twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
}
if ($get_notice) {
@ -330,6 +330,9 @@ class ApiAction extends Action
$in_reply_to = $parent->id;
} catch (NoParentNoticeException $e) {
$in_reply_to = null;
} catch (NoResultException $e) {
// the in_reply_to message has probably been deleted
$in_reply_to = null;
}
$twitter_status['in_reply_to_status_id'] = $in_reply_to;
@ -648,6 +651,11 @@ class ApiAction extends Action
break;
default:
if (strncmp($element, 'statusnet_', 10) == 0) {
if ($element === 'statusnet_in_groups' && is_array($value)) {
// QVITTERFIX because it would cause an array to be sent as $value
// THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
$value = json_encode($value);
}
$this->element('statusnet:'.substr($element, 10), null, $value);
} else {
$this->element($element, null, $value);

View File

@ -295,7 +295,7 @@ class ApiAuthAction extends ApiAction
// TRANS: Client error thrown when authentication fails because a user clicked "Cancel".
$this->clientError(_('Could not authenticate you.'), 401);
} elseif ($required) {
} else {
// $this->auth_user_nickname - i.e. PHP_AUTH_USER - will have a value since it was not empty
$user = common_check_user($this->auth_user_nickname,
@ -314,10 +314,10 @@ class ApiAuthAction extends ApiAction
$this->auth_user = null;
}
// By default, basic auth users have rw access
$this->access = self::READ_WRITE;
if (!$this->auth_user instanceof User) {
if ($required && $this->auth_user instanceof User) {
// By default, basic auth users have rw access
$this->access = self::READ_WRITE;
} elseif ($required) {
$msg = sprintf(
"basic auth nickname = %s",
$this->auth_user_nickname
@ -328,10 +328,10 @@ class ApiAuthAction extends ApiAction
header('WWW-Authenticate: Basic realm="' . $realm . '"');
// TRANS: Client error thrown when authentication fails.
$this->clientError(_('Could not authenticate you.'), 401);
} else {
// all get rw access for actions that don't require auth
$this->access = self::READ_WRITE;
}
} else {
// all get rw access for actions that don't require auth
$this->access = self::READ_WRITE;
}
}

View File

@ -58,7 +58,7 @@ class AttachmentList extends Widget
*
* @param Notice $notice stream of notices from DB_DataObject
*/
function __construct($notice, $out=null)
function __construct(Notice $notice, $out=null)
{
parent::__construct($out);
$this->notice = $notice;
@ -75,7 +75,6 @@ class AttachmentList extends Widget
function show()
{
$attachments = $this->notice->attachments();
$representable = false;
foreach ($attachments as $key=>$att) {
// Only show attachments representable with a title
if ($att->getTitle() === null) {

View File

@ -120,7 +120,7 @@ class AttachmentListItem extends Widget
try {
// Tell getThumbnail that we can show an animated image if it has one (4th arg, "force_still")
$thumb = $this->attachment->getThumbnail(null, null, false, false);
$this->out->element('img', array('class'=>'u-photo', 'src' => $thumb->getUrl(), 'alt' => ''));
$this->out->element('img', $thumb->getHtmlAttrs(['class'=>'u-photo', 'alt' => '']));
} catch (UseFileAsThumbnailException $e) {
$this->out->element('img', array('class'=>'u-photo', 'src' => $e->file->getUrl(), 'alt' => $e->file->title));
} catch (UnsupportedMediaException $e) {

View File

@ -140,11 +140,6 @@ $default =
'path' => $_path . '/avatar/',
'ssl' => null,
'maxsize' => 300),
'background' =>
array('server' => null,
'dir' => INSTALLDIR . '/background/',
'path' => $_path . '/background/',
'ssl' => null),
'public' =>
array('localonly' => false,
'blacklist' => array(),
@ -276,10 +271,6 @@ $default =
'maxpeople' => 500, // maximum no. of people with the same tag by the same user
'allow_tagging' => array('all' => true), // equivalent to array('local' => true, 'remote' => true)
'desclimit' => null),
'oembed' =>
array('endpoint' => null, // 'https://noembed.com/embed/' for proxied oEmbed data
'order' => array('built-in', 'well-known', 'service', 'discovery'),
),
'search' =>
array('type' => 'like'),
'sessions' =>
@ -305,12 +296,13 @@ $default =
'plugins' =>
array('core' => array(
'ActivityVerb' => array(),
'ActivityVerbPost' => array(),
'ActivityModeration' => array(),
'AuthCrypt' => array(),
'Cronish' => array(),
'Favorite' => array(),
'Share' => array(),
'LRDD' => array(),
'StrictTransportSecurity' => array(),
),
'default' => array(
'Activity' => array(),

View File

@ -68,11 +68,7 @@ class DoFollowListItem extends NoticeListItem
// FIXME: URL, image, video, audio
$this->out->elementStart('article', array('class' => 'e-content'));
if (!empty($this->notice->rendered)) {
$html = $this->notice->rendered;
} else {
$html = common_render_content($this->notice->content, $this->notice);
}
$html = $this->notice->getRendered();
if (common_config('nofollow', 'external') == 'sometimes') {
// remove the nofollow part

View File

@ -190,9 +190,9 @@ class Form extends Widget
return 'form';
}
function li()
function li($class=null)
{
$this->out->elementStart('li');
$this->out->elementStart('li', $class);
}
function unli()

View File

@ -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', 'alpha2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('GNUSOCIAL_LIFECYCLE', 'beta2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE);
@ -38,6 +38,7 @@ define('PROFILES_PER_PAGE', 20);
define('MESSAGES_PER_PAGE', 20);
define('GROUPS_PER_PAGE', 20);
define('APPS_PER_PAGE', 20);
define('PEOPLETAGS_PER_PAGE', 20);
define('GROUPS_PER_MINILIST', 8);
define('PROFILES_PER_MINILIST', 8);

View File

@ -383,15 +383,11 @@ class GNUsocial
if (isset($conffile)) {
$config_files = array($conffile);
} else {
$config_files = array('/etc/statusnet/statusnet.php',
'/etc/statusnet/laconica.php',
'/etc/laconica/laconica.php',
'/etc/statusnet/'.$_server.'.php',
'/etc/laconica/'.$_server.'.php');
$config_files = array('/etc/gnusocial/config.php',
'/etc/gnusocial/config.d/'.$_server.'.php');
if (strlen($_path) > 0) {
$config_files[] = '/etc/statusnet/'.$_server.'_'.$_path.'.php';
$config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php';
$config_files[] = '/etc/gnusocial/config.d/'.$_server.'_'.$_path.'.php';
}
$config_files[] = INSTALLDIR.'/config.php';

View File

@ -72,9 +72,9 @@ class GroupsNav extends MoreMenu
while ($this->groups instanceof User_group && $this->groups->fetch()) {
$items[] = array('placeholder',
array('nickname' => $this->groups->nickname,
array('nickname' => $this->groups->getNickname(),
'mainpage' => $this->groups->homeUrl()),
$this->groups->getBestName(),
$this->groups->getNickname(),
$this->groups->getBestName()
);
}

View File

@ -177,8 +177,17 @@ class HTTPClient extends HTTP_Request2
/**
* Quick static function to GET a URL
*/
public static function quickGet($url, $accept=null)
public static function quickGet($url, $accept=null, $params=array())
{
if (!empty($params)) {
$params = http_build_query($params, null, '&');
if (strpos($url, '?') === false) {
$url .= '?' . $params;
} else {
$url .= '&' . $params;
}
}
$client = new HTTPClient();
if (!is_null($accept)) {
$client->setHeader('Accept', $accept);
@ -191,6 +200,16 @@ class HTTPClient extends HTTP_Request2
return $response->getBody();
}
public static function quickGetJson($url, $params=array())
{
$data = json_decode(self::quickGet($url, null, $params));
if (is_null($data)) {
common_debug('Could not decode JSON data from URL: '.$url);
throw new ServerException('Could not decode JSON data from URL');
}
return $data;
}
/**
* Convenience function to run a GET request.
*

View File

@ -125,7 +125,7 @@ class ImageFile
$imgPath = null;
$media = common_get_mime_media($file->mimetype);
if (Event::handle('CreateFileImageThumbnailSource', array($file, &$imgPath, $media))) {
if (empty($file->filename)) {
if (empty($file->filename) && !file_exists($imgPath)) {
throw new UnsupportedMediaException(_('File without filename could not get a thumbnail source.'));
}

View File

@ -364,13 +364,16 @@ abstract class ImPlugin extends Plugin
protected function formatNotice(Notice $notice)
{
$profile = $notice->getProfile();
$nicknames = $profile->getNickname();
try {
$parent = $notice->getParent();
$orig_profile = $parent->getProfile();
$nicknames = sprintf('%1$s => %2$s', $profile->nickname, $orig_profile->nickname);
$nicknames = sprintf('%1$s => %2$s', $profile->getNickname(), $orig_profile->getNickname());
} catch (NoParentNoticeException $e) {
$nicknames = $profile->nickname;
// Not a reply, no parent notice stored
} catch (NoResultException $e) {
// Parent notice was probably deleted
}
return sprintf('%1$s: %2$s [%3$u]', $nicknames, $notice->content, $notice->id);

View File

@ -38,8 +38,7 @@ class ImQueueHandler extends QueueHandler
function handle($notice)
{
$this->plugin->broadcastNotice($notice);
if ($notice->is_local == Notice::LOCAL_PUBLIC ||
$notice->is_local == Notice::LOCAL_NONPUBLIC) {
if ($notice->isLocal()) {
$this->plugin->publicNotice($notice);
}
return true;

View File

@ -27,9 +27,7 @@
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
if (!defined('GNUSOCIAL')) { exit(1); }
class InlineAttachmentList extends AttachmentList
{
@ -51,35 +49,3 @@ class InlineAttachmentList extends AttachmentList
return new InlineAttachmentListItem($attachment, $this->out);
}
}
class InlineAttachmentListItem extends AttachmentListItem
{
function showLink() {
$this->out->element('a', $this->linkAttr(), $this->title());
$this->showRepresentation();
}
/**
* start a single notice.
*
* @return void
*/
function showStart()
{
// XXX: RDFa
// TODO: add notice_type class e.g., notice_video, notice_image
$this->out->elementStart('li', array('class' => 'inline-attachment'));
}
/**
* finish the notice
*
* Close the last elements in the notice list item
*
* @return void
*/
function showEnd()
{
$this->out->elementEnd('li');
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* widget for displaying notice attachments thumbnails
*
* 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 UI
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('GNUSOCIAL')) { exit(1); }
class InlineAttachmentListItem extends AttachmentListItem
{
function showLink() {
$this->out->element('a', $this->linkAttr(), $this->title());
$this->showRepresentation();
}
/**
* start a single notice.
*
* @return void
*/
function showStart()
{
// XXX: RDFa
// TODO: add notice_type class e.g., notice_video, notice_image
$this->out->elementStart('li', array('class' => 'inline-attachment'));
}
/**
* finish the notice
*
* Close the last elements in the notice list item
*
* @return void
*/
function showEnd()
{
$this->out->elementEnd('li');
}
}

View File

@ -96,8 +96,8 @@ abstract class Installer
}
}
if (version_compare(PHP_VERSION, '5.3.2', '<')) {
$this->warning('Require PHP version 5.3.2 or greater.');
if (version_compare(PHP_VERSION, '5.5.0', '<')) {
$this->warning('Require PHP version 5.5.0 or greater.');
$pass = false;
}
@ -133,10 +133,16 @@ abstract class Installer
}
// Check the subdirs used for file uploads
$fileSubdirs = array('avatar', 'background', 'file');
$fileSubdirs = array('avatar', 'file');
foreach ($fileSubdirs as $fileSubdir) {
$fileFullPath = INSTALLDIR."/$fileSubdir/";
if (!is_writable($fileFullPath)) {
$fileFullPath = INSTALLDIR."/$fileSubdir";
if (!file_exists($fileFullPath)) {
$pass = $pass && mkdir($fileFullPath);
} elseif (!is_dir($fileFullPath)) {
$this->warning(sprintf('GNU social expected a directory but found something else on this path: %s', $fileFullPath),
'Either make sure it goes to a directory or remove it and a directory will be created.');
$pass = false;
} elseif (!is_writable($fileFullPath)) {
$this->warning(sprintf('Cannot write to %s directory: <code>%s</code>', $fileSubdir, $fileFullPath),
sprintf('On your server, try this command: <code>chmod a+w %s</code>', $fileFullPath));
$pass = false;

View File

@ -336,7 +336,8 @@ function get_all_languages() {
'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
'eo' => array('q' => 0.8, 'lang' => 'eo', 'name' => 'Esperanto', 'direction' => 'ltr'),
'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'),
'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'),
'fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'),
'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French (France)', 'direction' => 'ltr'),
'fur' => array('q' => 0.8, 'lang' => 'fur', 'name' => 'Friulian', 'direction' => 'ltr'),
'gl' => array('q' => 0.8, 'lang' => 'gl', 'name' => 'Galician', 'direction' => 'ltr'),
'ka' => array('q' => 0.8, 'lang' => 'ka', 'name' => 'Georgian', 'direction' => 'ltr'),

View File

@ -66,7 +66,7 @@ class ListsNav extends MoreMenu
while ($this->lists->fetch()) {
$mode = $this->lists->private ? 'private' : 'public';
$items[] = array('showprofiletag',
array('tagger' => $this->profile->nickname,
array('nickname' => $this->profile->getNickname(),
'tag' => $this->lists->tag),
$this->lists->tag,
'');

View File

@ -56,6 +56,23 @@ class Location
var $names = array();
/**
* Constructor that makes a Location from Notice::locationOptions(...)
*
* @param array $options an array for example provided by Notice::locationOptions(...)
*
* @return Location Location with the given options (lat, lon, id, name)
*/
static function fromOptions(array $options) {
$location = new Location();
foreach (['lat', 'lon', 'location_id', 'location_ns'] as $opt) {
if (isset($options[$opt])) {
$location->$opt = $options[$opt];
}
}
return $location;
}
/**
* Constructor that makes a Location from a string name
*

View File

@ -30,9 +30,7 @@
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
if (!defined('GNUSOCIAL')) { exit(1); }
require_once 'Mail.php';
@ -54,7 +52,7 @@ function mail_backend()
$backend = $mail->factory(common_config('mail', 'backend'),
common_config('mail', 'params') ?: array());
if ($_PEAR->isError($backend)) {
common_server_error($backend->getMessage(), 500);
throw new ServerException($backend->getMessage());
}
}
return $backend;
@ -71,6 +69,8 @@ function mail_backend()
*/
function mail_send($recipients, $headers, $body)
{
global $_PEAR;
try {
// XXX: use Mail_Queue... maybe
$backend = mail_backend();
@ -81,6 +81,9 @@ function mail_send($recipients, $headers, $body)
assert($backend); // throws an error if it's bad
$sent = $backend->send($recipients, $headers, $body);
if ($_PEAR->isError($sent)) {
throw new ServerException($sent->getMessage());
}
return true;
} catch (PEAR_Exception $e) {
common_log(

View File

@ -74,6 +74,11 @@ class MediaFile
return $this->short_fileurl;
}
function getEnclosure()
{
return $this->getFile()->getEnclosure();
}
function delete()
{
$filepath = File::path($this->filename);

View File

@ -64,6 +64,7 @@ class NoticeListItem extends Widget
protected $options = true;
protected $maxchars = 0; // if <= 0 it means use full posts
protected $item_tag = 'li';
protected $pa = null;
/**
* constructor
@ -150,7 +151,19 @@ class NoticeListItem extends Widget
$this->elementStart('section', array('class'=>'notice-headers'));
$this->showNoticeTitle();
$this->showAuthor();
if ($this->addressees) { $this->showAddressees(); }
if (!empty($this->notice->reply_to) || count($this->getProfileAddressees()) > 0) {
$this->elementStart('div', array('class' => 'parents'));
try {
$this->showParent();
} catch (NoParentNoticeException $e) {
// no parent notice
} catch (InvalidUrlException $e) {
// parent had an invalid URL so we can't show it
}
if ($this->addressees) { $this->showAddressees(); }
$this->elementEnd('div');
}
$this->elementEnd('section');
}
@ -166,7 +179,7 @@ class NoticeListItem extends Widget
function showNoticeTitle()
{
if (Event::handle('StartShowNoticeTitle', array($this))) {
$this->element('a', array('href' => $this->notice->getUrl(),
$this->element('a', array('href' => $this->notice->getUrl(true),
'class' => 'notice-title'),
$this->notice->getTitle());
Event::handle('EndShowNoticeTitle', array($this));
@ -235,8 +248,9 @@ class NoticeListItem extends Widget
function showAuthor()
{
$attrs = array('href' => $this->profile->profileurl,
'class' => 'h-card p-author',
'class' => 'h-card',
'title' => $this->profile->getNickname());
if(empty($this->repeat)) { $attrs['class'] .= ' p-author'; }
if (Event::handle('StartShowNoticeItemAuthor', array($this->profile, $this->out, &$attrs))) {
$this->out->elementStart('a', $attrs);
@ -247,6 +261,19 @@ class NoticeListItem extends Widget
}
}
function showParent()
{
$this->out->element(
'a',
array(
'href' => $this->notice->getParent()->getUrl(),
'class' => 'u-in-reply-to',
'rel' => 'in-reply-to'
),
'in reply to'
);
}
function showAddressees()
{
$pa = $this->getProfileAddressees();
@ -267,19 +294,20 @@ class NoticeListItem extends Widget
function getProfileAddressees()
{
$pa = array();
if($this->pa) { return $this->pa; }
$this->pa = array();
$attentions = $this->getReplyProfiles();
foreach ($attentions as $attn) {
$class = $attn->isGroup() ? 'group' : 'account';
$pa[] = array('href' => $attn->profileurl,
'title' => $attn->getNickname(),
'class' => "addressee {$class}",
'text' => $attn->getStreamName());
$this->pa[] = array('href' => $attn->profileurl,
'title' => $attn->getNickname(),
'class' => "addressee {$class}",
'text' => $attn->getStreamName());
}
return $pa;
return $this->pa;
}
function getReplyProfiles()
@ -317,13 +345,8 @@ class NoticeListItem extends Widget
if (Event::handle('StartShowNoticeContent', array($this->notice, $this->out, $this->out->getScoped()))) {
if ($this->maxchars > 0 && mb_strlen($this->notice->content) > $this->maxchars) {
$this->out->text(mb_substr($this->notice->content, 0, $this->maxchars) . '[…]');
} elseif ($this->notice->rendered) {
$this->out->raw($this->notice->rendered);
} else {
// XXX: may be some uncooked notices in the DB,
// we cook them right now. This should probably disappear in future
// versions (>> 0.4.x)
$this->out->raw(common_render_content($this->notice->content, $this->notice));
$this->out->raw($this->notice->getRendered());
}
Event::handle('EndShowNoticeContent', array($this->notice, $this->out, $this->out->getScoped()));
}
@ -351,7 +374,6 @@ class NoticeListItem extends Widget
'href' => Conversation::getUrlFromNotice($this->notice)));
$this->out->element('time', array('class' => 'dt-published',
'datetime' => common_date_iso8601($this->notice->created),
// TRANS: Timestamp title (tooltip text) for NoticeListItem
'title' => common_exact_date($this->notice->created)),
common_date_string($this->notice->created));
$this->out->elementEnd('a');
@ -516,9 +538,22 @@ class NoticeListItem extends Widget
if (!$this->notice->isLocal()) {
$class .= ' external';
}
try {
if($this->repeat) {
$this->out->element('a',
array('href' => $this->repeat->getUrl(),
'class' => 'u-url'),
'');
$class = str_replace('u-url', 'u-repost-of', $class);
}
} catch (InvalidUrlException $e) {
// no permalink available
}
try {
$this->out->element('a',
array('href' => $this->notice->getUrl(),
array('href' => $this->notice->getUrl(true),
'class' => $class),
// TRANS: Addition in notice list item for single-notice view.
_('permalink'));

View File

@ -9,6 +9,9 @@ abstract class NoticestreamAction extends ProfileAction
protected function prepare(array $args=array()) {
parent::prepare($args);
// In case we need more info than ProfileAction->doPreparation() gives us
$this->doStreamPreparation();
// fetch the actual stream stuff
$stream = $this->getStream();
$this->notice = $stream->getNotices(($this->page-1) * NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
@ -21,6 +24,11 @@ abstract class NoticestreamAction extends ProfileAction
return true;
}
protected function doStreamPreparation()
{
// pass by default
}
// this fetches the NoticeStream
abstract public function getStream();
}

20
lib/peopletag.php Normal file
View File

@ -0,0 +1,20 @@
<?php
if (!defined('GNUSOCIAL')) { exit(1); }
class Peopletag extends PeopletagListItem
{
protected $avatarSize = AVATAR_PROFILE_SIZE;
function showStart()
{
$mode = $this->peopletag->private ? 'private' : 'public';
$this->out->elementStart('div', array('class' => 'h-entry peopletag peopletag-profile mode-'.$mode,
'id' => 'peopletag-' . $this->peopletag->id));
}
function showEnd()
{
$this->out->elementEnd('div');
}
}

View File

@ -76,7 +76,7 @@ class PeopletagGroupNav extends Widget
{
$user = null;
// FIXME: we should probably pass this in
// FIXME: we should probably pass this in and check when PeopletagGroupNav is actually loaded etc.
$action = $this->action->trimmed('action');
@ -107,7 +107,7 @@ class PeopletagGroupNav extends Widget
if (Event::handle('StartPeopletagGroupNav', array($this))) {
// People tag timeline
$this->out->menuItem(common_local_url('showprofiletag', array('tagger' => $user_profile->nickname,
$this->out->menuItem(common_local_url('showprofiletag', array('nickname' => $user_profile->nickname,
'tag' => $tag->tag)),
// TRANS: Menu item in list navigation panel.
_m('MENU','List'),

View File

@ -27,13 +27,7 @@
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
require_once INSTALLDIR.'/lib/widget.php';
define('PEOPLETAGS_PER_PAGE', 20);
if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Widget to show a list of peopletags
@ -88,201 +82,3 @@ class PeopletagList extends Widget
$ptag->show();
}
}
class PeopletagListItem extends Widget
{
var $peopletag = null;
var $current = null;
var $profile = null;
/**
* constructor
*
* Also initializes the owner attribute.
*
* @param Notice $notice The notice we'll display
*/
function __construct($peopletag, $current, $out=null)
{
parent::__construct($out);
$this->peopletag = $peopletag;
$this->current = $current;
$this->profile = Profile::getKV('id', $this->peopletag->tagger);
}
/**
* recipe function for displaying a single peopletag.
*
* This uses all the other methods to correctly display a notice. Override
* it or one of the others to fine-tune the output.
*
* @return void
*/
function url()
{
return $this->peopletag->homeUrl();
}
function show()
{
if (empty($this->peopletag)) {
common_log(LOG_WARNING, "Trying to show missing peopletag; skipping.");
return;
}
if (Event::handle('StartShowPeopletagItem', array($this))) {
$this->showStart();
$this->showPeopletag();
$this->showStats();
$this->showEnd();
Event::handle('EndShowPeopletagItem', array($this));
}
}
function showStart()
{
$mode = ($this->peopletag->private) ? 'private' : 'public';
$this->out->elementStart('li', array('class' => 'h-entry peopletag mode-' . $mode,
'id' => 'peopletag-' . $this->peopletag->id));
}
function showEnd()
{
$this->out->elementEnd('li');
}
function showPeopletag()
{
$this->showCreator();
$this->showTag();
$this->showPrivacy();
$this->showUpdated();
$this->showActions();
$this->showDescription();
}
function showStats()
{
$this->out->elementStart('div', 'entry-summary entity_statistics');
$this->out->elementStart('span', 'tagged-count');
$this->out->element('a',
array('href' => common_local_url('peopletagged',
array('tagger' => $this->profile->nickname,
'tag' => $this->peopletag->tag))),
// TRANS: Link description for link to list of users tagged with a tag (so part of a list).
_('Listed'));
$this->out->raw($this->peopletag->taggedCount());
$this->out->elementEnd('span');
$this->out->elementStart('span', 'subscriber-count');
$this->out->element('a',
array('href' => common_local_url('peopletagsubscribers',
array('tagger' => $this->profile->nickname,
'tag' => $this->peopletag->tag))),
// TRANS: Link description for link to list of users subscribed to a tag.
_('Subscribers'));
$this->out->raw($this->peopletag->subscriberCount());
$this->out->elementEnd('span');
$this->out->elementEnd('div');
}
function showOwnerOptions()
{
$this->out->elementStart('li', 'entity_edit');
$this->out->element('a', array('href' =>
common_local_url('editpeopletag', array('tagger' => $this->profile->nickname,
'tag' => $this->peopletag->tag)),
// TRANS: Title for link to edit list settings.
'title' => _('Edit list settings.')),
// TRANS: Text for link to edit list settings.
_('Edit'));
$this->out->elementEnd('li');
}
function showSubscribeForm()
{
$this->out->elementStart('li');
if (Event::handle('StartSubscribePeopletagForm', array($this->out, $this->peopletag))) {
if ($this->current) {
if ($this->peopletag->hasSubscriber($this->current->id)) {
$form = new UnsubscribePeopletagForm($this->out, $this->peopletag);
$form->show();
} else {
$form = new SubscribePeopletagForm($this->out, $this->peopletag);
$form->show();
}
}
Event::handle('EndSubscribePeopletagForm', array($this->out, $this->peopletag));
}
$this->out->elementEnd('li');
}
function showCreator()
{
$attrs = array();
$attrs['href'] = $this->profile->profileurl;
$attrs['class'] = 'h-card p-author nickname p-name';
$attrs['rel'] = 'contact';
$attrs['title'] = $this->profile->getFancyName();
$this->out->elementStart('a', $attrs);
$this->showAvatar($this->profile);
$this->out->text($this->profile->getNickname());
$this->out->elementEnd('a');
}
function showUpdated()
{
if (!empty($this->peopletag->modified)) {
$this->out->element('abbr',
array('title' => common_date_w3dtf($this->peopletag->modified),
'class' => 'updated'),
common_date_string($this->peopletag->modified));
}
}
function showPrivacy()
{
if ($this->peopletag->private) {
$this->out->elementStart('a',
array('href' => common_local_url('peopletagsbyuser',
array('nickname' => $this->profile->nickname, 'private' => 1))));
// TRANS: Privacy mode text in list list item for private list.
$this->out->element('span', 'privacy_mode', _m('MODE','Private'));
$this->out->elementEnd('a');
}
}
function showTag()
{
$this->out->elementStart('span', 'entry-title tag');
$this->out->element('a',
array('rel' => 'bookmark',
'href' => $this->url()),
htmlspecialchars($this->peopletag->tag));
$this->out->elementEnd('span');
}
function showActions()
{
$this->out->elementStart('div', 'entity_actions');
$this->out->elementStart('ul');
if (!$this->peopletag->private) {
$this->showSubscribeForm();
}
if (!empty($this->current) && $this->profile->id == $this->current->id) {
$this->showOwnerOptions();
}
$this->out->elementEnd('ul');
$this->out->elementEnd('div');
}
function showDescription()
{
$this->out->element('div', 'e-content description', $this->peopletag->description);
}
}

228
lib/peopletaglistitem.php Normal file
View File

@ -0,0 +1,228 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Widget to show a list of peopletags
*
* 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 Public
* @package StatusNet
* @author Shashi Gowda <connect2shashi@gmail.com>
* @copyright 2008-2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('GNUSOCIAL')) { exit(1); }
class PeopletagListItem extends Widget
{
var $peopletag = null;
var $current = null;
var $profile = null;
/**
* constructor
*
* Also initializes the owner attribute.
*
* @param Notice $notice The notice we'll display
*/
function __construct($peopletag, $current, $out=null)
{
parent::__construct($out);
$this->peopletag = $peopletag;
$this->current = $current;
$this->profile = Profile::getKV('id', $this->peopletag->tagger);
}
/**
* recipe function for displaying a single peopletag.
*
* This uses all the other methods to correctly display a notice. Override
* it or one of the others to fine-tune the output.
*
* @return void
*/
function url()
{
return $this->peopletag->homeUrl();
}
function show()
{
if (empty($this->peopletag)) {
common_log(LOG_WARNING, "Trying to show missing peopletag; skipping.");
return;
}
if (Event::handle('StartShowPeopletagItem', array($this))) {
$this->showStart();
$this->showPeopletag();
$this->showStats();
$this->showEnd();
Event::handle('EndShowPeopletagItem', array($this));
}
}
function showStart()
{
$mode = ($this->peopletag->private) ? 'private' : 'public';
$this->out->elementStart('li', array('class' => 'h-entry peopletag mode-' . $mode,
'id' => 'peopletag-' . $this->peopletag->id));
}
function showEnd()
{
$this->out->elementEnd('li');
}
function showPeopletag()
{
$this->showCreator();
$this->showTag();
$this->showPrivacy();
$this->showUpdated();
$this->showActions();
$this->showDescription();
}
function showStats()
{
$this->out->elementStart('div', 'entry-summary entity_statistics');
$this->out->elementStart('span', 'tagged-count');
$this->out->element('a',
array('href' => common_local_url('peopletagged',
array('tagger' => $this->profile->nickname,
'tag' => $this->peopletag->tag))),
// TRANS: Link description for link to list of users tagged with a tag (so part of a list).
_('Listed'));
$this->out->raw($this->peopletag->taggedCount());
$this->out->elementEnd('span');
$this->out->elementStart('span', 'subscriber-count');
$this->out->element('a',
array('href' => common_local_url('peopletagsubscribers',
array('tagger' => $this->profile->nickname,
'tag' => $this->peopletag->tag))),
// TRANS: Link description for link to list of users subscribed to a tag.
_('Subscribers'));
$this->out->raw($this->peopletag->subscriberCount());
$this->out->elementEnd('span');
$this->out->elementEnd('div');
}
function showOwnerOptions()
{
$this->out->elementStart('li', 'entity_edit');
$this->out->element('a', array('href' =>
common_local_url('editpeopletag', array('tagger' => $this->profile->nickname,
'tag' => $this->peopletag->tag)),
// TRANS: Title for link to edit list settings.
'title' => _('Edit list settings.')),
// TRANS: Text for link to edit list settings.
_('Edit'));
$this->out->elementEnd('li');
}
function showSubscribeForm()
{
$this->out->elementStart('li');
if (Event::handle('StartSubscribePeopletagForm', array($this->out, $this->peopletag))) {
if ($this->current) {
if ($this->peopletag->hasSubscriber($this->current->id)) {
$form = new UnsubscribePeopletagForm($this->out, $this->peopletag);
$form->show();
} else {
$form = new SubscribePeopletagForm($this->out, $this->peopletag);
$form->show();
}
}
Event::handle('EndSubscribePeopletagForm', array($this->out, $this->peopletag));
}
$this->out->elementEnd('li');
}
function showCreator()
{
$attrs = array();
$attrs['href'] = $this->profile->profileurl;
$attrs['class'] = 'h-card p-author nickname p-name';
$attrs['rel'] = 'contact';
$attrs['title'] = $this->profile->getFancyName();
$this->out->elementStart('a', $attrs);
$this->showAvatar($this->profile);
$this->out->text($this->profile->getNickname());
$this->out->elementEnd('a');
}
function showUpdated()
{
if (!empty($this->peopletag->modified)) {
$this->out->element('abbr',
array('title' => common_date_w3dtf($this->peopletag->modified),
'class' => 'updated'),
common_date_string($this->peopletag->modified));
}
}
function showPrivacy()
{
if ($this->peopletag->private) {
$this->out->elementStart('a',
array('href' => common_local_url('peopletagsbyuser',
array('nickname' => $this->profile->nickname, 'private' => 1))));
// TRANS: Privacy mode text in list list item for private list.
$this->out->element('span', 'privacy_mode', _m('MODE','Private'));
$this->out->elementEnd('a');
}
}
function showTag()
{
$this->out->elementStart('span', 'entry-title tag');
$this->out->element('a',
array('rel' => 'bookmark',
'href' => $this->url()),
htmlspecialchars($this->peopletag->tag));
$this->out->elementEnd('span');
}
function showActions()
{
$this->out->elementStart('div', 'entity_actions');
$this->out->elementStart('ul');
if (!$this->peopletag->private) {
$this->showSubscribeForm();
}
if (!empty($this->current) && $this->profile->id == $this->current->id) {
$this->showOwnerOptions();
}
$this->out->elementEnd('ul');
$this->out->elementEnd('div');
}
function showDescription()
{
$this->out->element('div', 'e-content description', $this->peopletag->description);
}
}

View File

@ -315,7 +315,7 @@ abstract class ProfileAction extends ManagedAction
$url = $lists->mainpage;
} else {
$url = common_local_url('showprofiletag',
array('tagger' => $this->target->getNickname(),
array('nickname' => $this->target->getNickname(),
'tag' => $lists->tag));
}
if (!$first) {

View File

@ -921,6 +921,7 @@ class Router
$m->connect('all/:tag',
array('action' => 'showprofiletag',
'nickname' => $nickname,
'tag' => self::REGEX_TAG));
foreach (array('subscriptions', 'subscribers') as $a) {
@ -1003,9 +1004,9 @@ class Router
'tagger_id' => '[0-9]+',
'id' => '[0-9]+'));
$m->connect(':tagger/all/:tag',
array('action' => 'showprofiletag',
'tagger' => Nickname::DISPLAY_FMT,
$m->connect(':nickname/all/:tag',
array('action' => 'showprofiletag'),
array('nickname' => Nickname::DISPLAY_FMT,
'tag' => self::REGEX_TAG));
foreach (array('subscriptions', 'subscribers') as $a) {

View File

@ -54,7 +54,7 @@ class SearchEngine
'nickname_desc sort mode can only be use when searching profile.'
);
} else {
return $this->target->orderBy('nickname DESC');
return $this->target->orderBy(sprintf('%1$s.nickname DESC', $this->table));
}
break;
case 'nickname_asc':
@ -63,7 +63,7 @@ class SearchEngine
'nickname_desc sort mode can only be use when searching profile.'
);
} else {
return $this->target->orderBy('nickname ASC');
return $this->target->orderBy(sprintf('%1$s.nickname ASC', $this->table));
}
break;
default:
@ -112,11 +112,13 @@ class MySQLLikeSearch extends SearchEngine
function query($q)
{
if ('profile' === $this->table) {
$qry = sprintf('(nickname LIKE "%%%1$s%%" OR '.
' fullname LIKE "%%%1$s%%" OR '.
' location LIKE "%%%1$s%%" OR '.
' bio LIKE "%%%1$s%%" OR '.
' homepage LIKE "%%%1$s%%")', $this->target->escape($q, true));
$qry = sprintf('(%2$s.nickname LIKE "%%%1$s%%" OR '.
' %2$s.fullname LIKE "%%%1$s%%" OR '.
' %2$s.location LIKE "%%%1$s%%" OR '.
' %2$s.bio LIKE "%%%1$s%%" OR '.
' %2$s.homepage LIKE "%%%1$s%%")',
$this->target->escape($q, true),
$this->table);
} else if ('notice' === $this->table) {
$qry = sprintf('content LIKE "%%%1$s%%"', $this->target->escape($q, true));
} else {

View File

@ -45,7 +45,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
class ServerException extends Exception
{
public function __construct($message = null, $code = 400) {
public function __construct($message = null, $code = 500) {
parent::__construct($message, $code);
}

View File

@ -77,7 +77,7 @@ abstract class SpawningDaemon extends Daemon
for ($i = 1; $i <= $this->threads; $i++) {
$pid = pcntl_fork();
if ($pid < 0) {
$this->log(LOG_ERROR, "Couldn't fork for thread $i; aborting\n");
$this->log(LOG_ERR, "Couldn't fork for thread $i; aborting\n");
exit(1);
} else if ($pid == 0) {
$this->initAndRunChild($i);
@ -113,7 +113,7 @@ abstract class SpawningDaemon extends Daemon
$pid = pcntl_fork();
if ($pid < 0) {
$this->log(LOG_ERROR, "Couldn't fork to respawn thread $i; aborting thread.\n");
$this->log(LOG_ERR, "Couldn't fork to respawn thread $i; aborting thread.\n");
} else if ($pid == 0) {
$this->initAndRunChild($i);
} else {
@ -141,7 +141,7 @@ abstract class SpawningDaemon extends Daemon
$this->parentWriter = $sockets[0];
$this->parentReader = $sockets[1];
} else {
$this->log(LOG_ERROR, "Couldn't create inter-process sockets");
$this->log(LOG_ERR, "Couldn't create inter-process sockets");
exit(1);
}
}

View File

@ -606,14 +606,15 @@ function common_remove_unicode_formatting($text)
/**
* Partial notice markup rendering step: build links to !group references.
*
* @param string $text partially rendered HTML
* @param Notice $notice in whose context we're working
* @param string $text partially rendered HTML
* @param Profile $author the Profile that is composing the current notice
* @param Notice $parent the Notice this is sent in reply to, if any
* @return string partially rendered HTML
*/
function common_render_content($text, Notice $notice)
function common_render_content($text, Profile $author, Notice $parent=null)
{
$text = common_render_text($text);
$text = common_linkify_mentions($text, $notice);
$text = common_linkify_mentions($text, $author, $parent);
return $text;
}
@ -623,13 +624,14 @@ function common_render_content($text, Notice $notice)
*
* Should generally not be called except from common_render_content().
*
* @param string $text partially-rendered HTML
* @param Notice $notice in-progress or complete Notice object for context
* @param string $text partially-rendered HTML
* @param Profile $author the Profile that is composing the current notice
* @param Notice $parent the Notice this is sent in reply to, if any
* @return string partially-rendered HTML
*/
function common_linkify_mentions($text, Notice $notice)
function common_linkify_mentions($text, Profile $author, Notice $parent=null)
{
$mentions = common_find_mentions($text, $notice);
$mentions = common_find_mentions($text, $author, $parent);
// We need to go through in reverse order by position,
// so our positions stay valid despite our fudging with the
@ -648,7 +650,7 @@ function common_linkify_mentions($text, Notice $notice)
$linkText = common_linkify_mention($mention);
$text = substr_replace($text, $linkText, $position, mb_strlen($mention['text']));
$text = substr_replace($text, $linkText, $position, $mention['length']);
}
return $text;
@ -687,33 +689,25 @@ function common_linkify_mention(array $mention)
* Note the return data format is internal, to be used for building links and
* such. Should not be used directly; rather, call common_linkify_mentions().
*
* @param string $text
* @param Notice $notice notice in whose context we're building links
* @param string $text
* @param Profile $sender the Profile that is sending the current text
* @param Notice $parent the Notice this text is in reply to, if any
*
* @return array
*
* @access private
*/
function common_find_mentions($text, Notice $notice)
function common_find_mentions($text, Profile $sender, Notice $parent=null)
{
// The getProfile call throws NoProfileException on failure
$sender = $notice->getProfile();
$mentions = array();
if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
// Get the context of the original notice, if any
$origAuthor = null;
$origNotice = null;
$origMentions = array();
// Is it a reply?
try {
$origNotice = $notice->getParent();
$origAuthor = $origNotice->getProfile();
$ids = $origNotice->getReplies();
// Does it have a parent notice for context?
if ($parent instanceof Notice) {
$ids = $parent->getReplies(); // replied-to _profile ids_
foreach ($ids as $id) {
try {
@ -723,8 +717,6 @@ function common_find_mentions($text, Notice $notice)
// continue foreach
}
}
} catch (NoParentNoticeException $e) {
// It wasn't a reply to anything, so we can't harvest nickname-relations.
}
$matches = common_find_mentions_raw($text);
@ -741,34 +733,33 @@ function common_find_mentions($text, Notice $notice)
// Start with conversation context, then go to
// sender context.
if ($origAuthor instanceof Profile && $origAuthor->nickname == $nickname) {
$mentioned = $origAuthor;
if ($parent instanceof Notice && $parent->getProfile()->getNickname() === $nickname) {
$mentioned = $parent->getProfile();
} else if (!empty($origMentions) &&
array_key_exists($nickname, $origMentions)) {
$mentioned = $origMentions[$nickname];
} else {
// sets to null if no match
$mentioned = common_relative_profile($sender, $nickname);
}
if ($mentioned instanceof Profile) {
$user = User::getKV('id', $mentioned->id);
if ($user instanceof User) {
$url = common_local_url('userbyid', array('id' => $user->id));
} else {
$url = $mentioned->profileurl;
try {
$url = $mentioned->getUrl();
} catch (InvalidUrlException $e) {
$url = common_local_url('userbyid', array('id' => $mentioned->getID()));
}
$mention = array('mentioned' => array($mentioned),
'type' => 'mention',
'text' => $match[0],
'position' => $match[1],
'length' => mb_strlen($match[0]),
'title' => $mentioned->getFullname(),
'url' => $url);
if (!empty($mentioned->fullname)) {
$mention['title'] = $mentioned->fullname;
}
$mentions[] = $mention;
}
}
@ -779,20 +770,21 @@ function common_find_mentions($text, Notice $notice)
$text, $hmatches, PREG_OFFSET_CAPTURE);
foreach ($hmatches[1] as $hmatch) {
$tag = common_canonical_tag($hmatch[0]);
$plist = Profile_list::getByTaggerAndTag($sender->id, $tag);
$plist = Profile_list::getByTaggerAndTag($sender->getID(), $tag);
if (!$plist instanceof Profile_list || $plist->private) {
continue;
}
$tagged = $sender->getTaggedSubscribers($tag);
$url = common_local_url('showprofiletag',
array('tagger' => $sender->nickname,
array('nickname' => $sender->getNickname(),
'tag' => $tag));
$mentions[] = array('mentioned' => $tagged,
'type' => 'list',
'text' => $hmatch[0],
'position' => $hmatch[1],
'length' => mb_strlen($hmatch[0]),
'url' => $url);
}
@ -812,6 +804,7 @@ function common_find_mentions($text, Notice $notice)
'type' => 'group',
'text' => $hmatch[0],
'position' => $hmatch[1],
'length' => mb_strlen($hmatch[0]),
'url' => $group->permalink(),
'title' => $group->getFancyName());
}
@ -875,7 +868,7 @@ function common_replace_urls_callback($text, $callback, $arg = null) {
'(?:'.
'(?:'. //Known protocols
'(?:'.
'(?:(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|irc)://)'.
'(?:(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|ircs?)://)'.
'|'.
'(?:(?:mailto|aim|tel|xmpp):)'.
')'.
@ -985,20 +978,9 @@ function common_linkify($url) {
$canon = "mailto:$url";
$longurl = "mailto:$url";
} else {
$canon = File_redirection::_canonUrl($url);
$longurl_data = File_redirection::where($canon, common_config('attachments', 'process_links'));
if (is_array($longurl_data)) {
$longurl = $longurl_data['url'];
} elseif (is_string($longurl_data)) {
$longurl = $longurl_data;
} else {
// Unable to reach the server to verify contents, etc
// Just pass the link on through for now.
common_log(LOG_ERR, "Can't linkify url '$url'");
$longurl = $url;
}
$longurl = $longurl_data->url;
}
$attrs = array('href' => $canon, 'title' => $longurl);
@ -1554,14 +1536,24 @@ function common_root_url($ssl=false)
return $url;
}
/**
* returns $bytes bytes of raw random data
*/
function common_random_rawstr($bytes)
{
$rawstr = @file_exists('/dev/urandom')
? common_urandom($bytes)
: common_mtrand($bytes);
return $rawstr;
}
/**
* returns $bytes bytes of random data as a hexadecimal string
*/
function common_random_hexstr($bytes)
{
$str = @file_exists('/dev/urandom')
? common_urandom($bytes)
: common_mtrand($bytes);
$str = common_random_rawstr($bytes);
$hexstr = '';
for ($i = 0; $i < $bytes; $i++) {
@ -1866,6 +1858,7 @@ function common_get_mime_media($type)
return strtolower($tmp[0]);
}
// Get only the mimetype and not additional info (separated from bare mime with semi-colon)
function common_bare_mime($mimetype)
{
$mimetype = mb_strtolower($mimetype);

0
local/.gitignore vendored
View File

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Afrikaans (http://www.transifex.com/projects/p/gnu-social/language/af/)\n"
"Language-Team: Afrikaans (http://www.transifex.com/gnu-social/gnu-social/language/af/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Arabic (http://www.transifex.com/projects/p/gnu-social/language/ar/)\n"
"Language-Team: Arabic (http://www.transifex.com/gnu-social/gnu-social/language/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Arabic (Egypt) (http://www.transifex.com/projects/p/gnu-social/language/ar_EG/)\n"
"Language-Team: Arabic (Egypt) (http://www.transifex.com/gnu-social/gnu-social/language/ar_EG/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-05 17:29+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Asturian (http://www.transifex.com/projects/p/gnu-social/language/ast/)\n"
"Language-Team: Asturian (http://www.transifex.com/gnu-social/gnu-social/language/ast/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,12 +10,12 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Belarusian (Tarask) (http://www.transifex.com/projects/p/gnu-social/language/be@tarask/)\n"
"Language-Team: Belarusian (Tarask) (http://www.transifex.com/gnu-social/gnu-social/language/be@tarask/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: be@tarask\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#. TRANS: Database error message.
#: index.php:118

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Bulgarian (http://www.transifex.com/projects/p/gnu-social/language/bg/)\n"
"Language-Team: Bulgarian (http://www.transifex.com/gnu-social/gnu-social/language/bg/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Breton (http://www.transifex.com/projects/p/gnu-social/language/br/)\n"
"Language-Team: Breton (http://www.transifex.com/gnu-social/gnu-social/language/br/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Catalan (http://www.transifex.com/projects/p/gnu-social/language/ca/)\n"
"Language-Team: Catalan (http://www.transifex.com/gnu-social/gnu-social/language/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -11,7 +11,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Czech (http://www.transifex.com/projects/p/gnu-social/language/cs/)\n"
"Language-Team: Czech (http://www.transifex.com/gnu-social/gnu-social/language/cs/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Danish (http://www.transifex.com/projects/p/gnu-social/language/da/)\n"
"Language-Team: Danish (http://www.transifex.com/gnu-social/gnu-social/language/da/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -11,7 +11,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-18 23:41+0000\n"
"Last-Translator: D P\n"
"Language-Team: German (http://www.transifex.com/projects/p/gnu-social/language/de/)\n"
"Language-Team: German (http://www.transifex.com/gnu-social/gnu-social/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Greek (http://www.transifex.com/projects/p/gnu-social/language/el/)\n"
"Language-Team: Greek (http://www.transifex.com/gnu-social/gnu-social/language/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

File diff suppressed because it is too large Load Diff

View File

@ -3,14 +3,15 @@
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
# Manuel Ortega <manuel@elarte.coop>, 2015
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Esperanto (http://www.transifex.com/projects/p/gnu-social/language/eo/)\n"
"PO-Revision-Date: 2015-06-13 14:41+0000\n"
"Last-Translator: Manuel Ortega <manuel@elarte.coop>\n"
"Language-Team: Esperanto (http://www.transifex.com/gnu-social/gnu-social/language/eo/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -30,7 +31,7 @@ msgstr ""
#. TRANS: Error message.
#: index.php:137
msgid "An error occurred."
msgstr ""
msgstr "Eraro okazis."
#. TRANS: Error message displayed when there is no StatusNet configuration
#. file.
@ -1029,7 +1030,7 @@ msgstr ""
#. having the right to do so.
#: actions/apilistmembers.php:46
msgid "You are not allowed to add members to this list."
msgstr ""
msgstr "Vi ne rajtas aldoni membrojn al tiu ĉi listo."
#. TRANS: Client error displayed when trying to modify list members without
#. specifying them.

View File

@ -9,9 +9,9 @@ msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-27 15:36+0000\n"
"PO-Revision-Date: 2015-02-28 20:31+0000\n"
"Last-Translator: Juan Riquelme González <soulchainer@gmail.com>\n"
"Language-Team: Spanish (http://www.transifex.com/projects/p/gnu-social/language/es/)\n"
"Language-Team: Spanish (http://www.transifex.com/gnu-social/gnu-social/language/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -4241,7 +4241,7 @@ msgstr "Confirmar"
#: actions/passwordsettings.php:121 actions/recoverpassword.php:257
#: actions/register.php:420
msgid "Same as password above."
msgstr "Repetir la contraseña de arriba."
msgstr "Repite la contraseña anterior."
#. TRANS: Button text on page where to change password.
#: actions/passwordsettings.php:126

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Basque (http://www.transifex.com/projects/p/gnu-social/language/eu/)\n"
"Language-Team: Basque (http://www.transifex.com/gnu-social/gnu-social/language/eu/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Persian (http://www.transifex.com/projects/p/gnu-social/language/fa/)\n"
"Language-Team: Persian (http://www.transifex.com/gnu-social/gnu-social/language/fa/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Finnish (http://www.transifex.com/projects/p/gnu-social/language/fi/)\n"
"Language-Team: Finnish (http://www.transifex.com/gnu-social/gnu-social/language/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ msgstr ""
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 14:32+0000\n"
"Last-Translator: digitaldreamer <digitaldreamer@email.cz>\n"
"Language-Team: Friulian (http://www.transifex.com/projects/p/gnu-social/language/fur/)\n"
"Language-Team: Friulian (http://www.transifex.com/gnu-social/gnu-social/language/fur/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

Some files were not shown because too many files have changed in this diff Show More