@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make posts 1:1 with blogs and implement policy controls

Summary:
This leaves the UI in a pretty rough state, but implements blog policy controls and queries, and 1:1 relationships between posts and blogs. Needs a bunch more cleanup but seemed like an okayish breaking point in terms of cohesiveness.

Posts have these rules:

- Drafts are visible only to the author.
- Published posts are visible to anyone who can see the blog they appear on.
- Posts are only editable by the author.

...so we don't need any special policy UI or state to accommodate these rules.

Posts may have no blog if they're grandfathered in or you write a post to a blog and then lose the ability to see the blog. This is the messiest edge case -- specifically:

- You write a post to blog A.
- You publish the post.
- I edit the "Visible To:" for blog A and set it to exclude you.

What we do in this case is let you see the post in "My Posts", but you can no longer see the blog and you'll see the post as not being part of a blog. We can maybe give you some UI to let you move it later or something.

Test Plan: Hit all (I think?) of the interfaces without issues. Definitely some UI problems still right now.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1373

Differential Revision: https://secure.phabricator.com/D3694

+178 -194
+2
resources/sql/patches/phameoneblog.sql
··· 1 + ALTER TABLE `{$NAMESPACE}_phame`.`phame_post` 2 + ADD `blogPHID` varchar(64) COLLATE utf8_bin;
+10 -85
src/applications/phame/controller/blog/PhameBlogViewController.php
··· 19 19 /** 20 20 * @group phame 21 21 */ 22 - final class PhameBlogViewController 23 - extends PhameController { 22 + final class PhameBlogViewController extends PhameController { 24 23 25 24 private $blogPHID; 26 - private $bloggerPHIDs; 27 - private $postPHIDs; 28 - 29 - private function setPostPHIDs($post_phids) { 30 - $this->postPHIDs = $post_phids; 31 - return $this; 32 - } 33 - private function getPostPHIDs() { 34 - return $this->postPHIDs; 35 - } 36 - 37 - private function setBloggerPHIDs($blogger_phids) { 38 - $this->bloggerPHIDs = $blogger_phids; 39 - return $this; 40 - } 41 - private function getBloggerPHIDs() { 42 - return $this->bloggerPHIDs; 43 - } 44 25 45 26 private function setBlogPHID($blog_phid) { 46 27 $this->blogPHID = $blog_phid; ··· 54 35 $filter = 'blog/view/'.$this->getBlogPHID(); 55 36 return $filter; 56 37 } 38 + 57 39 protected function getSideNavExtraBlogFilters() { 58 40 $filters = array( 59 41 array('key' => $this->getSideNavFilter(), ··· 67 49 } 68 50 69 51 public function processRequest() { 70 - $request = $this->getRequest(); 71 - $user = $request->getUser(); 72 - $blog_phid = $this->getBlogPHID(); 52 + $request = $this->getRequest(); 53 + $user = $request->getUser(); 73 54 74 55 $blog = id(new PhameBlogQuery()) 75 56 ->setViewer($user) 76 - ->withPHIDs(array($blog_phid)) 57 + ->withPHIDs(array($this->getBlogPHID())) 77 58 ->executeOne(); 78 59 if (!$blog) { 79 60 return new Aphront404Response(); 80 61 } 81 62 82 - $this->loadEdges(); 83 - 84 - $blogger_phids = $this->getBloggerPHIDs(); 85 - if ($blogger_phids) { 86 - $bloggers = $this->loadViewerHandles($blogger_phids); 87 - } else { 88 - $bloggers = array(); 89 - } 90 - 91 - $post_phids = $this->getPostPHIDs(); 92 - if ($post_phids) { 93 - $posts = id(new PhamePostQuery()) 94 - ->withPHIDs($post_phids) 95 - ->withVisibility(PhamePost::VISIBILITY_PUBLISHED) 96 - ->execute(); 97 - } else { 98 - $posts = array(); 99 - } 100 - 101 - $notice = array(); 102 - if ($request->getExists('new')) { 103 - $notice = 104 - array( 105 - 'title' => 'Successfully created your blog.', 106 - 'body' => 'Time to write some posts.' 107 - ); 108 - } else if ($request->getExists('edit')) { 109 - $notice = 110 - array( 111 - 'title' => 'Successfully edited your blog.', 112 - 'body' => 'Time to write some posts.' 113 - ); 114 - } 63 + $posts = id(new PhamePostQuery()) 64 + ->setViewer($user) 65 + ->withBlogPHIDs(array($blog->getPHID())) 66 + ->execute(); 115 67 116 68 $skin = $blog->getSkinRenderer(); 117 69 $skin 118 70 ->setUser($this->getRequest()->getUser()) 119 - ->setNotice($notice) 120 - ->setBloggers($bloggers) 71 + ->setBloggers($this->loadViewerHandles(mpull($posts, 'getBloggerPHID'))) 121 72 ->setPosts($posts) 122 73 ->setBlog($blog) 123 74 ->setRequestURI($this->getRequest()->getRequestURI()); ··· 134 85 array( 135 86 'title' => $blog->getName(), 136 87 )); 137 - } 138 - 139 - private function loadEdges() { 140 - 141 - $edge_types = array( 142 - PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER, 143 - PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST, 144 - ); 145 - $blog_phid = $this->getBlogPHID(); 146 - $phids = array($blog_phid); 147 - 148 - $edges = id(new PhabricatorEdgeQuery()) 149 - ->withSourcePHIDs($phids) 150 - ->withEdgeTypes($edge_types) 151 - ->execute(); 152 - 153 - $blogger_phids = array_keys( 154 - $edges[$blog_phid][PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER] 155 - ); 156 - $this->setBloggerPHIDs($blogger_phids); 157 - 158 - $post_phids = array_keys( 159 - $edges[$blog_phid][PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST] 160 - ); 161 - $this->setPostPHIDs($post_phids); 162 - 163 88 } 164 89 }
+13 -12
src/applications/phame/controller/post/PhamePostDeleteController.php
··· 40 40 public function processRequest() { 41 41 $request = $this->getRequest(); 42 42 $user = $request->getUser(); 43 - $post_phid = $this->getPostPHID(); 44 - $posts = id(new PhamePostQuery()) 45 - ->withPHIDs(array($post_phid)) 46 - ->execute(); 47 - $post = reset($posts); 48 - if (empty($post)) { 43 + 44 + $post = id(new PhamePostQuery()) 45 + ->setViewer($user) 46 + ->withPHIDs(array($this->getPostPHID())) 47 + ->requireCapabilities( 48 + array( 49 + PhabricatorPolicyCapability::CAN_EDIT, 50 + )) 51 + ->executeOne(); 52 + if (!$post) { 49 53 return new Aphront404Response(); 50 - } 51 - if ($post->getBloggerPHID() != $user->getPHID()) { 52 - return new Aphront403Response(); 53 54 } 54 55 $post_noun = $post->getHumanName(); 55 56 56 57 if ($request->isFormPost()) { 57 58 $edge_type = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; 58 59 $edges = id(new PhabricatorEdgeQuery()) 59 - ->withSourcePHIDs(array($post_phid)) 60 + ->withSourcePHIDs(array($post->getPHID())) 60 61 ->withEdgeTypes(array($edge_type)) 61 62 ->execute(); 62 63 63 - $blog_edges = $edges[$post_phid][$edge_type]; 64 + $blog_edges = $edges[$post->getPHID()][$edge_type]; 64 65 $blog_phids = array_keys($blog_edges); 65 66 $editor = id(new PhabricatorEdgeEditor()) 66 67 ->setActor($user); 67 68 foreach ($blog_phids as $phid) { 68 - $editor->removeEdge($post_phid, $edge_type, $phid); 69 + $editor->removeEdge($post->getPHID(), $edge_type, $phid); 69 70 } 70 71 $editor->save(); 71 72
+9 -7
src/applications/phame/controller/post/PhamePostEditController.php
··· 115 115 $errors = array(); 116 116 117 117 if ($this->isPostEdit()) { 118 - $posts = id(new PhamePostQuery()) 118 + $post = id(new PhamePostQuery()) 119 + ->setViewer($user) 119 120 ->withPHIDs(array($this->getPostPHID())) 120 - ->execute(); 121 - $post = reset($posts); 122 - if (empty($post)) { 121 + ->requireCapabilities( 122 + array( 123 + PhabricatorPolicyCapability::CAN_EDIT, 124 + )) 125 + ->executeOne(); 126 + if (!$post) { 123 127 return new Aphront404Response(); 124 128 } 125 - if ($post->getBloggerPHID() != $user->getPHID()) { 126 - return new Aphront403Response(); 127 - } 129 + 128 130 $post_noun = ucfirst($post->getHumanName()); 129 131 $cancel_uri = $post->getViewURI($user->getUsername()); 130 132 $submit_button = 'Save Changes';
+14 -22
src/applications/phame/controller/post/PhamePostViewController.php
··· 19 19 /** 20 20 * @group phame 21 21 */ 22 - final class PhamePostViewController 23 - extends PhameController { 22 + final class PhamePostViewController extends PhameController { 24 23 25 24 private $postPHID; 26 25 private $phameTitle; ··· 75 74 public function processRequest() { 76 75 $request = $this->getRequest(); 77 76 $user = $request->getUser(); 78 - $post_phid = null; 79 77 80 78 if ($this->getPostPHID()) { 81 - $post_phid = $this->getPostPHID(); 82 - if (!$post_phid) { 79 + $post = id(new PhamePostQuery()) 80 + ->setViewer($user) 81 + ->withPHIDs(array($this->getPostPHID())) 82 + ->executeOne(); 83 + 84 + if (!$post) { 83 85 return new Aphront404Response(); 84 86 } 85 87 86 - $posts = id(new PhamePostQuery()) 87 - ->withPHIDs(array($post_phid)) 88 - ->execute(); 89 - $post = reset($posts); 90 - 91 - if ($post) { 92 - $this->setPhameTitle($post->getPhameTitle()); 93 - $blogger = PhabricatorObjectHandleData::loadOneHandle( 94 - $post->getBloggerPHID(), 95 - $user); 96 - if (!$blogger) { 97 - return new Aphront404Response(); 98 - } 99 - } 100 - 88 + $this->setPhameTitle($post->getPhameTitle()); 89 + $blogger = PhabricatorObjectHandleData::loadOneHandle( 90 + $post->getBloggerPHID(), 91 + $user); 101 92 } else if ($this->getBloggerName() && $this->getPhameTitle()) { 102 93 $phame_title = $this->getPhameTitle(); 103 94 $phame_title = PhabricatorSlug::normalize($phame_title); ··· 114 105 return new Aphront404Response(); 115 106 } 116 107 $posts = id(new PhamePostQuery()) 117 - ->withBloggerPHID($blogger->getPHID()) 118 - ->withPhameTitle($phame_title) 108 + ->setViewer($user) 109 + ->withBloggerPHIDs(array($blogger->getPHID())) 110 + ->withPhameTitles(array($phame_title)) 119 111 ->execute(); 120 112 $post = reset($posts); 121 113
+2 -2
src/applications/phame/controller/post/list/PhameAllPostListController.php
··· 76 76 public function processRequest() { 77 77 $user = $this->getRequest()->getUser(); 78 78 79 - $query = new PhamePostQuery(); 80 - $query->withVisibility(PhamePost::VISIBILITY_PUBLISHED); 79 + $query = id(new PhamePostQuery()) 80 + ->setViewer($user); 81 81 $this->setPhamePostQuery($query); 82 82 83 83 $this->setActions(array());
+4 -3
src/applications/phame/controller/post/list/PhameBloggerPostListController.php
··· 59 59 } 60 60 $this->setActions($actions); 61 61 62 - $query = new PhamePostQuery(); 63 - $query->withBloggerPHID($blogger_phid); 64 - $query->withVisibility(PhamePost::VISIBILITY_PUBLISHED); 62 + $query = id(new PhamePostQuery()) 63 + ->setViewer($user) 64 + ->withBloggerPHIDs(array($blogger_phid)); 65 + 65 66 $this->setPhamePostQuery($query); 66 67 67 68 $page_title = 'Posts by '.$this->getBloggerName();
+5 -3
src/applications/phame/controller/post/list/PhameDraftListController.php
··· 51 51 $user = $this->getRequest()->getUser(); 52 52 $phid = $user->getPHID(); 53 53 54 - $query = new PhamePostQuery(); 55 - $query->withBloggerPHID($phid); 56 - $query->withVisibility(PhamePost::VISIBILITY_DRAFT); 54 + $query = id(new PhamePostQuery()) 55 + ->setViewer($user) 56 + ->withBloggerPHIDs(array($phid)) 57 + ->withVisibility(PhamePost::VISIBILITY_DRAFT); 58 + 57 59 $this->setPhamePostQuery($query); 58 60 59 61 $actions = array('view', 'edit');
+4 -3
src/applications/phame/controller/post/list/PhameUserPostListController.php
··· 47 47 $user = $this->getRequest()->getUser(); 48 48 $phid = $user->getPHID(); 49 49 50 - $query = new PhamePostQuery(); 51 - $query->withBloggerPHID($phid); 52 - $query->withVisibility(PhamePost::VISIBILITY_PUBLISHED); 50 + $query = id(new PhamePostQuery()) 51 + ->setViewer($user) 52 + ->withBloggerPHIDs(array($phid)); 53 + 53 54 $this->setPhamePostQuery($query); 54 55 55 56 $actions = array('view', 'edit');
+51 -45
src/applications/phame/query/PhamePostQuery.php
··· 19 19 /** 20 20 * @group phame 21 21 */ 22 - final class PhamePostQuery extends PhabricatorOffsetPagedQuery { 22 + final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { 23 23 24 - private $bloggerPHID; 25 - private $withoutBloggerPHID; 26 - private $phameTitle; 24 + private $blogPHIDs; 25 + private $bloggerPHIDs; 26 + private $phameTitles; 27 27 private $visibility; 28 28 private $phids; 29 29 30 - /** 31 - * Mutually exlusive with @{method:withoutBloggerPHID}. 32 - * 33 - * @{method:withBloggerPHID} wins because being positive and inclusive is 34 - * cool. 35 - */ 36 - public function withBloggerPHID($blogger_phid) { 37 - $this->bloggerPHID = $blogger_phid; 30 + public function withPHIDs(array $phids) { 31 + $this->phids = $phids; 38 32 return $this; 39 33 } 40 - public function withoutBloggerPHID($blogger_phid) { 41 - $this->withoutBloggerPHID = $blogger_phid; 34 + 35 + public function withBloggerPHIDs(array $blogger_phids) { 36 + $this->bloggerPHIDs = $blogger_phids; 42 37 return $this; 43 38 } 44 39 45 - public function withPhameTitle($phame_title) { 46 - $this->phameTitle = $phame_title; 40 + public function withBlogPHIDs(array $blog_phids) { 41 + $this->blogPHIDs = $blog_phids; 42 + return $this; 43 + } 44 + 45 + public function withPhameTitles(array $phame_titles) { 46 + $this->phameTitles = $phame_titles; 47 47 return $this; 48 48 } 49 49 ··· 52 52 return $this; 53 53 } 54 54 55 - public function withPHIDs($phids) { 56 - $this->phids = $phids; 57 - return $this; 58 - } 59 - 60 - public function execute() { 55 + protected function loadPage() { 61 56 $table = new PhamePost(); 62 57 $conn_r = $table->establishConnection('r'); 63 58 ··· 75 70 76 71 $posts = $table->loadAllFromArray($data); 77 72 73 + if ($posts) { 74 + // We require these to do visibility checks, so load them unconditionally. 75 + $blog_phids = mpull($posts, 'getBlogPHID'); 76 + $blogs = id(new PhameBlogQuery()) 77 + ->setViewer($this->getViewer()) 78 + ->withPHIDs($blog_phids) 79 + ->execute(); 80 + $blogs = mpull($blogs, 'getPHID'); 81 + foreach ($posts as $post) { 82 + if (isset($blogs[$post->getBlogPHID()])) { 83 + $post->setBlog($blogs[$post->getBlogPHID()]); 84 + } 85 + } 86 + } 87 + 78 88 return $posts; 79 89 } 80 90 81 - private function buildWhereClause($conn_r) { 91 + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { 82 92 $where = array(); 83 93 84 94 if ($this->phids) { 85 95 $where[] = qsprintf( 86 96 $conn_r, 87 - 'phid IN (%Ls)', 88 - $this->phids 89 - ); 97 + 'p.phid IN (%Ls)', 98 + $this->phids); 90 99 } 91 100 92 - if ($this->bloggerPHID) { 101 + if ($this->bloggerPHIDs) { 93 102 $where[] = qsprintf( 94 103 $conn_r, 95 - 'bloggerPHID = %s', 96 - $this->bloggerPHID 97 - ); 98 - } else if ($this->withoutBloggerPHID) { 104 + 'p.bloggerPHID IN (%Ls)', 105 + $this->bloggerPHIDs); 106 + } 107 + 108 + if ($this->phameTitles) { 99 109 $where[] = qsprintf( 100 110 $conn_r, 101 - 'bloggerPHID != %s', 102 - $this->withoutBloggerPHID 103 - ); 111 + 'p.phameTitle IN (%Ls)', 112 + $this->phameTitles); 104 113 } 105 114 106 - if ($this->phameTitle) { 115 + if ($this->visibility !== null) { 107 116 $where[] = qsprintf( 108 117 $conn_r, 109 - 'phameTitle = %s', 110 - $this->phameTitle 111 - ); 118 + 'p.visibility = %d', 119 + $this->visibility); 112 120 } 113 121 114 - if ($this->visibility !== null) { 122 + if ($this->blogPHIDs) { 115 123 $where[] = qsprintf( 116 124 $conn_r, 117 - 'visibility = %d', 118 - $this->visibility 119 - ); 125 + 'p.blogPHID in (%Ls)', 126 + $this->blogPHIDs); 120 127 } 121 128 129 + $where[] = $this->buildPagingClause($conn_r); 130 + 122 131 return $this->formatWhereClause($where); 123 132 } 124 133 125 - private function buildOrderClause($conn_r) { 126 - return 'ORDER BY datePublished DESC, id DESC'; 127 - } 128 134 }
+59 -1
src/applications/phame/storage/PhamePost.php
··· 19 19 /** 20 20 * @group phame 21 21 */ 22 - final class PhamePost extends PhameDAO { 22 + final class PhamePost extends PhameDAO implements PhabricatorPolicyInterface { 23 23 24 24 const VISIBILITY_DRAFT = 0; 25 25 const VISIBILITY_PUBLISHED = 1; ··· 33 33 protected $visibility; 34 34 protected $configData; 35 35 protected $datePublished; 36 + protected $blogPHID; 37 + 38 + private $blog; 39 + 40 + public function setBlog(PhameBlog $blog) { 41 + $this->blog = $blog; 42 + return $this; 43 + } 44 + 45 + public function getBlog() { 46 + return $this->blog; 47 + } 36 48 37 49 public function getViewURI($blogger_name = '') { 38 50 // go for the pretty uri if we can ··· 44 56 } 45 57 return $uri; 46 58 } 59 + 47 60 public function getEditURI() { 48 61 return $this->getActionURI('edit'); 49 62 } 63 + 50 64 public function getDeleteURI() { 51 65 return $this->getActionURI('delete'); 52 66 } 67 + 53 68 public function getChangeVisibilityURI() { 54 69 return $this->getActionURI('changevisibility'); 55 70 } 71 + 56 72 private function getActionURI($action) { 57 73 return '/phame/post/'.$action.'/'.$this->getPHID().'/'; 58 74 } ··· 78 94 } 79 95 return idx($config_data, 'comments_widget', 'none'); 80 96 } 97 + 81 98 public function getConfiguration() { 82 99 return array( 83 100 self::CONFIG_AUX_PHID => true, ··· 114 131 $options['none'] = 'None'; 115 132 116 133 return $options; 134 + } 135 + 136 + 137 + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 138 + 139 + 140 + public function getCapabilities() { 141 + return array( 142 + PhabricatorPolicyCapability::CAN_VIEW, 143 + PhabricatorPolicyCapability::CAN_EDIT, 144 + ); 145 + } 146 + 147 + 148 + public function getPolicy($capability) { 149 + // Draft posts are visible only to the author. Published posts are visible 150 + // to whoever the blog is visible to. 151 + 152 + switch ($capability) { 153 + case PhabricatorPolicyCapability::CAN_VIEW: 154 + if (!$this->isDraft() && $this->getBlog()) { 155 + return $this->getBlog()->getViewPolicy(); 156 + } else { 157 + return PhabricatorPolicies::POLICY_NOONE; 158 + } 159 + break; 160 + case PhabricatorPolicyCapability::CAN_EDIT: 161 + return PhabricatorPolicies::POLICY_NOONE; 162 + } 163 + } 164 + 165 + 166 + public function hasAutomaticCapability($capability, PhabricatorUser $user) { 167 + // A blog post's author can always view it, and is the only user allowed 168 + // to edit it. 169 + 170 + switch ($capability) { 171 + case PhabricatorPolicyCapability::CAN_VIEW: 172 + case PhabricatorPolicyCapability::CAN_EDIT: 173 + return ($user->getPHID() == $this->getBloggerPHID()); 174 + } 117 175 } 118 176 119 177 }
-7
src/applications/phame/view/PhameBlogDetailView.php
··· 81 81 'class' => 'description' 82 82 ), 83 83 $description 84 - ). 85 - phutil_render_tag( 86 - 'div', 87 - array( 88 - 'class' => 'bloggers' 89 - ), 90 - 'Current bloggers: '.$this->getBloggersHTML($bloggers) 91 84 ) 92 85 ); 93 86
+1 -4
src/applications/phame/view/PhameBlogListView.php
··· 72 72 ->setHeader($blog->getName()) 73 73 ->setHref($blog->getViewURI()) 74 74 ->addDetail( 75 - 'Bloggers', 76 - implode(', ', mpull($bloggers, 'renderLink'))) 77 - ->addDetail( 78 75 'Custom Domain', 79 - $blog->getDomain()); 76 + phutil_escape_html($blog->getDomain())); 80 77 81 78 $can_edit = PhabricatorPolicyFilter::hasCapability( 82 79 $user,
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1008 1008 'type' => 'sql', 1009 1009 'name' => $this->getPatchPath('phamepolicy.sql'), 1010 1010 ), 1011 + 'phameoneblog.sql' => array( 1012 + 'type' => 'sql', 1013 + 'name' => $this->getPatchPath('phameoneblog.sql'), 1014 + ), 1011 1015 ); 1012 1016 } 1013 1017