diff --git a/actions/attachment.php b/actions/attachment.php new file mode 100644 index 0000000000..981882a5bb --- /dev/null +++ b/actions/attachment.php @@ -0,0 +1,250 @@ +. + * + * @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; + + /** + * Profile of the notice object + */ + +// var $profile = null; + + /** + * Avatar of the profile of the notice object + */ + +// var $avatar = 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 showAside() { + $notice = new Notice; + $f2p = new File_to_post; + $f2p->file_id = $this->attachment->id; + $notice->joinAdd($f2p); + $notice->orderBy('created desc'); + $x = $notice->find(); + $this->elementStart('ol'); + while($notice->fetch()) { + $this->elementStart('li'); + $profile = $notice->getProfile(); + $this->element('a', array('href' => $notice->uri), $profile->nickname . ' on ' . $notice->created); + $this->elementEnd('li'); + } + $this->elementEnd('ol'); + $notice->free(); + $f2p->free(); + + $notice_tag = new Notice_tag; + $attachment = new File; + + $query = 'select tag,count(tag) as c from notice_tag join file_to_post on (notice_tag.notice_id=post_id) join notice on notice_id = notice.id where file_id=' . $notice_tag->escape($this->attachment->id) . ' group by tag order by c desc'; + + $notice_tag->query($query); + $this->elementStart('ol'); + while($notice_tag->fetch()) { + $this->elementStart('li'); + $href = common_local_url('tag', array('tag' => $notice_tag->tag)); + $this->element('a', array('href' => $href), $notice_tag->tag . ' (' . $notice_tag->c . ')'); + $this->elementEnd('li'); + } + $this->elementEnd('ol'); + } +} 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/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/tag.php b/actions/tag.php index 02f3e35225..2202f9bb07 100644 --- a/actions/tag.php +++ b/actions/tag.php @@ -49,8 +49,18 @@ class TagAction extends Action { $pop = new PopularNoticeSection($this); $pop->show(); - } + $notice_tag = new Notice_tag; + $query = 'select file_id, count(file_id) as c from notice_tag join file_to_post on post_id = notice_id where tag="' . $notice_tag->escape($this->tag) . '" group by file_id order by c desc'; + $notice_tag->query($query); + $this->elementStart('ol'); + while ($notice_tag->fetch()) { + $this->elementStart('li'); + $this->element('a', array('class' => 'attachment', 'href' => common_local_url('attachment', array('attachment' => $notice_tag->file_id))), "Attachment tagged {$notice_tag->c} times"); + $this->elementEnd('li'); + } + $this->elementEnd('ol'); + } function title() { 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..a71d1c0831 --- /dev/null +++ b/classes/File_redirection.php @@ -0,0 +1,276 @@ +. + */ + +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; +if(1) { + // 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..30508070e5 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.'); @@ -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($this->id); + $post->query($query); + $post->fetch(); + $n_attachments = intval($post->n_attachments); + $post->free(); + return $n_attachments; + } + function blowCaches($blowLast=false) { $this->blowSubsCache($blowLast); 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/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+"
"+"