diff --git a/.gitignore b/.gitignore index 83a53dfa3f..da6947bfdc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ dataobject.ini *.rej .#* *.swp +.buildpath +.project +.settings diff --git a/EVENTS.txt b/EVENTS.txt index 5edf59245a..8e917f11de 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -100,6 +100,20 @@ StartPublicGroupNav: Showing the public group nav menu EndPublicGroupNav: At the end of the public group nav menu - $action: the current action +StartSubGroupNav: Showing the subscriptions group nav menu +- $action: the current action + +EndSubGroupNav: At the end of the subscriptions group nav menu +- $action: the current action + RouterInitialized: After the router instance has been initialized - $m: the Net_URL_Mapper that has just been set up +StartLogout: Before logging out +- $action: the logout action + +EndLogout: After logging out +- $action: the logout action + +ArgsInitialized: After the argument array has been initialized +- $args: associative array of arguments, can be modified diff --git a/README b/README index 136b537c7d..9207f3e900 100644 --- a/README +++ b/README @@ -1133,6 +1133,9 @@ welcome: nickname of a user account that sends welcome messages to new busy servers it may be a good idea to keep that one just for 'urgent' messages. Default is null; no message. +If either of these special user accounts are specified, the users should +be created before the configuration is updated. + snapshot -------- diff --git a/actions/accesstoken.php b/actions/accesstoken.php index bb68d3314d..46b43c7021 100644 --- a/actions/accesstoken.php +++ b/actions/accesstoken.php @@ -59,7 +59,7 @@ class AccesstokenAction extends Action try { common_debug('getting request from env variables', __FILE__); common_remove_magic_from_request(); - $req = OAuthRequest::from_request('POST', common_locale_url('accesstoken')); + $req = OAuthRequest::from_request('POST', common_local_url('accesstoken')); common_debug('getting a server', __FILE__); $server = omb_oauth_server(); common_debug('fetching the access token', __FILE__); diff --git a/actions/all.php b/actions/all.php index a92e554623..a53bbea07b 100644 --- a/actions/all.php +++ b/actions/all.php @@ -93,7 +93,7 @@ class AllAction extends ProfileAction if (common_logged_in()) { $current_user = common_current_user(); if ($this->user->id === $current_user->id) { - $message .= _('Try subscribing to more people, [join a group](%%action.groups) or post something yourself.'); + $message .= _('Try subscribing to more people, [join a group](%%action.groups%%) or post something yourself.'); } else { $message .= sprintf(_('You can try to [nudge %s](../%s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname); } diff --git a/actions/api.php b/actions/api.php index d2f0a2eff0..8762b4bcd3 100644 --- a/actions/api.php +++ b/actions/api.php @@ -130,6 +130,7 @@ class ApiAction extends Action 'statuses/friends_timeline', 'statuses/friends', 'statuses/replies', + 'statuses/mentions', 'statuses/followers', 'favorites/favorites'); diff --git a/actions/attachment.php b/actions/attachment.php new file mode 100644 index 0000000000..b9187ff081 --- /dev/null +++ b/actions/attachment.php @@ -0,0 +1,209 @@ +. + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +//require_once INSTALLDIR.'/lib/personalgroupnav.php'; +//require_once INSTALLDIR.'/lib/feedlist.php'; +require_once INSTALLDIR.'/lib/attachmentlist.php'; + +/** + * Show notice attachments + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class AttachmentAction extends Action +{ + /** + * Attachment object to show + */ + + var $attachment = null; + + /** + * Load attributes based on database arguments + * + * Loads all the DB stuff + * + * @param array $args $_REQUEST array + * + * @return success flag + */ + + function prepare($args) + { + parent::prepare($args); + + $id = $this->arg('attachment'); + + $this->attachment = File::staticGet($id); + + if (!$this->attachment) { + $this->clientError(_('No such attachment.'), 404); + return false; + } + return true; + } + + /** + * Is this action read-only? + * + * @return boolean true + */ + + function isReadOnly($args) + { + return true; + } + + /** + * Title of the page + * + * @return string title of the page + */ + function title() + { + $a = new Attachment($this->attachment); + return $a->title(); + } + + /** + * Last-modified date for page + * + * When was the content of this page last modified? Based on notice, + * profile, avatar. + * + * @return int last-modified date as unix timestamp + */ +/* + function lastModified() + { + return max(strtotime($this->notice->created), + strtotime($this->profile->modified), + ($this->avatar) ? strtotime($this->avatar->modified) : 0); + } +*/ + + /** + * An entity tag for this page + * + * Shows the ETag for the page, based on the notice ID and timestamps + * for the notice, profile, and avatar. It's weak, since we change + * the date text "one hour ago", etc. + * + * @return string etag + */ +/* + function etag() + { + $avtime = ($this->avatar) ? + strtotime($this->avatar->modified) : 0; + + return 'W/"' . implode(':', array($this->arg('action'), + common_language(), + $this->notice->id, + strtotime($this->notice->created), + strtotime($this->profile->modified), + $avtime)) . '"'; + } +*/ + + + /** + * Handle input + * + * Only handles get, so just show the page. + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + /** + * Don't show local navigation + * + * @return void + */ + + function showLocalNavBlock() + { + } + + /** + * Fill the content area of the page + * + * Shows a single notice list item. + * + * @return void + */ + + function showContent() + { + $this->elementStart('ul', array('class' => 'attachments')); + $ali = new Attachment($this->attachment, $this); + $cnt = $ali->show(); + $this->elementEnd('ul'); + } + + /** + * Don't show page notice + * + * @return void + */ + + function showPageNoticeBlock() + { + } + + /** + * Show aside: this attachments appears in what notices + * + * @return void + */ + function showSections() { + $ns = new AttachmentNoticeSection($this); + $ns->show(); + $atcs = new AttachmentTagCloudSection($this); + $atcs->show(); + } +} + diff --git a/actions/attachment_ajax.php b/actions/attachment_ajax.php new file mode 100644 index 0000000000..1620b27dda --- /dev/null +++ b/actions/attachment_ajax.php @@ -0,0 +1,141 @@ +. + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/actions/attachment.php'; + +/** + * Show notice attachments + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class Attachment_ajaxAction extends AttachmentAction +{ + /** + * Load attributes based on database arguments + * + * Loads all the DB stuff + * + * @param array $args $_REQUEST array + * + * @return success flag + */ + + function prepare($args) + { + parent::prepare($args); + if (!$this->attachment) { + $this->clientError(_('No such attachment.'), 404); + return false; + } + return true; + } + + /** + * Show page, a template method. + * + * @return nothing + */ + function showPage() + { + if (Event::handle('StartShowBody', array($this))) { + $this->showCore(); + Event::handle('EndShowBody', array($this)); + } + } + + /** + * Show core. + * + * Shows local navigation, content block and aside. + * + * @return nothing + */ + function showCore() + { + $this->elementStart('div', array('id' => 'core')); + if (Event::handle('StartShowContentBlock', array($this))) { + $this->showContentBlock(); + Event::handle('EndShowContentBlock', array($this)); + } + $this->elementEnd('div'); + } + + + + /** + * Last-modified date for page + * + * When was the content of this page last modified? Based on notice, + * profile, avatar. + * + * @return int last-modified date as unix timestamp + */ +/* + function lastModified() + { + return max(strtotime($this->notice->created), + strtotime($this->profile->modified), + ($this->avatar) ? strtotime($this->avatar->modified) : 0); + } +*/ + + /** + * An entity tag for this page + * + * Shows the ETag for the page, based on the notice ID and timestamps + * for the notice, profile, and avatar. It's weak, since we change + * the date text "one hour ago", etc. + * + * @return string etag + */ +/* + function etag() + { + $avtime = ($this->avatar) ? + strtotime($this->avatar->modified) : 0; + + return 'W/"' . implode(':', array($this->arg('action'), + common_language(), + $this->notice->id, + strtotime($this->notice->created), + strtotime($this->profile->modified), + $avtime)) . '"'; + } +*/ +} + diff --git a/actions/attachments.php b/actions/attachments.php new file mode 100644 index 0000000000..6b31c839da --- /dev/null +++ b/actions/attachments.php @@ -0,0 +1,292 @@ +. + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +//require_once INSTALLDIR.'/lib/personalgroupnav.php'; +//require_once INSTALLDIR.'/lib/feedlist.php'; +require_once INSTALLDIR.'/lib/attachmentlist.php'; + +/** + * Show notice attachments + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class AttachmentsAction extends Action +{ + /** + * Notice object to show + */ + + var $notice = null; + + /** + * Profile of the notice object + */ + + var $profile = null; + + /** + * Avatar of the profile of the notice object + */ + + var $avatar = null; + + /** + * Is this action read-only? + * + * @return boolean true + */ + + function isReadOnly($args) + { + return true; + } + + /** + * Last-modified date for page + * + * When was the content of this page last modified? Based on notice, + * profile, avatar. + * + * @return int last-modified date as unix timestamp + */ + + function lastModified() + { + return max(strtotime($this->notice->created), + strtotime($this->profile->modified), + ($this->avatar) ? strtotime($this->avatar->modified) : 0); + } + + /** + * An entity tag for this page + * + * Shows the ETag for the page, based on the notice ID and timestamps + * for the notice, profile, and avatar. It's weak, since we change + * the date text "one hour ago", etc. + * + * @return string etag + */ + + function etag() + { + $avtime = ($this->avatar) ? + strtotime($this->avatar->modified) : 0; + + return 'W/"' . implode(':', array($this->arg('action'), + common_language(), + $this->notice->id, + strtotime($this->notice->created), + strtotime($this->profile->modified), + $avtime)) . '"'; + } + + /** + * Title of the page + * + * @return string title of the page + */ + + function title() + { + return sprintf(_('%1$s\'s status on %2$s'), + $this->profile->nickname, + common_exact_date($this->notice->created)); + } + + + /** + * Load attributes based on database arguments + * + * Loads all the DB stuff + * + * @param array $args $_REQUEST array + * + * @return success flag + */ + + function prepare($args) + { + parent::prepare($args); + + $id = $this->arg('notice'); + + $this->notice = Notice::staticGet($id); + + if (!$this->notice) { + $this->clientError(_('No such notice.'), 404); + return false; + } + + +/* +// STOP if there are no attachments +// maybe even redirect if there's a single one +// RYM FIXME TODO + $this->clientError(_('No such attachment.'), 404); + return false; + +*/ + + + + + $this->profile = $this->notice->getProfile(); + + if (!$this->profile) { + $this->serverError(_('Notice has no profile'), 500); + return false; + } + + $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + return true; + } + + + + /** + * Handle input + * + * Only handles get, so just show the page. + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + if ($this->notice->is_local == 0) { + if (!empty($this->notice->url)) { + common_redirect($this->notice->url, 301); + } else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) { + common_redirect($this->notice->uri, 301); + } + } else { + $f2p = new File_to_post; + $f2p->post_id = $this->notice->id; + $file = new File; + $file->joinAdd($f2p); + $file->selectAdd(); + $file->selectAdd('file.id as id'); + $count = $file->find(true); + if (!$count) return; + if (1 === $count) { + common_redirect(common_local_url('attachment', array('attachment' => $file->id)), 301); + } else { + $this->showPage(); + } + } + } + + /** + * Don't show local navigation + * + * @return void + */ + + function showLocalNavBlock() + { + } + + /** + * Fill the content area of the page + * + * Shows a single notice list item. + * + * @return void + */ + + function showContent() + { + $al = new AttachmentList($this->notice, $this); + $cnt = $al->show(); + } + + /** + * Don't show page notice + * + * @return void + */ + + function showPageNoticeBlock() + { + } + + /** + * Don't show aside + * + * @return void + */ + + function showAside() { + } + + /** + * Extra content + * + * We show the microid(s) for the author, if any. + * + * @return void + */ + + function extraHead() + { + $user = User::staticGet($this->profile->id); + + if (!$user) { + return; + } + + if ($user->emailmicroid && $user->email && $this->notice->uri) { + $id = new Microid('mailto:'. $user->email, + $this->notice->uri); + $this->element('meta', array('name' => 'microid', + 'content' => $id->toString())); + } + + if ($user->jabbermicroid && $user->jabber && $this->notice->uri) { + $id = new Microid('xmpp:', $user->jabber, + $this->notice->uri); + $this->element('meta', array('name' => 'microid', + 'content' => $id->toString())); + } + } +} + diff --git a/actions/attachments_ajax.php b/actions/attachments_ajax.php new file mode 100644 index 0000000000..402d8b5e79 --- /dev/null +++ b/actions/attachments_ajax.php @@ -0,0 +1,115 @@ +. + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +//require_once INSTALLDIR.'/lib/personalgroupnav.php'; +//require_once INSTALLDIR.'/lib/feedlist.php'; +require_once INSTALLDIR.'/actions/attachments.php'; + +/** + * Show notice attachments + * + * @category Personal + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class Attachments_ajaxAction extends AttachmentsAction +{ + function showContent() + { + } + + /** + * Fill the content area of the page + * + * Shows a single notice list item. + * + * @return void + */ + + function showContentBlock() + { + $al = new AttachmentList($this->notice, $this); + $cnt = $al->show(); + } + + /** + * Extra content + * + * We show the microid(s) for the author, if any. + * + * @return void + */ + + function extraHead() + { + } + + + /** + * Show page, a template method. + * + * @return nothing + */ + function showPage() + { + if (Event::handle('StartShowBody', array($this))) { + $this->showCore(); + Event::handle('EndShowBody', array($this)); + } + } + + /** + * Show core. + * + * Shows local navigation, content block and aside. + * + * @return nothing + */ + function showCore() + { + $this->elementStart('div', array('id' => 'core')); + if (Event::handle('StartShowContentBlock', array($this))) { + $this->showContentBlock(); + Event::handle('EndShowContentBlock', array($this)); + } + $this->elementEnd('div'); + } + + + + +} + diff --git a/actions/conversation.php b/actions/conversation.php index 05cfb76e3c..ef189016aa 100644 --- a/actions/conversation.php +++ b/actions/conversation.php @@ -11,7 +11,7 @@ * @link http://laconi.ca/ * * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,7 +31,7 @@ if (!defined('LACONICA')) { exit(1); } -require_once(INSTALLDIR.'/lib/noticelist.php'); +require_once INSTALLDIR.'/lib/noticelist.php'; /** * Conversation tree in the browser @@ -42,9 +42,10 @@ require_once(INSTALLDIR.'/lib/noticelist.php'); * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://laconi.ca/ */ + class ConversationAction extends Action { - var $id = null; + var $id = null; var $page = null; /** @@ -69,24 +70,47 @@ class ConversationAction extends Action return true; } + /** + * Handle the action + * + * @param array $args Web and URL arguments + * + * @return void + */ + function handle($args) { parent::handle($args); $this->showPage(); } + /** + * Returns the page title + * + * @return string page title + */ + function title() { return _("Conversation"); } + /** + * Show content. + * + * Display a hierarchical unordered list in the content area. + * Uses ConversationTree to do most of the heavy lifting. + * + * @return void + */ + function showContent() { // FIXME this needs to be a tree, not a list $qry = 'SELECT * FROM notice WHERE conversation = %s '; - $offset = ($this->page-1)*NOTICES_PER_PAGE; + $offset = ($this->page-1) * NOTICES_PER_PAGE; $limit = NOTICES_PER_PAGE + 1; $txt = sprintf($qry, $this->id); @@ -95,9 +119,9 @@ class ConversationAction extends Action 'notice:conversation:'.$this->id, $offset, $limit); - $nl = new NoticeList($notices, $this); + $ct = new ConversationTree($notices, $this); - $cnt = $nl->show(); + $cnt = $ct->show(); $this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE, $this->page, 'conversation', array('id' => $this->id)); @@ -105,3 +129,170 @@ class ConversationAction extends Action } +/** + * Conversation tree + * + * The widget class for displaying a hierarchical list of notices. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + */ + +class ConversationTree extends NoticeList +{ + var $tree = null; + var $table = null; + + /** + * Show the tree of notices + * + * @return void + */ + + function show() + { + $cnt = 0; + + $this->tree = array(); + $this->table = array(); + + while ($this->notice->fetch()) { + + $cnt++; + + $id = $this->notice->id; + $notice = clone($this->notice); + + $this->table[$id] = $notice; + + if (is_null($notice->reply_to)) { + $this->tree['root'] = array($notice->id); + } else if (array_key_exists($notice->reply_to, $this->tree)) { + $this->tree[$notice->reply_to][] = $notice->id; + } else { + $this->tree[$notice->reply_to] = array($notice->id); + } + } + + $this->out->elementStart('div', array('id' =>'notices_primary')); + $this->out->element('h2', null, _('Notices')); + $this->out->elementStart('ul', array('class' => 'notices')); + + if (array_key_exists('root', $this->tree)) { + $rootid = $this->tree['root'][0]; + $this->showNoticePlus($rootid); + } + + $this->out->elementEnd('ul'); + $this->out->elementEnd('div'); + + return $cnt; + } + + /** + * Shows a notice plus its list of children. + * + * @param integer $id ID of the notice to show + * + * @return void + */ + + function showNoticePlus($id) + { + $notice = $this->table[$id]; + + // We take responsibility for doing the li + + $this->out->elementStart('li', array('class' => 'hentry notice', + 'id' => 'notice-' . $this->notice->id)); + + $item = $this->newListItem($notice); + $item->show(); + + if (array_key_exists($id, $this->tree)) { + $children = $this->tree[$id]; + + $this->out->elementStart('ul', array('class' => 'notices')); + + foreach ($children as $child) { + $this->showNoticePlus($child); + } + + $this->out->elementEnd('ul'); + } + + $this->out->elementEnd('li'); + } + + /** + * Override parent class to return our preferred item. + * + * @param Notice $notice Notice to display + * + * @return NoticeListItem a list item to show + */ + + function newListItem($notice) + { + return new ConversationTreeItem($notice, $this->out); + } +} + +/** + * Conversation tree list item + * + * Special class of NoticeListItem for use inside conversation trees. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + */ + +class ConversationTreeItem extends NoticeListItem +{ + /** + * start a single notice. + * + * The default creates the
  • ; we skip, since the ConversationTree + * takes care of that. + * + * @return void + */ + + function showStart() + { + return; + } + + /** + * finish the notice + * + * The default closes the
  • ; we skip, since the ConversationTree + * takes care of that. + * + * @return void + */ + + function showEnd() + { + return; + } + + /** + * show link to notice conversation page + * + * Since we're only used on the conversation page, we skip this + * + * @return void + */ + + function showContext() + { + return; + } +} \ No newline at end of file diff --git a/actions/deletenotice.php b/actions/deletenotice.php index 6c350b33ab..e733f9650a 100644 --- a/actions/deletenotice.php +++ b/actions/deletenotice.php @@ -112,8 +112,8 @@ class DeletenoticeAction extends DeleteAction $this->hidden('token', common_session_token()); $this->hidden('notice', $this->trimmed('notice')); $this->element('p', null, _('Are you sure you want to delete this notice?')); - $this->submit('form_action-yes', _('Yes'), 'submit form_action-primary', 'yes'); - $this->submit('form_action-no', _('No'), 'submit form_action-secondary', 'no'); + $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not delete this notice")); + $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Delete this notice')); $this->elementEnd('fieldset'); $this->elementEnd('form'); } diff --git a/actions/designsettings.php b/actions/designsettings.php index cdd950e78c..315e5a199c 100644 --- a/actions/designsettings.php +++ b/actions/designsettings.php @@ -76,14 +76,22 @@ class DesignsettingsAction extends AccountSettingsAction 'action' => common_local_url('designsettings'))); $this->elementStart('fieldset'); -// $this->element('legend', null, _('Design settings')); $this->hidden('token', common_session_token()); $this->elementStart('fieldset', array('id' => 'settings_design_background-image')); $this->element('legend', null, _('Change background image')); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - $this->element('p', null, _('Upload background image')); + $this->element('label', array('for' => 'design_ background-image_file'), + _('Upload file')); + $this->element('input', array('name' => 'design_background-image_file', + 'type' => 'file', + 'id' => 'design_background-image_file')); + $this->element('p', 'form_guide', _('You can upload your personal background image. The maximum file size is 2Mb.')); + $this->element('input', array('name' => 'MAX_FILE_SIZE', + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => ImageFile::maxFileSizeInt())); $this->elementEnd('li'); $this->elementEnd('ul'); $this->elementEnd('fieldset'); @@ -91,28 +99,59 @@ class DesignsettingsAction extends AccountSettingsAction $this->elementStart('fieldset', array('id' => 'settings_design_color')); $this->element('legend', null, _('Change colours')); $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->input('color-1', _('Background color'), '#F0F2F5', null); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('color-2', _('Content background color'), '#FFFFFF', null); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('color-3', _('Sidebar background color'), '#CEE1E9', null); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('color-4', _('Text color'), '#000000', null); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('color-5', _('Link color'), '#002E6E', null); - $this->elementEnd('li'); + + //This is a JSON object in the DB field. Here for testing. Remove later. + $userSwatch = '{"body":{"background-color":"#F0F2F5"}, + "#content":{"background-color":"#FFFFFF"}, + "#aside_primary":{"background-color":"#CEE1E9"}, + "html body":{"color":"#000000"}, + "a":{"color":"#002E6E"}}'; + + //Default theme swatch -- Where should this be stored? + $defaultSwatch = array('body' => array('background-color' => '#F0F2F5'), + '#content' => array('background-color' => '#FFFFFF'), + '#aside_primary' => array('background-color' => '#CEE1E9'), + 'html body' => array('color' => '#000000'), + 'a' => array('color' => '#002E6E')); + + $userSwatch = ($userSwatch) ? json_decode($userSwatch, true) : $defaultSwatch; + + $s = 0; + $labelSwatch = array('Background', + 'Content', + 'Sidebar', + 'Text', + 'Links'); + foreach($userSwatch as $propertyvalue => $value) { + $foo = array_values($value); + $this->elementStart('li'); + $this->element('label', array('for' => 'swatch-'.$s), _($labelSwatch[$s])); + $this->element('input', array('name' => 'swatch-'.$s, //prefer swatch[$s] ? + 'type' => 'text', + 'id' => 'swatch-'.$s, + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => $foo[0])); + $this->elementEnd('li'); + $s++; + } + $this->elementEnd('ul'); - $this->element('div', array('id' => 'color-picker')); $this->elementEnd('fieldset'); + $this->element('input', array('id' => 'settings_design_reset', + 'type' => 'reset', + 'value' => 'Reset', + 'class' => 'submit form_action-primary', + 'title' => _('Reset back to default'))); + $this->submit('save', _('Save'), 'submit form_action-secondary', 'save', _('Save design')); - $this->submit('save', _('Save')); - +/*TODO: Check submitted form values: +json_encode(form values) +if submitted Swatch == DefaultSwatch, don't store in DB. +else store in BD +*/ $this->elementEnd('fieldset'); $this->elementEnd('form'); @@ -187,7 +226,7 @@ class DesignsettingsAction extends AccountSettingsAction /** - * Add the jCrop stylesheet + * Add the Farbtastic stylesheet * * @return void */ @@ -205,7 +244,7 @@ class DesignsettingsAction extends AccountSettingsAction } /** - * Add the jCrop scripts + * Add the Farbtastic scripts * * @return void */ @@ -214,14 +253,12 @@ class DesignsettingsAction extends AccountSettingsAction { parent::showScripts(); -// if ($this->mode == 'crop') { - $farbtasticPack = common_path('js/farbtastic/farbtastic.js'); - $farbtasticGo = common_path('js/farbtastic/farbtastic.go.js'); + $farbtasticPack = common_path('js/farbtastic/farbtastic.js'); + $farbtasticGo = common_path('js/farbtastic/farbtastic.go.js'); - $this->element('script', array('type' => 'text/javascript', - 'src' => $farbtasticPack)); - $this->element('script', array('type' => 'text/javascript', - 'src' => $farbtasticGo)); -// } + $this->element('script', array('type' => 'text/javascript', + 'src' => $farbtasticPack)); + $this->element('script', array('type' => 'text/javascript', + 'src' => $farbtasticGo)); } } diff --git a/actions/favoritesrss.php b/actions/favoritesrss.php index f85bf1b190..6b46b8dec7 100644 --- a/actions/favoritesrss.php +++ b/actions/favoritesrss.php @@ -107,7 +107,7 @@ class FavoritesrssAction extends Rss10Action $c = array('url' => common_local_url('favoritesrss', array('nickname' => $user->nickname)), - 'title' => sprintf(_("%s favorite notices"), $user->nickname), + 'title' => sprintf(_("%s's favorite notices"), $user->nickname), 'link' => common_local_url('showfavorites', array('nickname' => $user->nickname)), diff --git a/actions/finishopenidlogin.php b/actions/finishopenidlogin.php index 952185742f..b08b96df6c 100644 --- a/actions/finishopenidlogin.php +++ b/actions/finishopenidlogin.php @@ -191,11 +191,28 @@ class FinishopenidloginAction extends Action { # FIXME: save invite code before redirect, and check here - if (common_config('site', 'closed') || common_config('site', 'inviteonly')) { + if (common_config('site', 'closed')) { $this->clientError(_('Registration not allowed.')); return; } + $invite = null; + + if (common_config('site', 'inviteonly')) { + $code = $_SESSION['invitecode']; + if (empty($code)) { + $this->clientError(_('Registration not allowed.')); + return; + } + + $invite = Invitation::staticGet($code); + + if (empty($invite)) { + $this->clientError(_('Not a valid invitation code.')); + return; + } + } + $nickname = $this->trimmed('newname'); if (!Validate::string($nickname, array('min_length' => 1, @@ -257,10 +274,16 @@ class FinishopenidloginAction extends Action # XXX: add language # XXX: add timezone - $user = User::register(array('nickname' => $nickname, - 'email' => $email, - 'fullname' => $fullname, - 'location' => $location)); + $args = array('nickname' => $nickname, + 'email' => $email, + 'fullname' => $fullname, + 'location' => $location); + + if (!empty($invite)) { + $args['code'] = $invite->code; + } + + $user = User::register($args); $result = oid_link_user($user->id, $canonical, $display); diff --git a/actions/grouprss.php b/actions/grouprss.php index a9a2eef877..0b7280a11c 100644 --- a/actions/grouprss.php +++ b/actions/grouprss.php @@ -34,7 +34,7 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/rssaction.php'; -define('MEMBERS_PER_SECTION', 81); +define('MEMBERS_PER_SECTION', 27); /** * Group RSS feed diff --git a/actions/logout.php b/actions/logout.php index 9f3bfe2470..c34b10987a 100644 --- a/actions/logout.php +++ b/actions/logout.php @@ -70,10 +70,20 @@ class LogoutAction extends Action if (!common_logged_in()) { $this->clientError(_('Not logged in.')); } else { - common_set_user(null); - common_real_login(false); // not logged in - common_forgetme(); // don't log back in! + if (Event::handle('StartLogout', array($this))) { + $this->logout(); + } + Event::handle('EndLogout', array($this)); + common_redirect(common_local_url('public'), 303); } } + + function logout() + { + common_set_user(null); + common_real_login(false); // not logged in + common_forgetme(); // don't log back in! + } + } diff --git a/actions/newmessage.php b/actions/newmessage.php index 82276ff341..52d4899ba2 100644 --- a/actions/newmessage.php +++ b/actions/newmessage.php @@ -172,15 +172,54 @@ class NewmessageAction extends Action $this->notify($user, $this->other, $message); - $url = common_local_url('outbox', array('nickname' => $user->nickname)); + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, _('Message sent')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'command_result'), + sprintf(_('Direct message to %s sent'), + $this->other->nickname)); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('outbox', + array('nickname' => $user->nickname)); + common_redirect($url, 303); + } + } - common_redirect($url, 303); + /** + * Show an Ajax-y error message + * + * Goes back to the browser, where it's shown in a popup. + * + * @param string $msg Message to show + * + * @return void + */ + + function ajaxErrorMsg($msg) + { + $this->startHTML('text/xml;charset=utf-8', true); + $this->elementStart('head'); + $this->element('title', null, _('Ajax Error')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'error'), $msg); + $this->elementEnd('body'); + $this->elementEnd('html'); } function showForm($msg = null) { - $this->msg = $msg; + if ($msg && $this->boolean('ajax')) { + $this->ajaxErrorMsg($msg); + return; + } + $this->msg = $msg; $this->showPage(); } diff --git a/actions/newnotice.php b/actions/newnotice.php index cbd04c58b2..ae0ff96363 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -158,7 +158,8 @@ class NewnoticeAction extends Action $replyto = 'false'; } - $notice = Notice::saveNew($user->id, $content, 'web', 1, +// $notice = Notice::saveNew($user->id, $content_shortened, 'web', 1, + $notice = Notice::saveNew($user->id, $content_shortened, 'web', 1, ($replyto == 'false') ? null : $replyto); if (is_string($notice)) { @@ -166,6 +167,8 @@ class NewnoticeAction extends Action return; } + $this->saveUrls($notice); + common_broadcast_notice($notice); if ($this->boolean('ajax')) { @@ -191,6 +194,24 @@ class NewnoticeAction extends Action } } + /** save all urls in the notice to the db + * + * follow redirects and save all available file information + * (mimetype, date, size, oembed, etc.) + * + * @param class $notice Notice to pull URLs from + * + * @return void + */ + function saveUrls($notice) { + common_replace_urls_callback($notice->content, array($this, 'saveUrl'), $notice->id); + } + + function saveUrl($data) { + list($url, $notice_id) = $data; + $zzz = File::processNew($url, $notice_id); + } + /** * Show an Ajax-y error message * diff --git a/actions/openidsettings.php b/actions/openidsettings.php index 92469d20f8..5f59ebc014 100644 --- a/actions/openidsettings.php +++ b/actions/openidsettings.php @@ -67,8 +67,8 @@ class OpenidsettingsAction extends AccountSettingsAction function getInstructions() { - return _('[OpenID](%%doc.openid%%) lets you log into many sites ' . - ' with the same user account. '. + return _('[OpenID](%%doc.openid%%) lets you log into many sites' . + ' with the same user account.'. ' Manage your associated OpenIDs from here.'); } diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 60f7c0796e..fb847680b9 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -91,67 +91,68 @@ class ProfilesettingsAction extends AccountSettingsAction $this->element('legend', null, _('Profile information')); $this->hidden('token', common_session_token()); - # too much common patterns here... abstractable? - + // too much common patterns here... abstractable? $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->input('nickname', _('Nickname'), - ($this->arg('nickname')) ? $this->arg('nickname') : $profile->nickname, - _('1-64 lowercase letters or numbers, no punctuation or spaces')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('fullname', _('Full name'), - ($this->arg('fullname')) ? $this->arg('fullname') : $profile->fullname); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('homepage', _('Homepage'), - ($this->arg('homepage')) ? $this->arg('homepage') : $profile->homepage, - _('URL of your homepage, blog, or profile on another site')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->textarea('bio', _('Bio'), - ($this->arg('bio')) ? $this->arg('bio') : $profile->bio, - _('Describe yourself and your interests in 140 chars')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('location', _('Location'), - ($this->arg('location')) ? $this->arg('location') : $profile->location, - _('Where you are, like "City, State (or Region), Country"')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('tags', _('Tags'), - ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $user->getSelfTags()), - _('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated')); - $this->elementEnd('li'); - $this->elementStart('li'); - $language = common_language(); - $this->dropdown('language', _('Language'), - get_nice_language_list(), _('Preferred language'), - true, $language); - $this->elementEnd('li'); - $timezone = common_timezone(); - $timezones = array(); - foreach(DateTimeZone::listIdentifiers() as $k => $v) { - $timezones[$v] = $v; + if (Event::handle('StartProfileFormData', array($this))) { + $this->elementStart('li'); + $this->input('nickname', _('Nickname'), + ($this->arg('nickname')) ? $this->arg('nickname') : $profile->nickname, + _('1-64 lowercase letters or numbers, no punctuation or spaces')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('fullname', _('Full name'), + ($this->arg('fullname')) ? $this->arg('fullname') : $profile->fullname); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('homepage', _('Homepage'), + ($this->arg('homepage')) ? $this->arg('homepage') : $profile->homepage, + _('URL of your homepage, blog, or profile on another site')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->textarea('bio', _('Bio'), + ($this->arg('bio')) ? $this->arg('bio') : $profile->bio, + _('Describe yourself and your interests in 140 chars')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('location', _('Location'), + ($this->arg('location')) ? $this->arg('location') : $profile->location, + _('Where you are, like "City, State (or Region), Country"')); + $this->elementEnd('li'); + Event::handle('EndProfileFormData', array($this)); + $this->elementStart('li'); + $this->input('tags', _('Tags'), + ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $user->getSelfTags()), + _('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated')); + $this->elementEnd('li'); + $this->elementStart('li'); + $language = common_language(); + $this->dropdown('language', _('Language'), + get_nice_language_list(), _('Preferred language'), + false, $language); + $this->elementEnd('li'); + $timezone = common_timezone(); + $timezones = array(); + foreach(DateTimeZone::listIdentifiers() as $k => $v) { + $timezones[$v] = $v; + } + $this->elementStart('li'); + $this->dropdown('timezone', _('Timezone'), + $timezones, _('What timezone are you normally in?'), + true, $timezone); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('autosubscribe', + _('Automatically subscribe to whoever '. + 'subscribes to me (best for non-humans)'), + ($this->arg('autosubscribe')) ? + $this->boolean('autosubscribe') : $user->autosubscribe); + $this->elementEnd('li'); } - $this->elementStart('li'); - $this->dropdown('timezone', _('Timezone'), - $timezones, _('What timezone are you normally in?'), - true, $timezone); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('autosubscribe', - _('Automatically subscribe to whoever '. - 'subscribes to me (best for non-humans)'), - ($this->arg('autosubscribe')) ? - $this->boolean('autosubscribe') : $user->autosubscribe); - $this->elementEnd('li'); $this->elementEnd('ul'); $this->submit('save', _('Save')); $this->elementEnd('fieldset'); $this->elementEnd('form'); - } /** @@ -165,158 +166,158 @@ class ProfilesettingsAction extends AccountSettingsAction function handlePost() { - # CSRF protection - + // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { $this->showForm(_('There was a problem with your session token. '. - 'Try again, please.')); + 'Try again, please.')); return; } - $nickname = $this->trimmed('nickname'); - $fullname = $this->trimmed('fullname'); - $homepage = $this->trimmed('homepage'); - $bio = $this->trimmed('bio'); - $location = $this->trimmed('location'); - $autosubscribe = $this->boolean('autosubscribe'); - $language = $this->trimmed('language'); - $timezone = $this->trimmed('timezone'); - $tagstring = $this->trimmed('tags'); + if (Event::handle('StartProfileSaveForm', array($this))) { - # Some validation + $nickname = $this->trimmed('nickname'); + $fullname = $this->trimmed('fullname'); + $homepage = $this->trimmed('homepage'); + $bio = $this->trimmed('bio'); + $location = $this->trimmed('location'); + $autosubscribe = $this->boolean('autosubscribe'); + $language = $this->trimmed('language'); + $timezone = $this->trimmed('timezone'); + $tagstring = $this->trimmed('tags'); - if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { - $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.')); - return; - } else if (!User::allowed_nickname($nickname)) { - $this->showForm(_('Not a valid nickname.')); - return; - } else if (!is_null($homepage) && (strlen($homepage) > 0) && - !Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) { - $this->showForm(_('Homepage is not a valid URL.')); - return; - } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { - $this->showForm(_('Full name is too long (max 255 chars).')); - return; - } else if (!is_null($bio) && mb_strlen($bio) > 140) { - $this->showForm(_('Bio is too long (max 140 chars).')); - return; - } else if (!is_null($location) && mb_strlen($location) > 255) { - $this->showForm(_('Location is too long (max 255 chars).')); - return; - } else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) { - $this->showForm(_('Timezone not selected.')); - return; - } else if ($this->nicknameExists($nickname)) { - $this->showForm(_('Nickname already in use. Try another one.')); - return; - } else if (!is_null($language) && strlen($language) > 50) { - $this->showForm(_('Language is too long (max 50 chars).')); - return; - } - - if ($tagstring) { - $tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring)); - } else { - $tags = array(); - } - - foreach ($tags as $tag) { - if (!common_valid_profile_tag($tag)) { - $this->showForm(sprintf(_('Invalid tag: "%s"'), $tag)); + // Some validation + if (!Validate::string($nickname, array('min_length' => 1, + 'max_length' => 64, + 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { + $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.')); + return; + } else if (!User::allowed_nickname($nickname)) { + $this->showForm(_('Not a valid nickname.')); + return; + } else if (!is_null($homepage) && (strlen($homepage) > 0) && + !Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) { + $this->showForm(_('Homepage is not a valid URL.')); + return; + } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { + $this->showForm(_('Full name is too long (max 255 chars).')); + return; + } else if (!is_null($bio) && mb_strlen($bio) > 140) { + $this->showForm(_('Bio is too long (max 140 chars).')); + return; + } else if (!is_null($location) && mb_strlen($location) > 255) { + $this->showForm(_('Location is too long (max 255 chars).')); + return; + } else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) { + $this->showForm(_('Timezone not selected.')); + return; + } else if ($this->nicknameExists($nickname)) { + $this->showForm(_('Nickname already in use. Try another one.')); + return; + } else if (!is_null($language) && strlen($language) > 50) { + $this->showForm(_('Language is too long (max 50 chars).')); return; } - } - $user = common_current_user(); - - $user->query('BEGIN'); - - if ($user->nickname != $nickname || - $user->language != $language || - $user->timezone != $timezone) { - - common_debug('Updating user nickname from ' . $user->nickname . ' to ' . $nickname, - __FILE__); - common_debug('Updating user language from ' . $user->language . ' to ' . $language, - __FILE__); - common_debug('Updating user timezone from ' . $user->timezone . ' to ' . $timezone, - __FILE__); - - $original = clone($user); - - $user->nickname = $nickname; - $user->language = $language; - $user->timezone = $timezone; - - $result = $user->updateKeys($original); - - if ($result === false) { - common_log_db_error($user, 'UPDATE', __FILE__); - $this->serverError(_('Couldn\'t update user.')); - return; + if ($tagstring) { + $tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring)); } else { - # Re-initialize language environment if it changed - common_init_language(); + $tags = array(); } - } - # XXX: XOR + foreach ($tags as $tag) { + if (!common_valid_profile_tag($tag)) { + $this->showForm(sprintf(_('Invalid tag: "%s"'), $tag)); + return; + } + } - if ($user->autosubscribe ^ $autosubscribe) { + $user = common_current_user(); - $original = clone($user); + $user->query('BEGIN'); - $user->autosubscribe = $autosubscribe; + if ($user->nickname != $nickname || + $user->language != $language || + $user->timezone != $timezone) { - $result = $user->update($original); + common_debug('Updating user nickname from ' . $user->nickname . ' to ' . $nickname, + __FILE__); + common_debug('Updating user language from ' . $user->language . ' to ' . $language, + __FILE__); + common_debug('Updating user timezone from ' . $user->timezone . ' to ' . $timezone, + __FILE__); - if ($result === false) { - common_log_db_error($user, 'UPDATE', __FILE__); - $this->serverError(_('Couldn\'t update user for autosubscribe.')); + $original = clone($user); + + $user->nickname = $nickname; + $user->language = $language; + $user->timezone = $timezone; + + $result = $user->updateKeys($original); + + if ($result === false) { + common_log_db_error($user, 'UPDATE', __FILE__); + $this->serverError(_('Couldn\'t update user.')); + return; + } else { + // Re-initialize language environment if it changed + common_init_language(); + } + } + +// XXX: XOR + if ($user->autosubscribe ^ $autosubscribe) { + + $original = clone($user); + + $user->autosubscribe = $autosubscribe; + + $result = $user->update($original); + + if ($result === false) { + common_log_db_error($user, 'UPDATE', __FILE__); + $this->serverError(_('Couldn\'t update user for autosubscribe.')); + return; + } + } + + $profile = $user->getProfile(); + + $orig_profile = clone($profile); + + $profile->nickname = $user->nickname; + $profile->fullname = $fullname; + $profile->homepage = $homepage; + $profile->bio = $bio; + $profile->location = $location; + $profile->profileurl = common_profile_url($nickname); + + common_debug('Old profile: ' . common_log_objstring($orig_profile), __FILE__); + common_debug('New profile: ' . common_log_objstring($profile), __FILE__); + + $result = $profile->update($orig_profile); + + if (!$result) { + common_log_db_error($profile, 'UPDATE', __FILE__); + $this->serverError(_('Couldn\'t save profile.')); return; } + + // Set the user tags + $result = $user->setSelfTags($tags); + + if (!$result) { + $this->serverError(_('Couldn\'t save tags.')); + return; + } + + $user->query('COMMIT'); + Event::handle('EndProfileSaveForm', array($this)); + common_broadcast_profile($profile); + + $this->showForm(_('Settings saved.'), true); + } - - $profile = $user->getProfile(); - - $orig_profile = clone($profile); - - $profile->nickname = $user->nickname; - $profile->fullname = $fullname; - $profile->homepage = $homepage; - $profile->bio = $bio; - $profile->location = $location; - $profile->profileurl = common_profile_url($nickname); - - common_debug('Old profile: ' . common_log_objstring($orig_profile), __FILE__); - common_debug('New profile: ' . common_log_objstring($profile), __FILE__); - - $result = $profile->update($orig_profile); - - if (!$result) { - common_log_db_error($profile, 'UPDATE', __FILE__); - $this->serverError(_('Couldn\'t save profile.')); - return; - } - - # Set the user tags - - $result = $user->setSelfTags($tags); - - if (!$result) { - $this->serverError(_('Couldn\'t save tags.')); - return; - } - - $user->query('COMMIT'); - - common_broadcast_profile($profile); - - $this->showForm(_('Settings saved.'), true); } function nicknameExists($nickname) diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index 620fe7eb8e..82263fcd59 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -151,11 +151,11 @@ class RecoverpasswordAction extends Action $this->element('p', null, _('If you\'ve forgotten or lost your' . ' password, you can get a new one sent to' . - ' the email address you have stored ' . + ' the email address you have stored' . ' in your account.')); } else if ($this->mode == 'reset') { $this->element('p', null, - _('You\'ve been identified. Enter a ' . + _('You\'ve been identified. Enter a' . ' new password below. ')); } $this->elementEnd('div'); diff --git a/actions/register.php b/actions/register.php index 5c6fe39d3d..dcbbbdb6a6 100644 --- a/actions/register.php +++ b/actions/register.php @@ -55,6 +55,45 @@ class RegisterAction extends Action var $registered = false; + /** + * Prepare page to run + * + * + * @param $args + * @return string title + */ + + function prepare($args) + { + parent::prepare($args); + $this->code = $this->trimmed('code'); + + if (empty($this->code)) { + common_ensure_session(); + if (array_key_exists('invitecode', $_SESSION)) { + $this->code = $_SESSION['invitecode']; + } + } + + if (common_config('site', 'inviteonly') && empty($this->code)) { + $this->clientError(_('Sorry, only invited people can register.')); + return false; + } + + if (!empty($this->code)) { + $this->invite = Invitation::staticGet('code', $this->code); + if (empty($this->invite)) { + $this->clientError(_('Sorry, invalid invitation code.')); + return false; + } + // Store this in case we need it + common_ensure_session(); + $_SESSION['invitecode'] = $this->code; + } + + return true; + } + /** * Title of the page * @@ -108,109 +147,109 @@ class RegisterAction extends Action function tryRegister() { - $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { - $this->showForm(_('There was a problem with your session token. '. - 'Try again, please.')); - return; - } + if (Event::handle('StartRegistrationTry', array($this))) { + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm(_('There was a problem with your session token. '. + 'Try again, please.')); + return; + } - $nickname = $this->trimmed('nickname'); - $email = $this->trimmed('email'); - $fullname = $this->trimmed('fullname'); - $homepage = $this->trimmed('homepage'); - $bio = $this->trimmed('bio'); - $location = $this->trimmed('location'); + $nickname = $this->trimmed('nickname'); + $email = $this->trimmed('email'); + $fullname = $this->trimmed('fullname'); + $homepage = $this->trimmed('homepage'); + $bio = $this->trimmed('bio'); + $location = $this->trimmed('location'); - // We don't trim these... whitespace is OK in a password! + // We don't trim these... whitespace is OK in a password! + $password = $this->arg('password'); + $confirm = $this->arg('confirm'); - $password = $this->arg('password'); - $confirm = $this->arg('confirm'); + // invitation code, if any + $code = $this->trimmed('code'); - // invitation code, if any + if ($code) { + $invite = Invitation::staticGet($code); + } - $code = $this->trimmed('code'); + if (common_config('site', 'inviteonly') && !($code && $invite)) { + $this->clientError(_('Sorry, only invited people can register.')); + return; + } - $invite = null; + // Input scrubbing + $nickname = common_canonical_nickname($nickname); + $email = common_canonical_email($email); - if ($code) { - $invite = Invitation::staticGet($code); - } + if (!$this->boolean('license')) { + $this->showForm(_('You can\'t register if you don\'t '. + 'agree to the license.')); + } else if ($email && !Validate::email($email, true)) { + $this->showForm(_('Not a valid email address.')); + } else if (!Validate::string($nickname, array('min_length' => 1, + 'max_length' => 64, + 'format' => NICKNAME_FMT))) { + $this->showForm(_('Nickname must have only lowercase letters '. + 'and numbers and no spaces.')); + } else if ($this->nicknameExists($nickname)) { + $this->showForm(_('Nickname already in use. Try another one.')); + } else if (!User::allowed_nickname($nickname)) { + $this->showForm(_('Not a valid nickname.')); + } else if ($this->emailExists($email)) { + $this->showForm(_('Email address already exists.')); + } else if (!is_null($homepage) && (strlen($homepage) > 0) && + !Validate::uri($homepage, + array('allowed_schemes' => + array('http', 'https')))) { + $this->showForm(_('Homepage is not a valid URL.')); + return; + } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { + $this->showForm(_('Full name is too long (max 255 chars).')); + return; + } else if (!is_null($bio) && mb_strlen($bio) > 140) { + $this->showForm(_('Bio is too long (max 140 chars).')); + return; + } else if (!is_null($location) && mb_strlen($location) > 255) { + $this->showForm(_('Location is too long (max 255 chars).')); + return; + } else if (strlen($password) < 6) { + $this->showForm(_('Password must be 6 or more characters.')); + return; + } else if ($password != $confirm) { + $this->showForm(_('Passwords don\'t match.')); + } else if ($user = User::register(array('nickname' => $nickname, + 'password' => $password, + 'email' => $email, + 'fullname' => $fullname, + 'homepage' => $homepage, + 'bio' => $bio, + 'location' => $location, + 'code' => $code))) { + if (!$user) { + $this->showForm(_('Invalid username or password.')); + return; + } + // success! + if (!common_set_user($user)) { + $this->serverError(_('Error setting user.')); + return; + } + // this is a real login + common_real_login(true); + if ($this->boolean('rememberme')) { + common_debug('Adding rememberme cookie for ' . $nickname); + common_rememberme($user); + } - if (common_config('site', 'inviteonly') && !($code && !empty($invite))) { - $this->clientError(_('Sorry, only invited people can register.')); - return; - } + Event::handle('EndRegistrationTry', array($this)); - // Input scrubbing - - $nickname = common_canonical_nickname($nickname); - $email = common_canonical_email($email); - - if (!$this->boolean('license')) { - $this->showForm(_('You can\'t register if you don\'t '. - 'agree to the license.')); - } else if ($email && !Validate::email($email, true)) { - $this->showForm(_('Not a valid email address.')); - } else if (!Validate::string($nickname, array('min_length' => 1, - 'max_length' => 64, - 'format' => NICKNAME_FMT))) { - $this->showForm(_('Nickname must have only lowercase letters '. - 'and numbers and no spaces.')); - } else if ($this->nicknameExists($nickname)) { - $this->showForm(_('Nickname already in use. Try another one.')); - } else if (!User::allowed_nickname($nickname)) { - $this->showForm(_('Not a valid nickname.')); - } else if ($this->emailExists($email)) { - $this->showForm(_('Email address already exists.')); - } else if (!is_null($homepage) && (strlen($homepage) > 0) && - !Validate::uri($homepage, - array('allowed_schemes' => - array('http', 'https')))) { - $this->showForm(_('Homepage is not a valid URL.')); - return; - } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { - $this->showForm(_('Full name is too long (max 255 chars).')); - return; - } else if (!is_null($bio) && mb_strlen($bio) > 140) { - $this->showForm(_('Bio is too long (max 140 chars).')); - return; - } else if (!is_null($location) && mb_strlen($location) > 255) { - $this->showForm(_('Location is too long (max 255 chars).')); - return; - } else if (strlen($password) < 6) { - $this->showForm(_('Password must be 6 or more characters.')); - return; - } else if ($password != $confirm) { - $this->showForm(_('Passwords don\'t match.')); - } else if ($user = User::register(array('nickname' => $nickname, - 'password' => $password, - 'email' => $email, - 'fullname' => $fullname, - 'homepage' => $homepage, - 'bio' => $bio, - 'location' => $location, - 'code' => $code))) { - if (!$user) { + // Re-init language env in case it changed (not yet, but soon) + common_init_language(); + $this->showSuccess(); + } else { $this->showForm(_('Invalid username or password.')); - return; } - // success! - if (!common_set_user($user)) { - $this->serverError(_('Error setting user.')); - return; - } - // this is a real login - common_real_login(true); - if ($this->boolean('rememberme')) { - common_debug('Adding rememberme cookie for ' . $nickname); - common_rememberme($user); - } - // Re-init language env in case it changed (not yet, but soon) - common_init_language(); - $this->showSuccess(); - } else { - $this->showForm(_('Invalid username or password.')); } } @@ -252,22 +291,24 @@ class RegisterAction extends Action // overrrided to add entry-title class function showPageTitle() { - $this->element('h1', array('class' => 'entry-title'), $this->title()); + if (Event::handle('StartShowPageTitle', array($this))) { + $this->element('h1', array('class' => 'entry-title'), $this->title()); + } } // overrided to add hentry, and content-inner class function showContentBlock() - { - $this->elementStart('div', array('id' => 'content', 'class' => 'hentry')); - $this->showPageTitle(); - $this->showPageNoticeBlock(); - $this->elementStart('div', array('id' => 'content_inner', - 'class' => 'entry-content')); - // show the actual content (forms, lists, whatever) - $this->showContent(); - $this->elementEnd('div'); - $this->elementEnd('div'); - } + { + $this->elementStart('div', array('id' => 'content', 'class' => 'hentry')); + $this->showPageTitle(); + $this->showPageNoticeBlock(); + $this->elementStart('div', array('id' => 'content_inner', + 'class' => 'entry-content')); + // show the actual content (forms, lists, whatever) + $this->showContent(); + $this->elementEnd('div'); + $this->elementEnd('div'); + } /** * Instructions or a notice for the page @@ -362,82 +403,85 @@ class RegisterAction extends Action $this->element('legend', null, 'Account settings'); $this->hidden('token', common_session_token()); - if ($code) { - $this->hidden('code', $code); + if ($this->code) { + $this->hidden('code', $this->code); } $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->input('nickname', _('Nickname'), $this->trimmed('nickname'), - _('1-64 lowercase letters or numbers, '. - 'no punctuation or spaces. Required.')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->password('password', _('Password'), - _('6 or more characters. Required.')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->password('confirm', _('Confirm'), - _('Same as password above. Required.')); - $this->elementEnd('li'); - $this->elementStart('li'); - if (!empty($invite) && $invite->address_type == 'email') { - $this->input('email', _('Email'), $invite->address, - _('Used only for updates, announcements, '. - 'and password recovery')); - } else { - $this->input('email', _('Email'), $this->trimmed('email'), - _('Used only for updates, announcements, '. - 'and password recovery')); + if (Event::handle('StartRegistrationFormData', array($this))) { + $this->elementStart('li'); + $this->input('nickname', _('Nickname'), $this->trimmed('nickname'), + _('1-64 lowercase letters or numbers, '. + 'no punctuation or spaces. Required.')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->password('password', _('Password'), + _('6 or more characters. Required.')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->password('confirm', _('Confirm'), + _('Same as password above. Required.')); + $this->elementEnd('li'); + $this->elementStart('li'); + if ($this->invite && $this->invite->address_type == 'email') { + $this->input('email', _('Email'), $this->invite->address, + _('Used only for updates, announcements, '. + 'and password recovery')); + } else { + $this->input('email', _('Email'), $this->trimmed('email'), + _('Used only for updates, announcements, '. + 'and password recovery')); + } + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('fullname', _('Full name'), + $this->trimmed('fullname'), + _('Longer name, preferably your "real" name')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('homepage', _('Homepage'), + $this->trimmed('homepage'), + _('URL of your homepage, blog, '. + 'or profile on another site')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->textarea('bio', _('Bio'), + $this->trimmed('bio'), + _('Describe yourself and your '. + 'interests in 140 chars')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('location', _('Location'), + $this->trimmed('location'), + _('Where you are, like "City, '. + 'State (or Region), Country"')); + $this->elementEnd('li'); + Event::handle('EndRegistrationFormData', array($this)); + $this->elementStart('li', array('id' => 'settings_rememberme')); + $this->checkbox('rememberme', _('Remember me'), + $this->boolean('rememberme'), + _('Automatically login in the future; '. + 'not for shared computers!')); + $this->elementEnd('li'); + $attrs = array('type' => 'checkbox', + 'id' => 'license', + 'class' => 'checkbox', + 'name' => 'license', + 'value' => 'true'); + if ($this->boolean('license')) { + $attrs['checked'] = 'checked'; + } + $this->elementStart('li'); + $this->element('input', $attrs); + $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license')); + $this->text(_('My text and files are available under ')); + $this->element('a', array('href' => common_config('license', 'url')), + common_config('license', 'title'), _("Creative Commons Attribution 3.0")); + $this->text(_(' except this private data: password, '. + 'email address, IM address, and phone number.')); + $this->elementEnd('label'); + $this->elementEnd('li'); } - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('fullname', _('Full name'), - $this->trimmed('fullname'), - _('Longer name, preferably your "real" name')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('homepage', _('Homepage'), - $this->trimmed('homepage'), - _('URL of your homepage, blog, '. - 'or profile on another site')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->textarea('bio', _('Bio'), - $this->trimmed('bio'), - _('Describe yourself and your '. - 'interests in 140 chars')); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->input('location', _('Location'), - $this->trimmed('location'), - _('Where you are, like "City, '. - 'State (or Region), Country"')); - $this->elementEnd('li'); - $this->elementStart('li', array('id' => 'settings_rememberme')); - $this->checkbox('rememberme', _('Remember me'), - $this->boolean('rememberme'), - _('Automatically login in the future; '. - 'not for shared computers!')); - $this->elementEnd('li'); - $attrs = array('type' => 'checkbox', - 'id' => 'license', - 'class' => 'checkbox', - 'name' => 'license', - 'value' => 'true'); - if ($this->boolean('license')) { - $attrs['checked'] = 'checked'; - } - $this->elementStart('li'); - $this->element('input', $attrs); - $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license')); - $this->text(_('My text and files are available under ')); - $this->element('a', array('href' => common_config('license', 'url')), - common_config('license', 'title'), _("Creative Commons Attribution 3.0")); - $this->text(_(' except this private data: password, '. - 'email address, IM address, and phone number.')); - $this->elementEnd('label'); - $this->elementEnd('li'); $this->elementEnd('ul'); $this->submit('submit', _('Register')); $this->elementEnd('fieldset'); @@ -519,3 +563,4 @@ class RegisterAction extends Action $nav->show(); } } + diff --git a/actions/showfavorites.php b/actions/showfavorites.php index 6e011d5ca5..eed62a2ab3 100644 --- a/actions/showfavorites.php +++ b/actions/showfavorites.php @@ -74,9 +74,9 @@ class ShowfavoritesAction extends Action function title() { if ($this->page == 1) { - return sprintf(_("%s favorite notices"), $this->user->nickname); + return sprintf(_("%s's favorite notices"), $this->user->nickname); } else { - return sprintf(_("%s favorite notices, page %d"), + return sprintf(_("%s's favorite notices, page %d"), $this->user->nickname, $this->page); } diff --git a/actions/showgroup.php b/actions/showgroup.php index 025f8383a2..a7df397273 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -35,7 +35,7 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/noticelist.php'; require_once INSTALLDIR.'/lib/feedlist.php'; -define('MEMBERS_PER_SECTION', 81); +define('MEMBERS_PER_SECTION', 27); /** * Group main page @@ -361,7 +361,7 @@ class ShowgroupAction extends Action $this->element('p', null, _('(None)')); } - if ($cnt == MEMBERS_PER_SECTION) { + if ($cnt > MEMBERS_PER_SECTION) { $this->element('a', array('href' => common_local_url('groupmembers', array('nickname' => $this->group->nickname))), _('All members')); diff --git a/actions/showstream.php b/actions/showstream.php index 82665e5b8b..678a3174c1 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -68,6 +68,9 @@ class ShowstreamAction extends ProfileAction } else { $base = $this->user->nickname; } + if (!empty($this->tag)) { + $base .= sprintf(_(' tagged %s'), $this->tag); + } if ($this->page == 1) { return $base; @@ -110,6 +113,15 @@ class ShowstreamAction extends ProfileAction function getFeeds() { + if (!empty($this->tag)) { + return array(new Feed(Feed::RSS1, + common_local_url('userrss', + array('nickname' => $this->user->nickname, + 'tag' => $this->tag)), + sprintf(_('Notice feed for %s tagged %s (RSS 1.0)'), + $this->user->nickname, $this->tag))); + } + return array(new Feed(Feed::RSS1, common_local_url('userrss', array('nickname' => $this->user->nickname)), @@ -363,7 +375,9 @@ class ShowstreamAction extends ProfileAction function showNotices() { - $notice = $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1); + $notice = empty($this->tag) + ? $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1) + : $this->user->getTaggedNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1, 0, 0, null, $this->tag); $pnl = new ProfileNoticeList($notice, $this); $cnt = $pnl->show(); diff --git a/actions/subscribers.php b/actions/subscribers.php index d91a7d4fd3..4482de9a7c 100644 --- a/actions/subscribers.php +++ b/actions/subscribers.php @@ -118,6 +118,16 @@ class SubscribersAction extends GalleryAction $this->raw(common_markup_to_html($message)); $this->elementEnd('div'); } + + function showSections() + { + parent::showSections(); + $cloud = new SubscribersPeopleTagCloudSection($this); + $cloud->show(); + + $cloud2 = new SubscribersPeopleSelfTagCloudSection($this); + $cloud2->show(); + } } class SubscribersList extends ProfileList diff --git a/actions/subscriptions.php b/actions/subscriptions.php index e6f3c54db8..095b18ad87 100644 --- a/actions/subscriptions.php +++ b/actions/subscriptions.php @@ -125,6 +125,16 @@ class SubscriptionsAction extends GalleryAction $this->raw(common_markup_to_html($message)); $this->elementEnd('div'); } + + function showSections() + { + parent::showSections(); + $cloud = new SubscriptionsPeopleTagCloudSection($this); + $cloud->show(); + + $cloud2 = new SubscriptionsPeopleSelfTagCloudSection($this); + $cloud2->show(); + } } class SubscriptionsList extends ProfileList diff --git a/actions/tag.php b/actions/tag.php index 02f3e35225..47420e4c33 100644 --- a/actions/tag.php +++ b/actions/tag.php @@ -49,9 +49,10 @@ class TagAction extends Action { $pop = new PopularNoticeSection($this); $pop->show(); + $freqatt = new FrequentAttachmentSection($this); + $freqatt->show(); } - function title() { if ($this->page == 1) { diff --git a/actions/twitapifriendships.php b/actions/twitapifriendships.php index c50c5e84a9..2f8250e0dc 100644 --- a/actions/twitapifriendships.php +++ b/actions/twitapifriendships.php @@ -133,11 +133,7 @@ class TwitapifriendshipsAction extends TwitterapiAction return; } - if ($user_a->isSubscribed($user_b)) { - $result = 'true'; - } else { - $result = 'false'; - } + $result = $user_a->isSubscribed($user_b); switch ($apidata['content-type']) { case 'xml': diff --git a/actions/twitapistatuses.php b/actions/twitapistatuses.php index 323c4f1f88..3abeba3672 100644 --- a/actions/twitapistatuses.php +++ b/actions/twitapistatuses.php @@ -144,10 +144,10 @@ class TwitapistatusesAction extends TwitterapiAction break; case 'atom': if (isset($apidata['api_arg'])) { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom'; } else { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/friends_timeline.atom'; } $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri); @@ -231,10 +231,10 @@ class TwitapistatusesAction extends TwitterapiAction break; case 'atom': if (isset($apidata['api_arg'])) { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom'; } else { - $selfuri = $selfuri = common_root_url() . + $selfuri = common_root_url() . 'api/statuses/user_timeline.atom'; } $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri); @@ -344,7 +344,7 @@ class TwitapistatusesAction extends TwitterapiAction $this->show($args, $apidata); } - function replies($args, $apidata) + function mentions($args, $apidata) { parent::handle($args); @@ -360,11 +360,13 @@ class TwitapistatusesAction extends TwitterapiAction $profile = $user->getProfile(); $sitename = common_config('site', 'name'); - $title = sprintf(_('%1$s / Updates replying to %2$s'), $sitename, $user->nickname); + $title = sprintf(_('%1$s / Updates mentioning %2$s'), + $sitename, $user->nickname); $taguribase = common_config('integration', 'taguri'); - $id = "tag:$taguribase:Replies:".$user->id; + $id = "tag:$taguribase:Mentions:".$user->id; $link = common_local_url('replies', array('nickname' => $user->nickname)); - $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), $sitename, $user->nickname, $profile->getBestName()); + $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), + $sitename, $user->nickname, $profile->getBestName()); if (!$page) { $page = 1; @@ -385,7 +387,8 @@ class TwitapistatusesAction extends TwitterapiAction $since = strtotime($this->arg('since')); - $notice = $user->getReplies((($page-1)*20), $count, $since_id, $before_id, $since); + $notice = $user->getReplies((($page-1)*20), + $count, $since_id, $before_id, $since); $notices = array(); while ($notice->fetch()) { @@ -400,14 +403,10 @@ class TwitapistatusesAction extends TwitterapiAction $this->show_rss_timeline($notices, $title, $link, $subtitle); break; case 'atom': - if (isset($apidata['api_arg'])) { - $selfuri = $selfuri = common_root_url() . - 'api/statuses/replies/' . $apidata['api_arg'] . '.atom'; - } else { - $selfuri = $selfuri = common_root_url() . - 'api/statuses/replies.atom'; - } - $this->show_atom_timeline($notices, $title, $id, $link, $subtitle, null, $selfuri); + $selfuri = common_root_url() . + ltrim($_SERVER['QUERY_STRING'], 'p='); + $this->show_atom_timeline($notices, $title, $id, $link, $subtitle, + null, $selfuri); break; case 'json': $this->show_json_timeline($notices); @@ -418,6 +417,11 @@ class TwitapistatusesAction extends TwitterapiAction } + function replies($args, $apidata) + { + call_user_func(array($this, 'mentions'), $args, $apidata); + } + function show($args, $apidata) { parent::handle($args); diff --git a/actions/twitapiusers.php b/actions/twitapiusers.php index 2894b7486d..1542cfb33e 100644 --- a/actions/twitapiusers.php +++ b/actions/twitapiusers.php @@ -82,8 +82,8 @@ class TwitapiusersAction extends TwitterapiAction $twitter_user['profile_text_color'] = ''; $twitter_user['profile_link_color'] = ''; $twitter_user['profile_sidebar_fill_color'] = ''; - $twitter_user['profile_sidebar_border_color'] = ''; - $twitter_user['profile_background_tile'] = 'false'; + $twitter_user['profile_sidebar_border_color'] = ''; + $twitter_user['profile_background_tile'] = false; $faves = DB_DataObject::factory('fave'); $faves->user_id = $user->id; @@ -103,24 +103,16 @@ class TwitapiusersAction extends TwitterapiAction if (isset($apidata['user'])) { - if ($apidata['user']->isSubscribed($profile)) { - $twitter_user['following'] = 'true'; - } else { - $twitter_user['following'] = 'false'; + $twitter_user['following'] = $apidata['user']->isSubscribed($profile); + + // Notifications on? + $sub = Subscription::pkeyGet(array('subscriber' => + $apidata['user']->id, 'subscribed' => $profile->id)); + + if ($sub) { + $twitter_user['notifications'] = ($sub->jabber || $sub->sms); } - - // Notifications on? - $sub = Subscription::pkeyGet(array('subscriber' => - $apidata['user']->id, 'subscribed' => $profile->id)); - - if ($sub) { - if ($sub->jabber || $sub->sms) { - $twitter_user['notifications'] = 'true'; - } else { - $twitter_user['notifications'] = 'false'; - } - } - } + } if ($apidata['content-type'] == 'xml') { $this->init_document('xml'); diff --git a/actions/twittersettings.php b/actions/twittersettings.php index 45725d3ff4..2b742788ee 100644 --- a/actions/twittersettings.php +++ b/actions/twittersettings.php @@ -138,7 +138,7 @@ class TwittersettingsAction extends ConnectSettingsAction $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - $this->checkbox('noticesync', + $this->checkbox('noticesend', _('Automatically send my notices to Twitter.'), ($flink) ? ($flink->noticesync & FOREIGN_NOTICE_SEND) : @@ -158,6 +158,22 @@ class TwittersettingsAction extends ConnectSettingsAction ($flink->friendsync & FOREIGN_FRIEND_RECV) : false); $this->elementEnd('li'); + + if (common_config('twitterbridge','enabled')) { + $this->elementStart('li'); + $this->checkbox('noticerecv', + _('Import my Friends Timeline.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_RECV) : + false); + $this->elementEnd('li'); + } else { + // preserve setting even if bidrection bridge toggled off + if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { + $this->hidden('noticerecv', true, 'noticerecv'); + } + } + $this->elementEnd('ul'); if ($flink) { @@ -261,7 +277,7 @@ class TwittersettingsAction extends ConnectSettingsAction 'alt' => ($other->fullname) ? $other->fullname : $other->nickname)); - + $this->element('span', 'fn nickname', $other->nickname); $this->elementEnd('a'); $this->elementEnd('li'); @@ -320,7 +336,8 @@ class TwittersettingsAction extends ConnectSettingsAction { $screen_name = $this->trimmed('twitter_username'); $password = $this->trimmed('twitter_password'); - $noticesync = $this->boolean('noticesync'); + $noticesend = $this->boolean('noticesend'); + $noticerecv = $this->boolean('noticerecv'); $replysync = $this->boolean('replysync'); $friendsync = $this->boolean('friendsync'); @@ -363,7 +380,7 @@ class TwittersettingsAction extends ConnectSettingsAction $flink->credentials = $password; $flink->created = common_sql_now(); - $flink->set_flags($noticesync, $replysync, $friendsync); + $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); $flink_id = $flink->insert(); @@ -375,6 +392,8 @@ class TwittersettingsAction extends ConnectSettingsAction if ($friendsync) { save_twitter_friends($user, $twit_user->id, $screen_name, $password); + $flink->last_friendsync = common_sql_now(); + $flink->update(); } $this->showForm(_('Twitter settings saved.'), true); @@ -419,7 +438,8 @@ class TwittersettingsAction extends ConnectSettingsAction function savePreferences() { - $noticesync = $this->boolean('noticesync'); + $noticesend = $this->boolean('noticesend'); + $noticerecv = $this->boolean('noticerecv'); $friendsync = $this->boolean('friendsync'); $replysync = $this->boolean('replysync'); @@ -448,7 +468,7 @@ class TwittersettingsAction extends ConnectSettingsAction $original = clone($flink); - $flink->set_flags($noticesync, $replysync, $friendsync); + $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); $result = $flink->update($original); diff --git a/actions/userrss.php b/actions/userrss.php index 5861d9ee36..2280509b22 100644 --- a/actions/userrss.php +++ b/actions/userrss.php @@ -25,14 +25,15 @@ require_once(INSTALLDIR.'/lib/rssaction.php'); class UserrssAction extends Rss10Action { - var $user = null; + var $tag = null; function prepare($args) { parent::prepare($args); - $nickname = $this->trimmed('nickname'); + $nickname = $this->trimmed('nickname'); $this->user = User::staticGet('nickname', $nickname); + $this->tag = $this->trimmed('tag'); if (!$this->user) { $this->clientError(_('No such user.')); @@ -42,6 +43,25 @@ class UserrssAction extends Rss10Action } } + function getTaggedNotices($tag = null, $limit=0) + { + $user = $this->user; + + if (is_null($user)) { + return null; + } + + $notice = $user->getTaggedNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit, 0, 0, null, $tag); + + $notices = array(); + while ($notice->fetch()) { + $notices[] = clone($notice); + } + + return $notices; + } + + function getNotices($limit=0) { diff --git a/classes/Fave.php b/classes/Fave.php index 24df5938c2..915b4572ff 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -4,7 +4,7 @@ */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Fave extends Memcached_DataObject +class Fave extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -31,9 +31,58 @@ class Fave extends Memcached_DataObject } return $fave; } - + function &pkeyGet($kv) { return Memcached_DataObject::pkeyGet('Fave', $kv); } + + function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE) + { + $ids = Notice::stream(array('Fave', '_streamDirect'), + array($user_id), + 'fave:ids_by_user:'.$user_id, + $offset, $limit); + return $ids; + } + + function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since) + { + $fav = new Fave(); + + $fav->user_id = $user_id; + + $fav->selectAdd(); + $fav->selectAdd('notice_id'); + + if ($since_id != 0) { + $fav->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $fav->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $fav->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + // NOTE: we sort by fave time, not by notice time! + + $fav->orderBy('modified DESC'); + + if (!is_null($offset)) { + $fav->limit($offset, $limit); + } + + $ids = array(); + + if ($fav->find()) { + while ($fav->fetch()) { + $ids[] = $fav->notice_id; + } + } + + return $ids; + } } diff --git a/classes/File.php b/classes/File.php new file mode 100644 index 0000000000..e5913115b7 --- /dev/null +++ b/classes/File.php @@ -0,0 +1,123 @@ +. + */ + +if (!defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; +require_once INSTALLDIR.'/classes/File_redirection.php'; +require_once INSTALLDIR.'/classes/File_oembed.php'; +require_once INSTALLDIR.'/classes/File_thumbnail.php'; +require_once INSTALLDIR.'/classes/File_to_post.php'; +//require_once INSTALLDIR.'/classes/File_redirection.php'; + +/** + * Table Definition for file + */ + +class File extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'file'; // table name + public $id; // int(11) not_null primary_key group_by + public $url; // varchar(255) unique_key + public $mimetype; // varchar(50) + public $size; // int(11) group_by + public $title; // varchar(255) + public $date; // int(11) group_by + public $protected; // int(1) group_by + + /* Static get */ + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + function isProtected($url) { + return 'http://www.facebook.com/login.php' === $url; + } + + function getAttachments($post_id) { + $query = "select file.* from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $this->escape($post_id); + $this->query($query); + $att = array(); + while ($this->fetch()) { + $att[] = clone($this); + } + $this->free(); + return $att; + } + + function saveNew($redir_data, $given_url) { + $x = new File; + $x->url = $given_url; + if (!empty($redir_data['protected'])) $x->protected = $redir_data['protected']; + if (!empty($redir_data['title'])) $x->title = $redir_data['title']; + if (!empty($redir_data['type'])) $x->mimetype = $redir_data['type']; + if (!empty($redir_data['size'])) $x->size = intval($redir_data['size']); + if (isset($redir_data['time']) && $redir_data['time'] > 0) $x->date = intval($redir_data['time']); + $file_id = $x->insert(); + + if (isset($redir_data['type']) + && ('text/html' === substr($redir_data['type'], 0, 9)) + && ($oembed_data = File_oembed::_getOembed($given_url)) + && isset($oembed_data['json'])) { + + File_oembed::saveNew($oembed_data['json'], $file_id); + } + return $x; + } + + function processNew($given_url, $notice_id) { + if (empty($given_url)) return -1; // error, no url to process + $given_url = File_redirection::_canonUrl($given_url); + if (empty($given_url)) return -1; // error, no url to process + $file = File::staticGet('url', $given_url); + if (empty($file->id)) { + $file_redir = File_redirection::staticGet('url', $given_url); + if (empty($file_redir->id)) { + $redir_data = File_redirection::where($given_url); + $redir_url = $redir_data['url']; + if ($redir_url === $given_url) { + $x = File::saveNew($redir_data, $given_url); + $file_id = $x->id; + + } else { + $x = File::processNew($redir_url, $notice_id); + $file_id = $x->id; + File_redirection::saveNew($redir_data, $file_id, $given_url); + } + } else { + $file_id = $file_redir->file_id; + } + } else { + $file_id = $file->id; + $x = $file; + } + + if (empty($x)) { + $x = File::staticGet($file_id); + if (empty($x)) die('Impossible!'); + } + + File_to_post::processNew($file_id, $notice_id); + return $x; + } +} diff --git a/classes/File_oembed.php b/classes/File_oembed.php new file mode 100644 index 0000000000..f1b2cb13c0 --- /dev/null +++ b/classes/File_oembed.php @@ -0,0 +1,87 @@ +. + */ + +if (!defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +/** + * Table Definition for file_oembed + */ + +class File_oembed extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'file_oembed'; // table name + public $id; // int(11) not_null primary_key group_by + public $file_id; // int(11) unique_key group_by + public $version; // varchar(20) + public $type; // varchar(20) + public $provider; // varchar(50) + public $provider_url; // varchar(255) + public $width; // int(11) group_by + public $height; // int(11) group_by + public $html; // blob(65535) blob + public $title; // varchar(255) + public $author_name; // varchar(50) + public $author_url; // varchar(255) + public $url; // varchar(255) + + /* Static get */ + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_oembed',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + + function _getOembed($url, $maxwidth = 500, $maxheight = 400, $format = 'json') { + $cmd = 'http://oohembed.com/oohembed/?url=' . urlencode($url); + if (is_int($maxwidth)) $cmd .= "&maxwidth=$maxwidth"; + if (is_int($maxheight)) $cmd .= "&maxheight=$maxheight"; + if (is_string($format)) $cmd .= "&format=$format"; + $oe = @file_get_contents($cmd); + if (false === $oe) return false; + return array($format => (('json' === $format) ? json_decode($oe, true) : $oe)); + } + + function saveNew($data, $file_id) { + $file_oembed = new File_oembed; + $file_oembed->file_id = $file_id; + $file_oembed->version = $data['version']; + $file_oembed->type = $data['type']; + if (!empty($data['provider_name'])) $file_oembed->provider = $data['provider_name']; + if (!isset($file_oembed->provider) && !empty($data['provide'])) $file_oembed->provider = $data['provider']; + if (!empty($data['provide_url'])) $file_oembed->provider_url = $data['provider_url']; + if (!empty($data['width'])) $file_oembed->width = intval($data['width']); + if (!empty($data['height'])) $file_oembed->height = intval($data['height']); + if (!empty($data['html'])) $file_oembed->html = $data['html']; + if (!empty($data['title'])) $file_oembed->title = $data['title']; + if (!empty($data['author_name'])) $file_oembed->author_name = $data['author_name']; + if (!empty($data['author_url'])) $file_oembed->author_url = $data['author_url']; + if (!empty($data['url'])) $file_oembed->url = $data['url']; + $file_oembed->insert(); + if (!empty($data['thumbnail_url'])) { + File_thumbnail::saveNew($data, $file_id); + } + } +} + + diff --git a/classes/File_redirection.php b/classes/File_redirection.php new file mode 100644 index 0000000000..0eae681783 --- /dev/null +++ b/classes/File_redirection.php @@ -0,0 +1,274 @@ +. + */ + +if (!defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; +require_once INSTALLDIR.'/classes/File.php'; +require_once INSTALLDIR.'/classes/File_oembed.php'; + +define('USER_AGENT', 'Laconica user agent / file probe'); + + +/** + * Table Definition for file_redirection + */ + +class File_redirection extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'file_redirection'; // table name + public $id; // int(11) not_null primary_key group_by + public $url; // varchar(255) unique_key + public $file_id; // int(11) group_by + public $redirections; // int(11) group_by + public $httpcode; // int(11) group_by + + /* Static get */ + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_redirection',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + + + function _commonCurl($url, $redirs) { + $curlh = curl_init(); + curl_setopt($curlh, CURLOPT_URL, $url); + curl_setopt($curlh, CURLOPT_AUTOREFERER, true); // # setup referer header when folowing redirects + curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 10); // # seconds to wait + curl_setopt($curlh, CURLOPT_MAXREDIRS, $redirs); // # max number of http redirections to follow + curl_setopt($curlh, CURLOPT_USERAGENT, USER_AGENT); + curl_setopt($curlh, CURLOPT_FOLLOWLOCATION, true); // Follow redirects + curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlh, CURLOPT_FILETIME, true); + curl_setopt($curlh, CURLOPT_HEADER, true); // Include header in output + return $curlh; + } + + function _redirectWhere_imp($short_url, $redirs = 10, $protected = false) { + if ($redirs < 0) return false; + + // let's see if we know this... + $a = File::staticGet('url', $short_url); + if (empty($a->id)) { + $b = File_redirection::staticGet('url', $short_url); + if (empty($b->id)) { + // we'll have to figure it out + } else { + // this is a redirect to $b->file_id + $a = File::staticGet($b->file_id); + $url = $a->url; + } + } else { + // this is a direct link to $a->url + $url = $a->url; + } + if (isset($url)) { + return $url; + } + + + + $curlh = File_redirection::_commonCurl($short_url, $redirs); + // Don't include body in output + curl_setopt($curlh, CURLOPT_NOBODY, true); + curl_exec($curlh); + $info = curl_getinfo($curlh); + curl_close($curlh); + + if (405 == $info['http_code']) { + $curlh = File_redirection::_commonCurl($short_url, $redirs); + curl_exec($curlh); + $info = curl_getinfo($curlh); + curl_close($curlh); + } + + if (!empty($info['redirect_count']) && File::isProtected($info['url'])) { + return File_redirection::_redirectWhere_imp($short_url, $info['redirect_count'] - 1, true); + } + + $ret = array('code' => $info['http_code'] + , 'redirects' => $info['redirect_count'] + , 'url' => $info['url']); + + if (!empty($info['content_type'])) $ret['type'] = $info['content_type']; + if ($protected) $ret['protected'] = true; + if (!empty($info['download_content_length'])) $ret['size'] = $info['download_content_length']; + if (isset($info['filetime']) && ($info['filetime'] > 0)) $ret['time'] = $info['filetime']; + return $ret; + } + + function where($in_url) { + $ret = File_redirection::_redirectWhere_imp($in_url); + return $ret; + } + + function makeShort($long_url) { + $long_url = File_redirection::_canonUrl($long_url); + // do we already know this long_url and have a short redirection for it? + $file = new File; + $file_redir = new File_redirection; + $file->url = $long_url; + $file->joinAdd($file_redir); + $file->selectAdd('length(file_redirection.url) as len'); + $file->limit(1); + $file->orderBy('len'); + $file->find(true); + if (!empty($file->id)) { + return $file->url; + } + + // if yet unknown, we must find a short url according to user settings + $short_url = File_redirection::_userMakeShort($long_url, common_current_user()); + return $short_url; + } + + function _userMakeShort($long_url, $user) { + if (empty($user)) { + // common current user does not find a user when called from the XMPP daemon + // therefore we'll set one here fix, so that XMPP given URLs may be shortened + $user->urlshorteningservice = 'ur1.ca'; + } + $curlh = curl_init(); + curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait + curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica'); + curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true); + + switch($user->urlshorteningservice) { + case 'ur1.ca': + require_once INSTALLDIR.'/lib/Shorturl_api.php'; + $short_url_service = new LilUrl; + $short_url = $short_url_service->shorten($long_url); + break; + + case '2tu.us': + $short_url_service = new TightUrl; + require_once INSTALLDIR.'/lib/Shorturl_api.php'; + $short_url = $short_url_service->shorten($long_url); + break; + + case 'ptiturl.com': + require_once INSTALLDIR.'/lib/Shorturl_api.php'; + $short_url_service = new PtitUrl; + $short_url = $short_url_service->shorten($long_url); + break; + + case 'bit.ly': + curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url)); + $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl; + break; + + case 'is.gd': + curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + case 'snipr.com': + curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + case 'metamark.net': + curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + case 'tinyurl.com': + curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + default: + $short_url = false; + } + + curl_close($curlh); + + if ($short_url) { + $short_url = (string)$short_url; + // store it + $file = File::staticGet('url', $long_url); + if (empty($file)) { + $redir_data = File_redirection::where($long_url); + $file = File::saveNew($redir_data, $long_url); + $file_id = $file->id; + if (!empty($redir_data['oembed']['json'])) { + File_oembed::saveNew($redir_data['oembed']['json'], $file_id); + } + } else { + $file_id = $file->id; + } + $file_redir = File_redirection::staticGet('url', $short_url); + if (empty($file_redir)) { + $file_redir = new File_redirection; + $file_redir->url = $short_url; + $file_redir->file_id = $file_id; + $file_redir->insert(); + } + return $short_url; + } + return $long_url; + } + + function _canonUrl($in_url, $default_scheme = 'http://') { + if (empty($in_url)) return false; + $out_url = $in_url; + $p = parse_url($out_url); + if (empty($p['host']) || empty($p['scheme'])) { + list($scheme) = explode(':', $in_url, 2); + switch ($scheme) { + case 'fax': + case 'tel': + $out_url = str_replace('.-()', '', $out_url); + break; + + case 'mailto': + case 'aim': + case 'jabber': + case 'xmpp': + // don't touch anything + break; + + default: + $out_url = $default_scheme . ltrim($out_url, '/'); + $p = parse_url($out_url); + if (empty($p['scheme'])) return false; + break; + } + } + + if (('ftp' == $p['scheme']) || ('http' == $p['scheme']) || ('https' == $p['scheme'])) { + if (empty($p['host'])) return false; + if (empty($p['path'])) { + $out_url .= '/'; + } + } + + return $out_url; + } + + function saveNew($data, $file_id, $url) { + $file_redir = new File_redirection; + $file_redir->url = $url; + $file_redir->file_id = $file_id; + $file_redir->redirections = intval($data['redirects']); + $file_redir->httpcode = intval($data['code']); + $file_redir->insert(); + } +} + diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php new file mode 100644 index 0000000000..1a65b92c90 --- /dev/null +++ b/classes/File_thumbnail.php @@ -0,0 +1,55 @@ +. + */ + +if (!defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +/** + * Table Definition for file_thumbnail + */ + +class File_thumbnail extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'file_thumbnail'; // table name + public $id; // int(11) not_null primary_key group_by + public $file_id; // int(11) unique_key group_by + public $url; // varchar(255) unique_key + public $width; // int(11) group_by + public $height; // int(11) group_by + + /* Static get */ + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_thumbnail',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + function saveNew($data, $file_id) { + $tn = new File_thumbnail; + $tn->file_id = $file_id; + $tn->url = $data['thumbnail_url']; + $tn->width = intval($data['thumbnail_width']); + $tn->height = intval($data['thumbnail_height']); + $tn->insert(); + } +} + diff --git a/classes/File_to_post.php b/classes/File_to_post.php new file mode 100644 index 0000000000..00ddebe6b8 --- /dev/null +++ b/classes/File_to_post.php @@ -0,0 +1,60 @@ +. + */ + +if (!defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +/** + * Table Definition for file_to_post + */ + +class File_to_post extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'file_to_post'; // table name + public $id; // int(11) not_null primary_key group_by + public $file_id; // int(11) multiple_key group_by + public $post_id; // int(11) group_by + + /* Static get */ + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_to_post',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + function processNew($file_id, $notice_id) { + static $seen = array(); + if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) { + $f2p = new File_to_post; + $f2p->file_id = $file_id; + $f2p->post_id = $notice_id; + $f2p->insert(); + if (empty($seen[$notice_id])) { + $seen[$notice_id] = array($file_id); + } else { + $seen[$notice_id][] = $file_id; + } + } + + } +} + diff --git a/classes/Foreign_link.php b/classes/Foreign_link.php index afc0e21804..6065609512 100644 --- a/classes/Foreign_link.php +++ b/classes/Foreign_link.php @@ -17,6 +17,8 @@ class Foreign_link extends Memcached_DataObject public $noticesync; // tinyint(1) not_null default_1 public $friendsync; // tinyint(1) not_null default_2 public $profilesync; // tinyint(1) not_null default_1 + public $last_noticesync; // datetime() + public $last_friendsync; // datetime() public $created; // datetime() not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP @@ -57,13 +59,19 @@ class Foreign_link extends Memcached_DataObject return null; } - function set_flags($noticesync, $replysync, $friendsync) + function set_flags($noticesend, $noticerecv, $replysync, $friendsync) { - if ($noticesync) { + if ($noticesend) { $this->noticesync |= FOREIGN_NOTICE_SEND; } else { $this->noticesync &= ~FOREIGN_NOTICE_SEND; } + + if ($noticerecv) { + $this->noticesync |= FOREIGN_NOTICE_RECV; + } else { + $this->noticesync &= ~FOREIGN_NOTICE_RECV; + } if ($replysync) { $this->noticesync |= FOREIGN_NOTICE_SEND_REPLY; diff --git a/classes/Group_inbox.php b/classes/Group_inbox.php old mode 100755 new mode 100644 diff --git a/classes/Group_member.php b/classes/Group_member.php old mode 100755 new mode 100644 diff --git a/classes/Notice.php b/classes/Notice.php index a399d97e0c..1b5c0ab0a5 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -124,8 +124,6 @@ class Notice extends Memcached_DataObject $profile = Profile::staticGet($profile_id); - $final = common_shorten_links($content); - if (!$profile) { common_log(LOG_ERR, 'Problem saving notice. Unknown user.'); return _('Problem saving notice. Unknown user.'); @@ -136,7 +134,7 @@ class Notice extends Memcached_DataObject return _('Too many notices too fast; take a breather and post again in a few minutes.'); } - if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { + if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $content)) { common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.'); } @@ -167,8 +165,8 @@ class Notice extends Memcached_DataObject $notice->reply_to = $reply_to; $notice->created = common_sql_now(); - $notice->content = $final; - $notice->rendered = common_render_content($final, $notice); + $notice->content = $content; + $notice->rendered = common_render_content($content, $notice); $notice->source = $source; $notice->uri = $uri; @@ -206,7 +204,12 @@ class Notice extends Memcached_DataObject $notice->saveTags(); $notice->saveGroups(); - $notice->addToInboxes(); + if (common_config('queue', 'enabled')) { + $notice->addToAuthorInbox(); + } else { + $notice->addToInboxes(); + } + $notice->query('COMMIT'); Event::handle('EndNoticeSave', array($notice)); @@ -216,7 +219,11 @@ class Notice extends Memcached_DataObject # XXX: someone clever could prepend instead of clearing the cache if (common_config('memcached', 'enabled')) { - $notice->blowCaches(); + if (common_config('queue', 'enabled')) { + $notice->blowAuthorCaches(); + } else { + $notice->blowCaches(); + } } return $notice; @@ -270,6 +277,16 @@ class Notice extends Memcached_DataObject return true; } + function hasAttachments() { + $post = clone $this; + $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id); + $post->query($query); + $post->fetch(); + $n_attachments = intval($post->n_attachments); + $post->free(); + return $n_attachments; + } + function blowCaches($blowLast=false) { $this->blowSubsCache($blowLast); @@ -280,6 +297,17 @@ class Notice extends Memcached_DataObject $this->blowGroupCache($blowLast); } + function blowAuthorCaches($blowLast=false) + { + // Clear the user's cache + $cache = common_memcache(); + if (!empty($cache)) { + $cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id)); + } + $this->blowNoticeCache($blowLast); + $this->blowPublicCache($blowLast); + } + function blowGroupCache($blowLast=false) { $cache = common_memcache(); @@ -288,17 +316,17 @@ class Notice extends Memcached_DataObject $group_inbox->notice_id = $this->id; if ($group_inbox->find()) { while ($group_inbox->fetch()) { - $cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id)); + $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id)); if ($blowLast) { - $cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id.';last')); + $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last')); } $member = new Group_member(); $member->group_id = $group_inbox->group_id; if ($member->find()) { while ($member->fetch()) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id)); + $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id . ';last')); + $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last')); } } } @@ -317,10 +345,7 @@ class Notice extends Memcached_DataObject $tag->notice_id = $this->id; if ($tag->find()) { while ($tag->fetch()) { - $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag)); - if ($blowLast) { - $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last')); - } + $tag->blowCache($blowLast); } } $tag->free(); @@ -341,9 +366,9 @@ class Notice extends Memcached_DataObject 'WHERE subscription.subscribed = ' . $this->profile_id); while ($user->fetch()) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id)); if ($blowLast) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last')); + $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last')); } } $user->free(); @@ -355,10 +380,10 @@ class Notice extends Memcached_DataObject { if ($this->is_local) { $cache = common_memcache(); - if ($cache) { - $cache->delete(common_cache_key('profile:notices:'.$this->profile_id)); + if (!empty($cache)) { + $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id)); if ($blowLast) { - $cache->delete(common_cache_key('profile:notices:'.$this->profile_id.';last')); + $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last')); } } } @@ -372,9 +397,9 @@ class Notice extends Memcached_DataObject $reply->notice_id = $this->id; if ($reply->find()) { while ($reply->fetch()) { - $cache->delete(common_cache_key('user:replies:'.$reply->profile_id)); + $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:replies:'.$reply->profile_id.';last')); + $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last')); } } } @@ -404,9 +429,9 @@ class Notice extends Memcached_DataObject $fave->notice_id = $this->id; if ($fave->find()) { while ($fave->fetch()) { - $cache->delete(common_cache_key('user:faves:'.$fave->user_id)); + $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id)); if ($blowLast) { - $cache->delete(common_cache_key('user:faves:'.$fave->user_id.';last')); + $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last')); } } } @@ -602,27 +627,80 @@ class Notice extends Memcached_DataObject return $wrapper; } + function getStreamByIds($ids) + { + $cache = common_memcache(); + + if (!empty($cache)) { + $notices = array(); + foreach ($ids as $id) { + $notices[] = Notice::staticGet('id', $id); + } + return new ArrayWrapper($notices); + } else { + $notice = new Notice(); + $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); + $notice->orderBy('id DESC'); + + $notice->find(); + return $notice; + } + } + function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) { + $ids = Notice::stream(array('Notice', '_publicStreamDirect'), + array(), + 'public', + $offset, $limit, $since_id, $before_id, $since); - $parts = array(); + return Notice::getStreamByIds($ids); + } - $qry = 'SELECT * FROM notice '; + function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->orderBy('id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } if (common_config('public', 'localonly')) { - $parts[] = 'is_local = 1'; + $notice->whereAdd('is_local = 1'); } else { # -1 == blacklisted - $parts[] = 'is_local != -1'; + $notice->whereAdd('is_local != -1'); } - if ($parts) { - $qry .= ' WHERE ' . implode(' AND ', $parts); + if ($since_id != 0) { + $notice->whereAdd('id > ' . $since_id); } - return Notice::getStream($qry, - 'public', - $offset, $limit, $since_id, $before_id, null, $since); + if ($before_id != 0) { + $notice->whereAdd('id < ' . $before_id); + } + + if (!is_null($since)) { + $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + $notice->free(); + $notice = NULL; + + return $ids; } function addToInboxes() @@ -648,6 +726,33 @@ class Notice extends Memcached_DataObject return; } + function addToAuthorInbox() + { + $enabled = common_config('inboxes', 'enabled'); + + if ($enabled === true || $enabled === 'transitional') { + $user = User::staticGet('id', $this->profile_id); + if (empty($user)) { + return; + } + $inbox = new Notice_inbox(); + $UT = common_config('db','type')=='pgsql'?'"user"':'user'; + $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' . + "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " . + "FROM $UT " . + "WHERE $UT.id = " . $this->profile_id . ' ' . + 'AND NOT EXISTS (SELECT user_id, notice_id ' . + 'FROM notice_inbox ' . + "WHERE user_id = " . $this->profile_id . ' '. + 'AND notice_id = ' . $this->id . ' )'; + if ($enabled === 'transitional') { + $qry .= " AND $UT.inboxed = 1"; + } + $inbox->query($qry); + } + return; + } + function saveGroups() { $enabled = common_config('inboxes', 'enabled'); @@ -700,24 +805,29 @@ class Notice extends Memcached_DataObject // FIXME: do this in an offline daemon - $inbox = new Notice_inbox(); - $UT = common_config('db','type')=='pgsql'?'"user"':'user'; - $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' . - "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " . - "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " . - 'WHERE group_member.group_id = ' . $group->id . ' ' . - 'AND NOT EXISTS (SELECT user_id, notice_id ' . - 'FROM notice_inbox ' . - "WHERE user_id = $UT.id " . - 'AND notice_id = ' . $this->id . ' )'; - if ($enabled === 'transitional') { - $qry .= " AND $UT.inboxed = 1"; - } - $result = $inbox->query($qry); + $this->addToGroupInboxes($group); } } } + function addToGroupInboxes($group) + { + $inbox = new Notice_inbox(); + $UT = common_config('db','type')=='pgsql'?'"user"':'user'; + $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' . + "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " . + "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " . + 'WHERE group_member.group_id = ' . $group->id . ' ' . + 'AND NOT EXISTS (SELECT user_id, notice_id ' . + 'FROM notice_inbox ' . + "WHERE user_id = $UT.id " . + 'AND notice_id = ' . $this->id . ' )'; + if ($enabled === 'transitional') { + $qry .= " AND $UT.inboxed = 1"; + } + $result = $inbox->query($qry); + } + function saveReplies() { // Alternative reply format @@ -913,4 +1023,59 @@ class Notice extends Memcached_DataObject array('notice' => $this->id)); } } + + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null, $tag=null) + { + $cache = common_memcache(); + + if (empty($cache) || + $since_id != 0 || $before_id != 0 || !is_null($since) || + ($offset + $limit) > NOTICE_CACHE_WINDOW) { + return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, + $before_id, $since, $tag))); + } + + $idkey = common_cache_key($cachekey); + + $idstr = $cache->get($idkey); + + if (!empty($idstr)) { + // Cache hit! Woohoo! + $window = explode(',', $idstr); + $ids = array_slice($window, $offset, $limit); + return $ids; + } + + $laststr = $cache->get($idkey.';last'); + + if (!empty($laststr)) { + $window = explode(',', $laststr); + $last_id = $window[0]; + $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, + $last_id, 0, null, $tag))); + + $new_window = array_merge($new_ids, $window); + + $new_windowstr = implode(',', $new_window); + + $result = $cache->set($idkey, $new_windowstr); + $result = $cache->set($idkey . ';last', $new_windowstr); + + $ids = array_slice($new_window, $offset, $limit); + + return $ids; + } + + $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, + 0, 0, null, $tag))); + + $windowstr = implode(',', $window); + + $result = $cache->set($idkey, $windowstr); + $result = $cache->set($idkey . ';last', $windowstr); + + $ids = array_slice($window, $offset, $limit); + + return $ids; + } } diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php index 81ddb45385..dec14b0d18 100644 --- a/classes/Notice_inbox.php +++ b/classes/Notice_inbox.php @@ -1,7 +1,7 @@ user_id = $user_id; + + if ($since_id != 0) { + $inbox->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $inbox->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $inbox->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $inbox->limit($offset, $limit); + } + + $ids = array(); + + if ($inbox->find()) { + while ($inbox->fetch()) { + $ids[] = $inbox->notice_id; + } + } + + return $ids; + } } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index f2247299a4..e5b7722430 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -37,21 +37,62 @@ class Notice_tag extends Memcached_DataObject ###END_AUTOCODE static function getStream($tag, $offset=0, $limit=20) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN notice_tag ON notice.id = notice_tag.notice_id ' . - "WHERE notice_tag.tag = '%s' "; - return Notice::getStream(sprintf($qry, $tag), - 'notice_tag:notice_stream:' . common_keyize($tag), - $offset, $limit); + $ids = Notice::stream(array('Notice_tag', '_streamDirect'), + array($tag), + 'notice_tag:notice_ids:' . common_keyize($tag), + $offset, $limit); + + return Notice::getStreamByIds($ids); } - function blowCache() + function _streamDirect($tag, $offset, $limit, $since_id, $before_id, $since) + { + $nt = new Notice_tag(); + + $nt->tag = $tag; + + $nt->selectAdd(); + $nt->selectAdd('notice_id'); + + if ($since_id != 0) { + $nt->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $nt->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $nt->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $nt->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $nt->limit($offset, $limit); + } + + $ids = array(); + + if ($nt->find()) { + while ($nt->fetch()) { + $ids[] = $nt->notice_id; + } + } + + return $ids; + } + + function blowCache($blowLast=false) { $cache = common_memcache(); if ($cache) { - $cache->delete(common_cache_key('notice_tag:notice_stream:' . $this->tag)); + $idkey = common_cache_key('notice_tag:notice_ids:' . common_keyize($this->tag)); + $cache->delete($idkey); + if ($blowLast) { + $cache->delete($idkey.';last'); + } } } diff --git a/classes/Profile.php b/classes/Profile.php index f3bfe299cf..afc0ea4f74 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -153,16 +153,101 @@ class Profile extends Memcached_DataObject return null; } - function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) + function getTaggedNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null, $tag=null) { - $qry = - 'SELECT * ' . - 'FROM notice ' . - 'WHERE profile_id = %d '; + // XXX: I'm not sure this is going to be any faster. It probably isn't. + $ids = Notice::stream(array($this, '_streamTaggedDirect'), + array(), + 'profile:notice_ids:' . $this->id, + $offset, $limit, $since_id, $before_id, $since, $tag); + common_debug(print_r($ids, true)); + return Notice::getStreamByIds($ids); + } - return Notice::getStream(sprintf($qry, $this->id), - 'profile:notices:'.$this->id, - $offset, $limit, $since_id, $before_id); + function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + { + // XXX: I'm not sure this is going to be any faster. It probably isn't. + $ids = Notice::stream(array($this, '_streamDirect'), + array(), + 'profile:notice_ids:' . $this->id, + $offset, $limit, $since_id, $before_id, $since); + + return Notice::getStreamByIds($ids); + } + + function _streamTaggedDirect($offset, $limit, $since_id, $before_id, $since=null, $tag=null) + { + common_debug('_streamTaggedDirect()'); + $notice = new Notice(); + $notice->profile_id = $this->id; + $query = "select id from notice join notice_tag on id=notice_id where tag='" . $notice->escape($tag) . "' and profile_id=" . $notice->escape($notice->profile_id); + if ($since_id != 0) { + $query .= " and id > $since_id"; + } + + if ($before_id != 0) { + $query .= " and id < $before_id"; + } + + if (!is_null($since)) { + $query .= " and created > '" . date('Y-m-d H:i:s', $since) . "'"; + } + + $query .= ' order by id DESC'; + + if (!is_null($offset)) { + $query .= " limit $offset, $limit"; + } + $notice->query($query); + $ids = array(); + + while ($notice->fetch()) { + common_debug(print_r($notice, true)); + $ids[] = $notice->id; + } + + return $ids; + } + + + + + function _streamDirect($offset, $limit, $since_id, $before_id, $since = null) + { + $notice = new Notice(); + + $notice->profile_id = $this->id; + + $notice->selectAdd(); + $notice->selectAdd('id'); + + if ($since_id != 0) { + $notice->whereAdd('id > ' . $since_id); + } + + if ($before_id != 0) { + $notice->whereAdd('id < ' . $before_id); + } + + if (!is_null($since)) { + $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $notice->orderBy('id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + return $ids; } function isMember($group) diff --git a/classes/Related_group.php b/classes/Related_group.php old mode 100755 new mode 100644 diff --git a/classes/Reply.php b/classes/Reply.php index af86aaf878..4439053b44 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -4,7 +4,7 @@ */ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Reply extends Memcached_DataObject +class Reply extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -13,7 +13,7 @@ class Reply extends Memcached_DataObject public $notice_id; // int(4) primary_key not_null public $profile_id; // int(4) primary_key not_null public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP - public $replied_id; // int(4) + public $replied_id; // int(4) /* Static get */ function staticGet($k,$v=null) @@ -21,4 +21,47 @@ class Reply extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + { + $ids = Notice::stream(array('Reply', '_streamDirect'), + array($user_id), + 'reply:stream:' . $user_id, + $offset, $limit, $since_id, $before_id, $since); + return $ids; + } + + function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) + { + $reply = new Reply(); + $reply->profile_id = $user_id; + + if ($since_id != 0) { + $reply->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $reply->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $reply->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $reply->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $reply->limit($offset, $limit); + } + + $ids = array(); + + if ($reply->find()) { + while ($reply->fetch()) { + $ids[] = $reply->notice_id; + } + } + + return $ids; + } } diff --git a/classes/Status_network.php b/classes/Status_network.php old mode 100755 new mode 100644 diff --git a/classes/User.php b/classes/User.php index 3b9b5cd839..ea8ba40817 100644 --- a/classes/User.php +++ b/classes/User.php @@ -349,30 +349,31 @@ class User extends Memcached_DataObject $cache = common_memcache(); // XXX: Kind of a hack. + if ($cache) { // This is the stream of favorite notices, in rev chron // order. This forces it into cache. - $faves = $this->favoriteNotices(0, NOTICE_CACHE_WINDOW); - $cnt = 0; - while ($faves->fetch()) { - if ($faves->id < $notice->id) { - // If we passed it, it's not a fave - return false; - } else if ($faves->id == $notice->id) { - // If it matches a cached notice, then it's a fave - return true; - } - $cnt++; + + $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW); + + // If it's in the list, then it's a fave + + if (in_array($notice->id, $ids)) { + return true; } + // If we're not past the end of the cache window, // then the cache has all available faves, so this one // is not a fave. - if ($cnt < NOTICE_CACHE_WINDOW) { + + if (count($ids) < NOTICE_CACHE_WINDOW) { return false; } + // Otherwise, cache doesn't have all faves; // fall through to the default } + $fave = Fave::pkeyGet(array('user_id' => $this->id, 'notice_id' => $notice->id)); return ((is_null($fave)) ? false : true); @@ -401,13 +402,18 @@ class User extends Memcached_DataObject function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN reply ON notice.id = reply.notice_id ' . - 'WHERE reply.profile_id = %d '; - return Notice::getStream(sprintf($qry, $this->id), - 'user:replies:'.$this->id, - $offset, $limit, $since_id, $before_id, null, $since); + $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id, $since); + common_debug("Ids = " . implode(',', $ids)); + return Notice::getStreamByIds($ids); + } + + function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) { + $profile = $this->getProfile(); + if (!$profile) { + return null; + } else { + return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id, $since); + } } function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) @@ -416,19 +422,14 @@ class User extends Memcached_DataObject if (!$profile) { return null; } else { - return $profile->getNotices($offset, $limit, $since_id, $before_id); + return $profile->getNotices($offset, $limit, $since_id, $before_id, $since); } } function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN fave ON notice.id = fave.notice_id ' . - 'WHERE fave.user_id = %d '; - return Notice::getStream(sprintf($qry, $this->id), - 'user:faves:'.$this->id, - $offset, $limit); + $ids = Fave::stream($this->id, $offset, $limit); + return Notice::getStreamByIds($ids); } function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) @@ -444,21 +445,17 @@ class User extends Memcached_DataObject 'SELECT notice.* ' . 'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' . 'WHERE subscription.subscriber = %d '; - $order = null; + return Notice::getStream(sprintf($qry, $this->id), + 'user:notices_with_friends:' . $this->id, + $offset, $limit, $since_id, $before_id, + $order, $since); } else if ($enabled === true || ($enabled == 'transitional' && $this->inboxed == 1)) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' . - 'WHERE notice_inbox.user_id = %d '; - // NOTE: we override ORDER - $order = null; + $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since); + + return Notice::getStreamByIds($ids); } - return Notice::getStream(sprintf($qry, $this->id), - 'user:notices_with_friends:' . $this->id, - $offset, $limit, $since_id, $before_id, - $order, $since); } function blowFavesCache() @@ -467,8 +464,8 @@ class User extends Memcached_DataObject if ($cache) { // Faves don't happen chronologically, so we need to blow // ;last cache, too - $cache->delete(common_cache_key('user:faves:'.$this->id)); - $cache->delete(common_cache_key('user:faves:'.$this->id).';last'); + $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id)); + $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last')); } } diff --git a/classes/User_group.php b/classes/User_group.php old mode 100755 new mode 100644 index d152f9d567..7cc31e7026 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -50,13 +50,50 @@ class User_group extends Memcached_DataObject function getNotices($offset, $limit) { - $qry = - 'SELECT notice.* ' . - 'FROM notice JOIN group_inbox ON notice.id = group_inbox.notice_id ' . - 'WHERE group_inbox.group_id = %d '; - return Notice::getStream(sprintf($qry, $this->id), - 'group:notices:'.$this->id, - $offset, $limit); + $ids = Notice::stream(array($this, '_streamDirect'), + array(), + 'user_group:notice_ids:' . $this->id, + $offset, $limit); + + return Notice::getStreamByIds($ids); + } + + function _streamDirect($offset, $limit, $since_id, $before_id, $since) + { + $inbox = new Group_inbox(); + + $inbox->group_id = $this->id; + + $inbox->selectAdd(); + $inbox->selectAdd('notice_id'); + + if ($since_id != 0) { + $inbox->whereAdd('notice_id > ' . $since_id); + } + + if ($before_id != 0) { + $inbox->whereAdd('notice_id < ' . $before_id); + } + + if (!is_null($since)) { + $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\''); + } + + $inbox->orderBy('notice_id DESC'); + + if (!is_null($offset)) { + $inbox->limit($offset, $limit); + } + + $ids = array(); + + if ($inbox->find()) { + while ($inbox->fetch()) { + $ids[] = $inbox->notice_id; + } + } + + return $ids; } function allowedNickname($nickname) @@ -91,7 +128,7 @@ class User_group extends Memcached_DataObject function setOriginal($filename) { $imagefile = new ImageFile($this->id, Avatar::path($filename)); - + $orig = clone($this); $this->original_logo = Avatar::url($filename); $this->homepage_logo = Avatar::url($imagefile->resize(AVATAR_PROFILE_SIZE)); diff --git a/classes/laconica.ini b/classes/laconica.ini old mode 100755 new mode 100644 index dd424bbdd3..92bbb35d4c --- a/classes/laconica.ini +++ b/classes/laconica.ini @@ -1,4 +1,3 @@ - [avatar] profile_id = 129 original = 17 @@ -47,6 +46,64 @@ modified = 384 notice_id = K user_id = K +[file] +id = 129 +url = 2 +mimetype = 2 +size = 1 +title = 2 +date = 1 +protected = 1 + +[file__keys] +id = N + +[file_oembed] +id = 129 +file_id = 1 +version = 2 +type = 2 +provider = 2 +provider_url = 2 +width = 1 +height = 1 +html = 34 +title = 2 +author_name = 2 +author_url = 2 +url = 2 + +[file_oembed__keys] +id = N + +[file_redirection] +id = 129 +url = 2 +file_id = 1 +redirections = 1 +httpcode = 1 + +[file_redirection__keys] +id = N + +[file_thumbnail] +id = 129 +file_id = 1 +url = 2 +width = 1 +height = 1 + +[file_thumbnail__keys] +id = N + +[file_to_post] +id = 129 +file_id = 1 +post_id = 1 + +[file_to_post__keys] +id = N + [foreign_link] user_id = 129 foreign_id = 129 @@ -55,6 +112,8 @@ credentials = 2 noticesync = 145 friendsync = 145 profilesync = 145 +last_noticesync = 14 +last_friendsync = 14 created = 142 modified = 384 diff --git a/classes/laconica.links.ini b/classes/laconica.links.ini index 173b187267..95c63f3c09 100644 --- a/classes/laconica.links.ini +++ b/classes/laconica.links.ini @@ -41,3 +41,17 @@ subscribed = profile:id [fave] notice_id = notice:id user_id = user:id + +[file_oembed] +file_id = file:id + +[file_redirection] +file_id = file:id + +[file_thumbnail] +file_id = file:id + +[file_to_post] +file_id = file:id +post_id = notice:id + diff --git a/db/foreign_services.sql b/db/foreign_services.sql index 557ede0246..79c04cee56 100644 --- a/db/foreign_services.sql +++ b/db/foreign_services.sql @@ -2,4 +2,5 @@ insert into foreign_service (id, name, description, created) values ('1','Twitter', 'Twitter Micro-blogging service', now()), - ('2','Facebook', 'Facebook', now()); + ('2','Facebook', 'Facebook', now()), + ('3','FacebookConnect', 'Facebook Connect', now()); diff --git a/db/laconica.sql b/db/laconica.sql index 20f0cabd17..0b20bc172c 100644 --- a/db/laconica.sql +++ b/db/laconica.sql @@ -119,6 +119,7 @@ create table notice ( index notice_profile_id_idx (profile_id), index notice_conversation_idx (conversation), index notice_created_idx (created), + index notice_replyto_idx (reply_to), FULLTEXT(content) ) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; @@ -290,6 +291,8 @@ create table foreign_link ( noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies', friendsync tinyint not null default 2 comment 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', profilesync tinyint not null default 1 comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', + last_noticesync datetime default null comment 'last time notices were imported', + last_friendsync datetime default null comment 'last time friends were imported', created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', @@ -422,3 +425,60 @@ create table group_inbox ( ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +create table file ( + id integer primary key auto_increment, + url varchar(255), mimetype varchar(50), + size integer, + title varchar(255), + date integer(11), + protected integer(1), + + unique(url) +) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; + +create table file_oembed ( + id integer primary key auto_increment, + file_id integer, + version varchar(20), + type varchar(20), + provider varchar(50), + provider_url varchar(255), + width integer, + height integer, + html text, + title varchar(255), + author_name varchar(50), + author_url varchar(255), + url varchar(255), + + unique(file_id) +) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; + +create table file_redirection ( + id integer primary key auto_increment, + url varchar(255), + file_id integer, + redirections integer, + httpcode integer, + + unique(url) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table file_thumbnail ( + id integer primary key auto_increment, + file_id integer, + url varchar(255), + width integer, + height integer, + + unique(file_id), + unique(url) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table file_to_post ( + id integer primary key auto_increment, + file_id integer, + post_id integer, + + unique(file_id, post_id) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql index f879d7936f..b213bbd502 100644 --- a/db/laconica_pg.sql +++ b/db/laconica_pg.sql @@ -291,6 +291,8 @@ create table foreign_link ( noticesync int not null default 1 /* comment 'notice synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies' */, friendsync int not null default 2 /* comment 'friend synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming */, profilesync int not null default 1 /* comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming' */, + last_noticesync timestamp default null /* comment 'last time notices were imported' */, + last_friendsync timestamp default null /* comment 'last time friends were imported' */, created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */, modified timestamp /* comment 'date this record was modified' */, @@ -425,6 +427,64 @@ create table group_inbox ( ); create index group_inbox_created_idx on group_inbox using btree(created); + +/*attachments and URLs stuff */ +create sequence file_seq; +create table file ( + id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */, + url varchar(255) unique, + mimetype varchar(50), + size integer, + title varchar(255), + date integer(11), + protected integer(1) +); + +create sequence file_oembed_seq; +create table file_oembed ( + id bigint default nextval('file_oembed_seq') primary key /* comment 'unique identifier' */, + file_id bigint unique, + version varchar(20), + type varchar(20), + provider varchar(50), + provider_url varchar(255), + width integer, + height integer, + html text, + title varchar(255), + author_name varchar(50), + author_url varchar(255), + url varchar(255), +); + +create sequence file_redirection_seq; +create table file_redirection ( + id bigint default nextval('file_redirection_seq') primary key /* comment 'unique identifier' */, + url varchar(255) unique, + file_id bigint, + redirections integer, + httpcode integer +); + +create sequence file_thumbnail_seq; +create table file_thumbnail ( + id bigint default nextval('file_thumbnail_seq') primary key /* comment 'unique identifier' */, + file_id bigint unique, + url varchar(255) unique, + width integer, + height integer +); + +create sequence file_to_post_seq; +create table file_to_post ( + id bigint default nextval('file_to_post_seq') primary key /* comment 'unique identifier' */, + file_id bigint, + post_id bigint, + + unique(file_id, post_id) +); + + /* Textsearch stuff */ create index textsearch_idx on profile using gist(textsearch); diff --git a/db/notice_source.sql b/db/notice_source.sql index d5124e223a..f026679d50 100644 --- a/db/notice_source.sql +++ b/db/notice_source.sql @@ -8,6 +8,7 @@ VALUES ('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()), ('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()), ('Facebook','Facebook','http://apps.facebook.com/identica/', now()), + ('feed2omb','feed2omb','http://projects.ciarang.com/p/feed2omb/', now()), ('Gwibber','Gwibber','http://launchpad.net/gwibber', now()), ('HelloTxt','HelloTxt','http://hellotxt.com/', now()), ('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()), @@ -23,6 +24,7 @@ VALUES ('peoplebrowsr', 'PeopleBrowsr', 'http://www.peoplebrowsr.com/', now()), ('Pikchur','Pikchur','http://www.pikchur.com/', now()), ('Ping.fm','Ping.fm','http://ping.fm/', now()), + ('pingvine','PingVine','http://pingvine.com/', now()), ('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()), ('posty','Posty','http://spreadingfunkyness.com/posty/', now()), ('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()), @@ -42,6 +44,7 @@ VALUES ('twidge','Twidge','http://software.complete.org/twidge', now()), ('twidroid','twidroid','http://www.twidroid.com/', now()), ('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()), + ('twitter','Twitter','http://twitter.com/', now()), ('twitterfeed','twitterfeed','http://twitterfeed.com/', now()), ('twitterphoto','TwitterPhoto','http://richfish.org/twitterphoto/', now()), ('twitterpm','Net::Twitter','http://search.cpan.org/dist/Net-Twitter/', now()), diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php index b1a1a4e218..0c6a13dc28 100644 --- a/extlib/DB/DataObject.php +++ b/extlib/DB/DataObject.php @@ -2357,6 +2357,8 @@ class DB_DataObject extends DB_DataObject_Overload $t= explode(' ',microtime()); $_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1]; + + do { if ($_DB_driver == 'DB') { $result = $DB->query($string); @@ -2374,8 +2376,19 @@ class DB_DataObject extends DB_DataObject_Overload break; } } - - + + // try to reconnect, at most 3 times + $again = false; + if (is_a($result, 'PEAR_Error') + AND $result->getCode() == DB_ERROR_NODBSELECTED + AND $cpt++<3) { + $DB->disconnect(); + sleep(1); + $DB->connect($DB->dsn); + $again = true; + } + + } while ($again); if (is_a($result,'PEAR_Error')) { if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) { diff --git a/extlib/facebook/facebook.php b/extlib/facebook/facebook.php index 35de6be509..fee1dd086a 100644 --- a/extlib/facebook/facebook.php +++ b/extlib/facebook/facebook.php @@ -1,5 +1,5 @@ api_client->auth_expireSession()) { - if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) { - $cookies = array('user', 'session_key', 'expires', 'ss'); - foreach ($cookies as $name) { - setcookie($this->api_key . '_' . $name, false, time() - 3600); - unset($_COOKIE[$this->api_key . '_' . $name]); - } - setcookie($this->api_key, false, time() - 3600); - unset($_COOKIE[$this->api_key]); - } - - // now, clear the rest of the stored state - $this->user = 0; - $this->api_client->session_key = 0; + $this->clear_cookie_state(); return true; } else { return false; } } + /** Logs the user out of all temporary application sessions as well as their + * Facebook session. Note this will only work if the user has a valid current + * session with the application. + * + * @param string $next URL to redirect to upon logging out + * + */ + public function logout($next) { + $logout_url = $this->get_logout_url($next); + + // Clear any stored state + $this->clear_cookie_state(); + + $this->redirect($logout_url); + } + + /** + * Clears any persistent state stored about the user, including + * cookies and information related to the current session in the + * client. + * + */ + public function clear_cookie_state() { + if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) { + $cookies = array('user', 'session_key', 'expires', 'ss'); + foreach ($cookies as $name) { + setcookie($this->api_key . '_' . $name, false, time() - 3600); + unset($_COOKIE[$this->api_key . '_' . $name]); + } + setcookie($this->api_key, false, time() - 3600); + unset($_COOKIE[$this->api_key]); + } + + // now, clear the rest of the stored state + $this->user = 0; + $this->api_client->session_key = 0; + } + public function redirect($url) { if ($this->in_fb_canvas()) { echo ''; @@ -249,7 +275,8 @@ class Facebook { } public function in_frame() { - return isset($this->fb_params['in_canvas']) || isset($this->fb_params['in_iframe']); + return isset($this->fb_params['in_canvas']) + || isset($this->fb_params['in_iframe']); } public function in_fb_canvas() { return isset($this->fb_params['in_canvas']); @@ -296,14 +323,42 @@ class Facebook { } public function get_add_url($next=null) { - return self::get_facebook_url().'/add.php?api_key='.$this->api_key . - ($next ? '&next=' . urlencode($next) : ''); + $page = self::get_facebook_url().'/add.php'; + $params = array('api_key' => $this->api_key); + + if ($next) { + $params['next'] = $next; + } + + return $page . '?' . http_build_query($params); } public function get_login_url($next, $canvas) { - return self::get_facebook_url().'/login.php?v=1.0&api_key=' . $this->api_key . - ($next ? '&next=' . urlencode($next) : '') . - ($canvas ? '&canvas' : ''); + $page = self::get_facebook_url().'/login.php'; + $params = array('api_key' => $this->api_key, + 'v' => '1.0'); + + if ($next) { + $params['next'] = $next; + } + if ($canvas) { + $params['canvas'] = '1'; + } + + return $page . '?' . http_build_query($params); + } + + public function get_logout_url($next) { + $page = self::get_facebook_url().'/logout.php'; + $params = array('app_key' => $this->api_key, + 'session_key' => $this->api_client->session_key); + + if ($next) { + $params['connect_next'] = 1; + $params['next'] = $next; + } + + return $page . '?' . http_build_query($params); } public function set_user($user, $session_key, $expires=null, $session_secret=null) { @@ -410,7 +465,20 @@ class Facebook { return $fb_params; } - /* + /** + * Validates the account that a user was trying to set up an + * independent account through Facebook Connect. + * + * @param user The user attempting to set up an independent account. + * @param hash The hash passed to the reclamation URL used. + * @return bool True if the user is the one that selected the + * reclamation link. + */ + public function verify_account_reclamation($user, $hash) { + return $hash == md5($user . $this->secret); + } + + /** * Validates that a given set of parameters match their signature. * Parameters all match a given input prefix, such as "fb_sig". * @@ -422,6 +490,37 @@ class Facebook { return self::generate_sig($fb_params, $this->secret) == $expected_sig; } + /** + * Validate the given signed public session data structure with + * public key of the app that + * the session proof belongs to. + * + * @param $signed_data the session info that is passed by another app + * @param string $public_key Optional public key of the app. If this + * is not passed, function will make an API call to get it. + * return true if the session proof passed verification. + */ + public function verify_signed_public_session_data($signed_data, + $public_key = null) { + + // If public key is not already provided, we need to get it through API + if (!$public_key) { + $public_key = $this->api_client->auth_getAppPublicKey( + $signed_data['api_key']); + } + + // Create data to verify + $data_to_serialize = $signed_data; + unset($data_to_serialize['sig']); + $serialized_data = implode('_', $data_to_serialize); + + // Decode signature + $signature = base64_decode($signed_data['sig']); + $result = openssl_verify($serialized_data, $signature, $public_key, + OPENSSL_ALGO_SHA1); + return $result == 1; + } + /* * Generate a signature using the application secret key. * diff --git a/extlib/facebook/facebook_desktop.php b/extlib/facebook/facebook_desktop.php index 90cdf66bd0..e79a2ca343 100644 --- a/extlib/facebook/facebook_desktop.php +++ b/extlib/facebook/facebook_desktop.php @@ -1,5 +1,5 @@ batch_mode = FacebookRestClient::BATCH_MODE_DEFAULT; $this->last_call_id = 0; $this->call_as_apikey = ''; - $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php'; + $this->use_curl_if_available = true; + $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php'; if (!empty($GLOBALS['facebook_config']['debug'])) { $this->cur_id = 0; @@ -122,40 +127,62 @@ function toggleDisplay(id, type) { $this->user = $uid; } + /** + * Normally, if the cURL library/PHP extension is available, it is used for + * HTTP transactions. This allows that behavior to be overridden, falling + * back to a vanilla-PHP implementation even if cURL is installed. + * + * @param $use_curl_if_available bool whether or not to use cURL if available + */ + public function set_use_curl_if_available($use_curl_if_available) { + $this->use_curl_if_available = $use_curl_if_available; + } + /** * Start a batch operation. */ public function begin_batch() { - if($this->batch_queue !== null) { + if ($this->pending_batch()) { $code = FacebookAPIErrorCodes::API_EC_BATCH_ALREADY_STARTED; - throw new FacebookRestClientException($code, - FacebookAPIErrorCodes::$api_error_descriptions[$code]); + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); } $this->batch_queue = array(); + $this->pending_batch = true; } /* * End current batch operation */ public function end_batch() { - if($this->batch_queue === null) { + if (!$this->pending_batch()) { $code = FacebookAPIErrorCodes::API_EC_BATCH_NOT_STARTED; - throw new FacebookRestClientException($code, - FacebookAPIErrorCodes::$api_error_descriptions[$code]); + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); } - $this->execute_server_side_batch(); + $this->pending_batch = false; + $this->execute_server_side_batch(); $this->batch_queue = null; } + /** + * are we currently queueing up calls for a batch? + */ + public function pending_batch() { + return $this->pending_batch; + } + private function execute_server_side_batch() { $item_count = count($this->batch_queue); $method_feed = array(); foreach($this->batch_queue as $batch_item) { - $method_feed[] = $this->create_post_string($batch_item['m'], - $batch_item['p']); + $method = $batch_item['m']; + $params = $batch_item['p']; + $this->finalize_params($method, $params); + $method_feed[] = $this->create_post_string($method, $params); } $method_feed_json = json_encode($method_feed); @@ -202,6 +229,18 @@ function toggleDisplay(id, type) { $this->call_as_apikey = ''; } + + /* + * If a page is loaded via HTTPS, then all images and static + * resources need to be printed with HTTPS urls to avoid + * mixed content warnings. If your page loads with an HTTPS + * url, then call set_use_ssl_resources to retrieve the correct + * urls. + */ + public function set_use_ssl_resources($is_ssl = true) { + $this->use_ssl_resources = $is_ssl; + } + /** * Returns public information for an application (as shown in the application * directory) by either application ID, API key, or canvas page name. @@ -231,7 +270,7 @@ function toggleDisplay(id, type) { * @return string An authentication token. */ public function auth_createToken() { - return $this->call_method('facebook.auth.createToken', array()); + return $this->call_method('facebook.auth.createToken'); } /** @@ -246,8 +285,7 @@ function toggleDisplay(id, type) { * @return array An assoc array containing session_key, uid */ public function auth_getSession($auth_token, $generate_session_secret=false) { - //Check if we are in batch mode - if($this->batch_queue === null) { + if (!$this->pending_batch()) { $result = $this->call_method('facebook.auth.getSession', array('auth_token' => $auth_token, 'generate_session_secret' => $generate_session_secret)); @@ -271,7 +309,7 @@ function toggleDisplay(id, type) { * API_EC_PARAM_UNKNOWN */ public function auth_promoteSession() { - return $this->call_method('facebook.auth.promoteSession', array()); + return $this->call_method('facebook.auth.promoteSession'); } /** @@ -282,7 +320,20 @@ function toggleDisplay(id, type) { * @return bool true if session expiration was successful, false otherwise */ public function auth_expireSession() { - return $this->call_method('facebook.auth.expireSession', array()); + return $this->call_method('facebook.auth.expireSession'); + } + + /** + * Revokes the given extended permission that the user granted at some + * prior time (for instance, offline_access or email). If no user is + * provided, it will be revoked for the user of the current session. + * + * @param string $perm The permission to revoke + * @param int $uid The user for whom to revoke the permission. + */ + public function auth_revokeExtendedPermission($perm, $uid=null) { + return $this->call_method('facebook.auth.revokeExtendedPermission', + array('perm' => $perm, 'uid' => $uid)); } /** @@ -302,6 +353,30 @@ function toggleDisplay(id, type) { array('uid' => $uid)); } + /** + * Get public key that is needed to verify digital signature + * an app may pass to other apps. The public key is only used by + * other apps for verification purposes. + * @param string API key of an app + * @return string The public key for the app. + */ + public function auth_getAppPublicKey($target_app_key) { + return $this->call_method('facebook.auth.getAppPublicKey', + array('target_app_key' => $target_app_key)); + } + + /** + * Get a structure that can be passed to another app + * as proof of session. The other app can verify it using public + * key of this app. + * + * @return signed public session data structure. + */ + public function auth_getSignedPublicSessionData() { + return $this->call_method('facebook.auth.getSignedPublicSessionData', + array()); + } + /** * Returns the number of unconnected friends that exist in this application. * This number is determined based on the accounts registered through @@ -363,8 +438,9 @@ function toggleDisplay(id, type) { * * @param int $uid (Optional) User associated with events. A null * parameter will default to the session user. - * @param array $eids (Optional) Filter by these event ids. A null - * parameter will get all events for the user. + * @param array/string $eids (Optional) Filter by these event + * ids. A null parameter will get all events for + * the user. (A csv list will work but is deprecated) * @param int $start_time (Optional) Filter with this unix time as lower * bound. A null or zero parameter indicates no * lower bound. @@ -718,12 +794,15 @@ function toggleDisplay(id, type) { * @param string $body_general (Optional) Additional markup that extends * the body of a short story. * @param int $story_size (Optional) A story size (see above) + * @param string $user_message (Optional) A user message for a short + * story. * * @return bool true on success */ public function &feed_publishUserAction( $template_bundle_id, $template_data, $target_ids='', $body_general='', - $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE) { + $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE, + $user_message='') { if (is_array($template_data)) { $template_data = json_encode($template_data); @@ -739,7 +818,107 @@ function toggleDisplay(id, type) { 'template_data' => $template_data, 'target_ids' => $target_ids, 'body_general' => $body_general, - 'story_size' => $story_size)); + 'story_size' => $story_size, + 'user_message' => $user_message)); + } + + + /** + * Publish a post to the user's stream. + * + * @param $message the user's message + * @param $attachment the post's attachment (optional) + * @param $action links the post's action links (optional) + * @param $target_id the user on whose wall the post will be posted + * (optional) + * @param $uid the actor (defaults to session user) + * @return string the post id + */ + public function stream_publish( + $message, $attachment = null, $action_links = null, $target_id = null, + $uid = null) { + + return $this->call_method( + 'facebook.stream.publish', + array('message' => $message, + 'attachment' => $attachment, + 'action_links' => $action_links, + 'target_id' => $target_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Remove a post from the user's stream. + * Currently, you may only remove stories you application created. + * + * @param $post_id the post id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_remove($post_id, $uid = null) { + return $this->call_method( + 'facebook.stream.remove', + array('post_id' => $post_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Add a comment to a stream post + * + * @param $post_id the post id + * @param $comment the comment text + * @param $uid the actor (defaults to session user) + * @return string the id of the created comment + */ + public function stream_addComment($post_id, $comment, $uid = null) { + return $this->call_method( + 'facebook.stream.addComment', + array('post_id' => $post_id, + 'comment' => $comment, + 'uid' => $this->get_uid($uid))); + } + + + /** + * Remove a comment from a stream post + * + * @param $comment_id the comment id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_removeComment($comment_id, $uid = null) { + return $this->call_method( + 'facebook.stream.removeComment', + array('comment_id' => $comment_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Add a like to a stream post + * + * @param $post_id the post id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_addLike($post_id, $uid = null) { + return $this->call_method( + 'facebook.stream.addLike', + array('post_id' => $post_id, + 'uid' => $this->get_uid($uid))); + } + + /** + * Remove a like from a stream post + * + * @param $post_id the post id + * @param $uid the actor (defaults to session user) + * @return bool + */ + public function stream_removeLike($post_id, $uid = null) { + return $this->call_method( + 'facebook.stream.removeLike', + array('post_id' => $post_id, + 'uid' => $this->get_uid($uid))); } /** @@ -750,7 +929,7 @@ function toggleDisplay(id, type) { * @return array An array of feed story objects. */ public function &feed_getAppFriendStories() { - return $this->call_method('facebook.feed.getAppFriendStories', array()); + return $this->call_method('facebook.feed.getAppFriendStories'); } /** @@ -771,33 +950,42 @@ function toggleDisplay(id, type) { * Returns whether or not pairs of users are friends. * Note that the Facebook friend relationship is symmetric. * - * @param array $uids1 array of ids (id_1, id_2,...) of some length X - * @param array $uids2 array of ids (id_A, id_B,...) of SAME length X + * @param array/string $uids1 list of ids (id_1, id_2,...) + * of some length X (csv is deprecated) + * @param array/string $uids2 list of ids (id_A, id_B,...) + * of SAME length X (csv is deprecated) * * @return array An array with uid1, uid2, and bool if friends, e.g.: * array(0 => array('uid1' => id_1, 'uid2' => id_A, 'are_friends' => 1), * 1 => array('uid1' => id_2, 'uid2' => id_B, 'are_friends' => 0) * ...) + * @error + * API_EC_PARAM_USER_ID_LIST */ public function &friends_areFriends($uids1, $uids2) { return $this->call_method('facebook.friends.areFriends', - array('uids1' => $uids1, 'uids2' => $uids2)); + array('uids1' => $uids1, + 'uids2' => $uids2)); } /** * Returns the friends of the current session user. * * @param int $flid (Optional) Only return friends on this friend list. + * @param int $uid (Optional) Return friends for this user. * * @return array An array of friends */ - public function &friends_get($flid=null) { + public function &friends_get($flid=null, $uid = null) { if (isset($this->friends_list)) { return $this->friends_list; } $params = array(); - if (isset($this->canvas_user)) { - $params['uid'] = $this->canvas_user; + if (!$uid && isset($this->canvas_user)) { + $uid = $this->canvas_user; + } + if ($uid) { + $params['uid'] = $uid; } if ($flid) { $params['flid'] = $flid; @@ -812,7 +1000,7 @@ function toggleDisplay(id, type) { * @return array An array of friend list objects */ public function &friends_getLists() { - return $this->call_method('facebook.friends.getLists', array()); + return $this->call_method('facebook.friends.getLists'); } /** @@ -822,7 +1010,7 @@ function toggleDisplay(id, type) { * @return array An array of friends also using the app */ public function &friends_getAppUsers() { - return $this->call_method('facebook.friends.getAppUsers', array()); + return $this->call_method('facebook.friends.getAppUsers'); } /** @@ -830,8 +1018,9 @@ function toggleDisplay(id, type) { * * @param int $uid (Optional) User associated with groups. A null * parameter will default to the session user. - * @param array $gids (Optional) Group ids to query. A null parameter will - * get all groups for the user. + * @param array/string $gids (Optional) Array of group ids to query. A null + * parameter will get all groups for the user. + * (csv is deprecated) * * @return array An array of group objects */ @@ -889,6 +1078,40 @@ function toggleDisplay(id, type) { 'path' => $path)); } + /** + * Retrieves links posted by the given user. + * + * @param int $uid The user whose links you wish to retrieve + * @param int $limit The maximimum number of links to retrieve + * @param array $link_ids (Optional) Array of specific link + * IDs to retrieve by this user + * + * @return array An array of links. + */ + public function &links_get($uid, $limit, $link_ids = null) { + return $this->call_method('links.get', + array('uid' => $uid, + 'limit' => $limit, + 'link_ids' => $link_ids)); + } + + /** + * Posts a link on Facebook. + * + * @param string $url URL/link you wish to post + * @param string $comment (Optional) A comment about this link + * @param int $uid (Optional) User ID that is posting this link; + * defaults to current session user + * + * @return bool + */ + public function &links_post($url, $comment='', $uid = null) { + return $this->call_method('links.post', + array('uid' => $uid, + 'url' => $url, + 'comment' => $comment)); + } + /** * Permissions API */ @@ -945,6 +1168,78 @@ function toggleDisplay(id, type) { array('permissions_apikey' => $permissions_apikey)); } + /** + * Creates a note with the specified title and content. + * + * @param string $title Title of the note. + * @param string $content Content of the note. + * @param int $uid (Optional) The user for whom you are creating a + * note; defaults to current session user + * + * @return int The ID of the note that was just created. + */ + public function ¬es_create($title, $content, $uid = null) { + return $this->call_method('notes.create', + array('uid' => $uid, + 'title' => $title, + 'content' => $content)); + } + + /** + * Deletes the specified note. + * + * @param int $note_id ID of the note you wish to delete + * @param int $uid (Optional) Owner of the note you wish to delete; + * defaults to current session user + * + * @return bool + */ + public function ¬es_delete($note_id, $uid = null) { + return $this->call_method('notes.delete', + array('uid' => $uid, + 'note_id' => $note_id)); + } + + /** + * Edits a note, replacing its title and contents with the title + * and contents specified. + * + * @param int $note_id ID of the note you wish to edit + * @param string $title Replacement title for the note + * @param string $content Replacement content for the note + * @param int $uid (Optional) Owner of the note you wish to edit; + * defaults to current session user + * + * @return bool + */ + public function ¬es_edit($note_id, $title, $content, $uid = null) { + return $this->call_method('notes.edit', + array('uid' => $uid, + 'note_id' => $note_id, + 'title' => $title, + 'content' => $content)); + } + + /** + * Retrieves all notes by a user. If note_ids are specified, + * retrieves only those specific notes by that user. + * + * @param int $uid User whose notes you wish to retrieve + * @param array $note_ids (Optional) List of specific note + * IDs by this user to retrieve + * + * @return array A list of all of the given user's notes, or an empty list + * if the viewer lacks permissions or if there are no visible + * notes. + */ + public function ¬es_get($uid, $note_ids = null) { + + return $this->call_method('notes.get', + array('uid' => $uid, + 'note_ids' => $note_ids)); + } + + /** * Returns the outstanding notifications for the session user. * @@ -954,13 +1249,15 @@ function toggleDisplay(id, type) { * and an eid list of 'event_invites' */ public function ¬ifications_get() { - return $this->call_method('facebook.notifications.get', array()); + return $this->call_method('facebook.notifications.get'); } /** * Sends a notification to the specified users. * * @return A comma separated list of successful recipients + * @error + * API_EC_PARAM_USER_ID_LIST */ public function ¬ifications_send($to_ids, $notification, $type) { return $this->call_method('facebook.notifications.send', @@ -972,12 +1269,14 @@ function toggleDisplay(id, type) { /** * Sends an email to the specified user of the application. * - * @param array $recipients id of the recipients + * @param array/string $recipients array of ids of the recipients (csv is deprecated) * @param string $subject subject of the email * @param string $text (plain text) body of the email * @param string $fbml fbml markup for an html version of the email * * @return string A comma separated list of successful recipients + * @error + * API_EC_PARAM_USER_ID_LIST */ public function ¬ifications_sendEmail($recipients, $subject, @@ -993,9 +1292,9 @@ function toggleDisplay(id, type) { /** * Returns the requested info fields for the requested set of pages. * - * @param array $page_ids an array of page ids - * @param array $fields an array of strings describing the info fields - * desired + * @param array/string $page_ids an array of page ids (csv is deprecated) + * @param array/string $fields an array of strings describing the + * info fields desired (csv is deprecated) * @param int $uid (Optional) limit results to pages of which this * user is a fan. * @param string type limits results to a particular type of page. @@ -1090,7 +1389,7 @@ function toggleDisplay(id, type) { 'tag_text' => $tag_text, 'x' => $x, 'y' => $y, - 'tags' => json_encode($tags), + 'tags' => (is_array($tags)) ? json_encode($tags) : null, 'owner_uid' => $this->get_uid($owner_uid))); } @@ -1128,7 +1427,8 @@ function toggleDisplay(id, type) { * @param int $subj_id (Optional) Filter by uid of user tagged in the photos. * @param int $aid (Optional) Filter by an album, as returned by * photos_getAlbums. - * @param array $pids (Optional) Restrict to a list of pids + * @param array/string $pids (Optional) Restrict to an array of pids + * (csv is deprecated) * * Note that at least one of these parameters needs to be specified, or an * error is returned. @@ -1143,9 +1443,10 @@ function toggleDisplay(id, type) { /** * Returns the albums created by the given user. * - * @param int $uid (Optional) The uid of the user whose albums you want. - * A null will return the albums of the session user. - * @param array $aids (Optional) A list of aids to restrict the query. + * @param int $uid (Optional) The uid of the user whose albums you want. + * A null will return the albums of the session user. + * @param string $aids (Optional) An array of aids to restrict + * the query. (csv is deprecated) * * Note that at least one of the (uid, aids) parameters must be specified. * @@ -1171,17 +1472,67 @@ function toggleDisplay(id, type) { array('pids' => $pids)); } + /** + * Uploads a photo. + * + * @param string $file The location of the photo on the local filesystem. + * @param int $aid (Optional) The album into which to upload the + * photo. + * @param string $caption (Optional) A caption for the photo. + * @param int uid (Optional) The user ID of the user whose photo you + * are uploading + * + * @return array An array of user objects + */ + public function photos_upload($file, $aid=null, $caption=null, $uid=null) { + return $this->call_upload_method('facebook.photos.upload', + array('aid' => $aid, + 'caption' => $caption, + 'uid' => $uid), + $file); + } + + + /** + * Uploads a video. + * + * @param string $file The location of the video on the local filesystem. + * @param string $title (Optional) A title for the video. Titles over 65 characters in length will be truncated. + * @param string $description (Optional) A description for the video. + * + * @return array An array with the video's ID, title, description, and a link to view it on Facebook. + */ + public function video_upload($file, $title=null, $description=null) { + return $this->call_upload_method('facebook.video.upload', + array('title' => $title, + 'description' => $description), + $file, + Facebook::get_facebook_url('api-video') . '/restserver.php'); + } + + /** + * Returns an array with the video limitations imposed on the current session's + * associated user. Maximum length is measured in seconds; maximum size is + * measured in bytes. + * + * @return array Array with "length" and "size" keys + */ + public function &video_getUploadLimits() { + return $this->call_method('facebook.video.getUploadLimits'); + } + /** * Returns the requested info fields for the requested set of users. * - * @param array $uids An array of user ids - * @param array $fields An array of info field names desired + * @param array/string $uids An array of user ids (csv is deprecated) + * @param array/string $fields An array of info field names desired (csv is deprecated) * * @return array An array of user objects */ public function &users_getInfo($uids, $fields) { return $this->call_method('facebook.users.getInfo', - array('uids' => $uids, 'fields' => $fields)); + array('uids' => $uids, + 'fields' => $fields)); } /** @@ -1194,14 +1545,15 @@ function toggleDisplay(id, type) { * users, use users.getInfo instead, so that proper privacy rules will be * applied. * - * @param array $uids An array of user ids - * @param array $fields An array of info field names desired + * @param array/string $uids An array of user ids (csv is deprecated) + * @param array/string $fields An array of info field names desired (csv is deprecated) * * @return array An array of user objects */ public function &users_getStandardInfo($uids, $fields) { return $this->call_method('facebook.users.getStandardInfo', - array('uids' => $uids, 'fields' => $fields)); + array('uids' => $uids, + 'fields' => $fields)); } /** @@ -1210,7 +1562,7 @@ function toggleDisplay(id, type) { * @return integer User id */ public function &users_getLoggedInUser() { - return $this->call_method('facebook.users.getLoggedInUser', array()); + return $this->call_method('facebook.users.getLoggedInUser'); } /** @@ -1238,6 +1590,17 @@ function toggleDisplay(id, type) { return $this->call_method('facebook.users.isAppUser', array('uid' => $uid)); } + /** + * Returns whether or not the user corresponding to the current + * session object is verified by Facebook. See the documentation + * for Users.isVerified for details. + * + * @return boolean true if the user is verified + */ + public function &users_isVerified() { + return $this->call_method('facebook.users.isVerified'); + } + /** * Sets the users' current status message. Message does NOT contain the * word "is" , so make sure to include a verb. @@ -1268,6 +1631,69 @@ function toggleDisplay(id, type) { return $this->call_method('facebook.users.setStatus', $args); } + /** + * Gets the stream on behalf of a user using a set of users. This + * call will return the latest $limit queries between $start_time + * and $end_time. + * + * @param int $viewer_id user making the call (def: session) + * @param array $source_ids users/pages to look at (def: all connections) + * @param int $start_time start time to look for stories (def: 1 day ago) + * @param int $end_time end time to look for stories (def: now) + * @param int $limit number of stories to attempt to fetch (def: 30) + * @param string $filter_key key returned by stream.getFilters to fetch + * + * @return array( + * 'posts' => array of posts, + * 'profiles' => array of profile metadata of users/pages in posts + * 'albums' => array of album metadata in posts + * ) + */ + public function &stream_get($viewer_id = null, + $source_ids = null, + $start_time = 0, + $end_time = 0, + $limit = 30, + $filter_key = '') { + $args = array( + 'viewer_id' => $viewer_id, + 'source_ids' => $source_ids, + 'start_time' => $start_time, + 'end_time' => $end_time, + 'limit' => $limit, + 'filter_key' => $filter_key); + return $this->call_method('facebook.stream.get', $args); + } + + /** + * Gets the filters (with relevant filter keys for stream.get) for a + * particular user. These filters are typical things like news feed, + * friend lists, networks. They can be used to filter the stream + * without complex queries to determine which ids belong in which groups. + * + * @param int $uid user to get filters for + * + * @return array of stream filter objects + */ + public function &stream_getFilters($uid = null) { + $args = array('uid' => $uid); + return $this->call_method('facebook.stream.getFilters', $args); + } + + /** + * Gets the full comments given a post_id from stream.get or the + * stream FQL table. Initially, only a set of preview comments are + * returned because some posts can have many comments. + * + * @param string $post_id id of the post to get comments for + * + * @return array of comment objects + */ + public function &stream_getComments($post_id) { + $args = array('post_id' => $post_id); + return $this->call_method('facebook.stream.getComments', $args); + } + /** * Sets the FBML for the profile of the user attached to this session. * @@ -1690,7 +2116,7 @@ function toggleDisplay(id, type) { * API_EC_DATA_UNKNOWN_ERROR */ public function &data_getObjectTypes() { - return $this->call_method('facebook.data.getObjectTypes', array()); + return $this->call_method('facebook.data.getObjectTypes'); } /** @@ -2315,12 +2741,14 @@ function toggleDisplay(id, type) { * * @param string $integration_point_name Name of an integration point * (see developer wiki for list). + * @param int $uid Specific user to check the limit. * * @return int Integration point allocation value */ - public function &admin_getAllocation($integration_point_name) { + public function &admin_getAllocation($integration_point_name, $uid=null) { return $this->call_method('facebook.admin.getAllocation', - array('integration_point_name' => $integration_point_name)); + array('integration_point_name' => $integration_point_name, + 'uid' => $uid)); } /** @@ -2376,28 +2804,75 @@ function toggleDisplay(id, type) { */ public function admin_getRestrictionInfo() { return json_decode( - $this->call_method('admin.getRestrictionInfo', array()), + $this->call_method('admin.getRestrictionInfo'), true); } + + /** + * Bans a list of users from the app. Banned users can't + * access the app's canvas page and forums. + * + * @param array $uids an array of user ids + * @return bool true on success + */ + public function admin_banUsers($uids) { + return $this->call_method( + 'admin.banUsers', array('uids' => json_encode($uids))); + } + + /** + * Unban users that have been previously banned with + * admin_banUsers(). + * + * @param array $uids an array of user ids + * @return bool true on success + */ + public function admin_unbanUsers($uids) { + return $this->call_method( + 'admin.unbanUsers', array('uids' => json_encode($uids))); + } + + /** + * Gets the list of users that have been banned from the application. + * $uids is an optional parameter that filters the result with the list + * of provided user ids. If $uids is provided, + * only banned user ids that are contained in $uids are returned. + * + * @param array $uids an array of user ids to filter by + * @return bool true on success + */ + + public function admin_getBannedUsers($uids = null) { + return $this->call_method( + 'admin.getBannedUsers', + array('uids' => $uids ? json_encode($uids) : null)); + } + /* UTILITY FUNCTIONS */ /** - * Calls the specified method with the specified parameters. + * Calls the specified normal POST method with the specified parameters. * * @param string $method Name of the Facebook method to invoke * @param array $params A map of param names => param values * - * @return mixed Result of method call + * @return mixed Result of method call; this returns a reference to support + * 'delayed returns' when in a batch context. + * See: http://wiki.developers.facebook.com/index.php/Using_batching_API */ - public function & call_method($method, $params) { - //Check if we are in batch mode - if($this->batch_queue === null) { + public function &call_method($method, $params = array()) { + if (!$this->pending_batch()) { if ($this->call_as_apikey) { $params['call_as_apikey'] = $this->call_as_apikey; } - $xml = $this->post_request($method, $params); - $result = $this->convert_xml_to_result($xml, $method, $params); + $data = $this->post_request($method, $params); + if (empty($params['format']) || strtolower($params['format']) != 'json') { + $result = $this->convert_xml_to_result($data, $method, $params); + } + else { + $result = json_decode($data, true); + } if (is_array($result) && isset($result['error_code'])) { throw new FacebookRestClientException($result['error_msg'], @@ -2413,11 +2888,46 @@ function toggleDisplay(id, type) { return $result; } - private function convert_xml_to_result($xml, $method, $params) { + /** + * Calls the specified file-upload POST method with the specified parameters + * + * @param string $method Name of the Facebook method to invoke + * @param array $params A map of param names => param values + * @param string $file A path to the file to upload (required) + * + * @return array A dictionary representing the response. + */ + public function call_upload_method($method, $params, $file, $server_addr = null) { + if (!$this->pending_batch()) { + if (!file_exists($file)) { + $code = + FacebookAPIErrorCodes::API_EC_PARAM; + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); + } + + $xml = $this->post_upload_request($method, $params, $file, $server_addr); + $result = $this->convert_xml_to_result($xml, $method, $params); + + if (is_array($result) && isset($result['error_code'])) { + throw new FacebookRestClientException($result['error_msg'], + $result['error_code']); + } + } + else { + $code = + FacebookAPIErrorCodes::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE; + $description = FacebookAPIErrorCodes::$api_error_descriptions[$code]; + throw new FacebookRestClientException($description, $code); + } + + return $result; + } + + protected function convert_xml_to_result($xml, $method, $params) { $sxml = simplexml_load_string($xml); $result = self::convert_simplexml_to_array($sxml); - if (!empty($GLOBALS['facebook_config']['debug'])) { // output the raw xml and its corresponding php object, for debugging: print '
    '; @@ -2436,7 +2946,25 @@ function toggleDisplay(id, type) { return $result; } - private function create_post_string($method, $params) { + private function finalize_params($method, &$params) { + $this->add_standard_params($method, $params); + // we need to do this before signing the params + $this->convert_array_values_to_json($params); + $params['sig'] = Facebook::generate_sig($params, $this->secret); + } + + private function convert_array_values_to_json(&$params) { + foreach ($params as $key => &$val) { + if (is_array($val)) { + $val = json_encode($val); + } + } + } + + private function add_standard_params($method, &$params) { + if ($this->call_as_apikey) { + $params['call_as_apikey'] = $this->call_as_apikey; + } $params['method'] = $method; $params['session_key'] = $this->session_key; $params['api_key'] = $this->api_key; @@ -2448,50 +2976,118 @@ function toggleDisplay(id, type) { if (!isset($params['v'])) { $params['v'] = '1.0'; } + if (isset($this->use_ssl_resources) && + $this->use_ssl_resources) { + $params['return_ssl_resources'] = true; + } + } + + private function create_post_string($method, $params) { $post_params = array(); foreach ($params as $key => &$val) { - if (is_array($val)) $val = implode(',', $val); $post_params[] = $key.'='.urlencode($val); } - $secret = $this->secret; - $post_params[] = 'sig='.Facebook::generate_sig($params, $secret); return implode('&', $post_params); } + private function run_multipart_http_transaction($method, $params, $file, $server_addr) { + + // the format of this message is specified in RFC1867/RFC1341. + // we add twenty pseudo-random digits to the end of the boundary string. + $boundary = '--------------------------FbMuLtIpArT' . + sprintf("%010d", mt_rand()) . + sprintf("%010d", mt_rand()); + $content_type = 'multipart/form-data; boundary=' . $boundary; + // within the message, we prepend two extra hyphens. + $delimiter = '--' . $boundary; + $close_delimiter = $delimiter . '--'; + $content_lines = array(); + foreach ($params as $key => &$val) { + $content_lines[] = $delimiter; + $content_lines[] = 'Content-Disposition: form-data; name="' . $key . '"'; + $content_lines[] = ''; + $content_lines[] = $val; + } + // now add the file data + $content_lines[] = $delimiter; + $content_lines[] = + 'Content-Disposition: form-data; filename="' . $file . '"'; + $content_lines[] = 'Content-Type: application/octet-stream'; + $content_lines[] = ''; + $content_lines[] = file_get_contents($file); + $content_lines[] = $close_delimiter; + $content_lines[] = ''; + $content = implode("\r\n", $content_lines); + return $this->run_http_post_transaction($content_type, $content, $server_addr); + } + public function post_request($method, $params) { - + $this->finalize_params($method, $params); $post_string = $this->create_post_string($method, $params); - - if (function_exists('curl_init')) { - // Use CURL if installed... + if ($this->use_curl_if_available && function_exists('curl_init')) { $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion(); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->server_addr); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_USERAGENT, $useragent); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); $result = curl_exec($ch); curl_close($ch); } else { - // Non-CURL based version... $content_type = 'application/x-www-form-urlencoded'; - $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) '.phpversion(); - $context = - array('http' => - array('method' => 'POST', - 'header' => 'Content-type: '.$content_type."\r\n". - 'User-Agent: '.$user_agent."\r\n". - 'Content-length: ' . strlen($post_string), - 'content' => $post_string)); - $contextid=stream_context_create($context); - $sock=fopen($this->server_addr, 'r', false, $contextid); - if ($sock) { - $result=''; - while (!feof($sock)) - $result.=fgets($sock, 4096); + $content = $post_string; + $result = $this->run_http_post_transaction($content_type, + $content, + $this->server_addr); + } + return $result; + } - fclose($sock); + private function post_upload_request($method, $params, $file, $server_addr = null) { + $server_addr = $server_addr ? $server_addr : $this->server_addr; + $this->finalize_params($method, $params); + if ($this->use_curl_if_available && function_exists('curl_init')) { + // prepending '@' causes cURL to upload the file; the key is ignored. + $params['_file'] = '@' . $file; + $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion(); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $server_addr); + // this has to come before the POSTFIELDS set! + curl_setopt($ch, CURLOPT_POST, 1 ); + // passing an array gets curl to use the multipart/form-data content type + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, $useragent); + $result = curl_exec($ch); + curl_close($ch); + } else { + $result = $this->run_multipart_http_transaction($method, $params, $file, $server_addr); + } + return $result; + } + + private function run_http_post_transaction($content_type, $content, $server_addr) { + + $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) ' . phpversion(); + $content_length = strlen($content); + $context = + array('http' => + array('method' => 'POST', + 'user_agent' => $user_agent, + 'header' => 'Content-Type: ' . $content_type . "\r\n" . + 'Content-Length: ' . $content_length, + 'content' => $content)); + $context_id = stream_context_create($context); + $sock = fopen($server_addr, 'r', false, $context_id); + + $result = ''; + if ($sock) { + while (!feof($sock)) { + $result .= fgets($sock, 4096); } + fclose($sock); } return $result; } @@ -2541,6 +3137,14 @@ class FacebookAPIErrorCodes { const API_EC_METHOD = 3; const API_EC_TOO_MANY_CALLS = 4; const API_EC_BAD_IP = 5; + const API_EC_HOST_API = 6; + const API_EC_HOST_UP = 7; + const API_EC_SECURE = 8; + const API_EC_RATE = 9; + const API_EC_PERMISSION_DENIED = 10; + const API_EC_DEPRECATED = 11; + const API_EC_VERSION = 12; + const API_EC_INTERNAL_FQL_ERROR = 13; /* * PARAMETER ERRORS @@ -2550,27 +3154,121 @@ class FacebookAPIErrorCodes { const API_EC_PARAM_SESSION_KEY = 102; const API_EC_PARAM_CALL_ID = 103; const API_EC_PARAM_SIGNATURE = 104; + const API_EC_PARAM_TOO_MANY = 105; const API_EC_PARAM_USER_ID = 110; const API_EC_PARAM_USER_FIELD = 111; const API_EC_PARAM_SOCIAL_FIELD = 112; + const API_EC_PARAM_EMAIL = 113; + const API_EC_PARAM_USER_ID_LIST = 114; + const API_EC_PARAM_FIELD_LIST = 115; const API_EC_PARAM_ALBUM_ID = 120; + const API_EC_PARAM_PHOTO_ID = 121; + const API_EC_PARAM_FEED_PRIORITY = 130; + const API_EC_PARAM_CATEGORY = 140; + const API_EC_PARAM_SUBCATEGORY = 141; + const API_EC_PARAM_TITLE = 142; + const API_EC_PARAM_DESCRIPTION = 143; + const API_EC_PARAM_BAD_JSON = 144; const API_EC_PARAM_BAD_EID = 150; const API_EC_PARAM_UNKNOWN_CITY = 151; + const API_EC_PARAM_BAD_PAGE_TYPE = 152; /* * USER PERMISSIONS ERRORS */ const API_EC_PERMISSION = 200; const API_EC_PERMISSION_USER = 210; + const API_EC_PERMISSION_NO_DEVELOPERS = 211; const API_EC_PERMISSION_ALBUM = 220; const API_EC_PERMISSION_PHOTO = 221; + const API_EC_PERMISSION_MESSAGE = 230; + const API_EC_PERMISSION_OTHER_USER = 240; + const API_EC_PERMISSION_STATUS_UPDATE = 250; + const API_EC_PERMISSION_PHOTO_UPLOAD = 260; + const API_EC_PERMISSION_VIDEO_UPLOAD = 261; + const API_EC_PERMISSION_SMS = 270; + const API_EC_PERMISSION_CREATE_LISTING = 280; + const API_EC_PERMISSION_CREATE_NOTE = 281; + const API_EC_PERMISSION_SHARE_ITEM = 282; const API_EC_PERMISSION_EVENT = 290; + const API_EC_PERMISSION_LARGE_FBML_TEMPLATE = 291; + const API_EC_PERMISSION_LIVEMESSAGE = 292; const API_EC_PERMISSION_RSVP_EVENT = 299; - const FQL_EC_PARSER = 601; + /* + * DATA EDIT ERRORS + */ + const API_EC_EDIT = 300; + const API_EC_EDIT_USER_DATA = 310; + const API_EC_EDIT_PHOTO = 320; + const API_EC_EDIT_ALBUM_SIZE = 321; + const API_EC_EDIT_PHOTO_TAG_SUBJECT = 322; + const API_EC_EDIT_PHOTO_TAG_PHOTO = 323; + const API_EC_EDIT_PHOTO_FILE = 324; + const API_EC_EDIT_PHOTO_PENDING_LIMIT = 325; + const API_EC_EDIT_PHOTO_TAG_LIMIT = 326; + const API_EC_EDIT_ALBUM_REORDER_PHOTO_NOT_IN_ALBUM = 327; + const API_EC_EDIT_ALBUM_REORDER_TOO_FEW_PHOTOS = 328; + + const API_EC_MALFORMED_MARKUP = 329; + const API_EC_EDIT_MARKUP = 330; + + const API_EC_EDIT_FEED_TOO_MANY_USER_CALLS = 340; + const API_EC_EDIT_FEED_TOO_MANY_USER_ACTION_CALLS = 341; + const API_EC_EDIT_FEED_TITLE_LINK = 342; + const API_EC_EDIT_FEED_TITLE_LENGTH = 343; + const API_EC_EDIT_FEED_TITLE_NAME = 344; + const API_EC_EDIT_FEED_TITLE_BLANK = 345; + const API_EC_EDIT_FEED_BODY_LENGTH = 346; + const API_EC_EDIT_FEED_PHOTO_SRC = 347; + const API_EC_EDIT_FEED_PHOTO_LINK = 348; + + const API_EC_EDIT_VIDEO_SIZE = 350; + const API_EC_EDIT_VIDEO_INVALID_FILE = 351; + const API_EC_EDIT_VIDEO_INVALID_TYPE = 352; + const API_EC_EDIT_VIDEO_FILE = 353; + + const API_EC_EDIT_FEED_TITLE_ARRAY = 360; + const API_EC_EDIT_FEED_TITLE_PARAMS = 361; + const API_EC_EDIT_FEED_BODY_ARRAY = 362; + const API_EC_EDIT_FEED_BODY_PARAMS = 363; + const API_EC_EDIT_FEED_PHOTO = 364; + const API_EC_EDIT_FEED_TEMPLATE = 365; + const API_EC_EDIT_FEED_TARGET = 366; + const API_EC_EDIT_FEED_MARKUP = 367; + + /** + * SESSION ERRORS + */ + const API_EC_SESSION_TIMED_OUT = 450; + const API_EC_SESSION_METHOD = 451; + const API_EC_SESSION_INVALID = 452; + const API_EC_SESSION_REQUIRED = 453; + const API_EC_SESSION_REQUIRED_FOR_SECRET = 454; + const API_EC_SESSION_CANNOT_USE_SESSION_SECRET = 455; + + + /** + * FQL ERRORS + */ + const FQL_EC_UNKNOWN_ERROR = 600; + const FQL_EC_PARSER = 601; // backwards compatibility + const FQL_EC_PARSER_ERROR = 601; const FQL_EC_UNKNOWN_FIELD = 602; const FQL_EC_UNKNOWN_TABLE = 603; - const FQL_EC_NOT_INDEXABLE = 604; + const FQL_EC_NOT_INDEXABLE = 604; // backwards compatibility + const FQL_EC_NO_INDEX = 604; + const FQL_EC_UNKNOWN_FUNCTION = 605; + const FQL_EC_INVALID_PARAM = 606; + const FQL_EC_INVALID_FIELD = 607; + const FQL_EC_INVALID_SESSION = 608; + const FQL_EC_UNSUPPORTED_APP_TYPE = 609; + const FQL_EC_SESSION_SECRET_NOT_ALLOWED = 610; + const FQL_EC_DEPRECATED_TABLE = 611; + const FQL_EC_EXTENDED_PERMISSION = 612; + const FQL_EC_RATE_LIMIT_EXCEEDED = 613; + + const API_EC_REF_SET_FAILED = 700; /** * DATA STORE API ERRORS @@ -2581,52 +3279,122 @@ class FacebookAPIErrorCodes { const API_EC_DATA_OBJECT_NOT_FOUND = 803; const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804; const API_EC_DATA_DATABASE_ERROR = 805; + const API_EC_DATA_CREATE_TEMPLATE_ERROR = 806; + const API_EC_DATA_TEMPLATE_EXISTS_ERROR = 807; + const API_EC_DATA_TEMPLATE_HANDLE_TOO_LONG = 808; + const API_EC_DATA_TEMPLATE_HANDLE_ALREADY_IN_USE = 809; + const API_EC_DATA_TOO_MANY_TEMPLATE_BUNDLES = 810; + const API_EC_DATA_MALFORMED_ACTION_LINK = 811; + const API_EC_DATA_TEMPLATE_USES_RESERVED_TOKEN = 812; /* - * Batch ERROR + * APPLICATION INFO ERRORS */ - const API_EC_BATCH_ALREADY_STARTED = 900; - const API_EC_BATCH_NOT_STARTED = 901; - const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 902; + const API_EC_NO_SUCH_APP = 900; + /* + * BATCH ERRORS + */ + const API_EC_BATCH_TOO_MANY_ITEMS = 950; + const API_EC_BATCH_ALREADY_STARTED = 951; + const API_EC_BATCH_NOT_STARTED = 952; + const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 953; + + /* + * EVENT API ERRORS + */ + const API_EC_EVENT_INVALID_TIME = 1000; + + /* + * INFO BOX ERRORS + */ + const API_EC_INFO_NO_INFORMATION = 1050; + const API_EC_INFO_SET_FAILED = 1051; + + /* + * LIVEMESSAGE API ERRORS + */ + const API_EC_LIVEMESSAGE_SEND_FAILED = 1100; + const API_EC_LIVEMESSAGE_EVENT_NAME_TOO_LONG = 1101; + const API_EC_LIVEMESSAGE_MESSAGE_TOO_LONG = 1102; + + /* + * CONNECT SESSION ERRORS + */ + const API_EC_CONNECT_FEED_DISABLED = 1300; + + /* + * Platform tag bundles errors + */ + const API_EC_TAG_BUNDLE_QUOTA = 1400; + + /* + * SHARE + */ + const API_EC_SHARE_BAD_URL = 1500; + + /* + * NOTES + */ + const API_EC_NOTE_CANNOT_MODIFY = 1600; + + /* + * COMMENTS + */ + const API_EC_COMMENTS_UNKNOWN = 1700; + const API_EC_COMMENTS_POST_TOO_LONG = 1701; + const API_EC_COMMENTS_DB_DOWN = 1702; + const API_EC_COMMENTS_INVALID_XID = 1703; + const API_EC_COMMENTS_INVALID_UID = 1704; + const API_EC_COMMENTS_INVALID_POST = 1705; + + /** + * This array is no longer maintained; to view the description of an error + * code, please look at the message element of the API response or visit + * the developer wiki at http://wiki.developers.facebook.com/. + */ public static $api_error_descriptions = array( - API_EC_SUCCESS => 'Success', - API_EC_UNKNOWN => 'An unknown error occurred', - API_EC_SERVICE => 'Service temporarily unavailable', - API_EC_METHOD => 'Unknown method', - API_EC_TOO_MANY_CALLS => 'Application request limit reached', - API_EC_BAD_IP => 'Unauthorized source IP address', - API_EC_PARAM => 'Invalid parameter', - API_EC_PARAM_API_KEY => 'Invalid API key', - API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid', - API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous', - API_EC_PARAM_SIGNATURE => 'Incorrect signature', - API_EC_PARAM_USER_ID => 'Invalid user id', - API_EC_PARAM_USER_FIELD => 'Invalid user info field', - API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field', - API_EC_PARAM_ALBUM_ID => 'Invalid album id', - API_EC_PARAM_BAD_EID => 'Invalid eid', - API_EC_PARAM_UNKNOWN_CITY => 'Unknown city', - API_EC_PERMISSION => 'Permissions error', - API_EC_PERMISSION_USER => 'User not visible', - API_EC_PERMISSION_ALBUM => 'Album not visible', - API_EC_PERMISSION_PHOTO => 'Photo not visible', - API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event', - API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event', - FQL_EC_PARSER => 'FQL: Parser Error', - FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field', - FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table', - FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable', - FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function', - FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in', - API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error', - API_EC_DATA_INVALID_OPERATION => 'Invalid operation', - API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded', - API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found', - API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists', - API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again', - API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first', - API_EC_BATCH_NOT_STARTED => 'end_batch called before start_batch', - API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode', + self::API_EC_SUCCESS => 'Success', + self::API_EC_UNKNOWN => 'An unknown error occurred', + self::API_EC_SERVICE => 'Service temporarily unavailable', + self::API_EC_METHOD => 'Unknown method', + self::API_EC_TOO_MANY_CALLS => 'Application request limit reached', + self::API_EC_BAD_IP => 'Unauthorized source IP address', + self::API_EC_PARAM => 'Invalid parameter', + self::API_EC_PARAM_API_KEY => 'Invalid API key', + self::API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid', + self::API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous', + self::API_EC_PARAM_SIGNATURE => 'Incorrect signature', + self::API_EC_PARAM_USER_ID => 'Invalid user id', + self::API_EC_PARAM_USER_FIELD => 'Invalid user info field', + self::API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field', + self::API_EC_PARAM_USER_ID_LIST => 'Invalid user id list', + self::API_EC_PARAM_FIELD_LIST => 'Invalid field list', + self::API_EC_PARAM_ALBUM_ID => 'Invalid album id', + self::API_EC_PARAM_BAD_EID => 'Invalid eid', + self::API_EC_PARAM_UNKNOWN_CITY => 'Unknown city', + self::API_EC_PERMISSION => 'Permissions error', + self::API_EC_PERMISSION_USER => 'User not visible', + self::API_EC_PERMISSION_NO_DEVELOPERS => 'Application has no developers', + self::API_EC_PERMISSION_ALBUM => 'Album not visible', + self::API_EC_PERMISSION_PHOTO => 'Photo not visible', + self::API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event', + self::API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event', + self::API_EC_EDIT_ALBUM_SIZE => 'Album is full', + self::FQL_EC_PARSER => 'FQL: Parser Error', + self::FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field', + self::FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table', + self::FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable', + self::FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function', + self::FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in', + self::API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error', + self::API_EC_DATA_INVALID_OPERATION => 'Invalid operation', + self::API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded', + self::API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found', + self::API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists', + self::API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again', + self::API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first', + self::API_EC_BATCH_NOT_STARTED => 'end_batch called before begin_batch', + self::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode' ); } diff --git a/index.php b/index.php index 3f25e004d7..b79fefc85a 100644 --- a/index.php +++ b/index.php @@ -63,6 +63,10 @@ function handleError($error) function main() { + // quick check for fancy URL auto-detection support in installer. + if (isset($_SERVER['REDIRECT_URL']) && ('/check-fancy' === $_SERVER['REDIRECT_URL'])) { + die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs."); + } global $user, $action, $config; Snapshot::check(); @@ -103,6 +107,8 @@ function main() $args = array_merge($args, $_REQUEST); + Event::handle('ArgsInitialize', array(&$args)); + $action = $args['action']; if (!$action || !preg_match('/^[a-zA-Z0-9_-]*$/', $action)) { diff --git a/install.php b/install.php index 87a99a6508..bc82e5e37a 100644 --- a/install.php +++ b/install.php @@ -35,15 +35,17 @@ function main() function checkPrereqs() { + $pass = true; + if (file_exists(INSTALLDIR.'/config.php')) { ?>

    Config file "config.php" already exists.

    Require PHP version 5 or greater.

    Cannot load required extension "".

    Cannot load required extension:

    Cannot write config file to "".

    -

    On your server, try this command:

    -
    chmod a+w
    + ?>

    Cannot write config file to:

    +

    On your server, try this command: chmod a+w

    Cannot write avatar directory "/avatar/".

    -

    On your server, try this command:

    -
    chmod a+w /avatar/
    + ?>

    Cannot write avatar directory: /avatar/

    +

    On your server, try this command: chmod a+w /avatar/

    -

    Enter your database connection information below to initialize the database.

    -
    -
    -
      -
    • - - -

      The name of your site

      -
    • -
    • -
    • - - -

      Database hostname

      -
    • -
    • - - -

      Database name

      -
    • -
    • - - -

      Database username

      -
    • -
    • - - -

      Database password

      -
    • -
    - -
    + $config_path = htmlentities(trim(dirname($_SERVER['REQUEST_URI']), '/')); + echo<< + + +
    +
    Page notice
    +
    +
    +

    Enter your database connection information below to initialize the database.

    +
    +
    +
    + +
    + Connection settings +
      +
    • + + +

      The name of your site

      +
    • +
    • + + enable
      + disable
      +

      Enable fancy (pretty) URLs. Auto-detection failed, it depends on Javascript.

      +
    • +
    • + + +

      Database hostname

      +
    • +
    • + + +

      Site path, following the "/" after the domain name in the URL. Empty is fine. Field should be filled automatically.

      +
    • +
    • + + +

      Database name

      +
    • +
    • + + +

      Database username

      +
    • +
    • + + +

      Database password

      +
    • +
    + +
    - -
  • - -
  • -> + + -
      - +
      +
      Page notice
      +
      +
        +new Laconica site."); ?> -
      -"); return $res; } @@ -253,21 +288,37 @@ function runDbScript($filename, $conn) } ?> - - - Install Laconica - - - - - -
      -
      -
      -

      Install Laconica

      + xml version="1.0" encoding="UTF-8" "; ?> + + + + Install Laconica + + + + + + + + + +
      + +
      +
      +

      Install Laconica

      -
      -
      -
      - +
      +
      +
      + diff --git a/js/farbtastic/farbtastic.go.js b/js/farbtastic/farbtastic.go.js index 21a1530bca..0149eca7d9 100644 --- a/js/farbtastic/farbtastic.go.js +++ b/js/farbtastic/farbtastic.go.js @@ -1,10 +1,85 @@ +/** Init for Farbtastic library and page setup + * + * @package Laconica + * @author Sarven Capadisli + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ $(document).ready(function() { - var f = $.farbtastic('#color-picker'); - var colors = $('#settings_design_color input'); + function UpdateColors(S) { + C = $(S).val(); + switch (parseInt(S.id.slice(-1))) { + case 0: default: + $('body').css({'background-color':C}); + break; + case 1: + $('#content').css({'background-color':C}); + break; + case 2: + $('#aside_primary').css({'background-color':C}); + break; + case 3: + $('body').css({'color':C}); + break; + case 4: + $('a').css({'color':C}); + break; + } + } - colors - .each(function () { f.linkTo(this); }) - .focus(function() { - f.linkTo(this); - }); + function UpdateFarbtastic(e) { + f.linked = e; + f.setColor(e.value); + } + + function UpdateSwatch(e) { + $(e).css({"background-color": e.value, + "color": f.hsl[2] > 0.5 ? "#000": "#fff"}); + } + + function SynchColors(e) { + var S = f.linked; + var C = f.color; + + if (S && S.value && S.value != C) { + S.value = C; + UpdateSwatch(S); + UpdateColors(S); + } + } + + function Init() { + $('#settings_design_color').append('
      '); + $('#color-picker').hide(); + + f = $.farbtastic('#color-picker', SynchColors); + swatches = $('#settings_design_color .swatch'); + + swatches + .each(SynchColors) + .blur(function() { + $(this).val($(this).val().toUpperCase()); + }) + .focus(function() { + $('#color-picker').show(); + UpdateFarbtastic(this); + }) + .change(function() { + UpdateFarbtastic(this); + UpdateSwatch(this); + UpdateColors(this); + }).change(); + } + + var f, swatches; + Init(); + $('#form_settings_design').bind('reset', function(){ + setTimeout(function(){ + swatches.each(function(){UpdateColors(this);}); + $('#color-picker').remove(); + swatches.unbind(); + Init(); + },10); + }); }); diff --git a/js/identica-badge.js b/js/identica-badge.js index 869230b7a4..ffa55ae93e 100644 --- a/js/identica-badge.js +++ b/js/identica-badge.js @@ -128,7 +128,7 @@ var a = document.createElement('A'); a.innerHTML = 'get this'; a.target = '_blank'; - a.href = 'http://identica/doc/badge'; + a.href = 'http://identi.ca/doc/badge'; $.s.f.appendChild(a); $.s.appendChild($.s.f); $.f.getUser(); diff --git a/js/install.js b/js/install.js new file mode 100644 index 0000000000..32a54111e2 --- /dev/null +++ b/js/install.js @@ -0,0 +1,18 @@ +$(document).ready(function(){ + $.ajax({url:'check-fancy', + type:'GET', + success:function(data, textStatus) { + $('#fancy-enable').attr('checked', true); + $('#fancy-disable').attr('checked', false); + $('#fancy-form_guide').text(data); + }, + error:function(XMLHttpRequest, textStatus, errorThrown) { + $('#fancy-enable').attr('checked', false); + $('#fancy-disable').attr('checked', true); + $('#fancy-enable').attr('disabled', true); + $('#fancy-disable').attr('disabled', true); + $('#fancy-form_guide').text("Fancy URL support detection failed, disabling this option. Make sure you renamed htaccess.sample to .htaccess."); + } + }); +}); + diff --git a/js/jcrop/jquery.Jcrop.go.js b/js/jcrop/jquery.Jcrop.go.js index a0399d5405..4e1cbfd1e7 100644 --- a/js/jcrop/jquery.Jcrop.go.js +++ b/js/jcrop/jquery.Jcrop.go.js @@ -1,39 +1,48 @@ - $(function(){ - var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0; - var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0; - var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width"); - var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height"); +/** Init for Jcrop library and page setup + * + * @package Laconica + * @author Sarven Capadisli + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ - jQuery("#avatar_original img").Jcrop({ - onChange: showPreview, - setSelect: [ x, y, w, h ], - onSelect: updateCoords, - aspectRatio: 1, - boxWidth: 480, - boxHeight: 480, - bgColor: '#000', - bgOpacity: .4 - }); - }); +$(function(){ + var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0; + var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0; + var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width"); + var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height"); - function showPreview(coords) { - var rx = 96 / coords.w; - var ry = 96 / coords.h; + jQuery("#avatar_original img").Jcrop({ + onChange: showPreview, + setSelect: [ x, y, w, h ], + onSelect: updateCoords, + aspectRatio: 1, + boxWidth: 480, + boxHeight: 480, + bgColor: '#000', + bgOpacity: .4 + }); +}); - var img_width = $("#avatar_original img").attr("width"); - var img_height = $("#avatar_original img").attr("height"); +function showPreview(coords) { + var rx = 96 / coords.w; + var ry = 96 / coords.h; - $('#avatar_preview img').css({ - width: Math.round(rx *img_width) + 'px', - height: Math.round(ry * img_height) + 'px', - marginLeft: '-' + Math.round(rx * coords.x) + 'px', - marginTop: '-' + Math.round(ry * coords.y) + 'px' - }); - }; + var img_width = $("#avatar_original img").attr("width"); + var img_height = $("#avatar_original img").attr("height"); - function updateCoords(c) { - $('#avatar_crop_x').val(c.x); - $('#avatar_crop_y').val(c.y); - $('#avatar_crop_w').val(c.w); - $('#avatar_crop_h').val(c.h); - }; + $('#avatar_preview img').css({ + width: Math.round(rx *img_width) + 'px', + height: Math.round(ry * img_height) + 'px', + marginLeft: '-' + Math.round(rx * coords.x) + 'px', + marginTop: '-' + Math.round(ry * coords.y) + 'px' + }); +}; + +function updateCoords(c) { + $('#avatar_crop_x').val(c.x); + $('#avatar_crop_y').val(c.y); + $('#avatar_crop_w').val(c.w); + $('#avatar_crop_h').val(c.h); +}; diff --git a/js/jquery.joverlay.min.js b/js/jquery.joverlay.min.js new file mode 100644 index 0000000000..c9168506a5 --- /dev/null +++ b/js/jquery.joverlay.min.js @@ -0,0 +1,6 @@ +/* Copyright (c) 2009 Alvaro A. Lima Jr http://alvarojunior.com/jquery/joverlay.html + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * Version: 0.6 (Abr 23, 2009) + * Requires: jQuery 1.3+ + */ +(function($){var f=$.browser.msie&&$.browser.version==6.0;var g=null;$.fn.jOverlay=function(b){var b=$.extend({},$.fn.jOverlay.options,b);if(g!=null){clearTimeout(g)}var c=this.is('*')?this:'#jOverlayContent';var d=f?'absolute':'fixed';var e=b.imgLoading?"":'';$('body').prepend(e+"
      "+"