Merge remote branch 'gitorious/1.0.x' into 1.0.x

Conflicts:
	actions/urlsettings.php
This commit is contained in:
Evan Prodromou 2010-11-14 06:49:43 -05:00
commit 01f32e3998
146 changed files with 33584 additions and 17503 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ lac08.log
php.log
.DS_Store
nbproject
*.mo

12
README
View File

@ -3,7 +3,7 @@ README
------
StatusNet 0.9.6 "Man on the Moon"
25 October 2010 - RELEASE CANDIDATE
29 October 2010
This is the README file for StatusNet, the Open Source microblogging
platform. It includes installation instructions, descriptions of
@ -122,6 +122,16 @@ Notable changes this version:
- Header metadata on notice pages to aid in manual reposting on Facebook
- Lots of little fixes...
Changes from 0.9.6 release candidate 1:
- fix for broken group pages when logged out
- fix for stuck ping queue entries when bad profile
- fix for bogus single-user nickname config entry error
- i18n updates
- nofollow updates
- SSL-only mode secure cookie fix
- experimental ApiLogger plugin for usage data gathering
- experimental follow-everyone plugin
A full changelog is available at http://status.net/wiki/StatusNet_0.9.6.
Prerequisites

View File

@ -55,7 +55,7 @@
Yes
@param status (Required) The URL-encoded text of the status update.
@param source (Optional) The source of the status.
@param source (Optional) The source application name, if using HTTP authentication or an anonymous OAuth consumer.
@param in_reply_to_status_id (Optional) The ID of an existing status that the update is in reply to.
@param lat (Optional) The latitude the status refers to.
@param long (Optional) The longitude the status refers to.
@ -67,7 +67,7 @@
@subsection usagenotes Usage notes
@li The URL pattern is relative to the @ref apiroot.
@li If the @e source parameter is not supplied the source of the status will default to 'api'.
@li If the @e source parameter is not supplied the source of the status will default to 'api'. When authenticated via a registered OAuth application, the application's registered name and URL will always override the source parameter.
@li The XML response uses <a href="http://georss.org/Main_Page">GeoRSS</a>
to encode the latitude and longitude (see example response below <georss:point>).
@li Data uploaded via the @e media parameter should be multipart/form-data encoded.

View File

@ -42,7 +42,6 @@ require_once INSTALLDIR.'/lib/attachmentlist.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class AttachmentAction extends Action
{
/**
@ -70,6 +69,7 @@ class AttachmentAction extends Action
}
if (empty($this->attachment)) {
// TRANS: Client error displayed trying to get a non-existing attachment.
$this->clientError(_('No such attachment.'), 404);
return false;
}
@ -81,7 +81,6 @@ class AttachmentAction extends Action
*
* @return boolean true
*/
function isReadOnly($args)
{
return true;
@ -129,7 +128,6 @@ class AttachmentAction extends Action
*
* @return void
*/
function handle($args)
{
parent::handle($args);
@ -150,7 +148,6 @@ class AttachmentAction extends Action
*
* @return void
*/
function showLocalNavBlock()
{
}
@ -162,7 +159,6 @@ class AttachmentAction extends Action
*
* @return void
*/
function showContent()
{
$ali = new Attachment($this->attachment, $this);
@ -174,7 +170,6 @@ class AttachmentAction extends Action
*
* @return void
*/
function showPageNoticeBlock()
{
}
@ -191,4 +186,3 @@ class AttachmentAction extends Action
$atcs->show();
}
}

View File

@ -42,7 +42,6 @@ require_once INSTALLDIR.'/actions/attachment.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class Attachment_ajaxAction extends AttachmentAction
{
/**
@ -80,4 +79,3 @@ class Attachment_ajaxAction extends AttachmentAction
$this->elementEnd('div');
}
}

View File

@ -42,10 +42,8 @@ require_once INSTALLDIR.'/actions/attachment.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class Attachment_thumbnailAction extends AttachmentAction
{
function handle($args)
{
$this->showPage();
@ -79,6 +77,4 @@ class Attachment_thumbnailAction extends AttachmentAction
}
$this->element('img', array('src' => $file_thumbnail->url, 'alt' => 'Thumbnail'));
}
}

View File

@ -48,7 +48,7 @@ class AvatarbynicknameAction extends Action
* Class handler.
*
* @param array $args query arguments
*
*
* @return boolean false if nickname or user isn't found
*/
function handle($args)
@ -56,27 +56,32 @@ class AvatarbynicknameAction extends Action
parent::handle($args);
$nickname = $this->trimmed('nickname');
if (!$nickname) {
// TRANS: Client error displayed trying to get an avatar without providing a nickname.
$this->clientError(_('No nickname.'));
return;
}
$size = $this->trimmed('size');
if (!$size) {
// TRANS: Client error displayed trying to get an avatar without providing an avatar size.
$this->clientError(_('No size.'));
return;
}
$size = strtolower($size);
if (!in_array($size, array('original', '96', '48', '24'))) {
// TRANS: Client error displayed trying to get an avatar providing an invalid avatar size.
$this->clientError(_('Invalid size.'));
return;
}
$user = User::staticGet('nickname', $nickname);
if (!$user) {
// TRANS: Client error displayed trying to get an avatar for a non-existing user.
$this->clientError(_('No such user.'));
return;
}
$profile = $user->getProfile();
if (!$profile) {
// TRANS: Client error displayed trying to get an avatar for a user without a profile.
$this->clientError(_('User has no profile.'));
return;
}
@ -103,4 +108,3 @@ class AvatarbynicknameAction extends Action
return true;
}
}

View File

@ -49,7 +49,6 @@ define('MAX_ORIGINAL', 480);
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class AvatarsettingsAction extends AccountSettingsAction
{
var $mode = null;
@ -61,9 +60,9 @@ class AvatarsettingsAction extends AccountSettingsAction
*
* @return string Title of the page
*/
function title()
{
// TRANS: Title for avatar upload page.
return _('Avatar');
}
@ -72,10 +71,12 @@ class AvatarsettingsAction extends AccountSettingsAction
*
* @return instructions for use
*/
function getInstructions()
{
return sprintf(_('You can upload your personal avatar. The maximum file size is %s.'), ImageFile::maxFileSize());
// TRANS: Instruction for avatar upload page.
// TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB".
return sprintf(_('You can upload your personal avatar. The maximum file size is %s.'),
ImageFile::maxFileSize());
}
/**
@ -103,6 +104,7 @@ class AvatarsettingsAction extends AccountSettingsAction
if (!$profile) {
common_log_db_error($user, 'SELECT', __FILE__);
// TRANS: Server error displayed in avatar upload page when no matching profile can be found for a user.
$this->serverError(_('User without matching profile.'));
return;
}
@ -116,14 +118,16 @@ class AvatarsettingsAction extends AccountSettingsAction
'action' =>
common_local_url('avatarsettings')));
$this->elementStart('fieldset');
// TRANS: Avatar upload page form legend.
$this->element('legend', null, _('Avatar settings'));
$this->hidden('token', common_session_token());
if (Event::handle('StartAvatarFormData', array($this))) {
$this->elementStart('ul', 'form_data');
if ($original) {
$this->elementStart('li', array('id' => 'avatar_original',
'class' => 'avatar_view'));
// TRANS: Header on avatar upload page for thumbnail of originally uploaded avatar (h2).
$this->element('h2', null, _("Original"));
$this->elementStart('div', array('id'=>'avatar_original_view'));
$this->element('img', array('src' => $original->url,
@ -139,6 +143,7 @@ class AvatarsettingsAction extends AccountSettingsAction
if ($avatar) {
$this->elementStart('li', array('id' => 'avatar_preview',
'class' => 'avatar_view'));
// TRANS: Header on avatar upload page for thumbnail of to be used rendition of uploaded avatar (h2).
$this->element('h2', null, _("Preview"));
$this->elementStart('div', array('id'=>'avatar_preview_view'));
$this->element('img', array('src' => $original->url,
@ -146,7 +151,8 @@ class AvatarsettingsAction extends AccountSettingsAction
'height' => AVATAR_PROFILE_SIZE,
'alt' => $user->nickname));
$this->elementEnd('div');
$this->submit('delete', _('Delete'));
// TRANS: Button on avatar upload page to delete current avatar.
$this->submit('delete', _m('BUTTON','Delete'));
$this->elementEnd('li');
}
@ -163,7 +169,8 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->elementStart('ul', 'form_actions');
$this->elementStart('li');
$this->submit('upload', _('Upload'));
// TRANS: Button on avatar upload page to upload an avatar.
$this->submit('upload', _m('BUTTON','Upload'));
$this->elementEnd('li');
$this->elementEnd('ul');
}
@ -171,7 +178,6 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
function showCropForm()
@ -182,6 +188,7 @@ class AvatarsettingsAction extends AccountSettingsAction
if (!$profile) {
common_log_db_error($user, 'SELECT', __FILE__);
// TRANS: Server error displayed in avatar upload page when no matching profile can be found for a user.
$this->serverError(_('User without matching profile.'));
return;
}
@ -194,6 +201,7 @@ class AvatarsettingsAction extends AccountSettingsAction
'action' =>
common_local_url('avatarsettings')));
$this->elementStart('fieldset');
// TRANS: Avatar upload page crop form legend.
$this->element('legend', null, _('Avatar settings'));
$this->hidden('token', common_session_token());
@ -202,6 +210,7 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->elementStart('li',
array('id' => 'avatar_original',
'class' => 'avatar_view'));
// TRANS: Header on avatar upload crop form for thumbnail of originally uploaded avatar (h2).
$this->element('h2', null, _("Original"));
$this->elementStart('div', array('id'=>'avatar_original_view'));
$this->element('img', array('src' => Avatar::url($this->filedata['filename']),
@ -214,6 +223,7 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->elementStart('li',
array('id' => 'avatar_preview',
'class' => 'avatar_view'));
// TRANS: Header on avatar upload crop form for thumbnail of to be used rendition of uploaded avatar (h2).
$this->element('h2', null, _("Preview"));
$this->elementStart('div', array('id'=>'avatar_preview_view'));
$this->element('img', array('src' => Avatar::url($this->filedata['filename']),
@ -228,13 +238,14 @@ class AvatarsettingsAction extends AccountSettingsAction
'type' => 'hidden',
'id' => $crop_info));
}
$this->submit('crop', _('Crop'));
// TRANS: Button on avatar upload crop form to confirm a selected crop as avatar.
$this->submit('crop', _m('BUTTON','Crop'));
$this->elementEnd('li');
$this->elementEnd('ul');
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
/**
@ -244,7 +255,6 @@ class AvatarsettingsAction extends AccountSettingsAction
*
* @return void
*/
function handlePost()
{
// Workaround for PHP returning empty $_POST and $_FILES when POST
@ -271,7 +281,7 @@ class AvatarsettingsAction extends AccountSettingsAction
'Try again, please.'));
return;
}
if (Event::handle('StartAvatarSaveForm', array($this))) {
if ($this->arg('upload')) {
$this->uploadAvatar();
@ -280,6 +290,7 @@ class AvatarsettingsAction extends AccountSettingsAction
} else if ($this->arg('delete')) {
$this->deleteAvatar();
} else {
// TRANS: Unexpected validation error on avatar upload form.
$this->showForm(_('Unexpected form submission.'));
}
Event::handle('EndAvatarSaveForm', array($this));
@ -294,7 +305,6 @@ class AvatarsettingsAction extends AccountSettingsAction
*
* @return void
*/
function uploadAvatar()
{
try {
@ -304,6 +314,7 @@ class AvatarsettingsAction extends AccountSettingsAction
return;
}
if ($imagefile === null) {
// TRANS: Validation error on avatar upload form when no file was uploaded.
$this->showForm(_('No file uploaded.'));
return;
}
@ -331,6 +342,7 @@ class AvatarsettingsAction extends AccountSettingsAction
$this->mode = 'crop';
// TRANS: Avatar upload form unstruction after uploading a file.
$this->showForm(_('Pick a square area of the image to be your avatar'),
true);
}
@ -340,12 +352,12 @@ class AvatarsettingsAction extends AccountSettingsAction
*
* @return void
*/
function cropAvatar()
{
$filedata = $_SESSION['FILEDATA'];
if (!$filedata) {
// TRANS: Server error displayed if an avatar upload went wrong somehow server side.
$this->serverError(_('Lost our file data.'));
return;
}
@ -369,24 +381,25 @@ class AvatarsettingsAction extends AccountSettingsAction
@unlink($filedata['filepath']);
unset($_SESSION['FILEDATA']);
$this->mode = 'upload';
// TRANS: Success message for having updated a user avatar.
$this->showForm(_('Avatar updated.'), true);
common_broadcast_profile($profile);
} else {
// TRANS: Error displayed on the avatar upload page if the avatar could not be updated for an unknown reason.
$this->showForm(_('Failed updating avatar.'));
}
}
/**
* Get rid of the current avatar.
*
* @return void
*/
function deleteAvatar()
{
$user = common_current_user();
$profile = $user->getProfile();
$avatar = $profile->getOriginalAvatar();
if($avatar) $avatar->delete();
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
@ -396,6 +409,7 @@ class AvatarsettingsAction extends AccountSettingsAction
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
if($avatar) $avatar->delete();
// TRANS: Success message for deleting a user avatar.
$this->showForm(_('Avatar deleted.'), true);
}
@ -416,7 +430,6 @@ class AvatarsettingsAction extends AccountSettingsAction
*
* @return void
*/
function showScripts()
{
parent::showScripts();

View File

@ -42,7 +42,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class BlockAction extends ProfileFormAction
{
var $profile = null;
@ -54,7 +53,6 @@ class BlockAction extends ProfileFormAction
*
* @return boolean success flag
*/
function prepare($args)
{
if (!parent::prepare($args)) {
@ -66,6 +64,7 @@ class BlockAction extends ProfileFormAction
assert(!empty($cur)); // checked by parent
if ($cur->hasBlocked($this->profile)) {
// TRANS: Client error displayed when blocking a user that has already been blocked.
$this->clientError(_('You already blocked that user.'));
return false;
}
@ -82,7 +81,6 @@ class BlockAction extends ProfileFormAction
*
* @return void
*/
function handle($args)
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
@ -104,6 +102,7 @@ class BlockAction extends ProfileFormAction
}
function title() {
// TRANS: Title for block user page.
return _('Block user');
}
@ -133,8 +132,10 @@ class BlockAction extends ProfileFormAction
'action' => common_local_url('block')));
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
// TRANS: Legend for block user form.
$this->element('legend', _('Block user'));
$this->element('p', null,
// TRANS: Explanation of consequences when blocking a user on the block user page.
_('Are you sure you want to block this user? '.
'Afterwards, they will be unsubscribed from you, '.
'unable to subscribe to you in the future, and '.
@ -184,6 +185,7 @@ class BlockAction extends ProfileFormAction
}
if (!$result) {
// TRANS: Server error displayed when blocking a user fails.
$this->serverError(_('Failed to save block information.'));
return;
}
@ -199,7 +201,7 @@ class BlockAction extends ProfileFormAction
* Override for form session token checks; on our first hit we're just
* requesting confirmation, which doesn't need a token. We need to be
* able to take regular GET requests from email!
*
*
* @throws ClientException if token is bad on POST request or if we have
* confirmation parameters which could trigger something.
*/
@ -216,7 +218,7 @@ class BlockAction extends ProfileFormAction
/**
* If we reached this form without returnto arguments, return to the
* current user's subscription list.
*
*
* @return string URL
*/
function defaultReturnTo()

View File

@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class BlockedfromgroupAction extends GroupDesignAction
{
var $page = null;
@ -70,6 +69,7 @@ class BlockedfromgroupAction extends GroupDesignAction
}
if (!$nickname) {
// TRANS: Client error displayed when requesting a list of blocked users for a group without providing a group nickname.
$this->clientError(_('No nickname.'), 404);
return false;
}
@ -77,6 +77,7 @@ class BlockedfromgroupAction extends GroupDesignAction
$local = Local_group::staticGet('nickname', $nickname);
if (!$local) {
// TRANS: Client error displayed when requesting a list of blocked users for a non-local group.
$this->clientError(_('No such group.'), 404);
return false;
}
@ -84,6 +85,7 @@ class BlockedfromgroupAction extends GroupDesignAction
$this->group = User_group::staticGet('id', $local->group_id);
if (!$this->group) {
// TRANS: Client error displayed when requesting a list of blocked users for a non-existing group.
$this->clientError(_('No such group.'), 404);
return false;
}
@ -94,9 +96,13 @@ class BlockedfromgroupAction extends GroupDesignAction
function title()
{
if ($this->page == 1) {
// TRANS: Title for first page with list of users blocked from a group.
// TRANS: %s is a group nickname.
return sprintf(_('%s blocked profiles'),
$this->group->nickname);
} else {
// TRANS: Title for any but the first page with list of users blocked from a group.
// TRANS: %1$s is a group nickname, %2$d is a page number.
return sprintf(_('%1$s blocked profiles, page %2$d'),
$this->group->nickname,
$this->page);
@ -112,6 +118,7 @@ class BlockedfromgroupAction extends GroupDesignAction
function showPageNotice()
{
$this->element('p', 'instructions',
// TRANS: Instructions for list of users blocked from a group.
_('A list of the users blocked from joining this group.'));
}
@ -205,7 +212,6 @@ class GroupBlockListItem extends ProfileListItem
*
* @see UnblockForm
*/
class GroupUnblockForm extends Form
{
/**
@ -234,7 +240,6 @@ class GroupUnblockForm extends Form
* @param User_group $group group to block user from
* @param array $args return-to args
*/
function __construct($out=null, $profile=null, $group=null, $args=null)
{
parent::__construct($out);
@ -249,7 +254,6 @@ class GroupUnblockForm extends Form
*
* @return int ID of the form
*/
function id()
{
// This should be unique for the page.
@ -261,7 +265,6 @@ class GroupUnblockForm extends Form
*
* @return string class of the form
*/
function formClass()
{
return 'form_group_unblock';
@ -272,7 +275,6 @@ class GroupUnblockForm extends Form
*
* @return string URL of the action
*/
function action()
{
return common_local_url('groupunblock');
@ -285,6 +287,7 @@ class GroupUnblockForm extends Form
*/
function formLegend()
{
// TRANS: Form legend for unblocking a user from a group.
$this->out->element('legend', null, _('Unblock user from group'));
}
@ -293,7 +296,6 @@ class GroupUnblockForm extends Form
*
* @return void
*/
function formData()
{
$this->out->hidden('unblockto-' . $this->profile->id,
@ -314,9 +316,14 @@ class GroupUnblockForm extends Form
*
* @return void
*/
function formActions()
{
$this->out->submit('submit', _('Unblock'), 'submit', null, _('Unblock this user'));
$this->out->submit('submit',
// TRANS: Button text for unblocking a user from a group.
_m('BUTTON','Unblock'),
'submit',
null,
// TRANS: Tooltip for button for unblocking a user from a group.
_('Unblock this user'));
}
}

View File

@ -34,7 +34,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
require_once INSTALLDIR . '/actions/newnotice.php';
/**
* Action for posting a notice
* Action for posting a notice
*
* @category Bookmarklet
* @package StatusNet
@ -42,12 +42,12 @@ require_once INSTALLDIR . '/actions/newnotice.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class BookmarkletAction extends NewnoticeAction
{
function showTitle()
{
// TRANS: Title for mini-posting window loaded from bookmarklet.
// TRANS: %s is the StatusNet site name.
$this->element('title', null, sprintf(_('Post to %s'), common_config('site', 'name')));
}
@ -73,4 +73,3 @@ class BookmarkletAction extends NewnoticeAction
{
}
}

View File

@ -44,7 +44,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class ConfirmaddressAction extends Action
{
/** type of confirmation. */
@ -61,7 +60,6 @@ class ConfirmaddressAction extends Action
*
* @return void
*/
function handle($args)
{
parent::handle($args);
@ -72,16 +70,19 @@ class ConfirmaddressAction extends Action
}
$code = $this->trimmed('code');
if (!$code) {
// TRANS: Client error displayed when not providing a confirmation code in the contact address confirmation action.
$this->clientError(_('No confirmation code.'));
return;
}
$confirm = Confirm_address::staticGet('code', $code);
if (!$confirm) {
// TRANS: Client error displayed when providing a non-existing confirmation code in the contact address confirmation action.
$this->clientError(_('Confirmation code not found.'));
return;
}
$cur = common_current_user();
if ($cur->id != $confirm->user_id) {
// TRANS: Client error displayed when not providing a confirmation code for another user in the contact address confirmation action.
$this->clientError(_('That confirmation code is not for you!'));
return;
}
@ -98,6 +99,7 @@ class ConfirmaddressAction extends Action
if (in_array($type, array('email', 'sms')))
{
if ($cur->$type == $confirm->address) {
// TRANS: Client error for an already confirmed email/jabber/sms address.
$this->clientError(_('That address has already been confirmed.'));
return;
}
@ -162,7 +164,9 @@ class ConfirmaddressAction extends Action
if (!$result) {
common_log_db_error($confirm, 'DELETE', __FILE__);
$this->serverError(_('Couldn\'t delete email confirmation.'));
// TRANS: Server error displayed when an address confirmation code deletion from the
// TRANS: database fails in the contact address confirmation action.
$this->serverError(_('Could not delete address confirmation.'));
return;
}
@ -175,9 +179,9 @@ class ConfirmaddressAction extends Action
*
* @return string title
*/
function title()
{
// TRANS: Title for the contact address confirmation action.
return _('Confirm address');
}
@ -186,12 +190,13 @@ class ConfirmaddressAction extends Action
*
* @return void
*/
function showContent()
{
$cur = common_current_user();
$this->element('p', null,
// TRANS: Success message for the contact address confirmation action.
// TRANS: %s can be 'email', 'jabber', or 'sms'.
sprintf(_('The address "%s" has been '.
'confirmed for your account.'),
$this->address));

View File

@ -45,7 +45,6 @@ require_once INSTALLDIR.'/lib/noticelist.php';
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class ConversationAction extends Action
{
var $id = null;
@ -58,7 +57,6 @@ class ConversationAction extends Action
*
* @return boolean false if id not passed in
*/
function prepare($args)
{
parent::prepare($args);
@ -81,7 +79,6 @@ class ConversationAction extends Action
*
* @return void
*/
function handle($args)
{
parent::handle($args);
@ -93,10 +90,10 @@ class ConversationAction extends Action
*
* @return string page title
*/
function title()
{
return _("Conversation");
// TRANS: Title for page with a conversion (multiple notices in context).
return _('Conversation');
}
/**
@ -107,7 +104,6 @@ class ConversationAction extends Action
*
* @return void
*/
function showContent()
{
$notices = Notice::conversationStream($this->id, null, null);
@ -134,7 +130,6 @@ class ConversationAction extends Action
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class ConversationTree extends NoticeList
{
var $tree = null;
@ -145,12 +140,12 @@ class ConversationTree extends NoticeList
*
* @return void
*/
function show()
{
$cnt = $this->_buildTree();
$this->out->elementStart('div', array('id' =>'notices_primary'));
// TRANS: Header on conversation page. Hidden by default (h2).
$this->out->element('h2', null, _('Notices'));
$this->out->elementStart('ol', array('class' => 'notices xoxo'));
@ -200,7 +195,6 @@ class ConversationTree extends NoticeList
*
* @return void
*/
function showNoticePlus($id)
{
$notice = $this->table[$id];
@ -237,7 +231,6 @@ class ConversationTree extends NoticeList
*
* @return NoticeListItem a list item to show
*/
function newListItem($notice)
{
return new ConversationTreeItem($notice, $this->out);
@ -255,7 +248,6 @@ class ConversationTree extends NoticeList
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class ConversationTreeItem extends NoticeListItem
{
/**
@ -266,7 +258,6 @@ class ConversationTreeItem extends NoticeListItem
*
* @return void
*/
function showStart()
{
return;
@ -280,7 +271,6 @@ class ConversationTreeItem extends NoticeListItem
*
* @return void
*/
function showEnd()
{
return;
@ -293,7 +283,6 @@ class ConversationTreeItem extends NoticeListItem
*
* @return void
*/
function showContext()
{
return;

View File

@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class DeleteapplicationAction extends Action
{
var $app = null;
@ -52,7 +51,6 @@ class DeleteapplicationAction extends Action
*
* @return boolean success flag
*/
function prepare($args)
{
if (!parent::prepare($args)) {
@ -60,6 +58,7 @@ class DeleteapplicationAction extends Action
}
if (!common_logged_in()) {
// TRANS: Client error displayed trying to delete an application while not logged in.
$this->clientError(_('You must be logged in to delete an application.'));
return false;
}
@ -68,6 +67,7 @@ class DeleteapplicationAction extends Action
$this->app = Oauth_application::staticGet('id', $id);
if (empty($this->app)) {
// TRANS: Client error displayed trying to delete an application that does not exist.
$this->clientError(_('Application not found.'));
return false;
}
@ -75,6 +75,7 @@ class DeleteapplicationAction extends Action
$cur = common_current_user();
if ($cur->id != $this->app->owner) {
// TRANS: Client error displayed trying to delete an application the current user does not own.
$this->clientError(_('You are not the owner of this application.'), 401);
return false;
}
@ -91,7 +92,6 @@ class DeleteapplicationAction extends Action
*
* @return void
*/
function handle($args)
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
@ -120,6 +120,7 @@ class DeleteapplicationAction extends Action
}
function title() {
// TRANS: Title for delete application page.
return _('Delete application');
}
@ -144,8 +145,10 @@ class DeleteapplicationAction extends Action
array('id' => $this->app->id))));
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
// TRANS: Fieldset legend on delete application page.
$this->element('legend', _('Delete application'));
$this->element('p', null,
// TRANS: Confirmation text on delete application page.
_('Are you sure you want to delete this application? '.
'This will clear all data about the application from the '.
'database, including all existing user connections.'));
@ -171,10 +174,8 @@ class DeleteapplicationAction extends Action
*
* @return void
*/
function handlePost()
{
$this->app->delete();
}
}

View File

@ -172,7 +172,7 @@ class DeletegroupAction extends RedirectingAction
}
function title() {
// TRANS: Title.
// TRANS: Title of delete group page.
return _('Delete group');
}
@ -201,8 +201,8 @@ class DeletegroupAction extends RedirectingAction
// TRANS: Form legend for deleting a group.
$this->element('legend', _('Delete group'));
if (Event::handle('StartDeleteGroupForm', array($this, $this->group))) {
// TRANS: Warning in form for deleleting a group.
$this->element('p', null,
// TRANS: Warning in form for deleleting a group.
_('Are you sure you want to delete this group? '.
'This will clear all data about the group from the '.
'database, without a backup. ' .

View File

@ -32,6 +32,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
// @todo FIXME: documentation needed.
class DeletenoticeAction extends Action
{
var $error = null;
@ -47,6 +48,7 @@ class DeletenoticeAction extends Action
$this->user = common_current_user();
if (!$this->user) {
// TRANS: Error message displayed trying to delete a notice while not logged in.
common_user_error(_('Not logged in.'));
exit;
}
@ -55,6 +57,7 @@ class DeletenoticeAction extends Action
$this->notice = Notice::staticGet($notice_id);
if (!$this->notice) {
// TRANS: Error message displayed trying to delete a non-existing notice.
common_user_error(_('No such notice.'));
exit;
}
@ -71,6 +74,7 @@ class DeletenoticeAction extends Action
if ($this->notice->profile_id != $this->user_profile->id &&
!$this->user->hasRight(Right::DELETEOTHERSNOTICE)) {
// TRANS: Error message displayed trying to delete a notice that was not made by the current user.
common_user_error(_('Can\'t delete this notice.'));
exit;
}
@ -90,7 +94,6 @@ class DeletenoticeAction extends Action
*
* @return void
*/
function showPageNotice()
{
$instr = $this->getInstructions();
@ -103,12 +106,14 @@ class DeletenoticeAction extends Action
function getInstructions()
{
// TRANS: Instructions for deleting a notice.
return _('You are about to permanently delete a notice. ' .
'Once this is done, it cannot be undone.');
}
function title()
{
// TRANS: Page title when deleting a notice.
return _('Delete notice');
}
@ -121,7 +126,6 @@ class DeletenoticeAction extends Action
*
* @return void
*/
function showForm($error = null)
{
$this->error = $error;
@ -133,7 +137,6 @@ class DeletenoticeAction extends Action
*
* @return void
*/
function showContent()
{
$this->elementStart('form', array('id' => 'form_notice_delete',
@ -141,9 +144,11 @@ class DeletenoticeAction extends Action
'method' => 'post',
'action' => common_local_url('deletenotice')));
$this->elementStart('fieldset');
// TRANS: Fieldset legend for the delete notice form.
$this->element('legend', null, _('Delete notice'));
$this->hidden('token', common_session_token());
$this->hidden('notice', $this->trimmed('notice'));
// TRANS: Message for the delete notice form.
$this->element('p', null, _('Are you sure you want to delete this notice?'));
$this->submit('form_action-no',
// TRANS: Button label on the delete notice form.

View File

@ -185,7 +185,7 @@ class EditApplicationAction extends OwnerDesignAction
return;
} elseif (mb_strlen($name) > 255) {
// TRANS: Validation error shown when providing too long a name in the "Edit application" form.
$this->showForm(_('Name is too long (max 255 characters).'));
$this->showForm(_('Name is too long (maximum 255 characters).'));
return;
} else if ($this->nameExists($name)) {
// TRANS: Validation error shown when providing a name for an application that already exists in the "Edit application" form.

View File

@ -45,14 +45,13 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class EditgroupAction extends GroupDesignAction
{
var $msg;
function title()
{
// TRANS: Title for form to edit a group. %s is a group nickname.
return sprintf(_('Edit %s group'), $this->group->nickname);
}
@ -65,6 +64,7 @@ class EditgroupAction extends GroupDesignAction
parent::prepare($args);
if (!common_logged_in()) {
// TRANS: Client error displayed trying to edit a group while not logged in.
$this->clientError(_('You must be logged in to create a group.'));
return false;
}
@ -81,6 +81,7 @@ class EditgroupAction extends GroupDesignAction
}
if (!$nickname) {
// TRANS: Client error displayed trying to edit a group while not proving a nickname for the group to edit.
$this->clientError(_('No nickname.'), 404);
return false;
}
@ -97,6 +98,7 @@ class EditgroupAction extends GroupDesignAction
}
if (!$this->group) {
// TRANS: Client error displayed trying to edit a non-existing group.
$this->clientError(_('No such group.'), 404);
return false;
}
@ -104,6 +106,7 @@ class EditgroupAction extends GroupDesignAction
$cur = common_current_user();
if (!$cur->isAdmin($this->group)) {
// TRANS: Client error displayed trying to edit a group while not being a group admin.
$this->clientError(_('You must be an admin to edit the group.'), 403);
return false;
}
@ -120,7 +123,6 @@ class EditgroupAction extends GroupDesignAction
*
* @return void
*/
function handle($args)
{
parent::handle($args);
@ -155,6 +157,7 @@ class EditgroupAction extends GroupDesignAction
$this->element('p', 'error', $this->msg);
} else {
$this->element('p', 'instructions',
// TRANS: Form instructions for group edit form.
_('Use this form to edit the group.'));
}
}
@ -169,6 +172,7 @@ class EditgroupAction extends GroupDesignAction
{
$cur = common_current_user();
if (!$cur->isAdmin($this->group)) {
// TRANS: Client error displayed trying to edit a group while not being a group admin.
$this->clientError(_('You must be an admin to edit the group.'), 403);
return;
}
@ -183,28 +187,39 @@ class EditgroupAction extends GroupDesignAction
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
// TRANS: Group edit form validation error.
$this->showForm(_('Nickname must have only lowercase letters '.
'and numbers and no spaces.'));
return;
} else if ($this->nicknameExists($nickname)) {
// TRANS: Group edit form validation error.
$this->showForm(_('Nickname already in use. Try another one.'));
return;
} else if (!User_group::allowedNickname($nickname)) {
// TRANS: Group edit form validation error.
$this->showForm(_('Not a valid nickname.'));
return;
} else if (!is_null($homepage) && (strlen($homepage) > 0) &&
!Validate::uri($homepage,
array('allowed_schemes' =>
array('http', 'https')))) {
// TRANS: Group edit form validation error.
$this->showForm(_('Homepage is not a valid URL.'));
return;
} else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
// TRANS: Group edit form validation error.
$this->showForm(_('Full name is too long (maximum 255 characters).'));
return;
} else if (User_group::descriptionTooLong($description)) {
$this->showForm(sprintf(_('Description is too long (max %d chars).'), User_group::maxDescription()));
$this->showForm(sprintf(
// TRANS: Group edit form validation error.
_m('Description is too long (maximum %d character).',
'Description is too long (maximum %d characters).',
User_group::maxDescription()),
User_group::maxDescription()));
return;
} else if (!is_null($location) && mb_strlen($location) > 255) {
// TRANS: Group edit form validation error.
$this->showForm(_('Location is too long (maximum 255 characters).'));
return;
}
@ -216,7 +231,11 @@ class EditgroupAction extends GroupDesignAction
}
if (count($aliases) > common_config('group', 'maxaliases')) {
$this->showForm(sprintf(_('Too many aliases! Maximum %d.'),
// TRANS: Group edit form validation error.
// TRANS: %d is the maximum number of allowed aliases.
$this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.',
'Too many aliases! Maximum %d allowed.',
common_config('group', 'maxaliases')),
common_config('group', 'maxaliases')));
return;
}
@ -225,16 +244,19 @@ class EditgroupAction extends GroupDesignAction
if (!Validate::string($alias, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
// TRANS: Group edit form validation error.
$this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
return;
}
if ($this->nicknameExists($alias)) {
// TRANS: Group edit form validation error.
$this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
$alias));
return;
}
// XXX assumes alphanum nicknames
if (strcmp($alias, $nickname) == 0) {
// TRANS: Group edit form validation error.
$this->showForm(_('Alias can\'t be the same as nickname.'));
return;
}
@ -255,12 +277,14 @@ class EditgroupAction extends GroupDesignAction
if (!$result) {
common_log_db_error($this->group, 'UPDATE', __FILE__);
// TRANS: Server error displayed when editing a group fails.
$this->serverError(_('Could not update group.'));
}
$result = $this->group->setAliases($aliases);
if (!$result) {
// TRANS: Server error displayed when group aliases could not be added.
$this->serverError(_('Could not create aliases.'));
}
@ -277,6 +301,7 @@ class EditgroupAction extends GroupDesignAction
array('nickname' => $nickname)),
303);
} else {
// TRANS: Group edit form success message.
$this->showForm(_('Options saved.'));
}
}
@ -300,4 +325,3 @@ class EditgroupAction extends GroupDesignAction
return false;
}
}

View File

@ -32,7 +32,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
}
/**
* Unlock a user from a group
* Unblock a user from a group
*
* @category Action
* @package StatusNet
@ -40,7 +40,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class GroupunblockAction extends Action
{
var $profile = null;
@ -53,11 +52,11 @@ class GroupunblockAction extends Action
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
if (!common_logged_in()) {
// TRANS: Client error displayed when trying to unblock a user from a group while not logged in.
$this->clientError(_('Not logged in.'));
return false;
}
@ -68,11 +67,13 @@ class GroupunblockAction extends Action
}
$id = $this->trimmed('unblockto');
if (empty($id)) {
// TRANS: Client error displayed when trying to unblock a user from a group without providing a profile.
$this->clientError(_('No profile specified.'));
return false;
}
$this->profile = Profile::staticGet('id', $id);
if (empty($this->profile)) {
// TRANS: Client error displayed when trying to unblock a user from a group without providing an existing profile.
$this->clientError(_('No profile with that ID.'));
return false;
}
@ -83,15 +84,18 @@ class GroupunblockAction extends Action
}
$this->group = User_group::staticGet('id', $group_id);
if (empty($this->group)) {
// TRANS: Client error displayed when trying to unblock a user from a non-existing group.
$this->clientError(_('No such group.'));
return false;
}
$user = common_current_user();
if (!$user->isAdmin($this->group)) {
// TRANS: Client error displayed when trying to unblock a user from a group without being an administrator for the group.
$this->clientError(_('Only an admin can unblock group members.'), 401);
return false;
}
if (!Group_block::isBlocked($this->group, $this->profile)) {
// TRANS: Client error displayed when trying to unblock a non-blocked user from a group.
$this->clientError(_('User is not blocked from group.'));
return false;
}
@ -105,7 +109,6 @@ class GroupunblockAction extends Action
*
* @return void
*/
function handle($args)
{
parent::handle($args);
@ -119,12 +122,12 @@ class GroupunblockAction extends Action
*
* @return void
*/
function unblockProfile()
{
$result = Group_block::unblockProfile($this->group, $this->profile);
if (!$result) {
// TRANS: Server error displayed when unblocking a user from a group fails because of an unknown error.
$this->serverError(_('Error removing the block.'));
return;
}
@ -146,4 +149,3 @@ class GroupunblockAction extends Action
}
}
}

View File

@ -142,7 +142,7 @@ class InviteAction extends CurrentUserDesignAction
$this->elementStart('ul');
foreach ($this->already as $other) {
// TRANS: Used as list item for already subscribed users (%1$s is nickname, %2$s is e-mail address).
$this->element('li', null, sprintf(_('%1$s (%2$s)'), $other->nickname, $other->email));
$this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email));
}
$this->elementEnd('ul');
}
@ -156,7 +156,7 @@ class InviteAction extends CurrentUserDesignAction
$this->elementStart('ul');
foreach ($this->subbed as $other) {
// TRANS: Used as list item for already registered people (%1$s is nickname, %2$s is e-mail address).
$this->element('li', null, sprintf(_('%1$s (%2$s)'), $other->nickname, $other->email));
$this->element('li', null, sprintf(_m('INVITE','%1$s (%2$s)'), $other->nickname, $other->email));
}
$this->elementEnd('ul');
}

View File

@ -153,7 +153,7 @@ class LicenseadminpanelAction extends AdminPanelAction
// Make sure the license title is not too long
if (mb_strlen($values['license']['type']) > 255) {
$this->clientError(
_("Invalid license title. Max length is 255 characters.")
_('Invalid license title. Maximum length is 255 characters.')
);
}

View File

@ -166,7 +166,7 @@ class NewApplicationAction extends OwnerDesignAction
$this->showForm(_('Name already in use. Try another one.'));
return;
} elseif (mb_strlen($name) > 255) {
$this->showForm(_('Name is too long (maximum 255 chars).'));
$this->showForm(_('Name is too long (maximum 255 characters).'));
return;
} elseif (empty($description)) {
$this->showForm(_('Description is required.'));
@ -196,7 +196,7 @@ class NewApplicationAction extends OwnerDesignAction
$this->showForm(_('Organization is required.'));
return;
} elseif (mb_strlen($organization) > 255) {
$this->showForm(_('Organization is too long (maximum 255 chars).'));
$this->showForm(_('Organization is too long (maximum 255 characters).'));
return;
} elseif (empty($homepage)) {
$this->showForm(_('Organization homepage is required.'));

View File

@ -43,25 +43,25 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class NewgroupAction extends Action
{
var $msg;
function title()
{
// TRANS: Title for form to create a group.
return _('New group');
}
/**
* Prepare to run
*/
function prepare($args)
{
parent::prepare($args);
if (!common_logged_in()) {
// TRANS: Client error displayed trying to create a group while not logged in.
$this->clientError(_('You must be logged in to create a group.'));
return false;
}
@ -78,7 +78,6 @@ class NewgroupAction extends Action
*
* @return void
*/
function handle($args)
{
parent::handle($args);
@ -107,6 +106,7 @@ class NewgroupAction extends Action
$this->element('p', 'error', $this->msg);
} else {
$this->element('p', 'instructions',
// TRANS: Form instructions for group create form.
_('Use this form to create a new group.'));
}
}
@ -123,33 +123,39 @@ class NewgroupAction extends Action
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
// TRANS: Group create form validation error.
$this->showForm(_('Nickname must have only lowercase letters '.
'and numbers and no spaces.'));
return;
} else if ($this->nicknameExists($nickname)) {
// TRANS: Group create form validation error.
$this->showForm(_('Nickname already in use. Try another one.'));
return;
} else if (!User_group::allowedNickname($nickname)) {
// TRANS: Group create form validation error.
$this->showForm(_('Not a valid nickname.'));
return;
} else if (!is_null($homepage) && (strlen($homepage) > 0) &&
!Validate::uri($homepage,
array('allowed_schemes' =>
array('http', 'https')))) {
// TRANS: Group create form validation error.
$this->showForm(_('Homepage is not a valid URL.'));
return;
} else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
// TRANS: Group create form validation error.
$this->showForm(_('Full name is too long (maximum 255 characters).'));
return;
} else if (User_group::descriptionTooLong($description)) {
// TRANS: Form validation error creating a new group because the description is too long.
// TRANS: Group create form validation error.
// TRANS: %d is the maximum number of allowed characters.
$this->showForm(sprintf(_m('Description is too long (maximum %d character).',
'Description is too long (maximum %d characters).',
User_group::maxDescription(),
User_group::maxDescription()),
User_group::maxDescription()));
return;
} else if (!is_null($location) && mb_strlen($location) > 255) {
// TRANS: Group create form validation error.
$this->showForm(_('Location is too long (maximum 255 characters).'));
return;
}
@ -161,7 +167,7 @@ class NewgroupAction extends Action
}
if (count($aliases) > common_config('group', 'maxaliases')) {
// TRANS: Client error shown when providing too many aliases during group creation.
// TRANS: Group create form validation error.
// TRANS: %d is the maximum number of allowed aliases.
$this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.',
'Too many aliases! Maximum %d allowed.',
@ -174,16 +180,19 @@ class NewgroupAction extends Action
if (!Validate::string($alias, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
// TRANS: Group create form validation error.
$this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
return;
}
if ($this->nicknameExists($alias)) {
// TRANS: Group create form validation error.
$this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
$alias));
return;
}
// XXX assumes alphanum nicknames
if (strcmp($alias, $nickname) == 0) {
// TRANS: Group create form validation error.
$this->showForm(_('Alias can\'t be the same as nickname.'));
return;
}
@ -227,4 +236,3 @@ class NewgroupAction extends Action
return false;
}
}

View File

@ -156,8 +156,11 @@ class NewnoticeAction extends Action
$content_shortened = common_shorten_links($content);
if (Notice::contentTooLong($content_shortened)) {
$this->clientError(sprintf(_('That\'s too long. '.
'Max notice size is %d chars.'),
// 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()));
}
@ -178,12 +181,10 @@ class NewnoticeAction extends Action
if (Notice::contentTooLong($content_shortened)) {
$upload->delete();
$this->clientError(
sprintf(
_('Max notice size is %d chars, including attachment URL.'),
Notice::maxContent()
)
);
$this->clientError(sprintf(_m('Maximum notice size is %d character, including attachment URL.',
'Maximum notice size is %d characters, including attachment URL.',
Notice::maxContent()),
Notice::maxContent()));
}
}

View File

@ -79,11 +79,7 @@ class OembedAction extends Action
if (empty($profile)) {
$this->serverError(_('Notice has no profile.'), 500);
}
if (!empty($profile->fullname)) {
$authorname = $profile->fullname . ' (' . $profile->nickname . ')';
} else {
$authorname = $profile->nickname;
}
$authorname = $profile->getFancyName();
$oembed['title'] = sprintf(_('%1$s\'s status on %2$s'),
$authorname,
common_exact_date($notice->created));

View File

@ -362,7 +362,7 @@ class RecoverpasswordAction extends Action
$confirm = $this->trimmed('confirm');
if (!$newpassword || strlen($newpassword) < 6) {
$this->showPasswordForm(_('Password must be 6 chars or more.'));
$this->showPasswordForm(_('Password must be 6 characters or more.'));
return;
}
if ($newpassword != $confirm) {

View File

@ -46,10 +46,8 @@ define('MEMBERS_PER_SECTION', 27);
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class ShowgroupAction extends GroupDesignAction
{
/** page we're viewing. */
var $page = null;
@ -58,7 +56,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return boolean true
*/
function isReadOnly($args)
{
return true;
@ -69,18 +66,16 @@ class ShowgroupAction extends GroupDesignAction
*
* @return string page title, with page number
*/
function title()
{
if (!empty($this->group->fullname)) {
$base = $this->group->fullname . ' (' . $this->group->nickname . ')';
} else {
$base = $this->group->nickname;
}
$base = $this->group->getFancyName();
if ($this->page == 1) {
// TRANS: Page title for first group page. %s is a group name.
return sprintf(_('%s group'), $base);
} else {
// TRANS: Page title for any but first group page.
// TRANS: %1$s is a group name, $2$s is a page number.
return sprintf(_('%1$s group, page %2$d'),
$base,
$this->page);
@ -96,7 +91,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
@ -118,6 +112,7 @@ class ShowgroupAction extends GroupDesignAction
}
if (!$nickname) {
// TRANS: Client error displayed if no nickname argument was given requesting a group page.
$this->clientError(_('No nickname.'), 404);
return false;
}
@ -135,6 +130,7 @@ class ShowgroupAction extends GroupDesignAction
return false;
} else {
common_log(LOG_NOTICE, "Couldn't find local group for nickname '$nickname'");
// TRANS: Client error displayed if no remote group with a given name was found requesting group page.
$this->clientError(_('No such group.'), 404);
return false;
}
@ -143,6 +139,7 @@ class ShowgroupAction extends GroupDesignAction
$this->group = User_group::staticGet('id', $local->group_id);
if (!$this->group) {
// TRANS: Client error displayed if no local group with a given name was found requesting group page.
$this->clientError(_('No such group.'), 404);
return false;
}
@ -160,7 +157,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function handle($args)
{
$this->showPage();
@ -171,7 +167,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showLocalNav()
{
$nav = new GroupNav($this, $this->group);
@ -183,7 +178,6 @@ class ShowgroupAction extends GroupDesignAction
*
* Shows a group profile and a list of group notices
*/
function showContent()
{
$this->showGroupProfile();
@ -195,7 +189,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showGroupNotices()
{
$notice = $this->group->getNotices(($this->page-1)*NOTICES_PER_PAGE,
@ -218,15 +211,16 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showGroupProfile()
{
$this->elementStart('div', array('id' => 'i',
'class' => 'entity_profile vcard author'));
// TRANS: Group profile header (h2). Text hidden by default.
$this->element('h2', null, _('Group profile'));
$this->elementStart('dl', 'entity_depiction');
// TRANS: Label for group avatar (dt). Text hidden by default.
$this->element('dt', null, _('Avatar'));
$this->elementStart('dd');
@ -242,6 +236,7 @@ class ShowgroupAction extends GroupDesignAction
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_nickname');
// TRANS: Label for group nickname (dt). Text hidden by default.
$this->element('dt', null, _('Nickname'));
$this->elementStart('dd');
$hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid';
@ -253,6 +248,7 @@ class ShowgroupAction extends GroupDesignAction
if ($this->group->fullname) {
$this->elementStart('dl', 'entity_fn');
// TRANS: Label for full group name (dt). Text hidden by default.
$this->element('dt', null, _('Full name'));
$this->elementStart('dd');
$this->element('span', 'fn org', $this->group->fullname);
@ -262,6 +258,7 @@ class ShowgroupAction extends GroupDesignAction
if ($this->group->location) {
$this->elementStart('dl', 'entity_location');
// TRANS: Label for group location (dt). Text hidden by default.
$this->element('dt', null, _('Location'));
$this->element('dd', 'label', $this->group->location);
$this->elementEnd('dl');
@ -269,6 +266,7 @@ class ShowgroupAction extends GroupDesignAction
if ($this->group->homepage) {
$this->elementStart('dl', 'entity_url');
// TRANS: Label for group URL (dt). Text hidden by default.
$this->element('dt', null, _('URL'));
$this->elementStart('dd');
$this->element('a', array('href' => $this->group->homepage,
@ -280,6 +278,7 @@ class ShowgroupAction extends GroupDesignAction
if ($this->group->description) {
$this->elementStart('dl', 'entity_note');
// TRANS: Label for group description or group note (dt). Text hidden by default.
$this->element('dt', null, _('Note'));
$this->element('dd', 'note', $this->group->description);
$this->elementEnd('dl');
@ -290,6 +289,7 @@ class ShowgroupAction extends GroupDesignAction
if (!empty($aliases)) {
$this->elementStart('dl', 'entity_aliases');
// TRANS: Label for group aliases (dt). Text hidden by default.
$this->element('dt', null, _('Aliases'));
$this->element('dd', 'aliases', implode(' ', $aliases));
$this->elementEnd('dl');
@ -300,6 +300,7 @@ class ShowgroupAction extends GroupDesignAction
$cur = common_current_user();
$this->elementStart('div', 'entity_actions');
// TRANS: Group actions header (h2). Text hidden by default.
$this->element('h2', null, _('Group actions'));
$this->elementStart('ul');
$this->elementStart('li', 'entity_subscribe');
@ -331,7 +332,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function getFeeds()
{
$url =
@ -341,23 +341,27 @@ class ShowgroupAction extends GroupDesignAction
return array(new Feed(Feed::RSS1,
common_local_url('grouprss',
array('nickname' => $this->group->nickname)),
// TRANS: Tooltip for feed link. %s is a group nickname.
sprintf(_('Notice feed for %s group (RSS 1.0)'),
$this->group->nickname)),
new Feed(Feed::RSS2,
common_local_url('ApiTimelineGroup',
array('format' => 'rss',
'id' => $this->group->id)),
// TRANS: Tooltip for feed link. %s is a group nickname.
sprintf(_('Notice feed for %s group (RSS 2.0)'),
$this->group->nickname)),
new Feed(Feed::ATOM,
common_local_url('ApiTimelineGroup',
array('format' => 'atom',
'id' => $this->group->id)),
// TRANS: Tooltip for feed link. %s is a group nickname.
sprintf(_('Notice feed for %s group (Atom)'),
$this->group->nickname)),
new Feed(Feed::FOAF,
common_local_url('foafgroup',
array('nickname' => $this->group->nickname)),
// TRANS: Tooltip for feed link. %s is a group nickname.
sprintf(_('FOAF for %s group'),
$this->group->nickname)));
}
@ -367,7 +371,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showSections()
{
$this->showMembers();
@ -382,7 +385,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showMembers()
{
$member = $this->group->getMembers(0, MEMBERS_PER_SECTION);
@ -396,17 +398,22 @@ class ShowgroupAction extends GroupDesignAction
if (Event::handle('StartShowGroupMembersMiniList', array($this))) {
// TRANS: Header for mini list of group members on a group page (h2).
$this->element('h2', null, _('Members'));
$gmml = new GroupMembersMiniList($member, $this);
$cnt = $gmml->show();
if ($cnt == 0) {
// TRANS: Description for mini list of group members on a group page when the group has no members.
$this->element('p', null, _('(None)'));
}
// @todo FIXME: Should be shown if a group has more than 27 members, but I do not see it displayed at
// for example http://identi.ca/group/statusnet. Broken?
if ($cnt > MEMBERS_PER_SECTION) {
$this->element('a', array('href' => common_local_url('groupmembers',
array('nickname' => $this->group->nickname))),
// TRANS: Link to all group members from mini list of group members if group has more than n members.
_('All members'));
}
@ -421,7 +428,6 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showAdmins()
{
$adminSection = new GroupAdminSection($this, $this->group);
@ -433,22 +439,26 @@ class ShowgroupAction extends GroupDesignAction
*
* @return void
*/
function showStatistics()
{
$this->elementStart('div', array('id' => 'entity_statistics',
'class' => 'section'));
// TRANS: Header for group statistics on a group page (h2).
$this->element('h2', null, _('Statistics'));
$this->elementStart('dl', 'entity_created');
$this->element('dt', null, _('Created'));
// @todo FIXME: i18n issue. This label gets a colon added from somewhere. Should be part of the message.
// TRANS: Label for creation date in statistics on group page.
$this->element('dt', null, _m('LABEL','Created'));
$this->element('dd', null, date('j M Y',
strtotime($this->group->created)));
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_members');
$this->element('dt', null, _('Members'));
// @todo FIXME: i18n issue. This label gets a colon added from somewhere. Should be part of the message.
// TRANS: Label for member count in statistics on group page.
$this->element('dt', null, _m('LABEL','Members'));
$this->element('dd', null, $this->group->getMemberCount());
$this->elementEnd('dl');
@ -458,12 +468,21 @@ class ShowgroupAction extends GroupDesignAction
function showAnonymousMessage()
{
if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
// @todo FIXME: use group full name here if available instead of (uglier) primary alias.
// TRANS: Notice on group pages for anonymous users for StatusNet sites that accept new registrations.
// TRANS: **%s** is the group alias, %%%%site.name%%%% is the site name,
// TRANS: %%%%action.register%%%% is the URL for registration, %%%%doc.help%%%% is a URL to help.
// TRANS: This message contains Markdown links. Ensure they are formatted correctly: [Description](link).
$m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. Its members share ' .
'short messages about their life and interests. '.
'[Join now](%%%%action.register%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'),
$this->group->nickname);
} else {
// @todo FIXME: use group full name here if available instead of (uglier) primary alias.
// TRANS: Notice on group pages for anonymous users for StatusNet sites that accept no new registrations.
// TRANS: **%s** is the group alias, %%%%site.name%%%% is the site name,
// TRANS: This message contains Markdown links. Ensure they are formatted correctly: [Description](link).
$m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. Its members share ' .
'short messages about their life and interests. '),
@ -492,6 +511,7 @@ class GroupAdminSection extends ProfileSection
function title()
{
// TRANS: Header for list of group administrators on a group page (h2).
return _('Admins');
}
@ -527,4 +547,3 @@ class GroupMembersMiniListItem extends ProfileMiniListItem
return $aAttrs;
}
}

View File

@ -26,8 +26,8 @@
* @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('STATUSNET') && !defined('LACONICA')) {
exit(1);
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
require_once INSTALLDIR.'/lib/mailbox.php';
@ -36,26 +36,24 @@ require_once INSTALLDIR.'/lib/mailbox.php';
* Show a single message
*
* // XXX: It is totally weird how this works!
*
*
* @category Personal
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class ShowmessageAction extends MailboxAction
{
/**
* Message object to show
*/
var $message = null;
/**
* The current user
*/
var $user = null;
/**
@ -67,17 +65,17 @@ class ShowmessageAction extends MailboxAction
*
* @return success flag
*/
function prepare($args)
{
parent::prepare($args);
$this->page = 1;
$id = $this->trimmed('message');
$this->message = Message::staticGet('id', $id);
if (!$this->message) {
// TRANS: Client error displayed requesting a single message that does not exist.
$this->clientError(_('No such message.'), 404);
return false;
}
@ -90,40 +88,47 @@ class ShowmessageAction extends MailboxAction
function handle($args)
{
Action::handle($args);
if ($this->user && ($this->user->id == $this->message->from_profile ||
if ($this->user && ($this->user->id == $this->message->from_profile ||
$this->user->id == $this->message->to_profile)) {
$this->showPage();
} else {
// TRANS: Client error displayed requesting a single direct message the requesting user was not a party in.
$this->clientError(_('Only the sender and recipient ' .
'may read this message.'), 403);
return;
}
}
function title()
{
{
if ($this->user->id == $this->message->from_profile) {
$to = $this->message->getTo();
return sprintf(_("Message to %1\$s on %2\$s"),
// @todo FIXME: Might be nice if the timestamp could be localised.
// TRANS: Page title for single direct message display when viewing user is the sender.
// TRANS: %1$s is the addressed user's nickname, $2$s is a timestamp.
return sprintf(_('Message to %1$s on %2$s'),
$to->nickname,
common_exact_date($this->message->created));
} else if ($this->user->id == $this->message->to_profile) {
$from = $this->message->getFrom();
return sprintf(_("Message from %1\$s on %2\$s"),
// @todo FIXME: Might be nice if the timestamp could be localised.
// TRANS: Page title for single message display.
// TRANS: %1$s is the sending user's nickname, $2$s is a timestamp.
return sprintf(_('Message from %1$s on %2$s'),
$from->nickname,
common_exact_date($this->message->created));
}
}
function getMessages()
{
function getMessages()
{
$message = new Message();
$message->id = $this->message->id;
$message->find();
return $message;
}
function getMessageProfile()
{
if ($this->user->id == $this->message->from_profile) {
@ -135,23 +140,21 @@ class ShowmessageAction extends MailboxAction
return null;
}
}
/**
* Don't show local navigation
*
* @return void
*/
function showLocalNavBlock()
{
}
/**
* Don't show page notice
*
* @return void
*/
function showPageNoticeBlock()
{
}
@ -161,17 +164,15 @@ class ShowmessageAction extends MailboxAction
*
* @return void
*/
function showAside()
function showAside()
{
}
/**
* Don't show any instructions
*
* @return string
*/
function getInstructions()
{
return '';

View File

@ -167,11 +167,7 @@ class ShownoticeAction extends OwnerDesignAction
function title()
{
if (!empty($this->profile->fullname)) {
$base = $this->profile->fullname . ' (' . $this->profile->nickname . ')';
} else {
$base = $this->profile->nickname;
}
$base = $this->profile->getFancyName();
return sprintf(_('%1$s\'s status on %2$s'),
$base,

View File

@ -63,21 +63,26 @@ class ShowstreamAction extends ProfileAction
function title()
{
if (!empty($this->profile->fullname)) {
$base = $this->profile->fullname . ' (' . $this->user->nickname . ') ';
} else {
$base = $this->user->nickname;
}
$base = $this->profile->getFancyName();
if (!empty($this->tag)) {
$base .= sprintf(_(' tagged %s'), $this->tag);
}
if ($this->page == 1) {
return $base;
if ($this->page == 1) {
// TRANS: Page title showing tagged notices in one user's stream. %1$s is the username, %2$s is the hash tag.
return sprintf(_('%1$s tagged %2$s'), $base, $this->tag);
} else {
// TRANS: Page title showing tagged notices in one user's stream.
// TRANS: %1$s is the username, %2$s is the hash tag, %1$d is the page number.
return sprintf(_('%1$s tagged %2$s, page %3$d'), $base, $this->tag, $this->page);
}
} else {
return sprintf(_('%1$s, page %2$d'),
$base,
$this->page);
if ($this->page == 1) {
return $base;
} else {
// TRANS: Extended page title showing tagged notices in one user's stream.
// TRANS: %1$s is the username, %2$d is the page number.
return sprintf(_('%1$s, page %2$d'),
$base,
$this->page);
}
}
}
@ -117,6 +122,8 @@ class ShowstreamAction extends ProfileAction
common_local_url('userrss',
array('nickname' => $this->user->nickname,
'tag' => $this->tag)),
// TRANS: Title for link to notice feed.
// TRANS: %1$s is a user nickname, %2$s is a hashtag.
sprintf(_('Notice feed for %1$s tagged %2$s (RSS 1.0)'),
$this->user->nickname, $this->tag)));
}
@ -124,6 +131,8 @@ class ShowstreamAction extends ProfileAction
return array(new Feed(Feed::RSS1,
common_local_url('userrss',
array('nickname' => $this->user->nickname)),
// TRANS: Title for link to notice feed.
// TRANS: %s is a user nickname.
sprintf(_('Notice feed for %s (RSS 1.0)'),
$this->user->nickname)),
new Feed(Feed::RSS2,
@ -131,6 +140,8 @@ class ShowstreamAction extends ProfileAction
array(
'id' => $this->user->id,
'format' => 'rss')),
// TRANS: Title for link to notice feed.
// TRANS: %s is a user nickname.
sprintf(_('Notice feed for %s (RSS 2.0)'),
$this->user->nickname)),
new Feed(Feed::ATOM,
@ -143,6 +154,8 @@ class ShowstreamAction extends ProfileAction
new Feed(Feed::FOAF,
common_local_url('foaf', array('nickname' =>
$this->user->nickname)),
// TRANS: Title for link to notice feed. FOAF stands for Friend of a Friend.
// TRANS: More information at http://www.foaf-project.org. %s is a user nickname.
sprintf(_('FOAF for %s'), $this->user->nickname)));
}
@ -188,17 +201,23 @@ class ShowstreamAction extends ProfileAction
function showEmptyListMessage()
{
$message = sprintf(_('This is the timeline for %1$s but %2$s hasn\'t posted anything yet.'), $this->user->nickname, $this->user->nickname) . ' ';
// TRANS: First sentence of empty list message for a stream. $1%s is a user nickname.
$message = sprintf(_('This is the timeline for %1$s, but %1$s hasn\'t posted anything yet.'), $this->user->nickname) . ' ';
if (common_logged_in()) {
$current_user = common_current_user();
if ($this->user->id === $current_user->id) {
// TRANS: Second sentence of empty list message for a stream for the user themselves.
$message .= _('Seen anything interesting recently? You haven\'t posted any notices yet, now would be a good time to start :)');
} else {
// TRANS: Second sentence of empty list message for a non-self stream. %1$s is a user nickname, %2$s is a part of a URL.
// TRANS: This message contains a Markdown link. Keep "](" together.
$message .= sprintf(_('You can try to nudge %1$s or [post something to them](%%%%action.newnotice%%%%?status_textarea=%2$s).'), $this->user->nickname, '@' . $this->user->nickname);
}
}
else {
// TRANS: Second sentence of empty message for anonymous users. %s is a user nickname.
// TRANS: This message contains a Markdown link. Keep "](" together.
$message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to them.'), $this->user->nickname);
}
@ -234,11 +253,15 @@ class ShowstreamAction extends ProfileAction
function showAnonymousMessage()
{
if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
// TRANS: Announcement for anonymous users showing a stream if site registrations are open.
// TRANS: This message contains a Markdown link. Keep "](" together.
$m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. ' .
'[Join now](%%%%action.register%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'),
$this->user->nickname, $this->user->nickname);
} else {
// TRANS: Announcement for anonymous users showing a stream if site registrations are closed or invite only.
// TRANS: This message contains a Markdown link. Keep "](" together.
$m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [StatusNet](http://status.net/) tool. '),
$this->user->nickname, $this->user->nickname);
@ -278,7 +301,6 @@ class ProfileNoticeListItem extends DoFollowListItem
*
* @return void
*/
function showRepeat()
{
if (!empty($this->repeat)) {
@ -289,13 +311,14 @@ class ProfileNoticeListItem extends DoFollowListItem
'class' => 'url');
if (!empty($this->profile->fullname)) {
$attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ')';
$attrs['title'] = $this->getFancyName();
}
$this->out->elementStart('span', 'repeat');
$text_link = XMLStringer::estring('a', $attrs, $this->profile->nickname);
// TRANS: Link to the author of a repeated notice. %s is a linked nickname.
$this->out->raw(sprintf(_('Repeat of %s'), $text_link));
$this->out->elementEnd('span');

View File

@ -42,7 +42,6 @@ require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class SitenoticeadminpanelAction extends AdminPanelAction
{
/**
@ -50,9 +49,9 @@ class SitenoticeadminpanelAction extends AdminPanelAction
*
* @return string page title
*/
function title()
{
// TRANS: Page title for site-wide notice tab in admin panel.
return _('Site Notice');
}
@ -61,9 +60,9 @@ class SitenoticeadminpanelAction extends AdminPanelAction
*
* @return string instructions
*/
function getInstructions()
{
// TRANS: Instructions for site-wide notice tab in admin panel.
return _('Edit site-wide message');
}
@ -72,7 +71,6 @@ class SitenoticeadminpanelAction extends AdminPanelAction
*
* @return void
*/
function showForm()
{
$form = new SiteNoticeAdminPanelForm($this);
@ -85,7 +83,6 @@ class SitenoticeadminpanelAction extends AdminPanelAction
*
* @return void
*/
function saveSettings()
{
$siteNotice = $this->trimmed('site-notice');
@ -100,6 +97,7 @@ class SitenoticeadminpanelAction extends AdminPanelAction
$result = Config::save('site', 'notice', $siteNotice);
if (!$result) {
// TRANS: Server error displayed when saving a site-wide notice was impossible.
$this->ServerError(_("Unable to save site notice."));
}
}
@ -110,7 +108,8 @@ class SitenoticeadminpanelAction extends AdminPanelAction
if (mb_strlen($siteNotice) > 255) {
$this->clientError(
_('Max length for the site-wide notice is 255 chars.')
// TRANS: Client error displayed when a site-wide notice was longer than allowed.
_('Maximum length for the site-wide notice is 255 characters.')
);
}
@ -173,9 +172,11 @@ class SiteNoticeAdminPanelForm extends AdminForm
$this->out->elementStart('li');
$this->out->textarea(
'site-notice',
// TRANS: Label for site-wide notice text field in admin panel.
_('Site notice text'),
common_config('site', 'notice'),
_('Site-wide notice text (255 chars max; HTML okay)')
// TRANS: Tooltip for site-wide notice text field in admin panel.
_('Site-wide notice text (255 characters maximum; HTML allowed)')
);
$this->out->elementEnd('li');
@ -192,9 +193,11 @@ class SiteNoticeAdminPanelForm extends AdminForm
{
$this->out->submit(
'submit',
_('Save'),
// TRANS: Button text for saving site notice in admin panel.
_m('BUTTON','Save'),
'submit',
null,
// TRANS: Title for button to save site notice in admin panel.
_('Save site notice')
);
}

View File

@ -19,6 +19,7 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
// @todo FIXME: Documentation needed.
class SubeditAction extends Action
{
var $profile = null;
@ -28,6 +29,7 @@ class SubeditAction extends Action
parent::prepare($args);
if (!common_logged_in()) {
// TRANS: Client error displayed trying a change a subscription while not logged in.
$this->clientError(_('Not logged in.'));
return false;
}
@ -43,6 +45,7 @@ class SubeditAction extends Action
$id = $this->trimmed('profile');
if (!$id) {
// TRANS: Client error displayed trying a change a subscription without providing a profile.
$this->clientError(_('No profile specified.'));
return false;
}
@ -50,6 +53,7 @@ class SubeditAction extends Action
$this->profile = Profile::staticGet('id', $id);
if (!$this->profile) {
// TRANS: Client error displayed trying a change a subscription for a non-existant profile ID.
$this->clientError(_('No profile with that ID.'));
return false;
}
@ -67,6 +71,7 @@ class SubeditAction extends Action
'subscribed' => $this->profile->id));
if (!$sub) {
// TRANS: Client error displayed trying a change a subscription for a non-subscribed profile.
$this->clientError(_('You are not subscribed to that profile.'));
return false;
}
@ -80,6 +85,7 @@ class SubeditAction extends Action
if (!$result) {
common_log_db_error($sub, 'UPDATE', __FILE__);
// TRANS: Server error displayed when updating a subscription fails with a database error.
$this->serverError(_('Could not save subscription.'));
return false;
}

View File

@ -68,6 +68,7 @@ class UrlsettingsAction extends AccountSettingsAction
function getInstructions()
{
// TRANS: Instructions for tab "Other" in user profile settings.
return _('Manage various other options.');
}
@ -107,6 +108,9 @@ class UrlsettingsAction extends AccountSettingsAction
{
$services[$name]=$name;
if($value['freeService']){
// TRANS: Used as a suffix for free URL shorteners in a dropdown list in the tab "Other" of a
// TRANS: user's profile settings. This message has one space at the beginning. Use your
// TRANS: language's word separator here if it has one (most likely a single space).
$services[$name].=_(' (free service)');
}
}
@ -115,7 +119,9 @@ class UrlsettingsAction extends AccountSettingsAction
asort($services);
$this->elementStart('li');
// TRANS: Label for dropdown with URL shortener services.
$this->dropdown('urlshorteningservice', _('Shorten URLs with'),
// TRANS: Tooltip for for dropdown with URL shortener services.
$services, _('Automatic shortening service to use.'),
false, $user->urlshorteningservice);
$this->elementEnd('li');
@ -135,7 +141,8 @@ class UrlsettingsAction extends AccountSettingsAction
_('URLs in notices longer than this will be shortened, 0 means always shorten.'));
$this->elementEnd('li');
$this->elementEnd('ul');
$this->submit('save', _('Save'));
// TRANS: Button text for saving "Other settings" in profile.
$this->submit('save', _m('BUTTON','Save'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
@ -162,7 +169,8 @@ class UrlsettingsAction extends AccountSettingsAction
$urlshorteningservice = $this->trimmed('urlshorteningservice');
if (!is_null($urlshorteningservice) && strlen($urlshorteningservice) > 50) {
$this->showForm(_('URL shortening service is too long (max 50 chars).'));
// TRANS: Form validation error for form "Other settings" in user profile.
$this->showForm(_('URL shortening service is too long (maximum 50 characters).'));
return;
}
@ -192,6 +200,7 @@ class UrlsettingsAction extends AccountSettingsAction
if ($result === false) {
common_log_db_error($user, 'UPDATE', __FILE__);
// TRANS: Server error displayed when "Other" settings in user profile could not be updated on the server.
$this->serverError(_('Couldn\'t update user.'));
return;
}

View File

@ -45,7 +45,6 @@ if (!defined('STATUSNET')) {
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
class UseradminpanelAction extends AdminPanelAction
{
/**
@ -53,7 +52,6 @@ class UseradminpanelAction extends AdminPanelAction
*
* @return string page title
*/
function title()
{
// TRANS: User admin panel title
@ -65,9 +63,9 @@ class UseradminpanelAction extends AdminPanelAction
*
* @return string instructions
*/
function getInstructions()
{
// TRANS: Instruction for user admin panel.
return _('User settings for this StatusNet site');
}
@ -76,7 +74,6 @@ class UseradminpanelAction extends AdminPanelAction
*
* @return void
*/
function showForm()
{
$form = new UserAdminPanelForm($this);
@ -89,7 +86,6 @@ class UseradminpanelAction extends AdminPanelAction
*
* @return void
*/
function saveSettings()
{
static $settings = array(
@ -147,13 +143,15 @@ class UseradminpanelAction extends AdminPanelAction
// Validate biolimit
if (!Validate::number($values['profile']['biolimit'])) {
$this->clientError(_("Invalid bio limit. Must be numeric."));
// TRANS: Form validation error in user admin panel when a non-numeric character limit was set.
$this->clientError(_('Invalid bio limit. Must be numeric.'));
}
// Validate welcome text
if (mb_strlen($values['newuser']['welcome']) > 255) {
$this->clientError(_("Invalid welcome text. Max length is 255 characters."));
// TRANS: Form validation error in user admin panel when welcome text is too long.
$this->clientError(_('Invalid welcome text. Maximum length is 255 characters.'));
}
// Validate default subscription
@ -163,7 +161,9 @@ class UseradminpanelAction extends AdminPanelAction
if (empty($defuser)) {
$this->clientError(
sprintf(
_('Invalid default subscripton: \'%1$s\' is not user.'),
// TRANS: Client error displayed when trying to set a non-existing user as default subscription for new
// TRANS: users in user admin panel. %1$s is the invalid nickname.
_('Invalid default subscripton: \'%1$s\' is not a user.'),
$values['newuser']['default']
)
);
@ -179,7 +179,6 @@ class UserAdminPanelForm extends AdminForm
*
* @return int ID of the form
*/
function id()
{
return 'useradminpanel';
@ -190,7 +189,6 @@ class UserAdminPanelForm extends AdminForm
*
* @return string class of the form
*/
function formClass()
{
return 'form_settings';
@ -201,7 +199,6 @@ class UserAdminPanelForm extends AdminForm
*
* @return string URL of the action
*/
function action()
{
return common_local_url('useradminpanel');
@ -212,7 +209,6 @@ class UserAdminPanelForm extends AdminForm
*
* @return void
*/
function formData()
{
$this->out->elementStart('fieldset', array('id' => 'settings_user-profile'));
@ -220,7 +216,9 @@ class UserAdminPanelForm extends AdminForm
$this->out->elementStart('ul', 'form_data');
$this->li();
// TRANS: Field label in user admin panel for setting the character limit for the bio field.
$this->input('biolimit', _('Bio Limit'),
// TRANS: Tooltip in user admin panel for setting the character limit for the bio field.
_('Maximum length of a profile bio in characters.'),
'profile');
$this->unli();
@ -229,17 +227,22 @@ class UserAdminPanelForm extends AdminForm
$this->out->elementEnd('fieldset');
$this->out->elementStart('fieldset', array('id' => 'settings_user-newusers'));
// TRANS: Form legend in user admin panel.
$this->out->element('legend', null, _('New users'));
$this->out->elementStart('ul', 'form_data');
$this->li();
// TRANS: Field label in user admin panel for setting new user welcome text.
$this->input('welcome', _('New user welcome'),
_('Welcome text for new users (Max 255 chars).'),
// TRANS: Tooltip in user admin panel for setting new user welcome text.
_('Welcome text for new users (maximum 255 characters).'),
'newuser');
$this->unli();
$this->li();
// TRANS: Field label in user admin panel for setting default subscription for new users.
$this->input('default', _('Default subscription'),
// TRANS: Tooltip in user admin panel for setting default subscription for new users.
_('Automatically subscribe new users to this user.'),
'newuser');
$this->unli();
@ -249,21 +252,21 @@ class UserAdminPanelForm extends AdminForm
$this->out->elementEnd('fieldset');
$this->out->elementStart('fieldset', array('id' => 'settings_user-invitations'));
// TRANS: Form legend in user admin panel.
$this->out->element('legend', null, _('Invitations'));
$this->out->elementStart('ul', 'form_data');
$this->li();
// TRANS: Field label for checkbox in user admin panel for allowing users to invite friend using site e-mail.
$this->out->checkbox('invite-enabled', _('Invitations enabled'),
(bool) $this->value('enabled', 'invite'),
// TRANS: Tooltip for checkbox in user admin panel for allowing users to invite friend using site e-mail.
_('Whether to allow users to invite new users.'));
$this->unli();
$this->out->elementEnd('ul');
$this->out->elementEnd('fieldset');
}
/**
@ -278,7 +281,6 @@ class UserAdminPanelForm extends AdminForm
*
* @return void
*/
function input($setting, $title, $instructions, $section='site')
{
$this->out->input("$section-$setting", $title, $this->value($setting, $section), $instructions);
@ -289,9 +291,14 @@ class UserAdminPanelForm extends AdminForm
*
* @return void
*/
function formActions()
{
$this->out->submit('submit', _('Save'), 'submit', null, _('Save user settings'));
$this->out->submit('submit',
// TRANS: Button text to save user settings in user admin panel.
_m('BUTTON','Save'),
'submit',
null,
// TRANS: Title for button to save user settings in user admin panel.
_('Save user settings'));
}
}

View File

@ -0,0 +1,154 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Wrapper for Memcached_DataObject which knows its own schema definition.
* Builds its own damn settings from a schema definition.
*
* @author Brion Vibber <brion@status.net>
*/
abstract class Managed_DataObject extends Memcached_DataObject
{
/**
* The One True Thingy that must be defined and declared.
*/
public static abstract function schemaDef();
/**
* get/set an associative array of table columns
*
* @access public
* @return array (associative)
*/
function table()
{
// Hack for PHP 5.2 not supporting late static binding
//$table = static::schemaDef();
$table = call_user_func(array(get_class($this), 'schemaDef'));
return array_map(array($this, 'columnBitmap'), $table['fields']);
}
/**
* get/set an array of table primary keys
*
* Key info is pulled from the table definition array.
*
* @access private
* @return array
*/
function keys()
{
return array_keys($this->keyTypes());
}
/**
* Get a sequence key
*
* Returns the first serial column defined in the table, if any.
*
* @access private
* @return array (column,use_native,sequence_name)
*/
function sequenceKey()
{
$table = call_user_func(array(get_class($this), 'schemaDef'));
foreach ($table['fields'] as $name => $column) {
if ($column['type'] == 'serial') {
// We have a serial/autoincrement column.
// Declare it to be a native sequence!
return array($name, true, false);
}
}
// No sequence key on this table.
return array(false, false, false);
}
/**
* Return key definitions for DB_DataObject and Memcache_DataObject.
*
* DB_DataObject needs to know about keys that the table has; this function
* defines them.
*
* @return array key definitions
*/
function keyTypes()
{
$table = call_user_func(array(get_class($this), 'schemaDef'));
if (!empty($table['unique keys'])) {
foreach ($table['unique keys'] as $idx => $fields) {
foreach ($fields as $name) {
$keys[$name] = 'U';
}
}
}
if (!empty($table['primary key'])) {
foreach ($table['primary key'] as $name) {
$keys[$name] = 'K';
}
}
return $keys;
}
/**
* Build the appropriate DB_DataObject bitfield map for this field.
*
* @param array $column
* @return int
*/
function columnBitmap($column)
{
$type = $column['type'];
// For quoting style...
$intTypes = array('int',
'integer',
'float',
'serial',
'numeric');
if (in_array($type, $intTypes)) {
$style = DB_DATAOBJECT_INT;
} else {
$style = DB_DATAOBJECT_STR;
}
// Data type formatting style...
$formatStyles = array('blob' => DB_DATAOBJECT_BLOB,
'text' => DB_DATAOBJECT_TXT,
'date' => DB_DATAOBJECT_DATE,
'time' => DB_DATAOBJECT_TIME,
'datetime' => DB_DATAOBJECT_DATE | DB_DATAOBJECT_TIME,
'timestamp' => DB_DATAOBJECT_MYSQLTIMESTAMP);
if (isset($formatStyles[$type])) {
$style |= $formatStyles[$type];
}
// Nullable?
if (!empty($column['not null'])) {
$style |= DB_DATAOBJECT_NOTNULL;
}
return $style;
}
}

View File

@ -141,11 +141,32 @@ class Profile extends Memcached_DataObject
return true;
}
/**
* Gets either the full name (if filled) or the nickname.
*
* @return string
*/
function getBestName()
{
return ($this->fullname) ? $this->fullname : $this->nickname;
}
/**
* Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone
* if no fullname is provided.
*
* @return string
*/
function getFancyName()
{
if ($this->fullname) {
// TRANS: Full name of a profile or group followed by nickname in parens
return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
} else {
return $this->nickname;
}
}
/**
* Get the most recent notice posted by this user, if any.
*
@ -553,14 +574,14 @@ class Profile extends Memcached_DataObject
function blowFavesCache()
{
$cache = common_memcache();
$cache = Cache::instance();
if ($cache) {
// Faves don't happen chronologically, so we need to blow
// ;last cache, too
$cache->delete(common_cache_key('fave:ids_by_user:'.$this->id));
$cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last'));
$cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id));
$cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id.';last'));
$cache->delete(Cache::key('fave:ids_by_user:'.$this->id));
$cache->delete(Cache::key('fave:ids_by_user:'.$this->id.';last'));
$cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id));
$cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id.';last'));
}
$this->blowFaveCount();
}

View File

@ -0,0 +1,22 @@
<?php
/**
* Table Definition for schema_version
*/
class Schema_version extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'schema_version'; // table name
public $table_name; // varchar(64) primary_key not_null
public $checksum; // varchar(64) not_null
public $modified; // datetime() not_null
/* Static get */
function staticGet($k,$v=null)
{ return Memcached_DataObject::staticGet('Schema_version',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
}

View File

@ -234,6 +234,22 @@ class User_group extends Memcached_DataObject
return ($this->fullname) ? $this->fullname : $this->nickname;
}
/**
* Gets the full name (if filled) with nickname as a parenthetical, or the nickname alone
* if no fullname is provided.
*
* @return string
*/
function getFancyName()
{
if ($this->fullname) {
// TRANS: Full name of a profile or group followed by nickname in parens
return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname);
} else {
return $this->nickname;
}
}
function getAliases()
{
$aliases = array();

View File

@ -509,6 +509,14 @@ replied_id = 1
notice_id = K
profile_id = K
[schema_version]
table_name = 130
checksum = 130
modified = 384
[schema_version__keys]
table_name = K
[session]
id = 130
session_data = 34

1026
db/core.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -56,6 +56,15 @@ var SN = { // StatusNet
NoticeDataGeoCookie: 'NoticeDataGeo',
NoticeDataGeoSelected: 'notice_data-geo_selected',
StatusNetInstance:'StatusNetInstance'
},
},
messages: {},
msg: function(key) {
if (typeof SN.messages[key] == "undefined") {
return '[' + key + ']';
} else {
return SN.messages[key];
}
},
@ -416,7 +425,7 @@ var SN = { // StatusNet
});
return false;
});
}).attr('title', SN.msg('showmore_tooltip'));
}
else {
$.fn.jOverlay.options = {

View File

@ -283,6 +283,7 @@ class Action extends HTMLOutputter // lawsuit
if (Event::handle('StartShowStatusNetScripts', array($this)) &&
Event::handle('StartShowLaconicaScripts', array($this))) {
$this->script('util.js');
$this->showScriptMessages();
// Frame-busting code to avoid clickjacking attacks.
$this->inlineScript('if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
Event::handle('EndShowStatusNetScripts', array($this));
@ -292,6 +293,54 @@ class Action extends HTMLOutputter // lawsuit
}
}
/**
* Exports a map of localized text strings to JavaScript code.
*
* Plugins can add to what's exported by hooking the StartScriptMessages or EndScriptMessages
* events and appending to the array. Try to avoid adding strings that won't be used, as
* they'll be added to HTML output.
*/
function showScriptMessages()
{
$messages = array();
if (Event::handle('StartScriptMessages', array($this, &$messages))) {
// Common messages needed for timeline views etc...
// TRANS: Localized tooltip for '...' expansion button on overlong remote messages.
$messages['showmore_tooltip'] = _m('TOOLTIP', 'Show more');
$messages = array_merge($messages, $this->getScriptMessages());
}
Event::handle('EndScriptMessages', array($this, &$messages));
if ($messages) {
$this->inlineScript('SN.messages=' . json_encode($messages));
}
return $messages;
}
/**
* If the action will need localizable text strings, export them here like so:
*
* return array('pool_deepend' => _('Deep end'),
* 'pool_shallow' => _('Shallow end'));
*
* The exported map will be available via SN.msg() to JS code:
*
* $('#pool').html('<div class="deepend"></div><div class="shallow"></div>');
* $('#pool .deepend').text(SN.msg('pool_deepend'));
* $('#pool .shallow').text(SN.msg('pool_shallow'));
*
* Exports a map of localized text strings to JavaScript code.
*
* Plugins can add to what's exported on any action by hooking the StartScriptMessages or
* EndScriptMessages events and appending to the array. Try to avoid adding strings that won't
* be used, as they'll be added to HTML output.
*/
function getScriptMessages()
{
return array();
}
/**
* Show OpenSearch headers
*
@ -824,16 +873,17 @@ class Action extends HTMLOutputter // lawsuit
// TRANS: Secondary navigation menu option leading to privacy policy.
_('Privacy'));
$this->menuItem(common_local_url('doc', array('title' => 'source')),
// TRANS: Secondary navigation menu option.
// TRANS: Secondary navigation menu option. Leads to information about StatusNet and its license.
_('Source'));
$this->menuItem(common_local_url('version'),
// TRANS: Secondary navigation menu option leading to version information on the StatusNet site.
_('Version'));
$this->menuItem(common_local_url('doc', array('title' => 'contact')),
// TRANS: Secondary navigation menu option leading to contact information on the StatusNet site.
// TRANS: Secondary navigation menu option leading to e-mail contact information on the
// TRANS: StatusNet site, where to report bugs, ...
_('Contact'));
$this->menuItem(common_local_url('doc', array('title' => 'badge')),
// TRANS: Secondary navigation menu option.
// TRANS: Secondary navigation menu option. Leads to information about embedding a timeline widget.
_('Badge'));
Event::handle('EndSecondaryNav', array($this));
}

View File

@ -423,7 +423,7 @@ class WhoisCommand extends Command
// TRANS: Whois output.
// TRANS: %1$s nickname of the queried user, %2$s is their profile URL.
$whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname,
$whois = sprintf(_m('WHOIS',"%1\$s (%2\$s)"), $recipient->nickname,
$recipient->profileurl);
if ($recipient->fullname) {
// TRANS: Whois output. %s is the full name of the queried user.
@ -483,9 +483,11 @@ class MessageCommand extends Command
if (Message::contentTooLong($this->text)) {
// XXX: i18n. Needs plural support.
// TRANS: Message given if content is too long.
// TRANS: Message given if content is too long. %1$sd is used for plural.
// TRANS: %1$d is the maximum number of characters, %2$d is the number of submitted characters.
$channel->error($this->user, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'),
$channel->error($this->user, sprintf(_m('Message too long - maximum is %1$d character, you sent %2$d.',
'Message too long - maximum is %1$d characters, you sent %2$d.',
Message::maxContent()),
Message::maxContent(), mb_strlen($this->text)));
return;
}
@ -584,9 +586,11 @@ class ReplyCommand extends Command
if (Notice::contentTooLong($this->text)) {
// XXX: i18n. Needs plural support.
// TRANS: Message given if content of a notice for a reply is too long.
// TRANS: Message given if content of a notice for a reply is too long. %1$d is used for plural.
// TRANS: %1$d is the maximum number of characters, %2$d is the number of submitted characters.
$channel->error($this->user, sprintf(_('Notice too long - maximum is %1$d characters, you sent %2$d.'),
$channel->error($this->user, sprintf(_m('Notice too long - maximum is %1$d character, you sent %2$d.',
'Notice too long - maximum is %1$d characters, you sent %2$d.',
Notice::maxContent()),
Notice::maxContent(), mb_strlen($this->text)));
return;
}

View File

@ -1,7 +1,7 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
* Copyright (C) 2008-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -19,131 +19,13 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
// @fixme shouldn't this be in index.php instead?
//exit with 200 response, if this is checking fancy from the installer
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
define('STATUSNET_VERSION', '0.9.6');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
define('STATUSNET_CODENAME', 'Man on the Moon');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
define('AVATAR_MINI_SIZE', 24);
define('NOTICES_PER_PAGE', 20);
define('PROFILES_PER_PAGE', 20);
define('FOREIGN_NOTICE_SEND', 1);
define('FOREIGN_NOTICE_RECV', 2);
define('FOREIGN_NOTICE_SEND_REPLY', 4);
define('FOREIGN_FRIEND_SEND', 1);
define('FOREIGN_FRIEND_RECV', 2);
define('NOTICE_INBOX_SOURCE_SUB', 1);
define('NOTICE_INBOX_SOURCE_GROUP', 2);
define('NOTICE_INBOX_SOURCE_REPLY', 3);
define('NOTICE_INBOX_SOURCE_FORWARD', 4);
define('NOTICE_INBOX_SOURCE_GATEWAY', -1);
# append our extlib dir as the last-resort place to find libs
set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/extlib/');
// To protect against upstream libraries which haven't updated
// for PHP 5.3 where dl() function may not be present...
if (!function_exists('dl')) {
// function_exists() returns false for things in disable_functions,
// but they still exist and we'll die if we try to redefine them.
//
// Fortunately trying to call the disabled one will only trigger
// a warning, not a fatal, so it's safe to leave it for our case.
// Callers will be suppressing warnings anyway.
$disabled = array_filter(array_map('trim', explode(',', ini_get('disable_functions'))));
if (!in_array('dl', $disabled)) {
function dl($library) {
return false;
}
}
}
# global configuration object
require_once('PEAR.php');
require_once('PEAR/Exception.php');
require_once('DB/DataObject.php');
require_once('DB/DataObject/Cast.php'); # for dates
require_once(INSTALLDIR.'/lib/language.php');
// This gets included before the config file, so that admin code and plugins
// can use it
require_once(INSTALLDIR.'/lib/event.php');
require_once(INSTALLDIR.'/lib/plugin.php');
function addPlugin($name, $attrs = null)
{
return StatusNet::addPlugin($name, $attrs);
}
function _have_config()
{
return StatusNet::haveConfig();
}
function __autoload($cls)
{
if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
require_once(INSTALLDIR.'/classes/' . $cls . '.php');
} else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) {
require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php');
} else if (mb_substr($cls, -6) == 'Action' &&
file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) {
require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
} else if ($cls == 'OAuthRequest') {
require_once('OAuth.php');
} else {
Event::handle('Autoload', array(&$cls));
}
}
// XXX: how many of these could be auto-loaded on use?
// XXX: note that these files should not use config options
// at compile time since DB config options are not yet loaded.
require_once 'Validate.php';
require_once 'markdown.php';
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
require_once INSTALLDIR.'/lib/util.php';
require_once INSTALLDIR.'/lib/action.php';
require_once INSTALLDIR.'/lib/mail.php';
require_once INSTALLDIR.'/lib/subs.php';
require_once INSTALLDIR.'/lib/clientexception.php';
require_once INSTALLDIR.'/lib/serverexception.php';
//set PEAR error handling to use regular PHP exceptions
function PEAR_ErrorToPEAR_Exception($err)
{
//DB_DataObject throws error when an empty set would be returned
//That behavior is weird, and not how the rest of StatusNet works.
//So just ignore those errors.
if ($err->getCode() == DB_DATAOBJECT_ERROR_NODATA) {
return;
}
if ($err->getCode()) {
throw new PEAR_Exception($err->getMessage(), $err->getCode());
}
throw new PEAR_Exception($err->getMessage());
}
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');
// All the fun stuff to actually initialize StatusNet's framework code,
// without loading up a site configuration.
require_once INSTALLDIR . '/lib/framework.php';
try {
StatusNet::init(@$server, @$path, @$conffile);

View File

@ -75,7 +75,8 @@ $default =
'schemacheck' => 'runtime', // 'runtime' or 'script'
'annotate_queries' => false, // true to add caller comments to queries, eg /* POST Notice::saveNew */
'log_queries' => false, // true to log all DB queries
'log_slow_queries' => 0), // if set, log queries taking over N seconds
'log_slow_queries' => 0, // if set, log queries taking over N seconds
'mysql_foreign_keys' => false), // if set, enables experimental foreign key support on MySQL
'syslog' =>
array('appname' => 'statusnet', # for syslog
'priority' => 'debug', # XXX: currently ignored

View File

@ -62,6 +62,7 @@ class FeedList extends Widget
if (!empty($feeds)) {
$this->out->elementStart('div', array('id' => 'export_data',
'class' => 'section'));
// TRANS: Header for feed links (h2).
$this->out->element('h2', null, _('Feeds'));
$this->out->elementStart('ul', array('class' => 'xoxo'));

143
lib/framework.php Normal file
View File

@ -0,0 +1,143 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
define('STATUSNET_VERSION', '0.9.6');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
define('STATUSNET_CODENAME', 'Man on the Moon');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
define('AVATAR_MINI_SIZE', 24);
define('NOTICES_PER_PAGE', 20);
define('PROFILES_PER_PAGE', 20);
define('FOREIGN_NOTICE_SEND', 1);
define('FOREIGN_NOTICE_RECV', 2);
define('FOREIGN_NOTICE_SEND_REPLY', 4);
define('FOREIGN_FRIEND_SEND', 1);
define('FOREIGN_FRIEND_RECV', 2);
define('NOTICE_INBOX_SOURCE_SUB', 1);
define('NOTICE_INBOX_SOURCE_GROUP', 2);
define('NOTICE_INBOX_SOURCE_REPLY', 3);
define('NOTICE_INBOX_SOURCE_FORWARD', 4);
define('NOTICE_INBOX_SOURCE_GATEWAY', -1);
# append our extlib dir as the last-resort place to find libs
set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/extlib/');
// To protect against upstream libraries which haven't updated
// for PHP 5.3 where dl() function may not be present...
if (!function_exists('dl')) {
// function_exists() returns false for things in disable_functions,
// but they still exist and we'll die if we try to redefine them.
//
// Fortunately trying to call the disabled one will only trigger
// a warning, not a fatal, so it's safe to leave it for our case.
// Callers will be suppressing warnings anyway.
$disabled = array_filter(array_map('trim', explode(',', ini_get('disable_functions'))));
if (!in_array('dl', $disabled)) {
function dl($library) {
return false;
}
}
}
# global configuration object
require_once('PEAR.php');
require_once('PEAR/Exception.php');
require_once('DB/DataObject.php');
require_once('DB/DataObject/Cast.php'); # for dates
require_once(INSTALLDIR.'/lib/language.php');
// This gets included before the config file, so that admin code and plugins
// can use it
require_once(INSTALLDIR.'/lib/event.php');
require_once(INSTALLDIR.'/lib/plugin.php');
function addPlugin($name, $attrs = null)
{
return StatusNet::addPlugin($name, $attrs);
}
function _have_config()
{
return StatusNet::haveConfig();
}
function __autoload($cls)
{
if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
require_once(INSTALLDIR.'/classes/' . $cls . '.php');
} else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) {
require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php');
} else if (mb_substr($cls, -6) == 'Action' &&
file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) {
require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
} else if ($cls == 'OAuthRequest') {
require_once('OAuth.php');
} else {
Event::handle('Autoload', array(&$cls));
}
}
// XXX: how many of these could be auto-loaded on use?
// XXX: note that these files should not use config options
// at compile time since DB config options are not yet loaded.
require_once 'Validate.php';
require_once 'markdown.php';
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
require_once INSTALLDIR.'/lib/util.php';
require_once INSTALLDIR.'/lib/action.php';
require_once INSTALLDIR.'/lib/mail.php';
require_once INSTALLDIR.'/lib/subs.php';
require_once INSTALLDIR.'/lib/clientexception.php';
require_once INSTALLDIR.'/lib/serverexception.php';
//set PEAR error handling to use regular PHP exceptions
function PEAR_ErrorToPEAR_Exception($err)
{
//DB_DataObject throws error when an empty set would be returned
//That behavior is weird, and not how the rest of StatusNet works.
//So just ignore those errors.
if ($err->getCode() == DB_DATAOBJECT_ERROR_NODATA) {
return;
}
if ($err->getCode()) {
throw new PEAR_Exception($err->getMessage(), $err->getCode());
}
throw new PEAR_Exception($err->getMessage());
}
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');

View File

@ -160,14 +160,17 @@ class GroupEditForm extends Form
$this->out->elementStart('li');
$this->out->input('homepage', _('Homepage'),
($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
_('URL of the homepage or blog of the group or topic'));
_('URL of the homepage or blog of the group or topic.'));
$this->out->elementEnd('li');
$this->out->elementStart('li');
$desclimit = User_group::maxDescription();
if ($desclimit == 0) {
$descinstr = _('Describe the group or topic');
} else {
$descinstr = sprintf(_('Describe the group or topic in %d characters'), $desclimit);
$descinstr = sprintf(_m('Describe the group or topic in %d character or less',
'Describe the group or topic in %d characters or less',
$desclimit),
$desclimit);
}
$this->out->textarea('description', _('Description'),
($this->out->arg('description')) ? $this->out->arg('description') : $description,
@ -176,7 +179,7 @@ class GroupEditForm extends Form
$this->out->elementStart('li');
$this->out->input('location', _('Location'),
($this->out->arg('location')) ? $this->out->arg('location') : $location,
_('Location for the group, if any, like "City, State (or Region), Country"'));
_('Location for the group, if any, like "City, State (or Region), Country".'));
$this->out->elementEnd('li');
if (common_config('group', 'maxaliases') > 0) {
$aliases = (empty($this->group)) ? array() : $this->group->getAliases();
@ -184,7 +187,9 @@ class GroupEditForm extends Form
$this->out->input('aliases', _('Aliases'),
($this->out->arg('aliases')) ? $this->out->arg('aliases') :
(!empty($aliases)) ? implode(' ', $aliases) : '',
sprintf(_('Extra nicknames for the group, comma- or space- separated, max %d'),
sprintf(_m('Extra nicknames for the group, separated with commas or spaces. Maximum %d alias allowed.',
'Extra nicknames for the group, separated with commas or spaces. Maximum %d aliases allowed.',
common_config('group', 'maxaliases')),
common_config('group', 'maxaliases')));;
$this->out->elementEnd('li');
}
@ -199,6 +204,6 @@ class GroupEditForm extends Form
function formActions()
{
$this->out->submit('submit', _('Save'));
$this->out->submit('submit', _m('BUTTON','Save'));
}
}

View File

@ -85,6 +85,8 @@ class ImageFile
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
// TRANS: Exception thrown when too large a file is uploaded.
// TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB".
throw new Exception(sprintf(_('That file is too big. The maximum file size is %s.'),
ImageFile::maxFileSize()));
return;
@ -241,11 +243,16 @@ class ImageFile
$value = ImageFile::maxFileSizeInt();
if ($value > 1024 * 1024) {
return ($value/(1024*1024)) . _('MB');
$value = $value/(1024*1024);
// TRANS: Number of megabytes. %d is the number.
return sprintf(_m('%dMB','%dMB',$value),$value);
} else if ($value > 1024) {
return ($value/(1024)) . _('kB');
$value = $value/1024;
// TRANS: Number of kilobytes. %d is the number.
return sprintf(_m('%dkB','%dkB',$value),$value);
} else {
return $value;
// TRANS: Number of bytes. %d is the number.
return sprintf(_m('%dB','%dB',$value),$value);
}
}

View File

@ -2,7 +2,7 @@
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009, StatusNet, Inc.
* Copyright (C) 2009-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -32,9 +32,10 @@
* @author Sarven Capadisli <csarven@status.net>
* @author Tom Adams <tom@holizz.com>
* @author Zach Copley <zach@status.net>
* @copyright 2009-2010 StatusNet, Inc http://status.net
* @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @license GNU Affero General Public License http://www.gnu.org/licenses/
* @version 0.9.x
* @version 1.0.x
* @link http://status.net
*/
@ -53,12 +54,12 @@ abstract class Installer
'mysql' => array(
'name' => 'MySQL',
'check_module' => 'mysqli',
'installer' => 'mysql_db_installer',
'scheme' => 'mysqli', // DSN prefix for PEAR::DB
),
'pgsql' => array(
'name' => 'PostgreSQL',
'check_module' => 'pgsql',
'installer' => 'pgsql_db_installer',
'scheme' => 'pgsql', // DSN prefix for PEAR::DB
),
);
@ -254,6 +255,7 @@ abstract class Installer
* Set up the database with the appropriate function for the selected type...
* Saves database info into $this->db.
*
* @fixme escape things in the connection string in case we have a funny pass etc
* @return mixed array of database connection params on success, false on failure
*/
function setupDatabase()
@ -261,119 +263,39 @@ abstract class Installer
if ($this->db) {
throw new Exception("Bad order of operations: DB already set up.");
}
$method = self::$dbModules[$this->dbtype]['installer'];
$db = call_user_func(array($this, $method),
$this->host,
$this->database,
$this->username,
$this->password);
$this->db = $db;
return $this->db;
}
/**
* Set up a database on PostgreSQL.
* Will output status updates during the operation.
*
* @param string $host
* @param string $database
* @param string $username
* @param string $password
* @return mixed array of database connection params on success, false on failure
*
* @fixme escape things in the connection string in case we have a funny pass etc
*/
function Pgsql_Db_installer($host, $database, $username, $password)
{
$connstring = "dbname=$database host=$host user=$username";
//No password would mean trust authentication used.
if (!empty($password)) {
$connstring .= " password=$password";
}
$this->updateStatus("Starting installation...");
if (empty($this->password)) {
$auth = '';
} else {
$auth = ":$this->password";
}
$scheme = self::$dbModules[$this->dbtype]['scheme'];
$dsn = "{$scheme}://{$this->username}{$auth}@{$this->host}/{$this->database}";
$this->updateStatus("Checking database...");
$conn = pg_connect($connstring);
$conn = $this->connectDatabase($dsn);
if ($conn ===false) {
$this->updateStatus("Failed to connect to database: $connstring");
return false;
}
//ensure database encoding is UTF8
$record = pg_fetch_object(pg_query($conn, 'SHOW server_encoding'));
if ($record->server_encoding != 'UTF8') {
$this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding));
return false;
}
$this->updateStatus("Running database script...");
//wrap in transaction;
pg_query($conn, 'BEGIN');
$res = $this->runDbScript('statusnet_pg.sql', $conn, 'pgsql');
if ($res === false) {
$this->updateStatus("Can't run database script.", true);
return false;
}
foreach (array('sms_carrier' => 'SMS carrier',
'notice_source' => 'notice source',
'foreign_services' => 'foreign service')
as $scr => $name) {
$this->updateStatus(sprintf("Adding %s data to database...", $name));
$res = $this->runDbScript($scr.'.sql', $conn, 'pgsql');
if ($res === false) {
$this->updateStatus(sprintf("Can't run %s script.", $name), true);
// ensure database encoding is UTF8
if ($this->dbtype == 'mysql') {
// @fixme utf8m4 support for mysql 5.5?
// Force the comms charset to utf8 for sanity
// This doesn't currently work. :P
//$conn->executes('set names utf8');
} else if ($this->dbtype == 'pgsql') {
$record = $conn->getRow('SHOW server_encoding');
if ($record->server_encoding != 'UTF8') {
$this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding));
return false;
}
}
pg_query($conn, 'COMMIT');
if (empty($password)) {
$sqlUrl = "pgsql://$username@$host/$database";
} else {
$sqlUrl = "pgsql://$username:$password@$host/$database";
}
$db = array('type' => 'pgsql', 'database' => $sqlUrl);
return $db;
}
/**
* Set up a database on MySQL.
* Will output status updates during the operation.
*
* @param string $host
* @param string $database
* @param string $username
* @param string $password
* @return mixed array of database connection params on success, false on failure
*
* @fixme escape things in the connection string in case we have a funny pass etc
*/
function Mysql_Db_installer($host, $database, $username, $password)
{
$this->updateStatus("Starting installation...");
$this->updateStatus("Checking database...");
$conn = mysqli_init();
if (!$conn->real_connect($host, $username, $password)) {
$this->updateStatus("Can't connect to server '$host' as '$username'.", true);
return false;
}
$this->updateStatus("Changing to database...");
if (!$conn->select_db($database)) {
$this->updateStatus("Can't change to database.", true);
$res = $this->updateStatus("Creating database tables...");
if (!$this->createCoreTables($conn)) {
$this->updateStatus("Error creating tables.", true);
return false;
}
$this->updateStatus("Running database script...");
$res = $this->runDbScript('statusnet.sql', $conn);
if ($res === false) {
$this->updateStatus("Can't run database script.", true);
return false;
}
foreach (array('sms_carrier' => 'SMS carrier',
'notice_source' => 'notice source',
'foreign_services' => 'foreign service')
@ -386,11 +308,54 @@ abstract class Installer
}
}
$sqlUrl = "mysqli://$username:$password@$host/$database";
$db = array('type' => 'mysql', 'database' => $sqlUrl);
$db = array('type' => $this->dbtype, 'database' => $dsn);
return $db;
}
/**
* Open a connection to the database.
*
* @param <type> $dsn
* @return <type>
*/
function connectDatabase($dsn)
{
// @fixme move this someplace more sensible
//set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());
require_once 'DB.php';
return DB::connect($dsn);
}
/**
* Create core tables on the given database connection.
*
* @param DB_common $conn
*/
function createCoreTables(DB_common $conn)
{
$schema = Schema::get($conn);
$tableDefs = $this->getCoreSchema();
foreach ($tableDefs as $name => $def) {
if (defined('DEBUG_INSTALLER')) {
echo " $name ";
}
$schema->ensureTable($name, $def);
}
return true;
}
/**
* Fetch the core table schema definitions.
*
* @return array of table names => table def arrays
*/
function getCoreSchema()
{
$schema = array();
include INSTALLDIR . '/db/core.php';
return $schema;
}
/**
* Return a parseable PHP literal for the given value.
* This will include quotes for strings, etc.
@ -463,13 +428,12 @@ abstract class Installer
/**
* Install schema into the database
*
* @param string $filename location of database schema file
* @param dbconn $conn connection to database
* @param string $type type of database, currently mysql or pgsql
* @param string $filename location of database schema file
* @param DB_common $conn connection to database
*
* @return boolean - indicating success or failure
*/
function runDbScript($filename, $conn, $type = 'mysqli')
function runDbScript($filename, DB_common $conn)
{
$sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename));
$stmts = explode(';', $sql);
@ -478,26 +442,12 @@ abstract class Installer
if (!mb_strlen($stmt)) {
continue;
}
// FIXME: use PEAR::DB or PDO instead of our own switch
switch ($type) {
case 'mysqli':
$res = $conn->query($stmt);
if ($res === false) {
$error = $conn->error;
}
break;
case 'pgsql':
$res = pg_query($conn, $stmt);
if ($res === false) {
$error = pg_last_error();
}
break;
default:
$this->updateStatus("runDbScript() error: unknown database type ". $type ." provided.");
}
if ($res === false) {
try {
$res = $conn->simpleQuery($stmt);
} catch (Exception $e) {
$error = $e->getMessage();
$this->updateStatus("ERROR ($error) for SQL '$stmt'");
return $res;
return false;
}
}
return true;
@ -510,9 +460,6 @@ abstract class Installer
*/
function registerInitialUser()
{
define('STATUSNET', true);
define('LACONICA', true); // compatibility
require_once INSTALLDIR . '/lib/common.php';
$data = array('nickname' => $this->adminNick,
@ -559,10 +506,22 @@ abstract class Installer
*/
function doInstall()
{
$this->db = $this->setupDatabase();
$this->updateStatus("Initializing...");
ini_set('display_errors', 1);
error_reporting(E_ALL);
define('STATUSNET', 1);
require_once INSTALLDIR . '/lib/framework.php';
StatusNet::initDefaults($this->server, $this->path);
if (!$this->db) {
// database connection failed, do not move on to create config file.
try {
$this->db = $this->setupDatabase();
if (!$this->db) {
// database connection failed, do not move on to create config file.
return false;
}
} catch (Exception $e) {
// Lower-level DB error!
$this->updateStatus("Database error: " . $e->getMessage(), true);
return false;
}

View File

@ -593,6 +593,10 @@ function mail_notify_fave($other, $user, $notice)
}
$profile = $user->getProfile();
if ($other->hasBlocked($profile)) {
// If the author has blocked us, don't spam them with a notification.
return;
}
$bestname = $profile->getBestName();

View File

@ -57,8 +57,9 @@ class MailHandler
$msg = $this->cleanup_msg($msg);
$msg = common_shorten_links($msg);
if (Notice::contentTooLong($msg)) {
$this->error($from, sprintf(_('That\'s too long. '.
'Max notice size is %d chars.'),
$this->error($from, sprintf(_('That\'s too long. Maximum notice size is %d character.',
'That\'s too long. Maximum notice size is %d characters.',
Notice::maxContent()),
Notice::maxContent()));
}

View File

@ -72,72 +72,127 @@ class MysqlSchema extends Schema
*
* Throws an exception if the table is not found.
*
* @param string $name Name of the table to get
* @param string $table Name of the table to get
*
* @return TableDef tabledef for that table.
* @throws SchemaTableMissingException
*/
public function getTableDef($name)
public function getTableDef($table)
{
$query = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS " .
"WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'";
$schema = $this->conn->dsn['database'];
$sql = sprintf($query, $schema, $name);
$res = $this->conn->query($sql);
$def = array();
$hasKeys = false;
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
if ($res->numRows() == 0) {
$res->free();
throw new SchemaTableMissingException("No such table: $name");
// Pull column data from INFORMATION_SCHEMA
$columns = $this->fetchMetaInfo($table, 'COLUMNS', 'ORDINAL_POSITION');
if (count($columns) == 0) {
throw new SchemaTableMissingException("No such table: $table");
}
$td = new TableDef();
foreach ($columns as $row) {
$td->name = $name;
$td->columns = array();
$name = $row['COLUMN_NAME'];
$field = array();
$row = array();
// warning -- 'unsigned' attr on numbers isn't given in DATA_TYPE and friends.
// It is stuck in on COLUMN_TYPE though (eg 'bigint(20) unsigned')
$field['type'] = $type = $row['DATA_TYPE'];
while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
$cd = new ColumnDef();
$cd->name = $row['COLUMN_NAME'];
$packed = $row['COLUMN_TYPE'];
if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
$cd->type = $match[1];
$cd->size = $match[2];
} else {
$cd->type = $packed;
if ($type == 'char' || $type == 'varchar') {
if ($row['CHARACTER_MAXIMUM_LENGTH'] !== null) {
$field['length'] = intval($row['CHARACTER_MAXIMUM_LENGTH']);
}
}
if ($type == 'decimal') {
// Other int types may report these values, but they're irrelevant.
// Just ignore them!
if ($row['NUMERIC_PRECISION'] !== null) {
$field['precision'] = intval($row['NUMERIC_PRECISION']);
}
if ($row['NUMERIC_SCALE'] !== null) {
$field['scale'] = intval($row['NUMERIC_SCALE']);
}
}
if ($row['IS_NULLABLE'] == 'NO') {
$field['not null'] = true;
}
if ($row['COLUMN_DEFAULT'] !== null) {
// Hack for timestamp cols
if ($type == 'timestamp' && $row['COLUMN_DEFAULT'] == 'CURRENT_TIMESTAMP') {
// skip
} else {
$field['default'] = $row['COLUMN_DEFAULT'];
if ($this->isNumericType($type)) {
$field['default'] = intval($field['default']);
}
}
}
if ($row['COLUMN_KEY'] !== null) {
// We'll need to look up key info...
$hasKeys = true;
}
if ($row['COLUMN_COMMENT'] !== null && $row['COLUMN_COMMENT'] != '') {
$field['description'] = $row['COLUMN_COMMENT'];
}
$cd->nullable = ($row['IS_NULLABLE'] == 'YES') ? true : false;
$cd->key = $row['COLUMN_KEY'];
$cd->default = $row['COLUMN_DEFAULT'];
$cd->extra = $row['EXTRA'];
// Autoincrement is stuck into the extra column.
// Pull it out so we don't accidentally mod it every time...
$extra = preg_replace('/(^|\s)auto_increment(\s|$)/i', '$1$2', $cd->extra);
if ($extra != $cd->extra) {
$cd->extra = trim($extra);
$cd->auto_increment = true;
$extra = $row['EXTRA'];
if ($extra) {
if (preg_match('/(^|\s)auto_increment(\s|$)/i', $extra)) {
$field['auto_increment'] = true;
}
// $row['EXTRA'] may contain 'on update CURRENT_TIMESTAMP'
// ^ ...... how to specify?
}
// mysql extensions -- not (yet) used by base class
$cd->charset = $row['CHARACTER_SET_NAME'];
$cd->collate = $row['COLLATION_NAME'];
if ($row['CHARACTER_SET_NAME'] !== null) {
// @fixme check against defaults?
//$def['charset'] = $row['CHARACTER_SET_NAME'];
//$def['collate'] = $row['COLLATION_NAME'];
}
$td->columns[] = $cd;
$def['fields'][$name] = $field;
}
$res->free();
return $td;
if ($hasKeys) {
// INFORMATION_SCHEMA's CONSTRAINTS and KEY_COLUMN_USAGE tables give
// good info on primary and unique keys but don't list ANY info on
// multi-value keys, which is lame-o. Sigh.
//
// Let's go old school and use SHOW INDEX :D
//
$keyInfo = $this->fetchIndexInfo($table);
$keys = array();
foreach ($keyInfo as $row) {
$name = $row['Key_name'];
$column = $row['Column_name'];
if (!isset($keys[$name])) {
$keys[$name] = array();
}
$keys[$name][] = $column;
if ($name == 'PRIMARY') {
$type = 'primary key';
} else if ($row['Non_unique'] == 0) {
$type = 'unique keys';
} else if ($row['Index_type'] == 'FULLTEXT') {
$type = 'fulltext indexes';
} else {
$type = 'indexes';
}
$keyTypes[$name] = $type;
}
foreach ($keyTypes as $name => $type) {
if ($type == 'primary key') {
// there can be only one
$def[$type] = $keys[$name];
} else {
$def[$type][$name] = $keys[$name];
}
}
}
return $def;
}
/**
@ -150,127 +205,81 @@ class MysqlSchema extends Schema
function getTableProperties($table, $props)
{
$query = "SELECT %s FROM INFORMATION_SCHEMA.TABLES " .
"WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'";
$schema = $this->conn->dsn['database'];
$sql = sprintf($query, implode(',', $props), $schema, $table);
$res = $this->conn->query($sql);
$row = array();
$ok = $res->fetchInto($row, DB_FETCHMODE_ASSOC);
$res->free();
if ($ok) {
return $row;
$data = $this->fetchMetaInfo($table, 'TABLES');
if ($data) {
return $data[0];
} else {
throw new SchemaTableMissingException("No such table: $table");
}
}
/**
* Gets a ColumnDef object for a single column.
* Pull some INFORMATION.SCHEMA data for the given table.
*
* Throws an exception if the table is not found.
*
* @param string $table name of the table
* @param string $column name of the column
*
* @return ColumnDef definition of the column or null
* if not found.
* @param string $table
* @return array of arrays
*/
public function getColumnDef($table, $column)
function fetchMetaInfo($table, $infoTable, $orderBy=null)
{
$td = $this->getTableDef($table);
foreach ($td->columns as $cd) {
if ($cd->name == $column) {
return $cd;
}
$query = "SELECT * FROM INFORMATION_SCHEMA.%s " .
"WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'";
$schema = $this->conn->dsn['database'];
$sql = sprintf($query, $infoTable, $schema, $table);
if ($orderBy) {
$sql .= ' ORDER BY ' . $orderBy;
}
return null;
return $this->fetchQueryData($sql);
}
/**
* Creates a table with the given names and columns.
* Pull 'SHOW INDEX' data for the given table.
*
* @param string $name Name of the table
* @param array $columns Array of ColumnDef objects
* for new table.
*
* @return boolean success flag
* @param string $table
* @return array of arrays
*/
public function createTable($name, $columns)
function fetchIndexInfo($table)
{
$uniques = array();
$primary = array();
$indices = array();
$sql = "CREATE TABLE $name (\n";
for ($i = 0; $i < count($columns); $i++) {
$cd =& $columns[$i];
if ($i > 0) {
$sql .= ",\n";
}
$sql .= $this->_columnSql($cd);
}
$idx = $this->_indexList($columns);
if ($idx['primary']) {
$sql .= ",\nconstraint primary key (" . implode(',', $idx['primary']) . ")";
}
foreach ($idx['uniques'] as $u) {
$key = $this->_uniqueKey($name, $u);
$sql .= ",\nunique index $key ($u)";
}
foreach ($idx['indices'] as $i) {
$key = $this->_key($name, $i);
$sql .= ",\nindex $key ($i)";
}
$sql .= ") ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; ";
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
$query = "SHOW INDEX FROM `%s`";
$sql = sprintf($query, $table);
return $this->fetchQueryData($sql);
}
/**
* Look over a list of column definitions and list up which
* indices will be present
* Append an SQL statement with an index definition for a full-text search
* index over one or more columns on a table.
*
* @param array $statements
* @param string $table
* @param string $name
* @param array $def
*/
private function _indexList(array $columns)
function appendCreateFulltextIndex(array &$statements, $table, $name, array $def)
{
$list = array('uniques' => array(),
'primary' => array(),
'indices' => array());
foreach ($columns as $cd) {
switch ($cd->key) {
case 'UNI':
$list['uniques'][] = $cd->name;
break;
case 'PRI':
$list['primary'][] = $cd->name;
break;
case 'MUL':
$list['indices'][] = $cd->name;
break;
}
$statements[] = "CREATE FULLTEXT INDEX $name ON $table " . $this->buildIndexList($def);
}
/**
* Close out a 'create table' SQL statement.
*
* @param string $name
* @param array $def
* @return string;
*
* @fixme ENGINE may need to be set differently in some cases,
* such as to support fulltext index.
*/
function endCreateTable($name, array $def)
{
$engine = $this->preferredEngine($def);
return ") ENGINE=$engine CHARACTER SET utf8 COLLATE utf8_bin";
}
function preferredEngine($def)
{
if (!empty($def['fulltext indexes'])) {
return 'MyISAM';
}
return $list;
return 'InnoDB';
}
/**
@ -289,344 +298,46 @@ class MysqlSchema extends Schema
return "{$tableName}_{$columnName}_idx";
}
/**
* Drops a table from the schema
* MySQL doesn't take 'DROP CONSTRAINT', need to treat unique keys as
* if they were indexes here.
*
* Throws an exception if the table is not found.
*
* @param string $name Name of the table to drop
*
* @return boolean success flag
* @param array $phrase
* @param <type> $keyName MySQL
*/
public function dropTable($name)
function appendAlterDropUnique(array &$phrase, $keyName)
{
$res = $this->conn->query("DROP TABLE $name");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
$phrase[] = 'DROP INDEX ' . $keyName;
}
/**
* Adds an index to a table.
*
* If no name is provided, a name will be made up based
* on the table name and column names.
*
* Throws an exception on database error, esp. if the table
* does not exist.
*
* @param string $table Name of the table
* @param array $columnNames Name of columns to index
* @param string $name (Optional) name of the index
*
* @return boolean success flag
* Throw some table metadata onto the ALTER TABLE if we have a mismatch
* in expected type, collation.
*/
public function createIndex($table, $columnNames, $name=null)
function appendAlterExtras(array &$phrase, $tableName, array $def)
{
if (!is_array($columnNames)) {
$columnNames = array($columnNames);
}
if (empty($name)) {
$name = "{$table}_".implode("_", $columnNames)."_idx";
}
$res = $this->conn->query("ALTER TABLE $table ".
"ADD INDEX $name (".
implode(",", $columnNames).")");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Drops a named index from a table.
*
* @param string $table name of the table the index is on.
* @param string $name name of the index
*
* @return boolean success flag
*/
public function dropIndex($table, $name)
{
$res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Adds a column to a table
*
* @param string $table name of the table
* @param ColumnDef $columndef Definition of the new
* column.
*
* @return boolean success flag
*/
public function addColumn($table, $columndef)
{
$sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Modifies a column in the schema.
*
* The name must match an existing column and table.
*
* @param string $table name of the table
* @param ColumnDef $columndef new definition of the column.
*
* @return boolean success flag
*/
public function modifyColumn($table, $columndef)
{
$sql = "ALTER TABLE $table MODIFY COLUMN " .
$this->_columnSql($columndef);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Drops a column from a table
*
* The name must match an existing column.
*
* @param string $table name of the table
* @param string $columnName name of the column to drop
*
* @return boolean success flag
*/
public function dropColumn($table, $columnName)
{
$sql = "ALTER TABLE $table DROP COLUMN $columnName";
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Ensures that a table exists with the given
* name and the given column definitions.
*
* If the table does not yet exist, it will
* create the table. If it does exist, it will
* alter the table to match the column definitions.
*
* @param string $tableName name of the table
* @param array $columns array of ColumnDef
* objects for the table
*
* @return boolean success flag
*/
public function ensureTable($tableName, $columns)
{
// XXX: DB engine portability -> toilet
try {
$td = $this->getTableDef($tableName);
} catch (SchemaTableMissingException $e) {
return $this->createTable($tableName, $columns);
}
$cur = $this->_names($td->columns);
$new = $this->_names($columns);
$dropIndex = array();
$toadd = array_diff($new, $cur);
$todrop = array_diff($cur, $new);
$same = array_intersect($new, $cur);
$tomod = array();
$addIndex = array();
$tableProps = array();
foreach ($same as $m) {
$curCol = $this->_byName($td->columns, $m);
$newCol = $this->_byName($columns, $m);
if (!$newCol->equals($curCol)) {
$tomod[] = $newCol->name;
continue;
}
// Earlier versions may have accidentally left tables at default
// charsets which might be latin1 or other freakish things.
if ($this->_isString($curCol)) {
if ($curCol->charset != 'utf8') {
$tomod[] = $newCol->name;
continue;
}
}
}
// Find any indices we have to change...
$curIdx = $this->_indexList($td->columns);
$newIdx = $this->_indexList($columns);
if ($curIdx['primary'] != $newIdx['primary']) {
if ($curIdx['primary']) {
$dropIndex[] = 'drop primary key';
}
if ($newIdx['primary']) {
$keys = implode(',', $newIdx['primary']);
$addIndex[] = "add constraint primary key ($keys)";
}
}
$dropUnique = array_diff($curIdx['uniques'], $newIdx['uniques']);
$addUnique = array_diff($newIdx['uniques'], $curIdx['uniques']);
foreach ($dropUnique as $columnName) {
$dropIndex[] = 'drop key ' . $this->_uniqueKey($tableName, $columnName);
}
foreach ($addUnique as $columnName) {
$addIndex[] = 'add constraint unique key ' . $this->_uniqueKey($tableName, $columnName) . " ($columnName)";;
}
$dropMultiple = array_diff($curIdx['indices'], $newIdx['indices']);
$addMultiple = array_diff($newIdx['indices'], $curIdx['indices']);
foreach ($dropMultiple as $columnName) {
$dropIndex[] = 'drop key ' . $this->_key($tableName, $columnName);
}
foreach ($addMultiple as $columnName) {
$addIndex[] = 'add key ' . $this->_key($tableName, $columnName) . " ($columnName)";
}
// Check for table properties: make sure we're using a sane
// engine type and charset/collation.
// @fixme make the default engine configurable?
$oldProps = $this->getTableProperties($tableName, array('ENGINE', 'TABLE_COLLATION'));
if (strtolower($oldProps['ENGINE']) != 'innodb') {
$tableProps['ENGINE'] = 'InnoDB';
$engine = $this->preferredEngine($def);
if (strtolower($oldProps['ENGINE']) != strtolower($engine)) {
$phrase[] = "ENGINE=$engine";
}
if (strtolower($oldProps['TABLE_COLLATION']) != 'utf8_bin') {
$tableProps['DEFAULT CHARSET'] = 'utf8';
$tableProps['COLLATE'] = 'utf8_bin';
$phrase[] = 'DEFAULT CHARSET=utf8';
$phrase[] = 'COLLATE=utf8_bin';
}
if (count($dropIndex) + count($toadd) + count($todrop) + count($tomod) + count($addIndex) + count($tableProps) == 0) {
// nothing to do
return true;
}
// For efficiency, we want this all in one
// query, instead of using our methods.
$phrase = array();
foreach ($dropIndex as $indexSql) {
$phrase[] = $indexSql;
}
foreach ($toadd as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
}
foreach ($todrop as $columnName) {
$phrase[] = 'DROP COLUMN ' . $columnName;
}
foreach ($tomod as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
}
foreach ($addIndex as $indexSql) {
$phrase[] = $indexSql;
}
foreach ($tableProps as $key => $val) {
$phrase[] = "$key=$val";
}
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
common_log(LOG_DEBUG, __METHOD__ . ': ' . $sql);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Returns the array of names from an array of
* ColumnDef objects.
*
* @param array $cds array of ColumnDef objects
*
* @return array strings for name values
* Is this column a string type?
*/
private function _names($cds)
private function _isString(array $cd)
{
$names = array();
foreach ($cds as $cd) {
$names[] = $cd->name;
}
return $names;
}
/**
* Get a ColumnDef from an array matching
* name.
*
* @param array $cds Array of ColumnDef objects
* @param string $name Name of the column
*
* @return ColumnDef matching item or null if no match.
*/
private function _byName($cds, $name)
{
foreach ($cds as $cd) {
if ($cd->name == $name) {
return $cd;
}
}
return null;
$strings = array('char', 'varchar', 'text');
return in_array(strtolower($cd['type']), $strings);
}
/**
@ -641,43 +352,93 @@ class MysqlSchema extends Schema
* @return string correct SQL for that column
*/
private function _columnSql($cd)
function columnSql(array $cd)
{
$sql = "{$cd->name} ";
$line = array();
$line[] = parent::columnSql($cd);
if (!empty($cd->size)) {
$sql .= "{$cd->type}({$cd->size}) ";
} else {
$sql .= "{$cd->type} ";
// This'll have been added from our transform of 'serial' type
if (!empty($cd['auto_increment'])) {
$line[] = 'auto_increment';
}
if ($this->_isString($cd)) {
$sql .= " CHARACTER SET utf8 ";
if (!empty($cd['description'])) {
$line[] = 'comment';
$line[] = $this->quoteValue($cd['description']);
}
if (!empty($cd->default)) {
$sql .= "default {$cd->default} ";
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
}
return implode(' ', $line);
}
function mapType($column)
{
$map = array('serial' => 'int',
'integer' => 'int',
'numeric' => 'decimal');
if (!empty($cd->auto_increment)) {
$sql .= " auto_increment ";
$type = $column['type'];
if (isset($map[$type])) {
$type = $map[$type];
}
if (!empty($cd->extra)) {
$sql .= "{$cd->extra} ";
if (!empty($column['size'])) {
$size = $column['size'];
if ($type == 'int' &&
in_array($size, array('tiny', 'small', 'medium', 'big'))) {
$type = $size . $type;
} else if (in_array($type, array('blob', 'text')) &&
in_array($size, array('tiny', 'medium', 'long'))) {
$type = $size . $type;
}
}
return $sql;
return $type;
}
function typeAndSize($column)
{
if ($column['type'] == 'enum') {
$vals = array_map(array($this, 'quote'), $column['enum']);
return 'enum(' . implode(',', $vals) . ')';
} else if ($this->_isString($column)) {
$col = parent::typeAndSize($column);
if (!empty($column['charset'])) {
$col .= ' CHARSET ' . $column['charset'];
}
if (!empty($column['collate'])) {
$col .= ' COLLATE ' . $column['collate'];
}
return $col;
} else {
return parent::typeAndSize($column);
}
}
/**
* Is this column a string type?
* Filter the given table definition array to match features available
* in this database.
*
* This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef().
*
* @param array $tableDef
*/
private function _isString(ColumnDef $cd)
function filterDef(array $tableDef)
{
$strings = array('char', 'varchar', 'text');
return in_array(strtolower($cd->type), $strings);
foreach ($tableDef['fields'] as $name => &$col) {
if ($col['type'] == 'serial') {
$col['type'] = 'int';
$col['auto_increment'] = true;
}
if ($col['type'] == 'datetime' && isset($col['default']) && $col['default'] == 'CURRENT_TIMESTAMP') {
$col['type'] = 'timestamp';
}
$col['type'] = $this->mapType($col);
unset($col['size']);
}
if (!common_config('db', 'mysql_foreign_keys')) {
unset($tableDef['foreign keys']);
}
return $tableDef;
}
}

View File

@ -306,7 +306,7 @@ class NoticeListItem extends Widget
$attrs = array('href' => $this->profile->profileurl,
'class' => 'url');
if (!empty($this->profile->fullname)) {
$attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ')';
$attrs['title'] = $this->profile->getFancyName();
}
$this->out->elementStart('a', $attrs);
$this->showAvatar();

View File

@ -87,8 +87,11 @@ class PersonalGroupNav extends Widget
if ($nickname) {
$user = User::staticGet('nickname', $nickname);
$user_profile = $user->getProfile();
$name = $user_profile->getBestName();
} else {
// @fixme can this happen? is this valid?
$user_profile = false;
$name = $nickname;
}
$this->out->elementStart('ul', array('class' => 'nav'));
@ -97,22 +100,22 @@ class PersonalGroupNav extends Widget
$this->out->menuItem(common_local_url('all', array('nickname' =>
$nickname)),
_('Personal'),
sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
sprintf(_('%s and friends'), $name),
$action == 'all', 'nav_timeline_personal');
$this->out->menuItem(common_local_url('replies', array('nickname' =>
$nickname)),
_('Replies'),
sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
sprintf(_('Replies to %s'), $name),
$action == 'replies', 'nav_timeline_replies');
$this->out->menuItem(common_local_url('showstream', array('nickname' =>
$nickname)),
_('Profile'),
($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname,
$name,
$action == 'showstream', 'nav_profile');
$this->out->menuItem(common_local_url('showfavorites', array('nickname' =>
$nickname)),
_('Favorites'),
sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')),
sprintf(_('%s\'s favorite notices'), ($user_profile) ? $name : _('User')),
$action == 'showfavorites', 'nav_timeline_favorites');
$cur = common_current_user();

View File

@ -42,6 +42,7 @@ if (!defined('STATUSNET')) {
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brenda Wallace <shiny@cpan.org>
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
@ -50,167 +51,209 @@ class PgsqlSchema extends Schema
{
/**
* Returns a TableDef object for the table
* Returns a table definition array for the table
* in the schema with the given name.
*
* Throws an exception if the table is not found.
*
* @param string $name Name of the table to get
* @param string $table Name of the table to get
*
* @return TableDef tabledef for that table.
* @return array tabledef for that table.
*/
public function getTableDef($name)
public function getTableDef($table)
{
$res = $this->conn->query("SELECT *, column_default as default, is_nullable as Null,
udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
$def = array();
$hasKeys = false;
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
// Pull column data from INFORMATION_SCHEMA
$columns = $this->fetchMetaInfo($table, 'columns', 'ordinal_position');
if (count($columns) == 0) {
throw new SchemaTableMissingException("No such table: $table");
}
$td = new TableDef();
// We'll need to match up fields by ordinal reference
$orderedFields = array();
$td->name = $name;
$td->columns = array();
foreach ($columns as $row) {
if ($res->numRows() == 0 ) {
throw new Exception('no such table'); //pretend to be the msyql error. yeah, this sucks.
$name = $row['column_name'];
$orderedFields[$row['ordinal_position']] = $name;
$field = array();
$field['type'] = $row['udt_name'];
if ($type == 'char' || $type == 'varchar') {
if ($row['character_maximum_length'] !== null) {
$field['length'] = intval($row['character_maximum_length']);
}
}
if ($type == 'numeric') {
// Other int types may report these values, but they're irrelevant.
// Just ignore them!
if ($row['numeric_precision'] !== null) {
$field['precision'] = intval($row['numeric_precision']);
}
if ($row['numeric_scale'] !== null) {
$field['scale'] = intval($row['numeric_scale']);
}
}
if ($row['is_nullable'] == 'NO') {
$field['not null'] = true;
}
if ($row['column_default'] !== null) {
$field['default'] = $row['column_default'];
if ($this->isNumericType($type)) {
$field['default'] = intval($field['default']);
}
}
$def['fields'][$name] = $field;
}
$row = array();
while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
$cd = new ColumnDef();
// Pulling index info from pg_class & pg_index
// This can give us primary & unique key info, but not foreign key constraints
// so we exclude them and pick them up later.
$indexInfo = $this->getIndexInfo($table);
foreach ($indexInfo as $row) {
$keyName = $row['key_name'];
$cd->name = $row['field'];
// Dig the column references out!
//
// These are inconvenient arrays with partial references to the
// pg_att table, but since we've already fetched up the column
// info on the current table, we can look those up locally.
$cols = array();
$colPositions = explode(' ', $row['indkey']);
foreach ($colPositions as $ord) {
if ($ord == 0) {
$cols[] = 'FUNCTION'; // @fixme
} else {
$cols[] = $orderedFields[$ord];
}
}
$packed = $row['type'];
$def['indexes'][$keyName] = $cols;
}
if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
$cd->type = $match[1];
$cd->size = $match[2];
// Pull constraint data from INFORMATION_SCHEMA:
// Primary key, unique keys, foreign keys
$keyColumns = $this->fetchMetaInfo($table, 'key_column_usage', 'constraint_name,ordinal_position');
$keys = array();
foreach ($keyColumns as $row) {
$keyName = $row['constraint_name'];
$keyCol = $row['column_name'];
if (!isset($keys[$keyName])) {
$keys[$keyName] = array();
}
$keys[$keyName][] = $keyCol;
}
foreach ($keys as $keyName => $cols) {
// name hack -- is this reliable?
if ($keyName == "{$table}_pkey") {
$def['primary key'] = $cols;
} else if (preg_match("/^{$table}_(.*)_fkey$/", $keyName, $matches)) {
$fkey = $this->getForeignKeyInfo($table, $keyName);
$colMap = array_combine($cols, $fkey['col_names']);
$def['foreign keys'][$keyName] = array($fkey['table_name'], $colMap);
} else {
$cd->type = $packed;
$def['unique keys'][$keyName] = $cols;
}
$cd->nullable = ($row['null'] == 'YES') ? true : false;
$cd->key = $row['Key'];
$cd->default = $row['default'];
$cd->extra = $row['Extra'];
$td->columns[] = $cd;
}
return $td;
return $def;
}
/**
* Gets a ColumnDef object for a single column.
* Pull some INFORMATION.SCHEMA data for the given table.
*
* Throws an exception if the table is not found.
*
* @param string $table name of the table
* @param string $column name of the column
*
* @return ColumnDef definition of the column or null
* if not found.
* @param string $table
* @return array of arrays
*/
public function getColumnDef($table, $column)
function fetchMetaInfo($table, $infoTable, $orderBy=null)
{
$td = $this->getTableDef($table);
foreach ($td->columns as $cd) {
if ($cd->name == $column) {
return $cd;
}
$query = "SELECT * FROM information_schema.%s " .
"WHERE table_name='%s'";
$sql = sprintf($query, $infoTable, $table);
if ($orderBy) {
$sql .= ' ORDER BY ' . $orderBy;
}
return null;
return $this->fetchQueryData($sql);
}
/**
* Creates a table with the given names and columns.
*
* @param string $name Name of the table
* @param array $columns Array of ColumnDef objects
* for new table.
*
* @return boolean success flag
* Pull some PG-specific index info
* @param string $table
* @return array of arrays
*/
public function createTable($name, $columns)
function getIndexInfo($table)
{
$uniques = array();
$primary = array();
$indices = array();
$onupdate = array();
$sql = "CREATE TABLE $name (\n";
for ($i = 0; $i < count($columns); $i++) {
$cd =& $columns[$i];
if ($i > 0) {
$sql .= ",\n";
}
$sql .= $this->_columnSql($cd);
switch ($cd->key) {
case 'UNI':
$uniques[] = $cd->name;
break;
case 'PRI':
$primary[] = $cd->name;
break;
case 'MUL':
$indices[] = $cd->name;
break;
}
}
if (count($primary) > 0) { // it really should be...
$sql .= ",\n PRIMARY KEY (" . implode(',', $primary) . ")";
}
$sql .= "); ";
foreach ($uniques as $u) {
$sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); ";
}
foreach ($indices as $i) {
$sql .= "CREATE index {$name}_{$i}_idx ON {$name} ($i)";
}
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage(). ' SQL was '. $sql);
}
return true;
$query = 'SELECT ' .
'(SELECT relname FROM pg_class WHERE oid=indexrelid) AS key_name, ' .
'* FROM pg_index ' .
'WHERE indrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' .
'AND indisprimary=\'f\' AND indisunique=\'f\' ' .
'ORDER BY indrelid, indexrelid';
$sql = sprintf($query, $table);
return $this->fetchQueryData($sql);
}
/**
* Drops a table from the schema
*
* Throws an exception if the table is not found.
*
* @param string $name Name of the table to drop
*
* @return boolean success flag
* Column names from the foreign table can be resolved with a call to getTableColumnNames()
* @param <type> $table
* @return array array of rows with keys: fkey_name, table_name, table_id, col_names (array of strings)
*/
public function dropTable($name)
function getForeignKeyInfo($table, $constraint_name)
{
$res = $this->conn->query("DROP TABLE $name");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
// In a sane world, it'd be easier to query the column names directly.
// But it's pretty hard to work with arrays such as col_indexes in direct SQL here.
$query = 'SELECT ' .
'(SELECT relname FROM pg_class WHERE oid=confrelid) AS table_name, ' .
'confrelid AS table_id, ' .
'(SELECT indkey FROM pg_index WHERE indexrelid=conindid) AS col_indexes ' .
'FROM pg_constraint ' .
'WHERE conrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' .
'AND conname=\'%s\' ' .
'AND contype=\'f\'';
$sql = sprintf($query, $table, $constraint_name);
$data = $this->fetchQueryData($sql);
if (count($data) < 1) {
throw new Exception("Could not find foreign key " . $constraint_name . " on table " . $table);
}
return true;
$row = $data[0];
return array(
'table_name' => $row['table_name'],
'col_names' => $this->getTableColumnNames($row['table_id'], $row['col_indexes'])
);
}
/**
*
* @param int $table_id
* @param array $col_indexes
* @return array of strings
*/
function getTableColumnNames($table_id, $col_indexes)
{
$indexes = array_map('intval', explode(' ', $col_indexes));
$query = 'SELECT attnum AS col_index, attname AS col_name ' .
'FROM pg_attribute where attrelid=%d ' .
'AND attnum IN (%s)';
$sql = sprintf($query, $table_id, implode(',', $indexes));
$data = $this->fetchQueryData($sql);
$byId = array();
foreach ($data as $row) {
$byId[$row['col_index']] = $row['col_name'];
}
$out = array();
foreach ($indexes as $id) {
$out[] = $byId[$id];
}
return $out;
}
/**
@ -229,262 +272,6 @@ class PgsqlSchema extends Schema
return $type;
}
/**
* Adds an index to a table.
*
* If no name is provided, a name will be made up based
* on the table name and column names.
*
* Throws an exception on database error, esp. if the table
* does not exist.
*
* @param string $table Name of the table
* @param array $columnNames Name of columns to index
* @param string $name (Optional) name of the index
*
* @return boolean success flag
*/
public function createIndex($table, $columnNames, $name=null)
{
if (!is_array($columnNames)) {
$columnNames = array($columnNames);
}
if (empty($name)) {
$name = "$table_".implode("_", $columnNames)."_idx";
}
$res = $this->conn->query("ALTER TABLE $table ".
"ADD INDEX $name (".
implode(",", $columnNames).")");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Drops a named index from a table.
*
* @param string $table name of the table the index is on.
* @param string $name name of the index
*
* @return boolean success flag
*/
public function dropIndex($table, $name)
{
$res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Adds a column to a table
*
* @param string $table name of the table
* @param ColumnDef $columndef Definition of the new
* column.
*
* @return boolean success flag
*/
public function addColumn($table, $columndef)
{
$sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Modifies a column in the schema.
*
* The name must match an existing column and table.
*
* @param string $table name of the table
* @param ColumnDef $columndef new definition of the column.
*
* @return boolean success flag
*/
public function modifyColumn($table, $columndef)
{
$sql = "ALTER TABLE $table ALTER COLUMN TYPE " .
$this->_columnSql($columndef);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Drops a column from a table
*
* The name must match an existing column.
*
* @param string $table name of the table
* @param string $columnName name of the column to drop
*
* @return boolean success flag
*/
public function dropColumn($table, $columnName)
{
$sql = "ALTER TABLE $table DROP COLUMN $columnName";
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Ensures that a table exists with the given
* name and the given column definitions.
*
* If the table does not yet exist, it will
* create the table. If it does exist, it will
* alter the table to match the column definitions.
*
* @param string $tableName name of the table
* @param array $columns array of ColumnDef
* objects for the table
*
* @return boolean success flag
*/
public function ensureTable($tableName, $columns)
{
// XXX: DB engine portability -> toilet
try {
$td = $this->getTableDef($tableName);
} catch (Exception $e) {
if (preg_match('/no such table/', $e->getMessage())) {
return $this->createTable($tableName, $columns);
} else {
throw $e;
}
}
$cur = $this->_names($td->columns);
$new = $this->_names($columns);
$toadd = array_diff($new, $cur);
$todrop = array_diff($cur, $new);
$same = array_intersect($new, $cur);
$tomod = array();
foreach ($same as $m) {
$curCol = $this->_byName($td->columns, $m);
$newCol = $this->_byName($columns, $m);
if (!$newCol->equals($curCol)) {
// BIG GIANT TODO!
// stop it detecting different types and trying to modify on every page request
// $tomod[] = $newCol->name;
}
}
if (count($toadd) + count($todrop) + count($tomod) == 0) {
// nothing to do
return true;
}
// For efficiency, we want this all in one
// query, instead of using our methods.
$phrase = array();
foreach ($toadd as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
}
foreach ($todrop as $columnName) {
$phrase[] = 'DROP COLUMN ' . $columnName;
}
foreach ($tomod as $columnName) {
$cd = $this->_byName($columns, $columnName);
/* brute force */
$phrase[] = 'DROP COLUMN ' . $columnName;
$phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
}
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
return true;
}
/**
* Returns the array of names from an array of
* ColumnDef objects.
*
* @param array $cds array of ColumnDef objects
*
* @return array strings for name values
*/
private function _names($cds)
{
$names = array();
foreach ($cds as $cd) {
$names[] = $cd->name;
}
return $names;
}
/**
* Get a ColumnDef from an array matching
* name.
*
* @param array $cds Array of ColumnDef objects
* @param string $name Name of the column
*
* @return ColumnDef matching item or null if no match.
*/
private function _byName($cds, $name)
{
foreach ($cds as $cd) {
if ($cd->name == $name) {
return $cd;
}
}
return null;
}
/**
* Return the proper SQL for creating or
* altering a column.
@ -492,41 +279,177 @@ class PgsqlSchema extends Schema
* Appropriate for use in CREATE TABLE or
* ALTER TABLE statements.
*
* @param ColumnDef $cd column to create
* @param array $cd column to create
*
* @return string correct SQL for that column
*/
private function _columnSql($cd)
function columnSql(array $cd)
{
$sql = "{$cd->name} ";
$type = $this->_columnTypeTranslation($cd->type);
$line = array();
$line[] = parent::columnSql($cd);
//handle those mysql enum fields that postgres doesn't support
if (preg_match('!^enum!', $type)) {
$allowed_values = preg_replace('!^enum!', '', $type);
$sql .= " text check ({$cd->name} in $allowed_values)";
return $sql;
/*
if ($table['foreign keys'][$name]) {
foreach ($table['foreign keys'][$name] as $foreignTable => $foreignColumn) {
$line[] = 'references';
$line[] = $this->quoteIdentifier($foreignTable);
$line[] = '(' . $this->quoteIdentifier($foreignColumn) . ')';
}
}
if (!empty($cd->auto_increment)) {
$type = "bigserial"; // FIXME: creates the wrong name for the sequence for some internal sequence-lookup function, so better fix this to do the real 'create sequence' dance.
*/
return implode(' ', $line);
}
/**
* Append phrase(s) to an array of partial ALTER TABLE chunks in order
* to alter the given column from its old state to a new one.
*
* @param array $phrase
* @param string $columnName
* @param array $old previous column definition as found in DB
* @param array $cd current column definition
*/
function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd)
{
$prefix = 'ALTER COLUMN ' . $this->quoteIdentifier($columnName) . ' ';
$oldType = $this->mapType($old);
$newType = $this->mapType($cd);
if ($oldType != $newType) {
$phrase[] = $prefix . 'TYPE ' . $newType;
}
if (!empty($cd->size)) {
$sql .= "{$type}({$cd->size}) ";
if (!empty($old['not null']) && empty($cd['not null'])) {
$phrase[] = $prefix . 'DROP NOT NULL';
} else if (empty($old['not null']) && !empty($cd['not null'])) {
$phrase[] = $prefix . 'SET NOT NULL';
}
if (isset($old['default']) && !isset($cd['default'])) {
$phrase[] = $prefix . 'DROP DEFAULT';
} else if (!isset($old['default']) && isset($cd['default'])) {
$phrase[] = $prefix . 'SET DEFAULT ' . $this->quoteDefaultValue($cd);
}
}
/**
* Append an SQL statement to drop an index from a table.
* Note that in PostgreSQL, index names are DB-unique.
*
* @param array $statements
* @param string $table
* @param string $name
* @param array $def
*/
function appendDropIndex(array &$statements, $table, $name)
{
$statements[] = "DROP INDEX $name";
}
/**
* Quote a db/table/column identifier if necessary.
*
* @param string $name
* @return string
*/
function quoteIdentifier($name)
{
return $this->conn->quoteIdentifier($name);
}
function mapType($column)
{
$map = array('serial' => 'bigserial', // FIXME: creates the wrong name for the sequence for some internal sequence-lookup function, so better fix this to do the real 'create sequence' dance.
'numeric' => 'decimal',
'datetime' => 'timestamp',
'blob' => 'bytea');
$type = $column['type'];
if (isset($map[$type])) {
$type = $map[$type];
}
if ($type == 'int') {
if (!empty($column['size'])) {
$size = $column['size'];
if ($size == 'small') {
return 'int2';
} else if ($size == 'big') {
return 'int8';
}
}
return 'int4';
}
return $type;
}
// @fixme need name... :P
function typeAndSize($column)
{
if ($column['type'] == 'enum') {
$vals = array_map(array($this, 'quote'), $column['enum']);
return "text check ($name in " . implode(',', $vals) . ')';
} else {
$sql .= "{$type} ";
return parent::typeAndSize($column);
}
}
if (!empty($cd->default)) {
$sql .= "default {$cd->default} ";
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
/**
* Filter the given table definition array to match features available
* in this database.
*
* This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef().
*
* @param array $tableDef
*/
function filterDef(array $tableDef)
{
foreach ($tableDef['fields'] as $name => &$col) {
// No convenient support for field descriptions
unset($col['description']);
/*
if (isset($col['size'])) {
// Don't distinguish between tinyint and int.
if ($col['size'] == 'tiny' && $col['type'] == 'int') {
unset($col['size']);
}
}
*/
$col['type'] = $this->mapType($col);
unset($col['size']);
}
if (!empty($tableDef['primary key'])) {
$tableDef['primary key'] = $this->filterKeyDef($tableDef['primary key']);
}
if (!empty($tableDef['unique keys'])) {
foreach ($tableDef['unique keys'] as $i => $def) {
$tableDef['unique keys'][$i] = $this->filterKeyDef($def);
}
}
return $tableDef;
}
// if (!empty($cd->extra)) {
// $sql .= "{$cd->extra} ";
// }
return $sql;
/**
* Filter the given key/index definition to match features available
* in this database.
*
* @param array $def
* @return array
*/
function filterKeyDef(array $def)
{
// PostgreSQL doesn't like prefix lengths specified on keys...?
foreach ($def as $i => $item)
{
if (is_array($item)) {
$def[$i] = $item[0];
}
}
return $def;
}
}

View File

@ -20,13 +20,12 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
function ping_broadcast_notice($notice) {
if ($notice->is_local != Notice::LOCAL_PUBLIC && $notice->is_local != Notice::LOCAL_NONPUBLIC) {
return true;
}
if ($notice->is_local != Notice::LOCAL_PUBLIC && $notice->is_local != Notice::LOCAL_NONPUBLIC) {
return true;
}
# Array of servers, URL => type
$notify = common_config('ping', 'notify');
# Array of servers, URL => type
$notify = common_config('ping', 'notify');
try {
$profile = $notice->getProfile();
} catch (Exception $e) {
@ -35,21 +34,21 @@ function ping_broadcast_notice($notice) {
common_log(LOG_ERR, "Exception getting notice profile: " . $e->getMessage());
return true;
}
$tags = ping_notice_tags($notice);
$tags = ping_notice_tags($notice);
foreach ($notify as $notify_url => $type) {
switch ($type) {
case 'xmlrpc':
case 'extended':
$req = xmlrpc_encode_request('weblogUpdates.ping',
array($profile->nickname, # site name
common_local_url('showstream',
array('nickname' => $profile->nickname)),
common_local_url('shownotice',
array('notice' => $notice->id)),
common_local_url('userrss',
array('nickname' => $profile->nickname)),
$tags));
foreach ($notify as $notify_url => $type) {
switch ($type) {
case 'xmlrpc':
case 'extended':
$req = xmlrpc_encode_request('weblogUpdates.ping',
array($profile->nickname, # site name
common_local_url('showstream',
array('nickname' => $profile->nickname)),
common_local_url('shownotice',
array('notice' => $notice->id)),
common_local_url('userrss',
array('nickname' => $profile->nickname)),
$tags));
$request = HTTPClient::start();
$request->setConfig('connect_timeout', common_config('ping', 'timeout'));
@ -79,9 +78,8 @@ function ping_broadcast_notice($notice) {
"Ping success for $notify_url $notice->id");
}
break;
case 'get':
case 'post':
case 'get':
case 'post':
$args = array('name' => $profile->nickname,
'url' => common_local_url('showstream',
array('nickname' => $profile->nickname)),
@ -108,26 +106,25 @@ function ping_broadcast_notice($notice) {
"'$result->body'");
}
break;
default:
common_log(LOG_WARNING, 'Unknown notify type for ' . $notify_url . ': ' . $type);
default:
common_log(LOG_WARNING, 'Unknown notify type for ' . $notify_url . ': ' . $type);
}
}
}
return true;
}
function ping_notice_tags($notice) {
$tag = new Notice_tag();
$tag->notice_id = $notice->id;
$tags = array();
if ($tag->find()) {
while ($tag->fetch()) {
$tags[] = $tag->tag;
}
$tag->free();
unset($tag);
return implode('|', $tags);
}
return NULL;
$tag = new Notice_tag();
$tag->notice_id = $notice->id;
$tags = array();
if ($tag->find()) {
while ($tag->fetch()) {
$tags[] = $tag->tag;
}
$tag->free();
unset($tag);
return implode('|', $tags);
}
return NULL;
}

View File

@ -41,6 +41,7 @@ if (!defined('STATUSNET')) {
* @category Database
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
@ -118,65 +119,216 @@ class Schema
/**
* Creates a table with the given names and columns.
*
* @param string $name Name of the table
* @param array $columns Array of ColumnDef objects
* for new table.
* @param string $tableName Name of the table
* @param array $def Table definition array listing fields and indexes.
*
* @return boolean success flag
*/
public function createTable($name, $columns)
public function createTable($tableName, $def)
{
$uniques = array();
$primary = array();
$indices = array();
$statements = $this->buildCreateTable($tableName, $def);
return $this->runSqlSet($statements);
}
$sql = "CREATE TABLE $name (\n";
/**
* Build a set of SQL statements to create a table with the given
* name and columns.
*
* @param string $name Name of the table
* @param array $def Table definition array
*
* @return boolean success flag
*/
public function buildCreateTable($name, $def)
{
$def = $this->validateDef($name, $def);
$def = $this->filterDef($def);
$sql = array();
for ($i = 0; $i < count($columns); $i++) {
foreach ($def['fields'] as $col => $colDef) {
$this->appendColumnDef($sql, $col, $colDef);
}
$cd =& $columns[$i];
// Primary, unique, and foreign keys are constraints, so go within
// the CREATE TABLE statement normally.
if (!empty($def['primary key'])) {
$this->appendPrimaryKeyDef($sql, $def['primary key']);
}
if ($i > 0) {
$sql .= ",\n";
}
$sql .= $this->_columnSql($cd);
switch ($cd->key) {
case 'UNI':
$uniques[] = $cd->name;
break;
case 'PRI':
$primary[] = $cd->name;
break;
case 'MUL':
$indices[] = $cd->name;
break;
if (!empty($def['unique keys'])) {
foreach ($def['unique keys'] as $col => $colDef) {
$this->appendUniqueKeyDef($sql, $col, $colDef);
}
}
if (count($primary) > 0) { // it really should be...
$sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
if (!empty($def['foreign keys'])) {
foreach ($def['foreign keys'] as $keyName => $keyDef) {
$this->appendForeignKeyDef($sql, $keyName, $keyDef);
}
}
foreach ($uniques as $u) {
$sql .= ",\nunique index {$name}_{$u}_idx ($u)";
// Wrap the CREATE TABLE around the main body chunks...
$statements = array();
$statements[] = $this->startCreateTable($name, $def) . "\n" .
implode($sql, ",\n") . "\n" .
$this->endCreateTable($name, $def);
// Multi-value indexes are advisory and for best portability
// should be created as separate statements.
if (!empty($def['indexes'])) {
foreach ($def['indexes'] as $col => $colDef) {
$this->appendCreateIndex($statements, $name, $col, $colDef);
}
}
if (!empty($def['fulltext indexes'])) {
foreach ($def['fulltext indexes'] as $col => $colDef) {
$this->appendCreateFulltextIndex($statements, $name, $col, $colDef);
}
}
foreach ($indices as $i) {
$sql .= ",\nindex {$name}_{$i}_idx ($i)";
return $statements;
}
/**
* Set up a 'create table' SQL statement.
*
* @param string $name table name
* @param array $def table definition
* @param $string
*/
function startCreateTable($name, array $def)
{
return 'CREATE TABLE ' . $this->quoteIdentifier($name) . ' (';
}
/**
* Close out a 'create table' SQL statement.
*
* @param string $name table name
* @param array $def table definition
* @return string
*/
function endCreateTable($name, array $def)
{
return ')';
}
/**
* Append an SQL fragment with a column definition in a CREATE TABLE statement.
*
* @param array $sql
* @param string $name
* @param array $def
*/
function appendColumnDef(array &$sql, $name, array $def)
{
$sql[] = "$name " . $this->columnSql($def);
}
/**
* Append an SQL fragment with a constraint definition for a primary
* key in a CREATE TABLE statement.
*
* @param array $sql
* @param array $def
*/
function appendPrimaryKeyDef(array &$sql, array $def)
{
$sql[] = "PRIMARY KEY " . $this->buildIndexList($def);
}
/**
* Append an SQL fragment with a constraint definition for a unique
* key in a CREATE TABLE statement.
*
* @param array $sql
* @param string $name
* @param array $def
*/
function appendUniqueKeyDef(array &$sql, $name, array $def)
{
$sql[] = "CONSTRAINT $name UNIQUE " . $this->buildIndexList($def);
}
/**
* Append an SQL fragment with a constraint definition for a foreign
* key in a CREATE TABLE statement.
*
* @param array $sql
* @param string $name
* @param array $def
*/
function appendForeignKeyDef(array &$sql, $name, array $def)
{
if (count($def) != 2) {
throw new Exception("Invalid foreign key def for $name: " . var_export($def, true));
}
list($refTable, $map) = $def;
$srcCols = array_keys($map);
$refCols = array_values($map);
$sql[] = "CONSTRAINT $name FOREIGN KEY " .
$this->buildIndexList($srcCols) .
" REFERENCES " .
$this->quoteIdentifier($refTable) .
" " .
$this->buildIndexList($refCols);
}
$sql .= "); ";
/**
* Append an SQL statement with an index definition for an advisory
* index over one or more columns on a table.
*
* @param array $statements
* @param string $table
* @param string $name
* @param array $def
*/
function appendCreateIndex(array &$statements, $table, $name, array $def)
{
$statements[] = "CREATE INDEX $name ON $table " . $this->buildIndexList($def);
}
$res = $this->conn->query($sql);
/**
* Append an SQL statement with an index definition for a full-text search
* index over one or more columns on a table.
*
* @param array $statements
* @param string $table
* @param string $name
* @param array $def
*/
function appendCreateFulltextIndex(array &$statements, $table, $name, array $def)
{
throw new Exception("Fulltext index not supported in this database");
}
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
/**
* Append an SQL statement to drop an index from a table.
*
* @param array $statements
* @param string $table
* @param string $name
* @param array $def
*/
function appendDropIndex(array &$statements, $table, $name)
{
$statements[] = "DROP INDEX $name ON " . $this->quoteIdentifier($table);
}
function buildIndexList(array $def)
{
// @fixme
return '(' . implode(',', array_map(array($this, 'buildIndexItem'), $def)) . ')';
}
function buildIndexItem($def)
{
if (is_array($def)) {
list($name, $size) = $def;
return $this->quoteIdentifier($name) . '(' . intval($size) . ')';
}
return true;
return $this->quoteIdentifier($def);
}
/**
@ -223,7 +375,7 @@ class Schema
}
if (empty($name)) {
$name = "$table_".implode("_", $columnNames)."_idx";
$name = "{$table}_".implode("_", $columnNames)."_idx";
}
$res = $this->conn->query("ALTER TABLE $table ".
@ -338,46 +490,80 @@ class Schema
* alter the table to match the column definitions.
*
* @param string $tableName name of the table
* @param array $columns array of ColumnDef
* objects for the table
* @param array $def Table definition array
*
* @return boolean success flag
*/
public function ensureTable($tableName, $columns)
public function ensureTable($tableName, $def)
{
// XXX: DB engine portability -> toilet
$statements = $this->buildEnsureTable($tableName, $def);
return $this->runSqlSet($statements);
}
/**
* Run a given set of SQL commands on the connection in sequence.
* Empty input is ok.
*
* @fixme if multiple statements, wrap in a transaction?
* @param array $statements
* @return boolean success flag
*/
function runSqlSet(array $statements)
{
$ok = true;
foreach ($statements as $sql) {
if (defined('DEBUG_INSTALLER')) {
echo "<tt>" . htmlspecialchars($sql) . "</tt><br/>\n";
}
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
}
return $ok;
}
/**
* Check a table's status, and if needed build a set
* of SQL statements which change it to be consistent
* with the given table definition.
*
* If the table does not yet exist, statements will
* be returned to create the table. If it does exist,
* statements will be returned to alter the table to
* match the column definitions.
*
* @param string $tableName name of the table
* @param array $columns array of ColumnDef
* objects for the table
*
* @return array of SQL statements
*/
function buildEnsureTable($tableName, array $def)
{
try {
$td = $this->getTableDef($tableName);
} catch (Exception $e) {
if (preg_match('/no such table/', $e->getMessage())) {
return $this->createTable($tableName, $columns);
} else {
throw $e;
}
$old = $this->getTableDef($tableName);
} catch (SchemaTableMissingException $e) {
return $this->buildCreateTable($tableName, $def);
}
$cur = $this->_names($td->columns);
$new = $this->_names($columns);
// Filter the DB-independent table definition to match the current
// database engine's features and limitations.
$def = $this->validateDef($tableName, $def);
$def = $this->filterDef($def);
$toadd = array_diff($new, $cur);
$todrop = array_diff($cur, $new);
$same = array_intersect($new, $cur);
$tomod = array();
$statements = array();
$fields = $this->diffArrays($old, $def, 'fields', array($this, 'columnsEqual'));
$uniques = $this->diffArrays($old, $def, 'unique keys');
$indexes = $this->diffArrays($old, $def, 'indexes');
$foreign = $this->diffArrays($old, $def, 'foreign keys');
foreach ($same as $m) {
$curCol = $this->_byName($td->columns, $m);
$newCol = $this->_byName($columns, $m);
if (!$newCol->equals($curCol)) {
$tomod[] = $newCol->name;
}
}
if (count($toadd) + count($todrop) + count($tomod) == 0) {
// nothing to do
return true;
// Drop any obsolete or modified indexes ahead...
foreach ($indexes['del'] + $indexes['mod'] as $indexName) {
$this->appendDropIndex($statements, $tableName, $indexName);
}
// For efficiency, we want this all in one
@ -385,31 +571,200 @@ class Schema
$phrase = array();
foreach ($toadd as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
foreach ($foreign['del'] + $foreign['mod'] as $keyName) {
$this->appendAlterDropForeign($phrase, $keyName);
}
foreach ($todrop as $columnName) {
$phrase[] = 'DROP COLUMN ' . $columnName;
foreach ($uniques['del'] + $uniques['mod'] as $keyName) {
$this->appendAlterDropUnique($phrase, $keyName);
}
foreach ($tomod as $columnName) {
$cd = $this->_byName($columns, $columnName);
$phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
foreach ($fields['add'] as $columnName) {
$this->appendAlterAddColumn($phrase, $columnName,
$def['fields'][$columnName]);
}
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
foreach ($fields['mod'] as $columnName) {
$this->appendAlterModifyColumn($phrase, $columnName,
$old['fields'][$columnName],
$def['fields'][$columnName]);
}
return true;
foreach ($fields['del'] as $columnName) {
$this->appendAlterDropColumn($phrase, $columnName);
}
foreach ($uniques['mod'] + $uniques['add'] as $keyName) {
$this->appendAlterAddUnique($phrase, $keyName, $def['unique keys'][$keyName]);
}
foreach ($foreign['mod'] + $foreign['add'] as $keyName) {
$this->appendAlterAddForeign($phrase, $keyName, $def['foreign keys'][$keyName]);
}
$this->appendAlterExtras($phrase, $tableName, $def);
if (count($phrase) > 0) {
$sql = 'ALTER TABLE ' . $tableName . ' ' . implode(",\n", $phrase);
$statements[] = $sql;
}
// Now create any indexes...
foreach ($indexes['mod'] + $indexes['add'] as $indexName) {
$this->appendCreateIndex($statements, $tableName, $indexName, $def['indexes'][$indexName]);
}
return $statements;
}
function diffArrays($oldDef, $newDef, $section, $compareCallback=null)
{
$old = isset($oldDef[$section]) ? $oldDef[$section] : array();
$new = isset($newDef[$section]) ? $newDef[$section] : array();
$oldKeys = array_keys($old);
$newKeys = array_keys($new);
$toadd = array_diff($newKeys, $oldKeys);
$todrop = array_diff($oldKeys, $newKeys);
$same = array_intersect($newKeys, $oldKeys);
$tomod = array();
$tokeep = array();
// Find which fields have actually changed definition
// in a way that we need to tweak them for this DB type.
foreach ($same as $name) {
if ($compareCallback) {
$same = call_user_func($compareCallback, $old[$name], $new[$name]);
} else {
$same = ($old[$name] == $new[$name]);
}
if ($same) {
$tokeep[] = $name;
continue;
}
$tomod[] = $name;
}
return array('add' => $toadd,
'del' => $todrop,
'mod' => $tomod,
'keep' => $tokeep,
'count' => count($toadd) + count($todrop) + count($tomod));
}
/**
* Append phrase(s) to an array of partial ALTER TABLE chunks in order
* to add the given column definition to the table.
*
* @param array $phrase
* @param string $columnName
* @param array $cd
*/
function appendAlterAddColumn(array &$phrase, $columnName, array $cd)
{
$phrase[] = 'ADD COLUMN ' .
$this->quoteIdentifier($columnName) .
' ' .
$this->columnSql($cd);
}
/**
* Append phrase(s) to an array of partial ALTER TABLE chunks in order
* to alter the given column from its old state to a new one.
*
* @param array $phrase
* @param string $columnName
* @param array $old previous column definition as found in DB
* @param array $cd current column definition
*/
function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd)
{
$phrase[] = 'MODIFY COLUMN ' .
$this->quoteIdentifier($columnName) .
' ' .
$this->columnSql($cd);
}
/**
* Append phrase(s) to an array of partial ALTER TABLE chunks in order
* to drop the given column definition from the table.
*
* @param array $phrase
* @param string $columnName
*/
function appendAlterDropColumn(array &$phrase, $columnName)
{
$phrase[] = 'DROP COLUMN ' . $this->quoteIdentifier($columnName);
}
function appendAlterAddUnique(array &$phrase, $keyName, array $def)
{
$sql = array();
$sql[] = 'ADD';
$this->appendUniqueKeyDef($sql, $keyName, $def);
$phrase[] = implode(' ', $sql);
}
function appendAlterAddForeign(array &$phrase, $keyName, array $def)
{
$sql = array();
$sql[] = 'ADD';
$this->appendForeignKeyDef($sql, $keyName, $def);
$phrase[] = implode(' ', $sql);
}
function appendAlterDropUnique(array &$phrase, $keyName)
{
$phrase[] = 'DROP CONSTRAINT ' . $keyName;
}
function appendAlterDropForeign(array &$phrase, $keyName)
{
$phrase[] = 'DROP FOREIGN KEY ' . $keyName;
}
function appendAlterExtras(array &$phrase, $tableName, array $def)
{
// no-op
}
/**
* Quote a db/table/column identifier if necessary.
*
* @param string $name
* @return string
*/
function quoteIdentifier($name)
{
return $name;
}
function quoteDefaultValue($cd)
{
if ($cd['type'] == 'datetime' && $cd['default'] == 'CURRENT_TIMESTAMP') {
return $cd['default'];
} else {
return $this->quoteValue($cd['default']);
}
}
function quoteValue($val)
{
return $this->conn->quoteSmart($val);
}
/**
* Check if two column definitions are equivalent.
* The default implementation checks _everything_ but in many cases
* you may be able to discard a bunch of equivalencies.
*
* @param array $a
* @param array $b
* @return boolean
*/
function columnsEqual(array $a, array $b)
{
return !array_diff_assoc($a, $b) && !array_diff_assoc($b, $a);
}
/**
@ -421,7 +776,7 @@ class Schema
* @return array strings for name values
*/
private function _names($cds)
protected function _names($cds)
{
$names = array();
@ -442,7 +797,7 @@ class Schema
* @return ColumnDef matching item or null if no match.
*/
private function _byName($cds, $name)
protected function _byName($cds, $name)
{
foreach ($cds as $cd) {
if ($cd->name == $name) {
@ -465,32 +820,194 @@ class Schema
* @return string correct SQL for that column
*/
private function _columnSql($cd)
function columnSql(array $cd)
{
$sql = "{$cd->name} ";
$line = array();
$line[] = $this->typeAndSize($cd);
if (!empty($cd->size)) {
$sql .= "{$cd->type}({$cd->size}) ";
} else {
$sql .= "{$cd->type} ";
if (isset($cd['default'])) {
$line[] = 'default';
$line[] = $this->quoteDefaultValue($cd);
} else if (!empty($cd['not null'])) {
// Can't have both not null AND default!
$line[] = 'not null';
}
if (!empty($cd->default)) {
$sql .= "default {$cd->default} ";
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
}
if (!empty($cd->auto_increment)) {
$sql .= " auto_increment ";
}
if (!empty($cd->extra)) {
$sql .= "{$cd->extra} ";
}
return $sql;
return implode(' ', $line);
}
/**
*
* @param string $column canonical type name in defs
* @return string native DB type name
*/
function mapType($column)
{
return $column;
}
function typeAndSize($column)
{
//$type = $this->mapType($column);
$type = $column['type'];
if (isset($column['size'])) {
$type = $column['size'] . $type;
}
$lengths = array();
if (isset($column['precision'])) {
$lengths[] = $column['precision'];
if (isset($column['scale'])) {
$lengths[] = $column['scale'];
}
} else if (isset($column['length'])) {
$lengths[] = $column['length'];
}
if ($lengths) {
return $type . '(' . implode(',', $lengths) . ')';
} else {
return $type;
}
}
/**
* Convert an old-style set of ColumnDef objects into the current
* Drupal-style schema definition array, for backwards compatibility
* with plugins written for 0.9.x.
*
* @param string $tableName
* @param array $defs: array of ColumnDef objects
* @return array
*/
protected function oldToNew($tableName, array $defs)
{
$table = array();
$prefixes = array(
'tiny',
'small',
'medium',
'big',
);
foreach ($defs as $cd) {
$column = array();
$column['type'] = $cd->type;
foreach ($prefixes as $prefix) {
if (substr($cd->type, 0, strlen($prefix)) == $prefix) {
$column['type'] = substr($cd->type, strlen($prefix));
$column['size'] = $prefix;
break;
}
}
if ($cd->size) {
if ($cd->type == 'varchar' || $cd->type == 'char') {
$column['length'] = $cd->size;
}
}
if (!$cd->nullable) {
$column['not null'] = true;
}
if ($cd->auto_increment) {
$column['type'] = 'serial';
}
if ($cd->default) {
$column['default'] = $cd->default;
}
$table['fields'][$cd->name] = $column;
if ($cd->key == 'PRI') {
// If multiple columns are defined as primary key,
// we'll pile them on in sequence.
if (!isset($table['primary key'])) {
$table['primary key'] = array();
}
$table['primary key'][] = $cd->name;
} else if ($cd->key == 'MUL') {
// Individual multiple-value indexes are only per-column
// using the old ColumnDef syntax.
$idx = "{$tableName}_{$cd->name}_idx";
$table['indexes'][$idx] = array($cd->name);
} else if ($cd->key == 'UNI') {
// Individual unique-value indexes are only per-column
// using the old ColumnDef syntax.
$idx = "{$tableName}_{$cd->name}_idx";
$table['unique keys'][$idx] = array($cd->name);
}
}
return $table;
}
/**
* Filter the given table definition array to match features available
* in this database.
*
* This lets us strip out unsupported things like comments, foreign keys,
* or type variants that we wouldn't get back from getTableDef().
*
* @param array $tableDef
*/
function filterDef(array $tableDef)
{
return $tableDef;
}
/**
* Validate a table definition array, checking for basic structure.
*
* If necessary, converts from an old-style array of ColumnDef objects.
*
* @param string $tableName
* @param array $def: table definition array
* @return array validated table definition array
*
* @throws Exception on wildly invalid input
*/
function validateDef($tableName, array $def)
{
if (isset($def[0]) && $def[0] instanceof ColumnDef) {
$def = $this->oldToNew($tableName, $def);
}
// A few quick checks :D
if (!isset($def['fields'])) {
throw new Exception("Invalid table definition for $tableName: no fields.");
}
return $def;
}
function isNumericType($type)
{
$type = strtolower($type);
$known = array('int', 'serial', 'numeric');
return in_array($type, $known);
}
/**
* Pull info from the query into a fun-fun array of dooooom
*
* @param string $sql
* @return array of arrays
*/
protected function fetchQueryData($sql)
{
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
throw new Exception($res->getMessage());
}
$out = array();
$row = array();
while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
$out[] = $row;
}
$res->free();
return $out;
}
}
class SchemaTableMissingException extends Exception

126
lib/schemaupdater.php Normal file
View File

@ -0,0 +1,126 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Database schema utilities
*
* 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 Database
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 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('STATUSNET')) {
exit(1);
}
class SchemaUpdater
{
public function __construct($schema)
{
$this->schema = $schema;
$this->checksums = $this->getChecksums();
}
/**
* @param string $tableName
* @param array $tableDef
*/
public function register($tableName, array $tableDef)
{
$this->tables[$tableName] = $tableDef;
}
/**
* Go ping em!
*
* @fixme handle tables that belong on different database servers...?
*/
public function checkSchema()
{
$checksums = $this->checksums;
foreach ($this->tables as $table => $def) {
$checksum = $this->checksum($def);
if (empty($checksums[$table])) {
common_log(LOG_DEBUG, "No previous schema_version for $table: updating to $checksum");
} else if ($checksums[$table] == $checksum) {
common_log(LOG_DEBUG, "Last schema_version for $table up to date: $checksum");
continue;
} else {
common_log(LOG_DEBUG, "Last schema_version for $table is {$checksums[$table]}: updating to $checksum");
}
//$this->conn->query('BEGIN');
$this->schema->ensureTable($table, $def);
$this->saveChecksum($table, $checksum);
//$this->conn->commit();
}
}
/**
* Calculate a checksum for this table definition array.
*
* @param array $def
* @return string
*/
public function checksum(array $def)
{
$flat = serialize($def);
return sha1($flat);
}
/**
* Pull all known table checksums into an array for easy lookup.
*
* @return array: associative array of table names to checksum strings
*/
protected function getChecksums()
{
$checksums = array();
$sv = new Schema_version();
$sv->find();
while ($sv->fetch()) {
$checksums[$sv->table_name] = $sv->checksum;
}
return $checksums;
}
/**
* Save or update current available checksums.
*
* @param string $table
* @param string $checksum
*/
protected function saveChecksum($table, $checksum)
{
$sv = new Schema_version();
$sv->table_name = $table;
$sv->checksum = $checksum;
$sv->modified = common_sql_now();
if (isset($this->checksums[$table])) {
$sv->update();
} else {
$sv->insert();
}
$this->checksums[$table] = $checksum;
}
}

View File

@ -70,7 +70,6 @@ class SearchAction extends Action
* @return void
* @see SearchGroupNav
*/
function showLocalNav()
{
$nav = new SearchGroupNav($this, $this->trimmed('q'));
@ -127,6 +126,7 @@ class SearchAction extends Action
// TRANS: Used as a field label for the field where one or more keywords
// TRANS: for searching can be entered.
$this->input('q', _('Keyword(s)'), $q);
// TRANS: Button text for searching site.
$this->submit('search', _m('BUTTON','Search'));
$this->elementEnd('li');
$this->elementEnd('ul');
@ -138,7 +138,7 @@ class SearchAction extends Action
}
function searchSuggestions($q) {
// @todo FIXME: This formatting does not make this string get picked up by gettext.
// @todo FIXME: i18n issue: This formatting does not make this string get picked up by gettext.
// TRANS: Standard search suggestions shown when a search does not give any results.
$message = _(<<<E_O_T
* Make sure all words are spelled correctly.
@ -150,7 +150,7 @@ E_O_T
);
if (!common_config('site', 'private')) {
$qe = urlencode($q);
// @todo FIXME: This formatting does not make this string get picked up by gettext.
// @todo FIXME: i18n issue: This formatting does not make this string get picked up by gettext.
// TRANS: Standard search suggestions shown when a search does not give any results.
$message .= sprintf(_(<<<E_O_T

View File

@ -245,7 +245,7 @@ class StatusNet
* Establish default configuration based on given or default server and path
* Sets global $_server, $_path, and $config
*/
protected static function initDefaults($server, $path)
public static function initDefaults($server, $path)
{
global $_server, $_path, $config;

View File

@ -163,9 +163,10 @@ class ThemeUploader
$estSize = $blockSize * max(1, intval(ceil($size / $blockSize)));
$totalSize += $estSize;
if ($totalSize > $sizeLimit) {
$msg = sprintf(_("Uploaded theme is too large; " .
"must be less than %d bytes uncompressed."),
$sizeLimit);
$msg = sprintf(_m('Uploaded theme is too large; must be less than %d byte uncompressed.',
'Uploaded theme is too large; must be less than %d bytes uncompressed.',
$sizeLimit),
$sizeLimit);
throw new ClientException($msg);
}

View File

@ -51,7 +51,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
abstract class UAPPlugin extends Plugin
{
public $mediumRectangle = null;
@ -66,7 +65,6 @@ abstract class UAPPlugin extends Plugin
*
* @return boolean hook flag
*/
function onEndShowStatusNetStyles($action)
{
// XXX: allow override by theme
@ -81,7 +79,6 @@ abstract class UAPPlugin extends Plugin
*
* @return boolean hook flag
*/
function onStartShowAside($action)
{
if (!is_null($this->mediumRectangle)) {
@ -95,6 +92,24 @@ abstract class UAPPlugin extends Plugin
$action->elementEnd('div');
}
// XXX: Hack to force ads to show on single-notice pages
if (!is_null($this->rectangle) &&
$action->trimmed('action') == 'shownotice') {
$action->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
if (Event::handle('StartShowSections', array($action))) {
$action->showSections();
Event::handle('EndShowSections', array($action));
}
$action->elementEnd('div');
return false;
}
return true;
}
@ -126,7 +141,6 @@ abstract class UAPPlugin extends Plugin
*
* @return boolean hook flag
*/
function onStartShowSections($action)
{
if (!is_null($this->rectangle)) {
@ -147,7 +161,6 @@ abstract class UAPPlugin extends Plugin
*
* @return boolean hook flag
*/
function onEndShowAside($action)
{
if (!is_null($this->wideSkyscraper)) {
@ -169,7 +182,6 @@ abstract class UAPPlugin extends Plugin
*
* @return void
*/
abstract protected function showMediumRectangle($action);
/**
@ -179,7 +191,6 @@ abstract class UAPPlugin extends Plugin
*
* @return void
*/
abstract protected function showRectangle($action);
/**
@ -189,7 +200,6 @@ abstract class UAPPlugin extends Plugin
*
* @return void
*/
abstract protected function showWideSkyscraper($action);
/**
@ -199,6 +209,5 @@ abstract class UAPPlugin extends Plugin
*
* @return void
*/
abstract protected function showLeaderboard($action);
}

View File

@ -44,7 +44,6 @@ if (!defined('STATUSNET')) {
*
* @see BlockForm
*/
class UnblockForm extends ProfileActionForm
{
/**
@ -52,7 +51,6 @@ class UnblockForm extends ProfileActionForm
*
* @return string Name of the action, lowercased.
*/
function target()
{
return 'unblock';
@ -63,11 +61,10 @@ class UnblockForm extends ProfileActionForm
*
* @return string Title of the form, internationalized
*/
function title()
{
// TRANS: Title for the form to unblock a user.
return _('Unblock');
return _m('TITLE','Unblock');
}
/**
@ -75,7 +72,6 @@ class UnblockForm extends ProfileActionForm
*
* @return string description of the form, internationalized
*/
function description()
{
// TRANS: Description of the form to unblock a user.

View File

@ -1038,7 +1038,7 @@ function common_group_link($sender_id, $nickname)
$attrs = array('href' => $group->permalink(),
'class' => 'url');
if (!empty($group->fullname)) {
$attrs['title'] = $group->fullname . ' (' . $group->nickname . ')';
$attrs['title'] = $group->getFancyName();
}
$xs = new XMLStringer();
$xs->elementStart('span', 'vcard');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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