@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

Add Profile Images to PhameBlog

Summary: Will use these more in the upcoming unbeta design of PhameBlog, likely. Also curious how this works.

Test Plan: Add an image to a blog, remove an image from a blog.

Reviewers: epriestley

Reviewed By: epriestley

Subscribers: Korvin

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

+291 -1
resources/builtin/blog.png

This is a binary file and will not be displayed.

+2
resources/sql/autopatches/20151128.phame.blog.picture.1.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phame.phame_blog 2 + ADD profileImagePHID VARBINARY(64);
+2
src/__phutil_library_map__.php
··· 3285 3285 'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', 3286 3286 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 3287 3287 'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php', 3288 + 'PhameBlogProfilePictureController' => 'applications/phame/controller/blog/PhameBlogProfilePictureController.php', 3288 3289 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', 3289 3290 'PhameBlogReplyHandler' => 'applications/phame/mail/PhameBlogReplyHandler.php', 3290 3291 'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php', ··· 7592 7593 'PhameBlogFeedController' => 'PhameBlogController', 7593 7594 'PhameBlogListController' => 'PhameBlogController', 7594 7595 'PhameBlogLiveController' => 'PhameBlogController', 7596 + 'PhameBlogProfilePictureController' => 'PhameBlogController', 7595 7597 'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7596 7598 'PhameBlogReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 7597 7599 'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine',
+1
src/applications/phame/application/PhabricatorPhameApplication.php
··· 63 63 'view/(?P<id>[^/]+)/' => 'PhameBlogViewController', 64 64 'feed/(?P<id>[^/]+)/' => 'PhameBlogFeedController', 65 65 'new/' => 'PhameBlogEditController', 66 + 'picture/(?P<id>[1-9]\d*)/' => 'PhameBlogProfilePictureController', 66 67 ), 67 68 ) + $this->getResourceSubroutes(), 68 69 );
+218
src/applications/phame/controller/blog/PhameBlogProfilePictureController.php
··· 1 + <?php 2 + 3 + final class PhameBlogProfilePictureController 4 + extends PhameBlogController { 5 + 6 + public function shouldRequireAdmin() { 7 + return false; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $viewer = $request->getViewer(); 12 + $id = $request->getURIData('id'); 13 + 14 + $blog = id(new PhameBlogQuery()) 15 + ->setViewer($viewer) 16 + ->withIDs(array($id)) 17 + ->needProfileImage(true) 18 + ->requireCapabilities( 19 + array( 20 + PhabricatorPolicyCapability::CAN_VIEW, 21 + PhabricatorPolicyCapability::CAN_EDIT, 22 + )) 23 + ->executeOne(); 24 + if (!$blog) { 25 + return new Aphront404Response(); 26 + } 27 + 28 + $blog_uri = '/phame/blog/view/'.$id; 29 + 30 + $supported_formats = PhabricatorFile::getTransformableImageFormats(); 31 + $e_file = true; 32 + $errors = array(); 33 + 34 + if ($request->isFormPost()) { 35 + $phid = $request->getStr('phid'); 36 + $is_default = false; 37 + if ($phid == PhabricatorPHIDConstants::PHID_VOID) { 38 + $phid = null; 39 + $is_default = true; 40 + } else if ($phid) { 41 + $file = id(new PhabricatorFileQuery()) 42 + ->setViewer($viewer) 43 + ->withPHIDs(array($phid)) 44 + ->executeOne(); 45 + } else { 46 + if ($request->getFileExists('picture')) { 47 + $file = PhabricatorFile::newFromPHPUpload( 48 + $_FILES['picture'], 49 + array( 50 + 'authorPHID' => $viewer->getPHID(), 51 + 'canCDN' => true, 52 + )); 53 + } else { 54 + $e_file = pht('Required'); 55 + $errors[] = pht( 56 + 'You must choose a file when uploading a new blog picture.'); 57 + } 58 + } 59 + 60 + if (!$errors && !$is_default) { 61 + if (!$file->isTransformableImage()) { 62 + $e_file = pht('Not Supported'); 63 + $errors[] = pht( 64 + 'This server only supports these image formats: %s.', 65 + implode(', ', $supported_formats)); 66 + } else { 67 + $xform = PhabricatorFileTransform::getTransformByKey( 68 + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); 69 + $xformed = $xform->executeTransform($file); 70 + } 71 + } 72 + 73 + if (!$errors) { 74 + if ($is_default) { 75 + $blog->setProfileImagePHID(null); 76 + } else { 77 + $blog->setProfileImagePHID($xformed->getPHID()); 78 + $xformed->attachToObject($blog->getPHID()); 79 + } 80 + $blog->save(); 81 + return id(new AphrontRedirectResponse())->setURI($blog_uri); 82 + } 83 + } 84 + 85 + $title = pht('Edit Blog Picture'); 86 + 87 + $form = id(new PHUIFormLayoutView()) 88 + ->setUser($viewer); 89 + 90 + $default_image = PhabricatorFile::loadBuiltin($viewer, 'blog.png'); 91 + 92 + $images = array(); 93 + 94 + $current = $blog->getProfileImagePHID(); 95 + $has_current = false; 96 + if ($current) { 97 + $files = id(new PhabricatorFileQuery()) 98 + ->setViewer($viewer) 99 + ->withPHIDs(array($current)) 100 + ->execute(); 101 + if ($files) { 102 + $file = head($files); 103 + if ($file->isTransformableImage()) { 104 + $has_current = true; 105 + $images[$current] = array( 106 + 'uri' => $file->getBestURI(), 107 + 'tip' => pht('Current Picture'), 108 + ); 109 + } 110 + } 111 + } 112 + 113 + $images[PhabricatorPHIDConstants::PHID_VOID] = array( 114 + 'uri' => $default_image->getBestURI(), 115 + 'tip' => pht('Default Picture'), 116 + ); 117 + 118 + require_celerity_resource('people-profile-css'); 119 + Javelin::initBehavior('phabricator-tooltips', array()); 120 + 121 + $buttons = array(); 122 + foreach ($images as $phid => $spec) { 123 + $button = javelin_tag( 124 + 'button', 125 + array( 126 + 'class' => 'grey profile-image-button', 127 + 'sigil' => 'has-tooltip', 128 + 'meta' => array( 129 + 'tip' => $spec['tip'], 130 + 'size' => 300, 131 + ), 132 + ), 133 + phutil_tag( 134 + 'img', 135 + array( 136 + 'height' => 50, 137 + 'width' => 50, 138 + 'src' => $spec['uri'], 139 + ))); 140 + 141 + $button = array( 142 + phutil_tag( 143 + 'input', 144 + array( 145 + 'type' => 'hidden', 146 + 'name' => 'phid', 147 + 'value' => $phid, 148 + )), 149 + $button, 150 + ); 151 + 152 + $button = phabricator_form( 153 + $viewer, 154 + array( 155 + 'class' => 'profile-image-form', 156 + 'method' => 'POST', 157 + ), 158 + $button); 159 + 160 + $buttons[] = $button; 161 + } 162 + 163 + if ($has_current) { 164 + $form->appendChild( 165 + id(new AphrontFormMarkupControl()) 166 + ->setLabel(pht('Current Picture')) 167 + ->setValue(array_shift($buttons))); 168 + } 169 + 170 + $form->appendChild( 171 + id(new AphrontFormMarkupControl()) 172 + ->setLabel(pht('Use Picture')) 173 + ->setValue($buttons)); 174 + 175 + $form_box = id(new PHUIObjectBoxView()) 176 + ->setHeaderText($title) 177 + ->setFormErrors($errors) 178 + ->setForm($form); 179 + 180 + $upload_form = id(new AphrontFormView()) 181 + ->setUser($viewer) 182 + ->setEncType('multipart/form-data') 183 + ->appendChild( 184 + id(new AphrontFormFileControl()) 185 + ->setName('picture') 186 + ->setLabel(pht('Upload Picture')) 187 + ->setError($e_file) 188 + ->setCaption( 189 + pht('Supported formats: %s', implode(', ', $supported_formats)))) 190 + ->appendChild( 191 + id(new AphrontFormSubmitControl()) 192 + ->addCancelButton($blog_uri) 193 + ->setValue(pht('Upload Picture'))); 194 + 195 + $upload_box = id(new PHUIObjectBoxView()) 196 + ->setHeaderText(pht('Upload New Picture')) 197 + ->setForm($upload_form); 198 + 199 + $crumbs = $this->buildApplicationCrumbs(); 200 + $crumbs->addTextCrumb( 201 + pht('Blogs'), 202 + $this->getApplicationURI('blog/')); 203 + $crumbs->addTextCrumb( 204 + $blog->getName(), 205 + $this->getApplicationURI('blog/view/'.$id)); 206 + $crumbs->addTextCrumb(pht('Blog Picture')); 207 + 208 + return $this->newPage() 209 + ->setTitle($title) 210 + ->setCrumbs($crumbs) 211 + ->appendChild( 212 + array( 213 + $form_box, 214 + $upload_box, 215 + )); 216 + 217 + } 218 + }
+12
src/applications/phame/controller/blog/PhameBlogViewController.php
··· 9 9 $blog = id(new PhameBlogQuery()) 10 10 ->setViewer($viewer) 11 11 ->withIDs(array($id)) 12 + ->needProfileImage(true) 12 13 ->executeOne(); 13 14 if (!$blog) { 14 15 return new Aphront404Response(); ··· 32 33 $header_color = 'bluegrey'; 33 34 } 34 35 36 + $picture = $blog->getProfileImageURI(); 37 + 35 38 $header = id(new PHUIHeaderView()) 36 39 ->setHeader($blog->getName()) 37 40 ->setUser($viewer) 38 41 ->setPolicyObject($blog) 42 + ->setImage($picture) 39 43 ->setStatus($header_icon, $header_color, $header_name); 40 44 41 45 $actions = $this->renderActions($blog, $viewer); ··· 166 170 ->setIcon('fa-pencil') 167 171 ->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/')) 168 172 ->setName(pht('Edit Blog')) 173 + ->setDisabled(!$can_edit) 174 + ->setWorkflow(!$can_edit)); 175 + 176 + $actions->addAction( 177 + id(new PhabricatorActionView()) 178 + ->setIcon('fa-picture-o') 179 + ->setHref($this->getApplicationURI('blog/picture/'.$blog->getID().'/')) 180 + ->setName(pht('Edit Blog Picture')) 169 181 ->setDisabled(!$can_edit) 170 182 ->setWorkflow(!$can_edit)); 171 183
+40
src/applications/phame/query/PhameBlogQuery.php
··· 6 6 private $phids; 7 7 private $domain; 8 8 private $statuses; 9 + 9 10 private $needBloggers; 11 + private $needProfileImage; 10 12 11 13 public function withIDs(array $ids) { 12 14 $this->ids = $ids; ··· 25 27 26 28 public function withStatuses(array $status) { 27 29 $this->statuses = $status; 30 + return $this; 31 + } 32 + 33 + public function needProfileImage($need) { 34 + $this->needProfileImage = $need; 28 35 return $this; 29 36 } 30 37 ··· 68 75 } 69 76 70 77 return $where; 78 + } 79 + 80 + protected function didFilterPage(array $blogs) { 81 + if ($this->needProfileImage) { 82 + $default = null; 83 + 84 + $file_phids = mpull($blogs, 'getProfileImagePHID'); 85 + $file_phids = array_filter($file_phids); 86 + if ($file_phids) { 87 + $files = id(new PhabricatorFileQuery()) 88 + ->setParentQuery($this) 89 + ->setViewer($this->getViewer()) 90 + ->withPHIDs($file_phids) 91 + ->execute(); 92 + $files = mpull($files, null, 'getPHID'); 93 + } else { 94 + $files = array(); 95 + } 96 + 97 + foreach ($blogs as $blog) { 98 + $file = idx($files, $blog->getProfileImagePHID()); 99 + if (!$file) { 100 + if (!$default) { 101 + $default = PhabricatorFile::loadBuiltin( 102 + $this->getViewer(), 103 + 'blog.png'); 104 + } 105 + $file = $default; 106 + } 107 + $blog->attachProfileImageFile($file); 108 + } 109 + } 110 + return $blogs; 71 111 } 72 112 73 113 public function getQueryApplicationClass() {
+16 -1
src/applications/phame/storage/PhameBlog.php
··· 11 11 PhabricatorApplicationTransactionInterface { 12 12 13 13 const MARKUP_FIELD_DESCRIPTION = 'markup:description'; 14 - 15 14 const SKIN_DEFAULT = 'oblivious'; 16 15 17 16 protected $name; ··· 23 22 protected $editPolicy; 24 23 protected $status; 25 24 protected $mailKey; 25 + protected $profileImagePHID; 26 26 27 + private $profileImageFile = self::ATTACHABLE; 27 28 private static $requestBlog; 28 29 29 30 const STATUS_ACTIVE = 'active'; ··· 41 42 'domain' => 'text128?', 42 43 'status' => 'text32', 43 44 'mailKey' => 'bytes20', 45 + 'profileImagePHID' => 'phid?', 44 46 45 47 // T6203/NULLABILITY 46 48 // These policies should always be non-null. ··· 241 243 public function getViewURI() { 242 244 $uri = '/phame/blog/view/'.$this->getID().'/'; 243 245 return PhabricatorEnv::getProductionURI($uri); 246 + } 247 + 248 + public function getProfileImageURI() { 249 + return $this->getProfileImageFile()->getBestURI(); 250 + } 251 + 252 + public function attachProfileImageFile(PhabricatorFile $file) { 253 + $this->profileImageFile = $file; 254 + return $this; 255 + } 256 + 257 + public function getProfileImageFile() { 258 + return $this->assertAttached($this->profileImageFile); 244 259 } 245 260 246 261