diff --git a/resources/builtin/blog.png b/resources/builtin/blog.png new file mode 100644 index 0000000000..0d0a838ef3 Binary files /dev/null and b/resources/builtin/blog.png differ diff --git a/resources/sql/autopatches/20151128.phame.blog.picture.1.sql b/resources/sql/autopatches/20151128.phame.blog.picture.1.sql new file mode 100644 index 0000000000..1db4ca8393 --- /dev/null +++ b/resources/sql/autopatches/20151128.phame.blog.picture.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_blog + ADD profileImagePHID VARBINARY(64); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2c50734c30..71ecf74449 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3285,6 +3285,7 @@ phutil_register_library_map(array( 'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php', + 'PhameBlogProfilePictureController' => 'applications/phame/controller/blog/PhameBlogProfilePictureController.php', 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', 'PhameBlogReplyHandler' => 'applications/phame/mail/PhameBlogReplyHandler.php', 'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php', @@ -7592,6 +7593,7 @@ phutil_register_library_map(array( 'PhameBlogFeedController' => 'PhameBlogController', 'PhameBlogListController' => 'PhameBlogController', 'PhameBlogLiveController' => 'PhameBlogController', + 'PhameBlogProfilePictureController' => 'PhameBlogController', 'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhameBlogReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index 8c6516dd96..7af74fd792 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -63,6 +63,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { 'view/(?P[^/]+)/' => 'PhameBlogViewController', 'feed/(?P[^/]+)/' => 'PhameBlogFeedController', 'new/' => 'PhameBlogEditController', + 'picture/(?P[1-9]\d*)/' => 'PhameBlogProfilePictureController', ), ) + $this->getResourceSubroutes(), ); diff --git a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php new file mode 100644 index 0000000000..33eec9f478 --- /dev/null +++ b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php @@ -0,0 +1,218 @@ +getViewer(); + $id = $request->getURIData('id'); + + $blog = id(new PhameBlogQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needProfileImage(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$blog) { + return new Aphront404Response(); + } + + $blog_uri = '/phame/blog/view/'.$id; + + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + $e_file = true; + $errors = array(); + + if ($request->isFormPost()) { + $phid = $request->getStr('phid'); + $is_default = false; + if ($phid == PhabricatorPHIDConstants::PHID_VOID) { + $phid = null; + $is_default = true; + } else if ($phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->executeOne(); + } else { + if ($request->getFileExists('picture')) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['picture'], + array( + 'authorPHID' => $viewer->getPHID(), + 'canCDN' => true, + )); + } else { + $e_file = pht('Required'); + $errors[] = pht( + 'You must choose a file when uploading a new blog picture.'); + } + } + + if (!$errors && !$is_default) { + if (!$file->isTransformableImage()) { + $e_file = pht('Not Supported'); + $errors[] = pht( + 'This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } else { + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + $xformed = $xform->executeTransform($file); + } + } + + if (!$errors) { + if ($is_default) { + $blog->setProfileImagePHID(null); + } else { + $blog->setProfileImagePHID($xformed->getPHID()); + $xformed->attachToObject($blog->getPHID()); + } + $blog->save(); + return id(new AphrontRedirectResponse())->setURI($blog_uri); + } + } + + $title = pht('Edit Blog Picture'); + + $form = id(new PHUIFormLayoutView()) + ->setUser($viewer); + + $default_image = PhabricatorFile::loadBuiltin($viewer, 'blog.png'); + + $images = array(); + + $current = $blog->getProfileImagePHID(); + $has_current = false; + if ($current) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($current)) + ->execute(); + if ($files) { + $file = head($files); + if ($file->isTransformableImage()) { + $has_current = true; + $images[$current] = array( + 'uri' => $file->getBestURI(), + 'tip' => pht('Current Picture'), + ); + } + } + } + + $images[PhabricatorPHIDConstants::PHID_VOID] = array( + 'uri' => $default_image->getBestURI(), + 'tip' => pht('Default Picture'), + ); + + require_celerity_resource('people-profile-css'); + Javelin::initBehavior('phabricator-tooltips', array()); + + $buttons = array(); + foreach ($images as $phid => $spec) { + $button = javelin_tag( + 'button', + array( + 'class' => 'grey profile-image-button', + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $spec['tip'], + 'size' => 300, + ), + ), + phutil_tag( + 'img', + array( + 'height' => 50, + 'width' => 50, + 'src' => $spec['uri'], + ))); + + $button = array( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'phid', + 'value' => $phid, + )), + $button, + ); + + $button = phabricator_form( + $viewer, + array( + 'class' => 'profile-image-form', + 'method' => 'POST', + ), + $button); + + $buttons[] = $button; + } + + if ($has_current) { + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Current Picture')) + ->setValue(array_shift($buttons))); + } + + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Use Picture')) + ->setValue($buttons)); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setForm($form); + + $upload_form = id(new AphrontFormView()) + ->setUser($viewer) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('picture') + ->setLabel(pht('Upload Picture')) + ->setError($e_file) + ->setCaption( + pht('Supported formats: %s', implode(', ', $supported_formats)))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($blog_uri) + ->setValue(pht('Upload Picture'))); + + $upload_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Upload New Picture')) + ->setForm($upload_form); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Blogs'), + $this->getApplicationURI('blog/')); + $crumbs->addTextCrumb( + $blog->getName(), + $this->getApplicationURI('blog/view/'.$id)); + $crumbs->addTextCrumb(pht('Blog Picture')); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $form_box, + $upload_box, + )); + + } +} diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index f74b838881..e6c6f02192 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -9,6 +9,7 @@ final class PhameBlogViewController extends PhameBlogController { $blog = id(new PhameBlogQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->needProfileImage(true) ->executeOne(); if (!$blog) { return new Aphront404Response(); @@ -32,10 +33,13 @@ final class PhameBlogViewController extends PhameBlogController { $header_color = 'bluegrey'; } + $picture = $blog->getProfileImageURI(); + $header = id(new PHUIHeaderView()) ->setHeader($blog->getName()) ->setUser($viewer) ->setPolicyObject($blog) + ->setImage($picture) ->setStatus($header_icon, $header_color, $header_name); $actions = $this->renderActions($blog, $viewer); @@ -169,6 +173,14 @@ final class PhameBlogViewController extends PhameBlogController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-picture-o') + ->setHref($this->getApplicationURI('blog/picture/'.$blog->getID().'/')) + ->setName(pht('Edit Blog Picture')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + if ($blog->isArchived()) { $actions->addAction( id(new PhabricatorActionView()) diff --git a/src/applications/phame/query/PhameBlogQuery.php b/src/applications/phame/query/PhameBlogQuery.php index 279ff91724..36ae306345 100644 --- a/src/applications/phame/query/PhameBlogQuery.php +++ b/src/applications/phame/query/PhameBlogQuery.php @@ -6,7 +6,9 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $phids; private $domain; private $statuses; + private $needBloggers; + private $needProfileImage; public function withIDs(array $ids) { $this->ids = $ids; @@ -28,6 +30,11 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function needProfileImage($need) { + $this->needProfileImage = $need; + return $this; + } + public function newResultObject() { return new PhameBlog(); } @@ -70,6 +77,39 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $where; } + protected function didFilterPage(array $blogs) { + if ($this->needProfileImage) { + $default = null; + + $file_phids = mpull($blogs, 'getProfileImagePHID'); + $file_phids = array_filter($file_phids); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } else { + $files = array(); + } + + foreach ($blogs as $blog) { + $file = idx($files, $blog->getProfileImagePHID()); + if (!$file) { + if (!$default) { + $default = PhabricatorFile::loadBuiltin( + $this->getViewer(), + 'blog.png'); + } + $file = $default; + } + $blog->attachProfileImageFile($file); + } + } + return $blogs; + } + public function getQueryApplicationClass() { // TODO: Can we set this without breaking public blogs? return null; diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index cdb629a804..9cec0f0a1b 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -11,7 +11,6 @@ final class PhameBlog extends PhameDAO PhabricatorApplicationTransactionInterface { const MARKUP_FIELD_DESCRIPTION = 'markup:description'; - const SKIN_DEFAULT = 'oblivious'; protected $name; @@ -23,7 +22,9 @@ final class PhameBlog extends PhameDAO protected $editPolicy; protected $status; protected $mailKey; + protected $profileImagePHID; + private $profileImageFile = self::ATTACHABLE; private static $requestBlog; const STATUS_ACTIVE = 'active'; @@ -41,6 +42,7 @@ final class PhameBlog extends PhameDAO 'domain' => 'text128?', 'status' => 'text32', 'mailKey' => 'bytes20', + 'profileImagePHID' => 'phid?', // T6203/NULLABILITY // These policies should always be non-null. @@ -243,6 +245,19 @@ final class PhameBlog extends PhameDAO return PhabricatorEnv::getProductionURI($uri); } + public function getProfileImageURI() { + return $this->getProfileImageFile()->getBestURI(); + } + + public function attachProfileImageFile(PhabricatorFile $file) { + $this->profileImageFile = $file; + return $this; + } + + public function getProfileImageFile() { + return $this->assertAttached($this->profileImageFile); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */