@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

Always automatically generate Phame slugs

Summary:
Fixes T9995. I think letting users customize slugs is not a hugely compelling as a product feature, and this fixes the issue with slugs that have "/" characters in them and makes the move to EditEngine easier since I don't have to deal with the weird JS thing.

Instead, just generate slugs automatically. No more JS, no more separate field, things automatically update if you rename a blog, and now that URIs have IDs in them the old URI will still work after a rename.

Test Plan:
- Applied migration.
- Created new posts.
- Edited existing posts.
- Visited various posts.
- Created a post with a bunch of "/" in the title, things still worked fine.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T9995

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

+7 -177
-8
resources/celerity/map.php
··· 410 410 'rsrc/js/application/owners/OwnersPathEditor.js' => 'aa1733d0', 411 411 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3', 412 412 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc', 413 - 'rsrc/js/application/phame/phame-post-preview.js' => 'd6bba572', 414 413 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '246dc085', 415 414 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'fbe497e7', 416 415 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf', ··· 648 647 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6', 649 648 'javelin-behavior-phabricator-transaction-list' => '13c739ea', 650 649 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d', 651 - 'javelin-behavior-phame-post-preview' => 'd6bba572', 652 650 'javelin-behavior-pholio-mock-edit' => '246dc085', 653 651 'javelin-behavior-pholio-mock-view' => 'fbe497e7', 654 652 'javelin-behavior-phui-dropdown-menu' => '54733475', ··· 1855 1853 'javelin-behavior', 1856 1854 'javelin-dom', 1857 1855 'javelin-stratcom', 1858 - ), 1859 - 'd6bba572' => array( 1860 - 'javelin-behavior', 1861 - 'javelin-dom', 1862 - 'javelin-util', 1863 - 'phabricator-shaped-request', 1864 1856 ), 1865 1857 'd75709e6' => array( 1866 1858 'javelin-behavior',
+5
resources/sql/autopatches/20151215.phame.1.autotitle.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phame.phame_post 2 + DROP KEY phameTitle; 3 + 4 + ALTER TABLE {$NAMESPACE}_phame.phame_post 5 + CHANGE phameTitle phameTitle VARCHAR(64) COLLATE {$COLLATE_SORT};
-7
src/applications/phame/conduit/PhameCreatePostConduitAPIMethod.php
··· 19 19 'blogPHID' => 'required phid', 20 20 'title' => 'required string', 21 21 'body' => 'required string', 22 - 'phameTitle' => 'optional string', 23 22 'bloggerPHID' => 'optional phid', 24 23 'isDraft' => 'optional bool', 25 24 ); ··· 89 88 $post->setVisibility(PhameConstants::VISIBILITY_PUBLISHED); 90 89 } 91 90 $post->setTitle($title); 92 - $phame_title = $request->getValue( 93 - 'phameTitle', 94 - id(new PhutilUTF8StringTruncator()) 95 - ->setMaximumBytes(64) 96 - ->truncateString($title)); 97 - $post->setPhameTitle(PhabricatorSlug::normalize($phame_title)); 98 91 $post->setBody($body); 99 92 $post->save(); 100 93
-6
src/applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php
··· 20 20 'phids' => 'optional list<phid>', 21 21 'blogPHIDs' => 'optional list<phid>', 22 22 'bloggerPHIDs' => 'optional list<phid>', 23 - 'phameTitles' => 'optional list<string>', 24 23 'published' => 'optional bool', 25 24 'publishedAfter' => 'optional date', 26 25 'before' => 'optional int', ··· 56 55 $blogger_phids = $request->getValue('bloggerPHIDs', array()); 57 56 if ($blogger_phids) { 58 57 $query->withBloggerPHIDs($blogger_phids); 59 - } 60 - 61 - $phame_titles = $request->getValue('phameTitles', array()); 62 - if ($phame_titles) { 63 - $query->withPhameTitles($phame_titles); 64 58 } 65 59 66 60 $published = $request->getValue('published', null);
-25
src/applications/phame/controller/post/PhamePostEditController.php
··· 60 60 } 61 61 62 62 $title = $post->getTitle(); 63 - $phame_title = $post->getPhameTitle(); 64 63 $body = $post->getBody(); 65 64 $visibility = $post->getVisibility(); 66 65 67 66 $e_title = true; 68 - $e_phame_title = true; 69 67 $validation_exception = null; 70 68 if ($request->isFormPost()) { 71 69 $title = $request->getStr('title'); 72 - $phame_title = $request->getStr('phame_title'); 73 - $phame_title = PhabricatorSlug::normalize($phame_title); 74 70 $body = $request->getStr('body'); 75 71 $v_projects = $request->getArr('projects'); 76 72 $v_cc = $request->getArr('cc'); ··· 80 76 id(new PhamePostTransaction()) 81 77 ->setTransactionType(PhamePostTransaction::TYPE_TITLE) 82 78 ->setNewValue($title), 83 - id(new PhamePostTransaction()) 84 - ->setTransactionType(PhamePostTransaction::TYPE_PHAME_TITLE) 85 - ->setNewValue($phame_title), 86 79 id(new PhamePostTransaction()) 87 80 ->setTransactionType(PhamePostTransaction::TYPE_BODY) 88 81 ->setNewValue($body), ··· 115 108 $validation_exception = $ex; 116 109 $e_title = $validation_exception->getShortMessage( 117 110 PhamePostTransaction::TYPE_TITLE); 118 - $e_phame_title = $validation_exception->getShortMessage( 119 - PhamePostTransaction::TYPE_PHAME_TITLE); 120 111 } 121 112 } 122 113 ··· 140 131 ->setID('post-title') 141 132 ->setError($e_title)) 142 133 ->appendChild( 143 - id(new AphrontFormTextControl()) 144 - ->setLabel(pht('Phame Title')) 145 - ->setName('phame_title') 146 - ->setValue(rtrim($phame_title, '/')) 147 - ->setID('post-phame-title') 148 - ->setCaption(pht('Up to 64 alphanumeric characters '. 149 - 'with underscores for spaces.')) 150 - ->setError($e_phame_title)) 151 - ->appendChild( 152 134 id(new AphrontFormSelectControl()) 153 135 ->setLabel(pht('Visibility')) 154 136 ->setName('visibility') ··· 186 168 ->setPreviewURI($this->getApplicationURI('post/preview/')) 187 169 ->setControlID('post-body') 188 170 ->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT); 189 - 190 - Javelin::initBehavior( 191 - 'phame-post-preview', 192 - array( 193 - 'title' => 'post-title', 194 - 'phame_title' => 'post-phame-title', 195 - )); 196 171 197 172 $form_box = id(new PHUIObjectBoxView()) 198 173 ->setHeaderText($page_title)
-44
src/applications/phame/editor/PhamePostEditor.php
··· 15 15 $types = parent::getTransactionTypes(); 16 16 17 17 $types[] = PhamePostTransaction::TYPE_TITLE; 18 - $types[] = PhamePostTransaction::TYPE_PHAME_TITLE; 19 18 $types[] = PhamePostTransaction::TYPE_BODY; 20 19 $types[] = PhamePostTransaction::TYPE_VISIBILITY; 21 20 $types[] = PhabricatorTransactions::TYPE_COMMENT; ··· 30 29 switch ($xaction->getTransactionType()) { 31 30 case PhamePostTransaction::TYPE_TITLE: 32 31 return $object->getTitle(); 33 - case PhamePostTransaction::TYPE_PHAME_TITLE: 34 - return $object->getPhameTitle(); 35 32 case PhamePostTransaction::TYPE_BODY: 36 33 return $object->getBody(); 37 34 case PhamePostTransaction::TYPE_VISIBILITY: ··· 45 42 46 43 switch ($xaction->getTransactionType()) { 47 44 case PhamePostTransaction::TYPE_TITLE: 48 - case PhamePostTransaction::TYPE_PHAME_TITLE: 49 45 case PhamePostTransaction::TYPE_BODY: 50 46 case PhamePostTransaction::TYPE_VISIBILITY: 51 47 return $xaction->getNewValue(); ··· 59 55 switch ($xaction->getTransactionType()) { 60 56 case PhamePostTransaction::TYPE_TITLE: 61 57 return $object->setTitle($xaction->getNewValue()); 62 - case PhamePostTransaction::TYPE_PHAME_TITLE: 63 - return $object->setPhameTitle($xaction->getNewValue()); 64 58 case PhamePostTransaction::TYPE_BODY: 65 59 return $object->setBody($xaction->getNewValue()); 66 60 case PhamePostTransaction::TYPE_VISIBILITY: ··· 81 75 82 76 switch ($xaction->getTransactionType()) { 83 77 case PhamePostTransaction::TYPE_TITLE: 84 - case PhamePostTransaction::TYPE_PHAME_TITLE: 85 78 case PhamePostTransaction::TYPE_BODY: 86 79 case PhamePostTransaction::TYPE_VISIBILITY: 87 80 return; ··· 113 106 $error->setIsMissingFieldError(true); 114 107 $errors[] = $error; 115 108 } 116 - break; 117 - case PhamePostTransaction::TYPE_PHAME_TITLE: 118 - if (!$xactions) { 119 - continue; 120 - } 121 - $missing = $this->validateIsEmptyTextField( 122 - $object->getPhameTitle(), 123 - $xactions); 124 - $phame_title = last($xactions)->getNewValue(); 125 - 126 - if ($missing || $phame_title == '/') { 127 - $error = new PhabricatorApplicationTransactionValidationError( 128 - $type, 129 - pht('Required'), 130 - pht('Phame title is required.'), 131 - nonempty(last($xactions), null)); 132 - 133 - $error->setIsMissingFieldError(true); 134 - $errors[] = $error; 135 - } 136 - 137 - $duplicate_post = id(new PhamePostQuery()) 138 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 139 - ->withPhameTitles(array($phame_title)) 140 - ->executeOne(); 141 - if ($duplicate_post && $duplicate_post->getID() != $object->getID()) { 142 - $error_text = pht( 143 - 'Phame title must be unique; another post already has this phame '. 144 - 'title.'); 145 - $error = new PhabricatorApplicationTransactionValidationError( 146 - $type, 147 - pht('Not Unique'), 148 - $error_text, 149 - nonempty(last($xactions), null)); 150 - $errors[] = $error; 151 - } 152 - 153 109 break; 154 110 } 155 111 return $errors;
-13
src/applications/phame/query/PhamePostQuery.php
··· 5 5 private $ids; 6 6 private $blogPHIDs; 7 7 private $bloggerPHIDs; 8 - private $phameTitles; 9 8 private $visibility; 10 9 private $publishedAfter; 11 10 private $phids; ··· 27 26 28 27 public function withBlogPHIDs(array $blog_phids) { 29 28 $this->blogPHIDs = $blog_phids; 30 - return $this; 31 - } 32 - 33 - public function withPhameTitles(array $phame_titles) { 34 - $this->phameTitles = $phame_titles; 35 29 return $this; 36 30 } 37 31 ··· 104 98 $conn, 105 99 'p.bloggerPHID IN (%Ls)', 106 100 $this->bloggerPHIDs); 107 - } 108 - 109 - if ($this->phameTitles) { 110 - $where[] = qsprintf( 111 - $conn, 112 - 'p.phameTitle IN (%Ls)', 113 - $this->phameTitles); 114 101 } 115 102 116 103 if ($this->visibility !== null) {
+2 -7
src/applications/phame/storage/PhamePost.php
··· 98 98 ), 99 99 self::CONFIG_COLUMN_SCHEMA => array( 100 100 'title' => 'text255', 101 - 'phameTitle' => 'sort64', 101 + 'phameTitle' => 'sort64?', 102 102 'visibility' => 'uint32', 103 103 'mailKey' => 'bytes20', 104 104 ··· 118 118 'columns' => array('phid'), 119 119 'unique' => true, 120 120 ), 121 - 'phameTitle' => array( 122 - 'columns' => array('bloggerPHID', 'phameTitle'), 123 - 'unique' => true, 124 - ), 125 121 'bloggerPosts' => array( 126 122 'columns' => array( 127 123 'bloggerPHID', ··· 147 143 } 148 144 149 145 public function getSlug() { 150 - return rtrim($this->getPhameTitle(), '/'); 146 + return PhabricatorSlug::normalizeProjectSlug($this->getTitle(), true); 151 147 } 152 148 153 149 public function toDictionary() { ··· 158 154 'bloggerPHID' => $this->getBloggerPHID(), 159 155 'viewURI' => $this->getViewURI(), 160 156 'title' => $this->getTitle(), 161 - 'phameTitle' => $this->getPhameTitle(), 162 157 'body' => $this->getBody(), 163 158 'summary' => PhabricatorMarkupEngine::summarize($this->getBody()), 164 159 'datePublished' => $this->getDatePublished(),
-67
webroot/rsrc/js/application/phame/phame-post-preview.js
··· 1 - /** 2 - * @provides javelin-behavior-phame-post-preview 3 - * @requires javelin-behavior 4 - * javelin-dom 5 - * javelin-util 6 - * phabricator-shaped-request 7 - */ 8 - 9 - JX.behavior('phame-post-preview', function(config) { 10 - 11 - var title = JX.$(config.title); 12 - var phame_title = JX.$(config.phame_title); 13 - var sync_titles = true; 14 - 15 - var titleCallback = function() { 16 - if (!sync_titles) { 17 - return; 18 - } 19 - var title_string = title.value; 20 - phame_title.value = normalizeSlug(title_string); 21 - }; 22 - 23 - var phameTitleKeyupCallback = function () { 24 - // stop sync'ing once user edits phame_title directly 25 - sync_titles = false; 26 - var normalized = normalizeSlug(phame_title.value, true); 27 - if (normalized == phame_title.value) { 28 - return; 29 - } 30 - var position = phame_title.value.length; 31 - if ('selectionStart' in phame_title) { 32 - position = phame_title.selectionStart; 33 - } 34 - phame_title.value = normalized; 35 - if ('setSelectionRange' in phame_title) { 36 - phame_title.focus(); 37 - phame_title.setSelectionRange(position, position); 38 - } 39 - }; 40 - 41 - var phameTitleBlurCallback = function () { 42 - phame_title.value = normalizeSlug(phame_title.value); 43 - }; 44 - 45 - // This is a sort of implementation of PhabricatorSlug::normalize 46 - var normalizeSlug = function (slug, spare_trailing_underscore) { 47 - var s = slug.toLowerCase().replace(/[^a-z0-9/]+/g, '_').substr(0, 63); 48 - if (spare_trailing_underscore) { 49 - // do nothing 50 - } else { 51 - s = s.replace(/_$/g, ''); 52 - } 53 - return s; 54 - }; 55 - 56 - var getdata = function() { 57 - return { 58 - title : title.value, 59 - phame_title : phame_title.value 60 - }; 61 - }; 62 - 63 - JX.DOM.listen(title, 'keyup', null, titleCallback); 64 - JX.DOM.listen(phame_title, 'keyup', null, phameTitleKeyupCallback); 65 - JX.DOM.listen(phame_title, 'blur', null, phameTitleBlurCallback); 66 - 67 - });