.+\.(?:css|js|jpg|png|swf|gif))'
=> 'CelerityResourceController',
),
);
}
public function buildRequest() {
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($_GET + $_POST);
$request->setApplicationConfiguration($this);
return $request;
}
public function handleException(Exception $ex) {
// Always log the unhandled exception.
phlog($ex);
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
$user = $this->getRequest()->getUser();
if (!$user) {
// If we hit an exception very early, we won't have a user.
$user = new PhabricatorUser();
}
if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) {
$trace = $this->renderStackTrace($ex->getTrace(), $user);
} else {
$trace = null;
}
$content =
''.
'
'.$message.'
'.
$trace.
'
';
$dialog = new AphrontDialogView();
$dialog
->setTitle('Unhandled Exception ("'.$class.'")')
->setClass('aphront-exception-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', 'Close');
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
$response->setRequest($request);
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax()) {
$view = new PhabricatorStandardPageView();
$view->setRequest($request);
$view->appendChild(
''.
$response->buildResponseString().
'
');
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
} else {
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
));
}
}
return $response;
}
public function build404Controller() {
return array(new Phabricator404Controller($this->getRequest()), array());
}
public function buildRedirectController($uri) {
return array(
new PhabricatorRedirectController($this->getRequest()),
array(
'uri' => $uri,
));
}
private function renderStackTrace($trace, PhabricatorUser $user) {
$libraries = PhutilBootloader::getInstance()->getAllLibraries();
// TODO: Make this configurable?
$path = 'https://secure.phabricator.com/diffusion/%s/browse/master/src/';
$callsigns = array(
'arcanist' => 'ARC',
'phutil' => 'PHU',
'phabricator' => 'P',
);
$rows = array();
$depth = count($trace);
foreach ($trace as $part) {
$lib = null;
$file = idx($part, 'file');
$relative = $file;
foreach ($libraries as $library) {
$root = phutil_get_library_root($library);
if (Filesystem::isDescendant($file, $root)) {
$lib = $library;
$relative = Filesystem::readablePath($file, $root);
break;
}
}
$where = '';
if (isset($part['class'])) {
$where .= $part['class'].'::';
}
if (isset($part['function'])) {
$where .= $part['function'].'()';
}
if ($file) {
if (isset($callsigns[$lib])) {
$attrs = array('title' => $file);
try {
$attrs['href'] = $user->loadEditorLink(
'/src/'.$relative,
$part['line'],
$callsigns[$lib]);
} catch (Exception $ex) {
// The database can be inaccessible.
}
if (empty($attrs['href'])) {
$attrs['href'] = sprintf($path, $callsigns[$lib]).
str_replace(DIRECTORY_SEPARATOR, '/', $relative).
'$'.$part['line'];
$attrs['target'] = '_blank';
}
$file_name = phutil_render_tag(
'a',
$attrs,
phutil_escape_html($relative));
} else {
$file_name = phutil_render_tag(
'span',
array(
'title' => $file,
),
phutil_escape_html($relative));
}
$file_name = $file_name.' : '.(int)$part['line'];
} else {
$file_name = '(Internal)';
}
$rows[] = array(
$depth--,
phutil_escape_html($lib),
$file_name,
phutil_escape_html($where),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Depth',
'Library',
'File',
'Where',
));
$table->setColumnClasses(
array(
'n',
'',
'',
'wide',
));
return
''.
''.
$table->render().
'
';
}
}
diff --git a/src/applications/markup/engine/PhabricatorMarkupEngine.php b/src/applications/markup/engine/PhabricatorMarkupEngine.php
index bcdc2a8170..5ca97efd87 100644
--- a/src/applications/markup/engine/PhabricatorMarkupEngine.php
+++ b/src/applications/markup/engine/PhabricatorMarkupEngine.php
@@ -1,186 +1,193 @@
markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorRemarkupRuleMention::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
// Disable image macros on the wiki since they're less useful, we don't
// cache documents, and the module is prohibitively expensive for large
// documents.
'macros' => false,
'header.generate-toc' => true,
));
}
+ public static function newPhameMarkupEngine() {
+ return self::newMarkupEngine(array(
+ 'macros' => false,
+ ));
+ }
+
+
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'custom-inline' => PhabricatorEnv::getEnvConfig(
'differential.custom-remarkup-rules'),
'custom-block' => PhabricatorEnv::getEnvConfig(
'differential.custom-remarkup-block-rules'),
'differential.diff' => idx($options, 'differential.diff'),
));
}
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
));
}
public static function newProfileMarkupEngine() {
return self::newMarkupEngine(array(
));
}
public static function newSlowvoteMarkupEngine() {
return self::newMarkupEngine(array(
));
}
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'fileproxy' => PhabricatorEnv::getEnvConfig('files.enable-proxy'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'custom-inline' => array(),
'custom-block' => array(),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
);
}
private static function newMarkupEngine(array $options) {
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', true);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$rules = array();
$rules[] = new PhutilRemarkupRuleEscapeRemarkup();
$rules[] = new PhutilRemarkupRuleMonospace();
$custom_rule_classes = $options['custom-inline'];
if ($custom_rule_classes) {
foreach ($custom_rule_classes as $custom_rule_class) {
PhutilSymbolLoader::loadClass($custom_rule_class);
$rules[] = newv($custom_rule_class, array());
}
}
if ($options['fileproxy']) {
$rules[] = new PhabricatorRemarkupRuleProxyImage();
}
if ($options['youtube']) {
$rules[] = new PhabricatorRemarkupRuleYoutube();
}
$rules[] = new PhutilRemarkupRuleHyperlink();
$rules[] = new PhabricatorRemarkupRulePhriction();
$rules[] = new PhabricatorRemarkupRuleDifferentialHandle();
$rules[] = new PhabricatorRemarkupRuleManiphestHandle();
$rules[] = new PhabricatorRemarkupRuleEmbedFile();
$rules[] = new PhabricatorRemarkupRuleDifferential();
$rules[] = new PhabricatorRemarkupRuleDiffusion();
$rules[] = new PhabricatorRemarkupRuleManiphest();
$rules[] = new PhabricatorRemarkupRulePaste();
if ($options['macros']) {
$rules[] = new PhabricatorRemarkupRuleImageMacro();
}
$rules[] = new PhabricatorRemarkupRuleMention();
$rules[] = new PhutilRemarkupRuleEscapeHTML();
$rules[] = new PhutilRemarkupRuleBold();
$rules[] = new PhutilRemarkupRuleItalic();
$rules[] = new PhutilRemarkupRuleDel();
$blocks = array();
$blocks[] = new PhutilRemarkupEngineRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupDefaultBlockRule();
$custom_block_rule_classes = $options['custom-block'];
if ($custom_block_rule_classes) {
foreach ($custom_block_rule_classes as $custom_block_rule_class) {
PhutilSymbolLoader::loadClass($custom_block_rule_class);
$blocks[] = newv($custom_block_rule_class, array());
}
}
foreach ($blocks as $block) {
if ($block instanceof PhutilRemarkupEngineRemarkupLiteralBlockRule) {
$literal_rules = array();
$literal_rules[] = new PhutilRemarkupRuleHyperlink();
$literal_rules[] = new PhutilRemarkupRuleEscapeHTML();
$literal_rules[] = new PhutilRemarkupRuleLinebreaks();
$block->setMarkupRules($literal_rules);
} else if (
!($block instanceof PhutilRemarkupEngineRemarkupCodeBlockRule)) {
$block->setMarkupRules($rules);
}
}
$engine->setBlockRules($blocks);
return $engine;
}
}
diff --git a/src/applications/phame/controller/base/PhameController.php b/src/applications/phame/controller/base/PhameController.php
new file mode 100644
index 0000000000..67ce808a90
--- /dev/null
+++ b/src/applications/phame/controller/base/PhameController.php
@@ -0,0 +1,91 @@
+showSideNav = (bool) $value;
+ return $this;
+ }
+ private function showSideNav() {
+ return $this->showSideNav;
+ }
+
+ public function buildStandardPageResponse($view, array $data) {
+
+ $page = $this->buildStandardPageView();
+
+ $page->setApplicationName('Phame');
+ $page->setBaseURI('/phame/');
+ $page->setTitle(idx($data, 'title'));
+ $page->setGlyph("\xe2\x9c\xa9");
+
+ $tabs = array(
+ 'help' => array(
+ 'name' => 'Help',
+ 'href' =>
+ PhabricatorEnv::getDoclink('article/Phame_User_Guide.html'),
+ ),
+ );
+ $page->setTabs($tabs, idx($data, 'tab'));
+ if ($this->showSideNav()) {
+ $nav = $this->renderSideNavFilterView($this->getSideNavFilter());
+ $nav->appendChild($view);
+ $page->appendChild($nav);
+ } else {
+ $page->appendChild($view);
+ }
+
+ $response = new AphrontWebpageResponse();
+ return $response->setContent($page->render());
+ }
+
+ private function renderSideNavFilterView($filter) {
+ $nav = new AphrontSideNavFilterView();
+ $nav->setBaseURI(new PhutilURI('/phame/'));
+ $nav->addLabel('Drafts');
+ $nav->addFilter('post/new',
+ 'New Draft');
+ $nav->addFilter('draft',
+ 'My Drafts');
+ $nav->addSpacer();
+ $nav->addLabel('Posts');
+ $nav->addFilter('post',
+ 'My Posts');
+ foreach ($this->getSideNavExtraPostFilters() as $post_filter) {
+ $nav->addFilter($post_filter['key'],
+ $post_filter['name']);
+ }
+
+ $nav->selectFilter($filter, 'post');
+
+ return $nav;
+ }
+
+ protected function getSideNavExtraPostFilters() {
+ return array();
+ }
+ protected function getSideNavFilter() {
+ return 'post';
+ }
+
+}
diff --git a/src/applications/phame/controller/base/__init__.php b/src/applications/phame/controller/base/__init__.php
new file mode 100644
index 0000000000..1e2554a34f
--- /dev/null
+++ b/src/applications/phame/controller/base/__init__.php
@@ -0,0 +1,18 @@
+phid = $phid;
+ return $this;
+ }
+ private function getPostPHID() {
+ return $this->phid;
+ }
+
+ protected function getSideNavFilter() {
+ return 'post/delete/'.$this->getPostPHID();
+ }
+
+ protected function getSideNavExtraPostFilters() {
+ $filters = array(
+ array('key' => $this->getSideNavFilter(),
+ 'name' => 'Delete Post')
+ );
+
+ return $filters;
+ }
+
+ public function willProcessRequest(array $data) {
+ $phid = $data['phid'];
+ $this->setPostPHID($phid);
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $user = $request->getUser();
+ $post = id(new PhamePost())->loadOneWhere(
+ 'phid = %s',
+ $this->getPostPHID());
+ if (empty($post)) {
+ return new Aphront404Response();
+ }
+ if ($post->getBloggerPHID() != $user->getPHID()) {
+ return new Aphront403Response();
+ }
+ $edit_uri = $post->getEditURI();
+
+ if ($request->isFormPost()) {
+ $post->delete();
+ return id(new AphrontRedirectResponse())->setURI('/phame/?deleted');
+ }
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($user)
+ ->setTitle('Delete post?')
+ ->appendChild('Really delete this post? It will be gone forever.')
+ ->addSubmitButton('Delete')
+ ->addCancelButton($edit_uri);
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+}
diff --git a/src/applications/phame/controller/post/delete/__init__.php b/src/applications/phame/controller/post/delete/__init__.php
new file mode 100644
index 0000000000..83a53fdff6
--- /dev/null
+++ b/src/applications/phame/controller/post/delete/__init__.php
@@ -0,0 +1,20 @@
+phid = $phid;
+ return $this;
+ }
+ private function getPostPHID() {
+ return $this->phid;
+ }
+ private function setIsPostEdit($is_post_edit) {
+ $this->isPostEdit = $is_post_edit;
+ return $this;
+ }
+ private function isPostEdit() {
+ return $this->isPostEdit;
+ }
+
+ protected function getSideNavFilter() {
+ if ($this->isPostEdit()) {
+ $filter = 'post/edit/'.$this->getPostPHID();
+ } else {
+ $filter = 'post/new';
+ }
+ return $filter;
+ }
+ protected function getSideNavExtraPostFilters() {
+ if ($this->isPostEdit()) {
+ $filters = array(
+ array('key' => 'post/edit/'.$this->getPostPHID(),
+ 'name' => 'Edit Post')
+ );
+ } else {
+ $filters = array();
+ }
+
+ return $filters;
+ }
+
+ public function willProcessRequest(array $data) {
+ $phid = idx($data, 'phid');
+ $this->setPostPHID($phid);
+ $this->setIsPostEdit((bool) $phid);
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $user = $request->getUser();
+ $e_phame_title = null;
+ $e_title = null;
+ $errors = array();
+
+ if ($this->isPostEdit()) {
+ $post = id(new PhamePost())->loadOneWhere(
+ 'phid = %s',
+ $this->getPostPHID());
+ if (empty($post)) {
+ return new Aphront404Response();
+ }
+ if ($post->getBloggerPHID() != $user->getPHID()) {
+ return new Aphront403Response();
+ }
+ $cancel_uri = $post->getViewURI($user->getUsername());
+ $submit_button = 'Save Changes';
+ $delete_button = javelin_render_tag(
+ 'a',
+ array(
+ 'href' => $post->getDeleteURI(),
+ 'class' => 'grey button',
+ 'sigil' => 'workflow',
+ ),
+ 'Delete Post');
+ $page_title = 'Edit Post';
+ } else {
+ $post = id(new PhamePost())
+ ->setBloggerPHID($user->getPHID())
+ ->setVisibility(PhamePost::VISIBILITY_DRAFT);
+ $cancel_uri = '/phame';
+ $submit_button = 'Create Post';
+ $delete_button = null;
+ $page_title = 'Create Post';
+ }
+
+ if ($request->isFormPost()) {
+ $saved = true;
+ $visibility = $request->getInt('visibility');
+ $comments = $request->getStr('comments_widget');
+ $data = array('comments_widget' => $comments);
+ $phame_title = $request->getStr('phame_title');
+ $phame_title = PhabricatorSlug::normalize($phame_title);
+ $title = $request->getStr('title');
+ $post->setTitle($title);
+ $post->setPhameTitle($phame_title);
+ $post->setBody($request->getStr('body'));
+ $post->setVisibility($visibility);
+ $post->setConfigData($data);
+ // only publish once...!
+ if ($visibility == PhamePost::VISIBILITY_PUBLISHED) {
+ if (!$post->getDatePublished()) {
+ $post->setDatePublished(time());
+ }
+ // this is basically a cast of null to 0 if its a new post
+ } else if (!$post->getDatePublished()) {
+ $post->setDatePublished(0);
+ }
+ if ($phame_title == '/') {
+ $errors[] = 'Phame title must be nonempty.';
+ $e_phame_title = 'Required';
+ }
+ if (empty($title)) {
+ $errors[] = 'Title must be nonempty.';
+ $e_title = 'Required';
+ }
+ if (empty($errors)) {
+ try {
+ $post->save();
+ } catch (AphrontQueryDuplicateKeyException $e) {
+ $saved = false;
+ $e_phame_title = 'Not Unique';
+ $errors[] = 'Another post already uses this slug. '.
+ 'Each post must have a unique slug.';
+ }
+ } else {
+ $saved = false;
+ }
+
+ if ($saved) {
+ $uri = new PhutilURI($post->getViewURI($user->getUsername()));
+ return id(new AphrontRedirectResponse())
+ ->setURI($uri);
+ }
+ }
+
+ $panel = new AphrontPanelView();
+ $panel->setHeader($page_title);
+ $panel->setWidth(AphrontPanelView::WIDTH_FULL);
+ if ($delete_button) {
+ $panel->addButton($delete_button);
+ }
+
+ $remarkup_reference = phutil_render_tag(
+ 'a',
+ array(
+ 'href' =>
+ PhabricatorEnv::getDoclink('article/Remarkup_Reference.html'),
+ 'tabindex' => '-1',
+ 'target' => '_blank',
+ ),
+ 'Formatting Reference');
+
+ $form = id(new AphrontFormView())
+ ->setUser($user)
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Title')
+ ->setName('title')
+ ->setValue($post->getTitle())
+ ->setID('post-title')
+ ->setError($e_title)
+ )
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Phame Title')
+ ->setName('phame_title')
+ ->setValue(rtrim($post->getPhameTitle(), '/'))
+ ->setID('post-phame-title')
+ ->setCaption('Up to 64 alphanumeric characters '.
+ 'with underscores for spaces. '.
+ 'Formatting is enforced.')
+ ->setError($e_phame_title)
+ )
+ ->appendChild(
+ id(new AphrontFormTextAreaControl())
+ ->setLabel('Body')
+ ->setName('body')
+ ->setValue($post->getBody())
+ ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
+ ->setEnableDragAndDropFileUploads(true)
+ ->setID('post-body')
+ ->setCaption($remarkup_reference)
+ )
+ ->appendChild(
+ id(new AphrontFormSelectControl())
+ ->setLabel('Visibility')
+ ->setName('visibility')
+ ->setValue($post->getVisibility())
+ ->setOptions(PhamePost::getVisibilityOptionsForSelect())
+ )
+ ->appendChild(
+ id(new AphrontFormSelectControl())
+ ->setLabel('Comments Widget')
+ ->setName('comments_widget')
+ ->setvalue($post->getCommentsWidget())
+ ->setOptions($post->getCommentsWidgetOptionsForSelect())
+ )
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->addCancelButton($cancel_uri)
+ ->setValue($submit_button)
+ );
+
+ $panel->appendChild($form);
+
+ $preview_panel =
+ '
+
+
+
+ Loading preview...
+
+
+
';
+
+ Javelin::initBehavior(
+ 'phame-post-preview',
+ array(
+ 'preview' => 'post-preview',
+ 'body' => 'post-body',
+ 'title' => 'post-title',
+ 'phame_title' => 'post-phame-title',
+ 'uri' => '/phame/post/preview/',
+ ));
+
+ if ($errors) {
+ $error_view = id(new AphrontErrorView())
+ ->setTitle('Errors saving post.')
+ ->setErrors($errors);
+ } else {
+ $error_view = null;
+ }
+
+ $this->setShowSideNav(true);
+ return $this->buildStandardPageResponse(
+ array(
+ $error_view,
+ $panel,
+ $preview_panel,
+ ),
+ array(
+ 'title' => $page_title,
+ ));
+ }
+}
diff --git a/src/applications/phame/controller/post/edit/__init__.php b/src/applications/phame/controller/post/edit/__init__.php
new file mode 100644
index 0000000000..14b6c8e65e
--- /dev/null
+++ b/src/applications/phame/controller/post/edit/__init__.php
@@ -0,0 +1,31 @@
+bloggerName = $blogger_name;
+ return $this;
+ }
+ private function getBloggerName() {
+ return $this->bloggerName;
+ }
+
+ protected function getSideNavExtraPostFilters() {
+ if ($this->isDraft() || !$this->getBloggerName()) {
+ return array();
+ }
+
+ return
+ array(array('key' => $this->getSideNavFilter(),
+ 'name' => 'Posts by '.$this->getBloggerName()));
+ }
+
+ protected function getSideNavFilter() {
+ if ($this->getBloggerName()) {
+ $filter = 'posts/'.$this->getBloggerName();
+ } else if ($this->isDraft()) {
+ $filter = 'draft';
+ } else {
+ $filter = 'posts';
+ }
+ return $filter;
+ }
+
+ private function isDraft() {
+ return (bool) $this->isDraft;
+ }
+ protected function setIsDraft($is_draft) {
+ $this->isDraft = $is_draft;
+ return $this;
+ }
+
+ public function willProcessRequest(array $data) {
+ $this->setBloggerName(idx($data, 'bloggername'));
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $user = $request->getUser();
+ $pager = new AphrontPagerView();
+ $page_size = 50;
+ $pager->setURI($request->getRequestURI(), 'offset');
+ $pager->setPageSize($page_size);
+ $pager->setOffset($request->getInt('offset'));
+
+ if ($this->getBloggerName()) {
+ $blogger = id(new PhabricatorUser())->loadOneWhere(
+ 'username = %s',
+ $this->getBloggerName());
+ if (!$blogger) {
+ return new Aphront404Response();
+ }
+ $page_title = 'Posts by '.$this->getBloggerName();
+ if ($blogger->getPHID() == $user->getPHID()) {
+ $actions = array('view', 'edit');
+ } else {
+ $actions = array('view');
+ }
+ $this->setShowSideNav(false);
+ } else {
+ $blogger = $user;
+ $page_title = 'Posts by '.$user->getUserName();
+ $actions = array('view', 'edit');
+ $this->setShowSideNav(true);
+ }
+ $phid = $blogger->getPHID();
+ // user gets to see their own unpublished stuff
+ if ($phid == $user->getPHID() && $this->isDraft()) {
+ $post_visibility = PhamePost::VISIBILITY_DRAFT;
+ } else {
+ $post_visibility = PhamePost::VISIBILITY_PUBLISHED;
+ }
+ $query = new PhamePostQuery();
+ $query->withBloggerPHID($phid);
+ $query->withVisibility($post_visibility);
+ $posts = $query->executeWithPager($pager);
+ $bloggers = array($blogger->getPHID() => $blogger);
+
+ $panel = id(new PhamePostListView())
+ ->setUser($user)
+ ->setBloggers($bloggers)
+ ->setPosts($posts)
+ ->setActions($actions)
+ ->setDraftList($this->isDraft());
+
+ return $this->buildStandardPageResponse(
+ array(
+ $panel,
+ $pager
+ ),
+ array(
+ 'title' => $page_title,
+ ));
+ }
+}
diff --git a/src/applications/phame/controller/post/list/base/__init__.php b/src/applications/phame/controller/post/list/base/__init__.php
new file mode 100644
index 0000000000..f9d36aaec9
--- /dev/null
+++ b/src/applications/phame/controller/post/list/base/__init__.php
@@ -0,0 +1,20 @@
+setIsDraft(true);
+ return parent::processRequest();
+ }
+}
diff --git a/src/applications/phame/controller/post/list/drafts/__init__.php b/src/applications/phame/controller/post/list/drafts/__init__.php
new file mode 100644
index 0000000000..4d94f43d21
--- /dev/null
+++ b/src/applications/phame/controller/post/list/drafts/__init__.php
@@ -0,0 +1,12 @@
+setIsDraft(false);
+ return parent::processRequest();
+ }
+}
diff --git a/src/applications/phame/controller/post/list/posts/__init__.php b/src/applications/phame/controller/post/list/posts/__init__.php
new file mode 100644
index 0000000000..53542d16ab
--- /dev/null
+++ b/src/applications/phame/controller/post/list/posts/__init__.php
@@ -0,0 +1,12 @@
+getRequest();
+ $user = $request->getUser();
+ $body = $request->getStr('body');
+ $title = $request->getStr('title');
+ $phame_title = $request->getStr('phame_title');
+
+ $post = id(new PhamePost())
+ ->setBody($body)
+ ->setTitle($title)
+ ->setPhameTitle($phame_title)
+ ->setDateModified(time());
+
+ $post_html = id(new PhamePostDetailView())
+ ->setUser($user)
+ ->setBlogger($user)
+ ->setPost($post)
+ ->setIsPreview(true)
+ ->render();
+
+ return id(new AphrontAjaxResponse())->setContent($post_html);
+ }
+}
diff --git a/src/applications/phame/controller/post/preview/__init__.php b/src/applications/phame/controller/post/preview/__init__.php
new file mode 100644
index 0000000000..f484d2775f
--- /dev/null
+++ b/src/applications/phame/controller/post/preview/__init__.php
@@ -0,0 +1,17 @@
+postPHID = $post_phid;
+ return $this;
+ }
+ private function getPostPHID() {
+ return $this->postPHID;
+ }
+ private function setPhameTitle($phame_title) {
+ $this->phameTitle = $phame_title;
+ return $this;
+ }
+ private function getPhameTitle() {
+ return $this->phameTitle;
+ }
+ private function setBloggerName($blogger_name) {
+ $this->bloggerName = $blogger_name;
+ return $this;
+ }
+ private function getBloggerName() {
+ return $this->bloggerName;
+ }
+
+ protected function getSideNavFilter() {
+ $filter = 'post/view/'.$this->getPostPHID();
+ return $filter;
+ }
+ protected function getSideNavExtraPostFilters() {
+ $filters = array(
+ array('key' => $this->getSideNavFilter(),
+ 'name' => $this->getPhameTitle())
+ );
+ return $filters;
+ }
+
+ public function willProcessRequest(array $data) {
+ $this->setPostPHID(idx($data, 'phid'));
+ $this->setPhameTitle(idx($data, 'phametitle'));
+ $this->setBloggerName(idx($data, 'bloggername'));
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $user = $request->getUser();
+ $post_phid = null;
+
+ if ($this->getPostPHID()) {
+ $post_phid = $this->getPostPHID();
+ if (!$post_phid) {
+ return new Aphront404Response();
+ }
+
+ $post = id(new PhamePost())->loadOneWhere(
+ 'phid = %s',
+ $post_phid);
+
+ if ($post) {
+ $this->setPhameTitle($post->getPhameTitle());
+ }
+
+ $blogger = id(new PhabricatorUser())->loadOneWhere(
+ 'phid = %s', $post->getBloggerPHID());
+ if (!$blogger) {
+ return new Aphront404Response();
+ }
+
+ } else if ($this->getBloggerName() && $this->getPhameTitle()) {
+ $phame_title = $this->getPhameTitle();
+ $phame_title = PhabricatorSlug::normalize($phame_title);
+ if ($phame_title != $this->getPhameTitle()) {
+ $uri = $post->getViewURI($this->getBloggerName());
+ return id(new AphrontRedirectResponse())->setURI($uri);
+ }
+ $blogger = id(new PhabricatorUser())->loadOneWhere(
+ 'username = %s',
+ $this->getBloggerName());
+ if (!$blogger) {
+ return new Aphront404Response();
+ }
+ $post = id(new PhamePost())->loadOneWhere(
+ 'bloggerPHID = %s AND phameTitle = %s',
+ $blogger->getPHID(),
+ $this->getPhameTitle());
+ }
+
+ if (!$post) {
+ return new Aphront404Response();
+ }
+
+ if ($post->isDraft() &&
+ $post->getBloggerPHID() != $user->getPHID()) {
+ return new Aphront404Response();
+ }
+
+ if ($post->isDraft()) {
+ $notice = id(new AphrontErrorView())
+ ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
+ ->setTitle('You are previewing a draft.')
+ ->setErrors(array(
+ 'Only you can see this draft until you publish it.',
+ 'If you chose a comment widget it will show up when you publish.',
+ ));
+ } else {
+ $notice = null;
+ }
+
+ $page_title = $this->getPhameTitle();
+ $page = id(new PhamePostDetailView())
+ ->setUser($user)
+ ->setRequestURI($request->getRequestURI())
+ ->setBlogger($blogger)
+ ->setPost($post);
+
+ $this->setShowSideNav(false);
+ return $this->buildStandardPageResponse(
+ array(
+ $notice,
+ $page,
+ ),
+ array(
+ 'title' => $page_title,
+ ));
+ }
+}
diff --git a/src/applications/phame/controller/post/view/__init__.php b/src/applications/phame/controller/post/view/__init__.php
new file mode 100644
index 0000000000..ad535a74cd
--- /dev/null
+++ b/src/applications/phame/controller/post/view/__init__.php
@@ -0,0 +1,21 @@
+bloggerPHID = $blogger_phid;
+ return $this;
+ }
+ public function withVisibility($visibility) {
+ $this->visibility = $visibility;
+ return $this;
+ }
+
+ public function execute() {
+ $table = new PhamePost();
+ $conn_r = $table->establishConnection('r');
+
+ $where_clause = $this->buildWhereClause($conn_r);
+ $order_clause = $this->buildOrderClause($conn_r);
+ $limit_clause = $this->buildLimitClause($conn_r);
+
+ $data = queryfx_all(
+ $conn_r,
+ 'SELECT * FROM %T e %Q %Q %Q',
+ $table->getTableName(),
+ $where_clause,
+ $order_clause,
+ $limit_clause);
+
+ $posts = $table->loadAllFromArray($data);
+
+ return $posts;
+ }
+
+ private function buildWhereClause($conn_r) {
+ $where = array();
+
+ if ($this->bloggerPHID) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'bloggerPHID = %s',
+ $this->bloggerPHID
+ );
+ }
+
+ if ($this->visibility !== null) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'visibility = %d',
+ $this->visibility
+ );
+ }
+
+ return $this->formatWhereClause($where);
+ }
+
+ private function buildOrderClause($conn_r) {
+ return 'ORDER BY datePublished DESC, id DESC';
+ }
+}
diff --git a/src/applications/phame/query/post/__init__.php b/src/applications/phame/query/post/__init__.php
new file mode 100644
index 0000000000..a1dac94c84
--- /dev/null
+++ b/src/applications/phame/query/post/__init__.php
@@ -0,0 +1,15 @@
+getPhameTitle());
+ $uri = phutil_escape_uri('/phame/posts/'.$blogger_name.'/'.$phame_title);
+ } else {
+ $uri = $this->getActionURI('view');
+ }
+ return $uri;
+ }
+ public function getEditURI() {
+ return $this->getActionURI('edit');
+ }
+ public function getDeleteURI() {
+ return $this->getActionURI('delete');
+ }
+ private function getActionURI($action) {
+ return '/phame/post/'.$action.'/'.$this->getPHID().'/';
+ }
+
+ public function isDraft() {
+ return $this->getVisibility() == self::VISIBILITY_DRAFT;
+ }
+
+ public function getCommentsWidget() {
+ $config_data = $this->getConfigData();
+ if (empty($config_data)) {
+ return 'none';
+ }
+ return idx($config_data, 'comments_widget', 'none');
+ }
+ public function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'configData' => self::SERIALIZATION_JSON,
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function generatePHID() {
+ return PhabricatorPHID::generateNewPHID(
+ PhabricatorPHIDConstants::PHID_TYPE_POST);
+ }
+
+ public static function getVisibilityOptionsForSelect() {
+ return array(
+ self::VISIBILITY_DRAFT => 'Draft: visible only to me.',
+ self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.',
+ );
+ }
+
+ public function getCommentsWidgetOptionsForSelect() {
+ $current = $this->getCommentsWidget();
+ $options = array();
+
+ if ($current == 'facebook' ||
+ PhabricatorEnv::getEnvConfig('facebook.application-id')) {
+ $options['facebook'] = 'Facebook';
+ }
+ if ($current == 'disqus' ||
+ PhabricatorEnv::getEnvConfig('disqus.shortname')) {
+ $options['disqus'] = 'Disqus';
+ }
+ $options['none'] = 'None';
+
+ return $options;
+ }
+
+}
diff --git a/src/applications/phame/storage/post/__init__.php b/src/applications/phame/storage/post/__init__.php
new file mode 100644
index 0000000000..1806d9c9b4
--- /dev/null
+++ b/src/applications/phame/storage/post/__init__.php
@@ -0,0 +1,19 @@
+isPreview = $is_preview;
+ return $this;
+ }
+ private function isPreview() {
+ return $this->isPreview;
+ }
+
+ public function setUser(PhabricatorUser $user) {
+ $this->user = $user;
+ return $this;
+ }
+ public function getUser() {
+ return $this->user;
+ }
+
+ public function setRequestURI(PhutilURI $uri) {
+ $uri = PhabricatorEnv::getProductionURI($uri->setQueryParams(array()));
+ $this->requestURI = $uri;
+ return $this;
+ }
+ private function getRequestURI() {
+ return $this->requestURI;
+ }
+
+ public function setBlogger(PhabricatorUser $blogger) {
+ $this->blogger = $blogger;
+ return $this;
+ }
+ private function getBlogger() {
+ return $this->blogger;
+ }
+
+ public function setPost(PhamePost $post) {
+ $this->post = $post;
+ return $this;
+ }
+ private function getPost() {
+ return $this->post;
+ }
+
+ public function render() {
+ require_celerity_resource('phabricator-remarkup-css');
+
+ $user = $this->getUser();
+ $blogger = $this->getBlogger();
+ $post = $this->getPost();
+ $engine = PhabricatorMarkupEngine::newPhameMarkupEngine();
+ $body = $engine->markupText($post->getBody());
+ if ($post->isDraft()) {
+ $uri = '/phame/draft/';
+ $label = 'Back to Your Drafts';
+ } else {
+ $uri = '/phame/posts/'.$blogger->getUsername();
+ $label = 'More Posts by '.phutil_escape_html($blogger->getUsername());
+ }
+ $button = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $uri,
+ 'class' => 'grey button',
+ ),
+ $label
+ );
+
+ $publish_date = $post->getDatePublished();
+ if ($publish_date) {
+ $caption = 'Published '.
+ phabricator_datetime($publish_date,
+ $user);
+ } else {
+ $caption = 'Last edited '.
+ phabricator_datetime($post->getDateModified(),
+ $user);
+ }
+ if ($this->isPreview()) {
+ $width = AphrontPanelView::WIDTH_FULL;
+ } else {
+ $width = AphrontPanelView::WIDTH_WIDE;
+ }
+ $panel = id(new AphrontPanelView())
+ ->setHeader(phutil_escape_html($post->getTitle()))
+ ->appendChild('')
+ ->setWidth($width)
+ ->addButton($button)
+ ->setCaption($caption);
+ if ($user->getPHID() == $post->getBloggerPHID()) {
+ if ($post->isDraft()) {
+ $label = 'Edit Draft';
+ } else {
+ $label = 'Edit Post';
+ }
+ $button = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $post->getEditURI(),
+ 'class' => 'grey button',
+ ),
+ $label);
+ $panel->addButton($button);
+ }
+ switch ($post->getCommentsWidget()) {
+ case 'facebook':
+ $comments = $this->renderFacebookComments();
+ break;
+ case 'disqus':
+ $comments = $this->renderDisqusComments();
+ break;
+ case 'none':
+ default:
+ $comments = null;
+ break;
+ }
+ $panel->appendChild($comments);
+
+ return $panel->render();
+ }
+
+ private function renderFacebookComments() {
+ $fb_id = PhabricatorEnv::getEnvConfig('facebook.application-id');
+ if (!$fb_id) {
+ return null;
+ }
+
+ $fb_root = phutil_render_tag('div',
+ array(
+ 'id' => 'fb-root',
+ )
+ );
+
+ $c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id;
+ $fb_js = jsprintf(
+ '',
+ $c_uri
+ );
+
+ $fb_comments = phutil_render_tag('div',
+ array(
+ 'class' => 'fb-comments',
+ 'data-href' => $this->getRequestURI(),
+ 'data-num-posts' => 5,
+ 'data-width' => 1080,
+ 'data-colorscheme' => 'dark',
+ )
+ );
+
+ return '
' . $fb_root . $fb_js . $fb_comments;
+ }
+
+ private function renderDisqusComments() {
+ $disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname');
+ if (!$disqus_shortname) {
+ return null;
+ }
+
+ $post = $this->getPost();
+
+ $disqus_thread = phutil_render_tag('div',
+ array(
+ 'id' => 'disqus_thread'
+ )
+ );
+
+ // protip - try some var disqus_developer = 1; action to test locally
+ $disqus_js = jsprintf(
+ '',
+ $post->getPHID(),
+ $this->getRequestURI(),
+ $post->getTitle()
+ );
+
+ return '
' . $disqus_thread . $disqus_js;
+ }
+
+}
diff --git a/src/applications/phame/view/postdetail/__init__.php b/src/applications/phame/view/postdetail/__init__.php
new file mode 100644
index 0000000000..cacc31e370
--- /dev/null
+++ b/src/applications/phame/view/postdetail/__init__.php
@@ -0,0 +1,21 @@
+draftList = $draft_list;
+ return $this;
+ }
+ public function isDraftList() {
+ return (bool) $this->draftList;
+ }
+ private function getPostNoun() {
+ if ($this->isDraftList()) {
+ $noun = 'Draft';
+ } else {
+ $noun = 'Post';
+ }
+ return $noun;
+ }
+
+ public function setUser(PhabricatorUser $user) {
+ $this->user = $user;
+ return $this;
+ }
+ private function getUser() {
+ return $this->user;
+ }
+ public function setPosts(array $posts) {
+ assert_instances_of($posts, 'PhamePost');
+ $this->posts = $posts;
+ return $this;
+ }
+ private function getPosts() {
+ return $this->posts;
+ }
+ public function setBloggers(array $bloggers) {
+ assert_instances_of($bloggers, 'PhabricatorUser');
+ $this->bloggers = $bloggers;
+ return $this;
+ }
+ private function getBloggers() {
+ return $this->bloggers;
+ }
+ public function setActions(array $actions) {
+ $this->actions = $actions;
+ return $this;
+ }
+ private function getActions() {
+ if ($this->actions) {
+ return $this->actions;
+ }
+ return array();
+ }
+
+ public function render() {
+ $user = $this->getUser();
+ $posts = $this->getPosts();
+ $bloggers = $this->getBloggers();
+ $noun = $this->getPostNoun();
+
+ if (empty($posts)) {
+ $panel = id(new AphrontPanelView())
+ ->setHeader(sprintf('No %ss... Yet!', $noun))
+ ->setCaption('Will you answer the call to phame?')
+ ->setCreateButton(sprintf('New %s', $noun),
+ sprintf('/phame/%s/new', strtolower($noun)));
+ return $panel->render();
+ }
+ require_celerity_resource('phabricator-remarkup-css');
+
+ $engine = PhabricatorMarkupEngine::newPhameMarkupEngine();
+ $html = array();
+ $actions = $this->getActions();
+ foreach ($posts as $post) {
+ $blogger_phid = $post->getBloggerPHID();
+ $blogger = $bloggers[$blogger_phid];
+ $updated = phabricator_datetime($post->getDateModified(),
+ $user);
+ $body = $engine->markupText($post->getBody());
+ $panel = id(new AphrontPanelView())
+ ->setHeader(phutil_escape_html($post->getTitle()))
+ ->setCaption('Last updated '.$updated)
+ ->appendChild('');
+ foreach ($actions as $action) {
+ switch ($action) {
+ case 'view':
+ $uri = $post->getViewURI($blogger->getUsername());
+ $label = 'View '.$noun;
+ break;
+ case 'edit':
+ $uri = $post->getEditURI();
+ $label = 'Edit '.$noun;
+ break;
+ default:
+ break;
+ }
+ $button = phutil_render_tag(
+ 'a',
+ array(
+ 'href' => $uri,
+ 'class' => 'grey button',
+ ),
+ $label);
+ $panel->addButton($button);
+ }
+
+ $html[] = $panel->render();
+ }
+
+ return implode('', $html);
+ }
+}
diff --git a/src/applications/phame/view/postlist/__init__.php b/src/applications/phame/view/postlist/__init__.php
new file mode 100644
index 0000000000..f3ebb90c6b
--- /dev/null
+++ b/src/applications/phame/view/postlist/__init__.php
@@ -0,0 +1,19 @@
+