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/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/designsettings.php b/actions/designsettings.php index cdd950e78c..a85b36a254 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,57 @@ 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->submit('save', _('Save')); + $this->element('input', array('type' => 'reset', + 'value' => 'Reset', + 'class' => 'form_action-secondary')); +/*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 +224,7 @@ class DesignsettingsAction extends AccountSettingsAction /** - * Add the jCrop stylesheet + * Add the Farbtastic stylesheet * * @return void */ @@ -205,7 +242,7 @@ class DesignsettingsAction extends AccountSettingsAction } /** - * Add the jCrop scripts + * Add the Farbtastic scripts * * @return void */ @@ -214,14 +251,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/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/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/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/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/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/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/Notice.php b/classes/Notice.php index 382d160ab3..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; @@ -279,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); @@ -1016,7 +1024,7 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) + function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null, $tag=null) { $cache = common_memcache(); @@ -1024,7 +1032,7 @@ class Notice extends Memcached_DataObject $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))); + $before_id, $since, $tag))); } $idkey = common_cache_key($cachekey); @@ -1044,7 +1052,7 @@ class Notice extends Memcached_DataObject $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))); + $last_id, 0, null, $tag))); $new_window = array_merge($new_ids, $window); @@ -1059,7 +1067,7 @@ class Notice extends Memcached_DataObject } $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - 0, 0, null))); + 0, 0, null, $tag))); $windowstr = implode(',', $window); diff --git a/classes/Profile.php b/classes/Profile.php index ae5641d79d..afc0ea4f74 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -153,18 +153,66 @@ 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) + { + // 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); + } + + 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); + $offset, $limit, $since_id, $before_id, $since); return Notice::getStreamByIds($ids); } - function _streamDirect($offset, $limit, $since_id, $before_id, $since) + 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(); diff --git a/classes/User.php b/classes/User.php index b5ac7b2206..ea8ba40817 100644 --- a/classes/User.php +++ b/classes/User.php @@ -407,13 +407,22 @@ class User extends Memcached_DataObject 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) { $profile = $this->getProfile(); if (!$profile) { return null; } else { - return $profile->getNotices($offset, $limit, $since_id, $before_id); + return $profile->getNotices($offset, $limit, $since_id, $before_id, $since); } } diff --git a/classes/laconica.ini b/classes/laconica.ini old mode 100755 new mode 100644 index 5a905a4bbe..316923af02 --- a/classes/laconica.ini +++ b/classes/laconica.ini @@ -1,4 +1,3 @@ - [avatar] profile_id = 129 original = 17 @@ -393,3 +392,63 @@ modified = 384 [user_openid__keys] canonical = K display = U + +[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 = 129 +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 = 129 +redirections = 1 +httpcode = 1 + +[file_redirection__keys] +id = N + +[file_thumbnail] +id = 129 +file_id = 129 +url = 2 +width = 1 +height = 1 + +[file_thumbnail__keys] +id = N + +[file_to_post] +id = 129 +file_id = 129 +post_id = 129 + +[file_to_post__keys] +id = N + + 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/laconica.sql b/db/laconica.sql index d9e21a7b51..344f0ff723 100644 --- a/db/laconica.sql +++ b/db/laconica.sql @@ -425,3 +425,62 @@ 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 a27a616f24..b213bbd502 100644 --- a/db/laconica_pg.sql +++ b/db/laconica_pg.sql @@ -427,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/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 e24bde9179..9ff1c2c56a 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; if (!_have_config()) { @@ -101,6 +105,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 66e8e87124..32915200b2 100644 --- a/install.php +++ b/install.php @@ -86,7 +86,8 @@ function checkExtension($name) function showForm() { -?> + $config_path = htmlentities(trim(dirname($_SERVER['REQUEST_URI']), '/')); + echo<< @@ -108,11 +109,21 @@ function showForm()

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.

    +
  • @@ -132,7 +143,8 @@ function showForm() -
    Page notice
    @@ -225,29 +239,34 @@ function handlePost() } updateStatus("Writing config file..."); $sqlUrl = "mysqli://$username:$password@$host/$database"; - $res = writeConf($sitename, $sqlUrl); + $res = writeConf($sitename, $sqlUrl, $fancy, $path); if (!$res) { updateStatus("Can't write config file.", true); showForm(); return; } updateStatus("Done!"); + if ($path) $path .= '/'; + updateStatus("You can visit your new Laconica site + +
    diff --git a/js/farbtastic/farbtastic.go.js b/js/farbtastic/farbtastic.go.js index 21a1530bca..e298c1dab0 100644 --- a/js/farbtastic/farbtastic.go.js +++ b/js/farbtastic/farbtastic.go.js @@ -1,10 +1,67 @@ $(document).ready(function() { - var f = $.farbtastic('#color-picker'); - var colors = $('#settings_design_color input'); + function UpdateColors(e) { + var S = f.linked; + var C = f.color; - colors - .each(function () { f.linkTo(this); }) - .focus(function() { - f.linkTo(this); + if (S && S.value && S.value != C) { + UpdateSwatch(S); + + switch (parseInt(f.linked.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; + } + S.value = C; + } + } + + 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" }); + } + + $('#settings_design_color').append('
    '); + $('#color-picker').hide(); + + var f = $.farbtastic('#color-picker', UpdateColors); + var swatches = $('#settings_design_color .swatch'); + + swatches + .each(UpdateColors) + + .blur(function() { + $(this).val($(this).val().toUpperCase()); + }) + + .focus(function() { + $('#color-picker').show(); + UpdateFarbtastic(this); + }) + + .change(function() { + UpdateFarbtastic(this); + UpdateSwatch(this); + }).change() + + ; + }); 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/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+"
    "+"