diff --git a/src/applications/phame/controller/post/PhamePostArchiveController.php b/src/applications/phame/controller/post/PhamePostArchiveController.php index b8647121ef..093e7019bf 100644 --- a/src/applications/phame/controller/post/PhamePostArchiveController.php +++ b/src/applications/phame/controller/post/PhamePostArchiveController.php @@ -1,56 +1,57 @@ getViewer(); $id = $request->getURIData('id'); $post = id(new PhamePostQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $cancel_uri = $post->getViewURI(); if ($request->isFormPost()) { $xactions = array(); $new_value = PhameConstants::VISIBILITY_ARCHIVED; $xactions[] = id(new PhamePostTransaction()) ->setTransactionType(PhamePostVisibilityTransaction::TRANSACTIONTYPE) ->setNewValue($new_value); id(new PhamePostEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($post, $xactions); return id(new AphrontRedirectResponse()) ->setURI($cancel_uri); } $title = pht('Archive Post'); $body = pht( - 'This post will revert to archived status and no longer be visible '. - 'to other users or members of this blog.'); + 'If you archive this post, it will only be visible to users who can '. + 'edit %s.', + $viewer->renderHandle($post->getBlogPHID())); $button = pht('Archive Post'); return $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addSubmitButton($button) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index a73876a197..63adedb7ae 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -1,349 +1,353 @@ setupLiveEnvironment(); if ($response) { return $response; } $viewer = $request->getViewer(); $moved = $request->getStr('moved'); $post = $this->getPost(); $blog = $this->getBlog(); $is_live = $this->getIsLive(); $is_external = $this->getIsExternal(); $header = id(new PHUIHeaderView()) ->addClass('phame-header-bar') ->setUser($viewer); $hero = $this->buildPhamePostHeader($post); if (!$is_external) { $actions = $this->renderActions($post); $header->setPolicyObject($post); $header->setActionList($actions); } $document = id(new PHUIDocumentViewPro()) ->setHeader($header); if ($moved) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('Post moved successfully.'))); } if ($post->isDraft()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Draft Post')) ->appendChild( - pht('Only you can see this draft until you publish it. '. - 'Use "Publish" to publish this post.'))); + pht( + 'This is a draft, and is only visible to you and other users '. + 'who can edit %s. Use "Publish" to publish this post.', + $viewer->renderHandle($post->getBlogPHID())))); } if ($post->isArchived()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_ERROR) ->setTitle(pht('Archived Post')) ->appendChild( - pht('Only you can see this archived post until you publish it. '. - 'Use "Publish" to publish this post.'))); + pht( + 'This post has been archived, and is only visible to you and '. + 'other users who can edit %s.', + $viewer->renderHandle($post->getBlogPHID())))); } if (!$post->getBlog()) { $document->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle(pht('Not On A Blog')) ->appendChild( pht('This post is not associated with a blog (the blog may have '. 'been deleted). Use "Move Post" to move it to a new blog.'))); } $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($post, PhamePost::MARKUP_FIELD_BODY) ->process(); $document->appendChild( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))); $blogger = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($post->getBloggerPHID())) ->needProfileImage(true) ->executeOne(); $blogger_profile = $blogger->loadUserProfile(); $author_uri = '/p/'.$blogger->getUsername().'/'; $author_uri = PhabricatorEnv::getURI($author_uri); $author = phutil_tag( 'a', array( 'href' => $author_uri, ), $blogger->getUsername()); $date = phabricator_datetime($post->getDatePublished(), $viewer); if ($post->isDraft()) { $subtitle = pht('Unpublished draft by %s.', $author); } else if ($post->isArchived()) { $subtitle = pht('Archived post by %s.', $author); } else { $subtitle = pht('Written by %s on %s.', $author, $date); } $user_icon = $blogger_profile->getIcon(); $user_icon = PhabricatorPeopleIconSet::getIconIcon($user_icon); $user_icon = id(new PHUIIconView())->setIcon($user_icon); $about = id(new PhameDescriptionView()) ->setTitle($subtitle) ->setDescription( array( $user_icon, ' ', $blogger_profile->getDisplayTitle(), )) ->setImage($blogger->getProfileImageURI()) ->setImageHref($author_uri); $monogram = $post->getMonogram(); $timeline = $this->buildTransactionTimeline( $post, id(new PhamePostTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $timeline->setQuoteRef($monogram); if ($is_external) { $add_comment = null; } else { $add_comment = $this->buildCommentForm($post, $timeline); $add_comment = phutil_tag_div('mlb mlt phame-comment-view', $add_comment); } $timeline = phutil_tag_div('phui-document-view-pro-box', $timeline); list($prev, $next) = $this->loadAdjacentPosts($post); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($post); $is_live = $this->getIsLive(); $is_external = $this->getIsExternal(); $next_view = new PhameNextPostView(); if ($next) { $next_view->setNext($next->getTitle(), $next->getBestURI($is_live, $is_external)); } if ($prev) { $next_view->setPrevious($prev->getTitle(), $prev->getBestURI($is_live, $is_external)); } $document->setFoot($next_view); $crumbs = $this->buildApplicationCrumbs(); $properties = phutil_tag_div('phui-document-view-pro-box', $properties); $page = $this->newPage() ->setTitle($post->getTitle()) ->setPageObjectPHIDs(array($post->getPHID())) ->setCrumbs($crumbs) ->appendChild( array( $hero, $document, $about, $properties, $timeline, $add_comment, )); if ($is_live) { $page ->setShowChrome(false) ->setShowFooter(false); } return $page; } private function renderActions(PhamePost $post) { $viewer = $this->getViewer(); $actions = id(new PhabricatorActionListView()) ->setObject($post) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $post, PhabricatorPolicyCapability::CAN_EDIT); $id = $post->getID(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('post/edit/'.$id.'/')) ->setName(pht('Edit Post')) ->setDisabled(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-camera-retro') ->setHref($this->getApplicationURI('post/header/'.$id.'/')) ->setName(pht('Edit Header Image')) ->setDisabled(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-arrows') ->setHref($this->getApplicationURI('post/move/'.$id.'/')) ->setName(pht('Move Post')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-history') ->setHref($this->getApplicationURI('post/history/'.$id.'/')) ->setName(pht('View History'))); if ($post->isDraft()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Publish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setHref($this->getApplicationURI('post/archive/'.$id.'/')) ->setName(pht('Archive')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else if ($post->isArchived()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Publish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-eye-slash') ->setHref($this->getApplicationURI('post/unpublish/'.$id.'/')) ->setName(pht('Unpublish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setHref($this->getApplicationURI('post/archive/'.$id.'/')) ->setName(pht('Archive')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } if ($post->isDraft()) { $live_name = pht('Preview'); } else { $live_name = pht('View Live'); } if (!$post->isArchived()) { $actions->addAction( id(new PhabricatorActionView()) ->setUser($viewer) ->setIcon('fa-globe') ->setHref($post->getLiveURI()) ->setName($live_name)); } return $actions; } private function buildCommentForm(PhamePost $post, $timeline) { $viewer = $this->getViewer(); $box = id(new PhamePostEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($post) ->setTransactionTimeline($timeline); return phutil_tag_div('phui-document-view-pro-box', $box); } private function loadAdjacentPosts(PhamePost $post) { $viewer = $this->getViewer(); $query = id(new PhamePostQuery()) ->setViewer($viewer) ->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)) ->withBlogPHIDs(array($post->getBlog()->getPHID())) ->setLimit(1); $prev = id(clone $query) ->setAfterID($post->getID()) ->execute(); $next = id(clone $query) ->setBeforeID($post->getID()) ->execute(); return array(head($prev), head($next)); } private function buildPhamePostHeader( PhamePost $post) { $image = null; if ($post->getHeaderImagePHID()) { $image = phutil_tag( 'div', array( 'class' => 'phame-header-hero', ), phutil_tag( 'img', array( 'src' => $post->getHeaderImageURI(), 'class' => 'phame-header-image', ))); } $title = phutil_tag_div('phame-header-title', $post->getTitle()); $subtitle = null; if ($post->getSubtitle()) { $subtitle = phutil_tag_div('phame-header-subtitle', $post->getSubtitle()); } return phutil_tag_div( 'phame-mega-header', array($image, $title, $subtitle)); } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index f87a37e7a4..a9525e0be7 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,389 +1,390 @@ setBloggerPHID($blogger->getPHID()) ->setBlogPHID($blog->getPHID()) ->attachBlog($blog) ->setDatePublished(PhabricatorTime::getNow()) ->setVisibility(PhameConstants::VISIBILITY_PUBLISHED); return $post; } public function attachBlog(PhameBlog $blog) { $this->blog = $blog; return $this; } public function getBlog() { return $this->assertAttached($this->blog); } public function getMonogram() { return 'J'.$this->getID(); } public function getLiveURI() { $blog = $this->getBlog(); $is_draft = $this->isDraft(); $is_archived = $this->isArchived(); if (strlen($blog->getDomain()) && !$is_draft && !$is_archived) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } public function getExternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $path = "/post/{$id}/{$slug}/"; $domain = $this->getBlog()->getDomain(); return (string)id(new PhutilURI('http://'.$domain)) ->setPath($path); } public function getInternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $blog_id = $this->getBlog()->getID(); return "/phame/live/{$blog_id}/post/{$id}/{$slug}/"; } public function getViewURI() { $id = $this->getID(); $slug = $this->getSlug(); return "/phame/post/view/{$id}/{$slug}/"; } public function getBestURI($is_live, $is_external) { if ($is_live) { if ($is_external) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } else { return $this->getViewURI(); } } public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } public function isDraft() { return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT); } public function isArchived() { return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'subtitle' => 'text64', 'phameTitle' => 'sort64?', 'visibility' => 'uint32', 'mailKey' => 'bytes20', 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These seem like they should always be non-null? 'blogPHID' => 'phid?', 'body' => 'text?', 'configData' => 'text?', // T6203/NULLABILITY // This one probably should be nullable? 'datePublished' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'bloggerPosts' => array( 'columns' => array( 'bloggerPHID', 'visibility', 'datePublished', 'id', ), ), ), ) + parent::getConfiguration(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePostPHIDType::TYPECONST); } public function getSlug() { return PhabricatorSlug::normalizeProjectSlug($this->getTitle()); } public function getHeaderImageURI() { return $this->getHeaderImageFile()->getBestURI(); } public function attachHeaderImageFile(PhabricatorFile $file) { $this->headerImageFile = $file; return $this; } public function getHeaderImageFile() { return $this->assertAttached($this->headerImageFile); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - // Draft posts are visible only to the author. Published posts are visible - // to whoever the blog is visible to. + // Draft and archived posts are visible only to the author and other + // users who can edit the blog. Published posts are visible to whoever + // the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } public function describeAutomaticCapability($capability) { return pht('The author of a blog post can always view and edit it.'); } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $content = $this->getMarkupText($field); return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhamePostEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhamePostTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->bloggerPHID == $phid); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('Title of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('slug') ->setType('string') ->setDescription(pht('Slug for the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('blogPHID') ->setType('phid') ->setDescription(pht('PHID of the blog that the post belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('PHID of the author of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('body') ->setType('string') ->setDescription(pht('Body of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('datePublished') ->setType('epoch?') ->setDescription(pht('Publish date, if the post has been published.')), ); } public function getFieldValuesForConduit() { if ($this->isDraft()) { $date_published = null; } else if ($this->isArchived()) { $date_published = null; } else { $date_published = (int)$this->getDatePublished(); } return array( 'title' => $this->getTitle(), 'slug' => $this->getSlug(), 'blogPHID' => $this->getBlogPHID(), 'authorPHID' => $this->getBloggerPHID(), 'body' => $this->getBody(), 'datePublished' => $date_published, ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhamePostFulltextEngine(); } }