From cd7d0eae4a8d46bb194dc49c7b4241527c032823 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 20 Jun 2011 01:11:35 -0400 Subject: [PATCH] First coded version of blog plugin A basic blog plugin. Allows posting open-ended HTML with a title. Not yet tested (even run). --- plugins/Blog/BlogEntry.php | 233 +++++++++++++++++++++++++++++ plugins/Blog/BlogPlugin.php | 210 ++++++++++++++++++++++++++ plugins/Blog/blogentryform.php | 133 ++++++++++++++++ plugins/Blog/blogentrylistitem.php | 79 ++++++++++ plugins/Blog/newblogentry.php | 138 +++++++++++++++++ plugins/Blog/showblogentry.php | 86 +++++++++++ 6 files changed, 879 insertions(+) create mode 100644 plugins/Blog/BlogEntry.php create mode 100644 plugins/Blog/BlogPlugin.php create mode 100644 plugins/Blog/blogentryform.php create mode 100644 plugins/Blog/blogentrylistitem.php create mode 100644 plugins/Blog/newblogentry.php create mode 100644 plugins/Blog/showblogentry.php diff --git a/plugins/Blog/BlogEntry.php b/plugins/Blog/BlogEntry.php new file mode 100644 index 0000000000..54940e06e5 --- /dev/null +++ b/plugins/Blog/BlogEntry.php @@ -0,0 +1,233 @@ +. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Data structure for blog entries + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class BlogEntry extends Managed_DataObject +{ + public $__table = 'blog_entry'; + + public $id; // UUID + public $profile_id; // int + public $title; // varchar(255) + public $summary; // text + public $content; // text + public $uri; // text + public $url; // text + public $created; // datetime + public $modified; // datetime + + const TYPE = 'http://activitystrea.ms/schema/1.0/blog-entry'; + + static function staticGet($k, $v=null) + { + return Managed_DataObject::staticGet('blog_entry', $k, $v); + } + + static function schemaDef() + { + return array( + 'description' => 'lite blog entry', + 'fields' => array( + 'id' => array('type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'Unique ID (UUID)'), + 'profile_id' => array('type' => 'int', + 'not null' => true, + 'description' => 'Author profile ID'), + 'title' => array('type' => 'varchar', + 'length' => 255, + 'description' => 'title of the entry'), + 'summary' => array('type' => 'text', + 'description' => 'initial summary'), + 'content' => array('type' => 'text', + 'description' => 'HTML content of the entry'), + 'uri' => array('type' => 'varchar', + 'length' => 255, + 'description' => 'URI (probably http://) for this entry'), + 'url' => array('type' => 'varchar', + 'length' => 255, + 'description' => 'URL (probably http://) for this entry'), + 'created' => array('type' => 'datetime', + 'not null' => true, + 'description' => 'date this record was created'), + 'modified' => array('type' => 'datetime', + 'not null' => true, + 'description' => 'date this record was created'), + ), + 'primary key' => array('id'), + 'foreign keys' => array( + 'blog_entry_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), + 'indexes' => array( + 'blog_entry_created_idx' => array('created'), + 'blog_entry_uri_idx' => array('uri'), + ), + ); + } + + static function saveNew($profile, $title, $content, $options=null) + { + if (is_null($options)) { + $options = array(); + } + + $be = new BlogEntry(); + $be->id = (string) new UUID(); + $be->profile_id = $profile->id; + $be->title = htmlspecialchars($title); + $be->content = $content; + + if (array_key_exists('summary', $options)) { + $be->summary = $options['summary']; + } else { + $be->summary = self::summarize($content); + } + + $url = common_local_url('showblogentry', array('id' => $be->id)); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $url; + } + + $be->uri = $options['uri']; + + if (!array_key_exists('url', $options)) { + $options['url'] = $url; + } + + $be->url = $options['url']; + + if (!array_key_exists('created', $options)) { + $be->created = common_sql_now(); + } + + $be->created = $options['created']; + + $be->modified = common_sql_now(); + + $be->insert(); + + // Use user's preferences for short URLs, if possible + + try { + $user = $profile->getUser(); + $shortUrl = File_redirection::makeShort($url, + empty($user) ? null : $user); + } catch (Exception $e) { + // Don't let this stop us. + $shortUrl = $url; + } + + // XXX: this might be too long. + + $options['rendered'] = $be->summary . ' ' . + XMLStringer::estring('a', array('href' => $shortUrl, + 'class' => 'blog-entry'), + _('More...')); + + $summaryText = html_entity_decode(strip_tags($summary), ENT_QUOTES, 'UTF-8'); + + if (Notice::contentTooLong($summaryText)) { + $summaryText = substr($summaryText, 0, Notice::maxContent() - mb_strlen($shortUrl) - 2) . + '… ' . $shortUrl; + } + + $content = $summaryText; + + // Override this no matter what. + + $options['object_type'] = self::TYPE; + + $source = array_key_exists('source', $options) ? + $options['source'] : 'web'; + + Notice::saveNew($profile->id, $content, $source, $options); + } + + /** + * Summarize the contents of a blog post + * + * We take the first div or paragraph of the blog post if there's a hit; + * Otherwise we take the whole thing. + * + * @param string $html HTML of full content + */ + static function summarize($html) + { + if (preg_match('#

.*?

#s', $html, $matches)) { + return $matches[0]; + } else if (preg_match('#
.*?
#s', $html, $matches)) { + return $matches[0]; + } else { + return $html; + } + } + + static function fromNotice($notice) + { + return BlogEntry::staticGet('uri', $notice->uri); + } + + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + function asActivityObject() + { + $obj = new ActivityObject(); + + $obj->id = $this->uri; + $obj->type = self::TYPE; + $obj->title = $this->title; + $obj->summary = $this->summary; + $obj->content = $this->content; + $obj->link = $this->url; + + return $obj; + } +} diff --git a/plugins/Blog/BlogPlugin.php b/plugins/Blog/BlogPlugin.php new file mode 100644 index 0000000000..7f8e8fd1d0 --- /dev/null +++ b/plugins/Blog/BlogPlugin.php @@ -0,0 +1,210 @@ +. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Blog plugin + * + * Many social systems have a way to write and share long-form texts with + * your network. This microapp plugin lets users post blog entries. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class BlogPlugin extends MicroAppPlugin +{ + /** + * Database schema setup + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onCheckSchema() + { + $schema = Schema::get(); + + $schema->ensureTable('blog_entry', BlogEntry::schemaDef()); + + return true; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'NewblogentryAction': + case 'ShowblogentryAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'BlogEntryForm': + case 'BlogEntryListItem': + include_once $dir . '/'.strtolower($cls).'.php'; + return false; + case 'BlogEntry': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Map URLs to actions + * + * @param Net_URL_Mapper $m path-to-action mapper + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onRouterInitialized($m) + { + $m->connect('blog/new', + array('action' => 'newblogentry')); + $m->connect('blog/:id', + array('action' => 'showblogentry'), + array('id' => UUID::REGEX)); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Blog', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Blog', + 'rawdescription' => + _m('Let users write and share long-form texts.')); + return true; + } + + function appTitle() + { + return _m('Blog'); + } + + function tag() + { + return 'blog'; + } + + function types() + { + return array(BlogEntry::TYPE); + } + + function saveNoticeFromActivity($activity, $actor, $options=array()) + { + if (count($activity->objects) != 1) { + // TRANS: Exception thrown when there are too many activity objects. + throw new ClientException(_m('Too many activity objects.')); + } + + $entryObj = $activity->objects[0]; + + if ($entryObj->type != BlogEntry::TYPE) { + // TRANS: Exception thrown when blog plugin comes across a non-event type object. + throw new ClientException(_m('Wrong type for object.')); + } + + $notice = null; + + switch ($activity->verb) { + case ActivityVerb::POST: + $notice = BlogEntry::saveNew($actor, + $entryObj->title, + $entryObj->content, + $options); + break; + default: + // TRANS: Exception thrown when blog plugin comes across a undefined verb. + throw new ClientException(_m('Unknown verb for blog entries.')); + } + + return $notice; + } + + function activityObjectFromNotice($notice) + { + $entry = BlogEntry::fromNotice($notice); + + if (empty($entry)) { + throw new ClientException(sprintf(_('No blog entry for notice %s'), + $notice->id)); + } + + return $entry->asActivityObject(); + } + + function entryForm($out) + { + return new BlogEntryForm($out); + } + + function deleteRelated($notice) + { + if ($notice->object_type == BlogEntry::TYPE) { + $entry = BlogEntry::fromNotice($notice); + if (exists($entry)) { + $entry->delete(); + } + } + } + + function adaptNoticeListItem($nli) + { + $notice = $nli->notice; + + if ($notice->object_type == BlogEntry::TYPE) { + return new BlogEntryListItem($nli); + } + + return null; + } +} diff --git a/plugins/Blog/blogentryform.php b/plugins/Blog/blogentryform.php new file mode 100644 index 0000000000..b21e76a7e8 --- /dev/null +++ b/plugins/Blog/blogentryform.php @@ -0,0 +1,133 @@ +. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Form for creating a blog entry + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class BlogEntryForm extends Form +{ + /** + * ID of the form + * + * @return int ID of the form + */ + function id() + { + return 'form_new_blog_entry'; + } + + /** + * class of the form + * + * @return string class of the form + */ + function formClass() + { + return 'form_settings ajax-notice'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('newblogentry'); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'new_blog_entry_data')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->out->input('blog-entry-title', + // TRANS: Field label on blog entry form. + _m('LABEL','Title'), + null, + // TRANS: Field title on blog entry form. + _m('Title of the blog entry.'), + 'title'); + $this->unli(); + + $this->li(); + $this->out->textarea('blog-entry-content', + // TRANS: Field label on event form. + _m('LABEL','Text'), + null, + // TRANS: Field title on event form. + _m('Text of the blog entry.'), + 'content'); + $this->unli(); + + $this->out->elementEnd('ul'); + + $toWidget = new ToSelector($this->out, + common_current_user(), + null); + $toWidget->show(); + + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Button text to save an event.. + $this->out->submit('blog-entry-submit', + _m('BUTTON', 'Save'), + 'submit', + 'submit'); + } +} diff --git a/plugins/Blog/blogentrylistitem.php b/plugins/Blog/blogentrylistitem.php new file mode 100644 index 0000000000..44775d8a3d --- /dev/null +++ b/plugins/Blog/blogentrylistitem.php @@ -0,0 +1,79 @@ +. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * NoticeListItem adapter for blog entries + * + * @category General + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class BlogEntryListItem extends NoticeListItemAdapter +{ + function showNotice() + { + $this->out->elementStart('div', 'entry-title'); + $this->showAuthor(); + $this->showContent(); + $this->out->elementEnd('div'); + } + + function showContent() + { + $notice = $this->nli->notice; + $out = $this->nli->out; + + $entry = BlogEntry::fromNotice($notice); + + if (empty($entry)) { + throw new Exception('BlogEntryListItem used for non-blog notice.'); + } + + $out->elementStart('h4', array('class' => 'blog-entry-title')); + $out->element('a', array('href' => $notice->bestUrl()), $entry->title); + $out->elementEnd('h4'); + + $out->element('div', 'blog-entry-summary', $entry->summary); + + // XXX: hide content initially; click More... for full text. + + $out->element('div', 'blog-entry-content', $entry->content); + } +} diff --git a/plugins/Blog/newblogentry.php b/plugins/Blog/newblogentry.php new file mode 100644 index 0000000000..94988c5335 --- /dev/null +++ b/plugins/Blog/newblogentry.php @@ -0,0 +1,138 @@ +. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Save a new blog entry + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class NewblogentryAction extends Action +{ + protected $user; + protected $title; + protected $content; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + if (!$this->isPost()) { + throw new ClientException(_('Must be a POST.'), 405); + } + + $this->user = common_current_user(); + + if (empty($this->user)) { + // TRANS: Client exception thrown when trying to post a blog entry while not logged in. + throw new ClientException(_m('Must be logged in to post a blog entry.'), + 403); + } + + $this->checkSessionToken(); + + $this->title = $this->trimmed('title'); + + if (empty($this->title)) { + // TRANS: Client exception thrown when trying to post a blog entry without providing a title. + throw new ClientException(_m('Title required.')); + } + + $this->content = $this->trimmed('content'); + + if (empty($this->content)) { + // TRANS: Client exception thrown when trying to post a blog entry without providing content. + throw new ClientException(_m('Content required.')); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + $options = array(); + + // Does the heavy-lifting for getting "To:" information + + ToSelector::fillOptions($this, $options); + + $options['source'] = 'web'; + + $profile = $this->user->getProfile(); + + $saved = BlogEntry::saveNew($profile, + $this->title, + $this->content, + $options); + + if ($this->boolean('ajax')) { + header('Content-Type: text/xml; charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending a notice. + $this->element('title', null, _m('Blog entry saved')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showNotice($saved); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect($saved->bestUrl(), 303); + } + } +} diff --git a/plugins/Blog/showblogentry.php b/plugins/Blog/showblogentry.php new file mode 100644 index 0000000000..4ddf7963e0 --- /dev/null +++ b/plugins/Blog/showblogentry.php @@ -0,0 +1,86 @@ +. + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Show a blog entry + * + * @category Blog + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ShowblogentryAction extends ShownoticeAction +{ + protected $id; + protected $entry; + + function getNotice() + { + $this->id = $this->trimmed('id'); + + $this->entry = BlogEntry::staticGet('id', $this->id); + + if (empty($this->entry)) { + // TRANS: Client exception thrown when referring to a non-existing blog entry. + throw new ClientException(_m('No such entry.'), 404); + } + + $notice = $this->entry->getNotice(); + + if (empty($notice)) { + // TRANS: Client exception thrown when referring to a non-existing blog entry. + throw new ClientException(_m('No such entry.'), 404); + } + + return $notice; + } + + /** + * Title of the page + * + * Used by Action class for layout. + * + * @return string page tile + */ + function title() + { + // XXX: check for double-encoding + return (empty($this->entry->title)) ? _m('Untitled') : $this->entry->title; + } +}