@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.

Merge branch 'master' into redesign-2015

+813 -242
+17
resources/sql/autopatches/20150605.diviner.edges.sql
··· 1 + CREATE TABLE {$NAMESPACE}_diviner.edge ( 2 + src VARBINARY(64) NOT NULL, 3 + type INT UNSIGNED NOT NULL, 4 + dst VARBINARY(64) NOT NULL, 5 + dateCreated INT UNSIGNED NOT NULL, 6 + seq INT UNSIGNED NOT NULL, 7 + dataID INT UNSIGNED, 8 + 9 + PRIMARY KEY (src, type, dst), 10 + KEY src (src, type, dateCreated, seq), 11 + UNIQUE KEY key_dst (dst, type, src) 12 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 13 + 14 + CREATE TABLE {$NAMESPACE}_diviner.edgedata ( 15 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 16 + data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT} 17 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+6
resources/sql/autopatches/20150605.diviner.editPolicy.sql
··· 1 + ALTER TABLE {$NAMESPACE}_diviner.diviner_livebook 2 + ADD COLUMN editPolicy VARBINARY(64) NOT NULL AFTER viewPolicy; 3 + 4 + UPDATE {$NAMESPACE}_diviner.diviner_livebook 5 + SET editPolicy = 'admin' 6 + WHERE editPolicy = '';
+19
resources/sql/autopatches/20150605.diviner.xaction.sql
··· 1 + CREATE TABLE {$NAMESPACE}_diviner.diviner_livebooktransaction ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + objectPHID VARBINARY(64) NOT NULL, 6 + viewPolicy VARBINARY(64) NOT NULL, 7 + editPolicy VARBINARY(64) NOT NULL, 8 + commentPHID VARBINARY(64) DEFAULT NULL, 9 + commentVersion INT UNSIGNED NOT NULL, 10 + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, 11 + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 12 + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 13 + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 14 + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, 15 + dateCreated INT UNSIGNED NOT NULL, 16 + dateModified INT UNSIGNED NOT NULL, 17 + UNIQUE KEY key_phid (phid), 18 + KEY key_object (objectPHID) 19 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+18
src/__phutil_library_map__.php
··· 648 648 'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php', 649 649 'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php', 650 650 'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php', 651 + 'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php', 651 652 'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php', 652 653 'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php', 653 654 'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php', 654 655 'DivinerBookSearchIndexer' => 'applications/diviner/search/DivinerBookSearchIndexer.php', 655 656 'DivinerController' => 'applications/diviner/controller/DivinerController.php', 656 657 'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php', 658 + 'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php', 657 659 'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php', 660 + 'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php', 658 661 'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php', 659 662 'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php', 660 663 'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php', 661 664 'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php', 662 665 'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php', 663 666 'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php', 667 + 'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php', 668 + 'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php', 669 + 'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php', 664 670 'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php', 665 671 'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php', 666 672 'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php', ··· 670 676 'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php', 671 677 'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php', 672 678 'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php', 679 + 'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php', 673 680 'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php', 674 681 'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php', 675 682 'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php', ··· 1269 1276 'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php', 1270 1277 'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php', 1271 1278 'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php', 1279 + 'PassphraseNoteCredentialType' => 'applications/passphrase/credentialtype/PassphraseNoteCredentialType.php', 1272 1280 'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php', 1273 1281 'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php', 1274 1282 'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php', ··· 4005 4013 'DivinerAtomizeWorkflow' => 'DivinerWorkflow', 4006 4014 'DivinerAtomizer' => 'Phobject', 4007 4015 'DivinerBookController' => 'DivinerController', 4016 + 'DivinerBookEditController' => 'DivinerController', 4008 4017 'DivinerBookItemView' => 'AphrontTagView', 4009 4018 'DivinerBookPHIDType' => 'PhabricatorPHIDType', 4010 4019 'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4011 4020 'DivinerBookSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 4012 4021 'DivinerController' => 'PhabricatorController', 4013 4022 'DivinerDAO' => 'PhabricatorLiskDAO', 4023 + 'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability', 4014 4024 'DivinerDefaultRenderer' => 'DivinerRenderer', 4025 + 'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability', 4015 4026 'DivinerDiskCache' => 'Phobject', 4016 4027 'DivinerFileAtomizer' => 'DivinerAtomizer', 4017 4028 'DivinerFindController' => 'DivinerController', ··· 4020 4031 'DivinerLiveBook' => array( 4021 4032 'DivinerDAO', 4022 4033 'PhabricatorPolicyInterface', 4034 + 'PhabricatorProjectInterface', 4023 4035 'PhabricatorDestructibleInterface', 4036 + 'PhabricatorApplicationTransactionInterface', 4024 4037 ), 4038 + 'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor', 4039 + 'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction', 4040 + 'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 4025 4041 'DivinerLivePublisher' => 'DivinerPublisher', 4026 4042 'DivinerLiveSymbol' => array( 4027 4043 'DivinerDAO', ··· 4036 4052 'DivinerPublisher' => 'Phobject', 4037 4053 'DivinerRenderer' => 'Phobject', 4038 4054 'DivinerReturnTableView' => 'AphrontTagView', 4055 + 'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec', 4039 4056 'DivinerSectionView' => 'AphrontTagView', 4040 4057 'DivinerStaticPublisher' => 'DivinerPublisher', 4041 4058 'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule', ··· 4761 4778 'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase', 4762 4779 'PassphraseCredentialViewController' => 'PassphraseController', 4763 4780 'PassphraseDAO' => 'PhabricatorLiskDAO', 4781 + 'PassphraseNoteCredentialType' => 'PassphraseCredentialType', 4764 4782 'PassphrasePasswordCredentialType' => 'PassphraseCredentialType', 4765 4783 'PassphrasePasswordKey' => 'PassphraseAbstractKey', 4766 4784 'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',
+1 -1
src/applications/audit/constants/PhabricatorAuditActionConstants.php
··· 19 19 self::ACCEPT => pht("Accept Commit \xE2\x9C\x94"), 20 20 self::RESIGN => pht('Resign from Audit'), 21 21 self::CLOSE => pht('Close Audit'), 22 - self::ADD_CCS => pht('Add CCs'), 22 + self::ADD_CCS => pht('Add Subscribers'), 23 23 self::ADD_AUDITORS => pht('Add Auditors'), 24 24 ); 25 25
+17 -6
src/applications/base/PhabricatorApplication.php
··· 449 449 $class, 450 450 PhabricatorUser $viewer) { 451 451 452 - if (!self::isClassInstalled($class)) { 453 - return false; 452 + $cache = PhabricatorCaches::getRequestCache(); 453 + $viewer_phid = $viewer->getPHID(); 454 + $key = 'app.'.$class.'.installed.'.$viewer_phid; 455 + 456 + $result = $cache->getKey($key); 457 + if ($result === null) { 458 + if (!self::isClassInstalled($class)) { 459 + $result = false; 460 + } else { 461 + $result = PhabricatorPolicyFilter::hasCapability( 462 + $viewer, 463 + self::getByClass($class), 464 + PhabricatorPolicyCapability::CAN_VIEW); 465 + } 466 + 467 + $cache->setKey($key, $result); 454 468 } 455 469 456 - return PhabricatorPolicyFilter::hasCapability( 457 - $viewer, 458 - self::getByClass($class), 459 - PhabricatorPolicyCapability::CAN_VIEW); 470 + return $result; 460 471 } 461 472 462 473
+7 -3
src/applications/calendar/controller/PhabricatorCalendarController.php
··· 9 9 ->setUser($this->getViewer()) 10 10 ->addAction( 11 11 id(new PhabricatorActionView()) 12 - ->setName(pht('Create Private Event')) 13 - ->setHref('/calendar/event/create/?mode=private')) 12 + ->setName(pht('Create Event')) 13 + ->setHref('/calendar/event/create/')) 14 14 ->addAction( 15 15 id(new PhabricatorActionView()) 16 16 ->setName(pht('Create Public Event')) 17 - ->setHref('/calendar/event/create/?mode=public')); 17 + ->setHref('/calendar/event/create/?mode=public')) 18 + ->addAction( 19 + id(new PhabricatorActionView()) 20 + ->setName(pht('Create Recurring Event')) 21 + ->setHref('/calendar/event/create/?mode=recurring')); 18 22 19 23 $crumbs->addAction( 20 24 id(new PHUIListItemView())
+3 -1
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 291 291 if ($event->getInstanceOfEventPHID()) { 292 292 $properties->addProperty( 293 293 pht('Recurrence of Event'), 294 - $viewer->renderHandle($event->getInstanceOfEventPHID())); 294 + pht('%s of %s', 295 + $event->getSequenceIndex(), 296 + $viewer->renderHandle($event->getInstanceOfEventPHID())->render())); 295 297 } 296 298 } 297 299
+7 -2
src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
··· 58 58 $min_range = $this->getDateFrom($saved)->getEpoch(); 59 59 $max_range = $this->getDateTo($saved)->getEpoch(); 60 60 61 + $user_datasource = id(new PhabricatorPeopleUserFunctionDatasource()) 62 + ->setViewer($viewer); 63 + 61 64 if ($this->isMonthView($saved) || 62 65 $this->isDayView($saved)) { 63 66 list($start_year, $start_month, $start_day) = ··· 124 127 } 125 128 126 129 $invited_phids = $saved->getParameter('invitedPHIDs'); 130 + $invited_phids = $user_datasource->evaluateTokens($invited_phids); 127 131 if ($invited_phids) { 128 132 $query->withInvitedPHIDs($invited_phids); 129 133 } 130 134 131 135 $creator_phids = $saved->getParameter('creatorPHIDs'); 136 + $creator_phids = $user_datasource->evaluateTokens($creator_phids); 132 137 if ($creator_phids) { 133 138 $query->withCreatorPHIDs($creator_phids); 134 139 } ··· 196 201 $form 197 202 ->appendControl( 198 203 id(new AphrontFormTokenizerControl()) 199 - ->setDatasource(new PhabricatorPeopleDatasource()) 204 + ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()) 200 205 ->setName('creators') 201 206 ->setLabel(pht('Created By')) 202 207 ->setValue($creator_phids)) 203 208 ->appendControl( 204 209 id(new AphrontFormTokenizerControl()) 205 - ->setDatasource(new PhabricatorPeopleDatasource()) 210 + ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()) 206 211 ->setName('invited') 207 212 ->setLabel(pht('Invited')) 208 213 ->setValue($invited_phids))
+3 -3
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 50 50 51 51 if ($mode == 'public') { 52 52 $view_policy = PhabricatorPolicies::getMostOpenPolicy(); 53 - } else if ($mode == 'recurring') { 53 + } 54 + 55 + if ($mode == 'recurring') { 54 56 $is_recurring = true; 55 - } else { 56 - $view_policy = $actor->getPHID(); 57 57 } 58 58 59 59 return id(new PhabricatorCalendarEvent())
+3 -3
src/applications/config/check/PhabricatorAuthSetupCheck.php
··· 27 27 'You have not configured any authentication providers yet. You '. 28 28 'should add a provider (like username/password, LDAP, or GitHub '. 29 29 'OAuth) so users can register and log in. You can add and configure '. 30 - 'providers using the [[%s | "Auth" application]].', 31 - '/auth/'); 30 + 'providers using the Auth Application.'); 32 31 33 32 $this 34 33 ->newIssue('auth.noproviders') 35 34 ->setShortName(pht('No Auth Providers')) 36 35 ->setName(pht('No Authentication Providers Configured')) 37 - ->setMessage($message); 36 + ->setMessage($message) 37 + ->addLink('/auth/', pht('Auth Application')); 38 38 } 39 39 } 40 40 }
+1 -2
src/applications/conpherence/query/ConpherenceThreadQuery.php
··· 355 355 $start_epoch = $epochs['start_epoch']; 356 356 $end_epoch = $epochs['end_epoch']; 357 357 358 + $events = array(); 358 359 if ($participant_phids) { 359 360 $events = id(new PhabricatorCalendarEventQuery()) 360 361 ->setViewer($this->getViewer()) ··· 363 364 ->withDateRange($start_epoch, $end_epoch) 364 365 ->execute(); 365 366 $events = mpull($events, null, 'getPHID'); 366 - } else { 367 - $events = null; 368 367 } 369 368 370 369 $invitees = array();
+13
src/applications/diviner/application/PhabricatorDivinerApplication.php
··· 39 39 'find/' => 'DivinerFindController', 40 40 ), 41 41 '/book/(?P<book>[^/]+)/' => 'DivinerBookController', 42 + '/book/(?P<book>[^/]+)/edit/' => 'DivinerBookEditController', 42 43 '/book/'. 43 44 '(?P<book>[^/]+)/'. 44 45 '(?P<type>[^/]+)/'. ··· 50 51 51 52 public function getApplicationGroup() { 52 53 return self::GROUP_UTILITIES; 54 + } 55 + 56 + protected function getCustomCapabilities() { 57 + return array( 58 + DivinerDefaultViewCapability::CAPABILITY => array( 59 + 'template' => DivinerBookPHIDType::TYPECONST, 60 + ), 61 + DivinerDefaultEditCapability::CAPABILITY => array( 62 + 'default' => PhabricatorPolicies::POLICY_ADMIN, 63 + 'template' => DivinerBookPHIDType::TYPECONST, 64 + ), 65 + ); 53 66 } 54 67 55 68 public function getRemarkupRules() {
+1 -1
src/applications/diviner/atomizer/DivinerArticleAtomizer.php
··· 20 20 $atom->setDocblockMetaValue('title', $title); 21 21 } 22 22 23 - // If the article has no @name, use the filename after stripping any 23 + // If the article has no `@name`, use the filename after stripping any 24 24 // extension. 25 25 $name = idx($meta, 'name'); 26 26 if (!$name) {
+11 -11
src/applications/diviner/atomizer/DivinerPHPAtomizer.php
··· 151 151 if (count($docs) < count($params)) { 152 152 $atom->addWarning( 153 153 pht( 154 - 'This call takes %d parameters, but only %d are documented.', 155 - count($params), 156 - count($docs))); 154 + 'This call takes %s parameter(s), but only %s are documented.', 155 + new PhutilNumber(count($params)), 156 + new PhutilNumber(count($docs)))); 157 157 } 158 158 } 159 159 ··· 212 212 if (preg_match('/@(return|param|task|author)/', $value, $matches)) { 213 213 $atom->addWarning( 214 214 pht( 215 - 'Atom "%s" is preceded by a comment containing "@%s", but the '. 216 - 'comment is not a documentation comment. Documentation '. 217 - 'comments must begin with "%s", followed by a newline. Did '. 215 + 'Atom "%s" is preceded by a comment containing `%s`, but '. 216 + 'the comment is not a documentation comment. Documentation '. 217 + 'comments must begin with `%s`, followed by a newline. Did '. 218 218 'you mean to use a documentation comment? (As the comment is '. 219 219 'not a documentation comment, it will be ignored.)', 220 220 $atom->getName(), 221 - $matches[1], 221 + '@'.$matches[1], 222 222 '/**')); 223 223 } 224 224 } ··· 248 248 if ($matches[1] !== $name) { 249 249 $atom->addWarning( 250 250 pht( 251 - 'Parameter "%s" is named "%s" in the documentation. The '. 252 - 'documentation may be out of date.', 251 + 'Parameter "%s" is named "%s" in the documentation. '. 252 + 'The documentation may be out of date.', 253 253 $name, 254 254 $matches[1])); 255 255 } ··· 292 292 if ($return) { 293 293 $atom->addWarning( 294 294 pht( 295 - 'Method %s has explicitly documented %s. The %s method always '. 296 - 'returns %s. Diviner documents this implicitly.', 295 + 'Method `%s` has explicitly documented `%s`. The `%s` method '. 296 + 'always returns `%s`. Diviner documents this implicitly.', 297 297 '__construct()', 298 298 '@return', 299 299 '__construct()',
+1
src/applications/diviner/cache/DivinerAtomCache.php
··· 18 18 19 19 public function delete() { 20 20 parent::delete(); 21 + 21 22 $this->fileHashMap = null; 22 23 $this->atomMap = null; 23 24 $this->atoms = array();
+2 -2
src/applications/diviner/cache/DivinerDiskCache.php
··· 26 26 * Convert a long-form hash key like `ccbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaN` into 27 27 * a shortened directory form, like `cc/bb/aaaaaaaaN`. In conjunction with 28 28 * @{class:PhutilDirectoryKeyValueCache}, this gives us nice directories 29 - * inside .divinercache instead of a million hash files with huge names at 30 - * top level. 29 + * inside `.divinercache` instead of a million hash files with huge names at 30 + * the top level. 31 31 */ 32 32 protected function getHashKey($hash) { 33 33 return implode(
+1
src/applications/diviner/cache/DivinerPublishCache.php
··· 45 45 46 46 /* -( Index )-------------------------------------------------------------- */ 47 47 48 + 48 49 public function getIndex() { 49 50 if ($this->index === null) { 50 51 $this->index = $this->getCache()->getKey('index', array());
+11
src/applications/diviner/capability/DivinerDefaultEditCapability.php
··· 1 + <?php 2 + 3 + final class DivinerDefaultEditCapability extends PhabricatorPolicyCapability { 4 + 5 + const CAPABILITY = 'diviner.default.edit'; 6 + 7 + public function getCapabilityName() { 8 + return pht('Default Edit Policy'); 9 + } 10 + 11 + }
+15
src/applications/diviner/capability/DivinerDefaultViewCapability.php
··· 1 + <?php 2 + 3 + final class DivinerDefaultViewCapability extends PhabricatorPolicyCapability { 4 + 5 + const CAPABILITY = 'diviner.default.view'; 6 + 7 + public function getCapabilityName() { 8 + return pht('Default View Policy'); 9 + } 10 + 11 + public function shouldAllowPublicPolicySetting() { 12 + return true; 13 + } 14 + 15 + }
+15 -26
src/applications/diviner/controller/DivinerAtomController.php
··· 2 2 3 3 final class DivinerAtomController extends DivinerController { 4 4 5 - private $bookName; 6 - private $atomType; 7 - private $atomName; 8 - private $atomContext; 9 - private $atomIndex; 10 - 11 5 public function shouldAllowPublic() { 12 6 return true; 13 7 } 14 8 15 - public function willProcessRequest(array $data) { 16 - $this->bookName = $data['book']; 17 - $this->atomType = $data['type']; 18 - $this->atomName = $data['name']; 19 - $this->atomContext = nonempty(idx($data, 'context'), null); 20 - $this->atomIndex = nonempty(idx($data, 'index'), null); 21 - } 9 + public function handleRequest(AphrontRequest $request) { 10 + $viewer = $request->getUser(); 22 11 23 - public function processRequest() { 24 - $request = $this->getRequest(); 25 - $viewer = $request->getUser(); 12 + $book_name = $request->getURIData('book'); 13 + $atom_type = $request->getURIData('type'); 14 + $atom_name = $request->getURIData('name'); 15 + $atom_context = nonempty($request->getURIData('context'), null); 16 + $atom_index = nonempty($request->getURIData('index'), null); 26 17 27 18 require_celerity_resource('diviner-shared-css'); 28 19 29 20 $book = id(new DivinerBookQuery()) 30 21 ->setViewer($viewer) 31 - ->withNames(array($this->bookName)) 22 + ->withNames(array($book_name)) 32 23 ->executeOne(); 33 24 34 25 if (!$book) { ··· 38 29 $symbol = id(new DivinerAtomQuery()) 39 30 ->setViewer($viewer) 40 31 ->withBookPHIDs(array($book->getPHID())) 41 - ->withTypes(array($this->atomType)) 42 - ->withNames(array($this->atomName)) 43 - ->withContexts(array($this->atomContext)) 44 - ->withIndexes(array($this->atomIndex)) 32 + ->withTypes(array($atom_type)) 33 + ->withNames(array($atom_name)) 34 + ->withContexts(array($atom_context)) 35 + ->withIndexes(array($atom_index)) 45 36 ->withIsDocumentable(true) 46 37 ->needAtoms(true) 47 38 ->needExtends(true) ··· 75 66 ->setName(DivinerAtom::getAtomTypeNameString( 76 67 $atom ? $atom->getType() : $symbol->getType()))); 77 68 78 - $properties = id(new PHUIPropertyListView()); 69 + $properties = new PHUIPropertyListView(); 79 70 80 71 $group = $atom ? $atom->getProperty('group') : $symbol->getGroupName(); 81 72 if ($group) { ··· 133 124 $document->appendChild( 134 125 id(new PHUIInfoView()) 135 126 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 136 - ->appendChild( 137 - pht( 138 - 'This atom no longer exists.'))); 127 + ->appendChild(pht('This atom no longer exists.'))); 139 128 } 140 129 141 130 if ($atom) { ··· 303 292 pht('Implements'), 304 293 phutil_implode_html(phutil_tag('br'), $items)); 305 294 } 306 - 307 295 } 308 296 309 297 private function renderAtomTag(DivinerLiveSymbol $symbol) { ··· 470 458 private function renderFullSignature( 471 459 DivinerLiveSymbol $symbol, 472 460 $is_link = false) { 461 + 473 462 switch ($symbol->getType()) { 474 463 case DivinerAtom::TYPE_CLASS: 475 464 case DivinerAtom::TYPE_INTERFACE:
+3 -8
src/applications/diviner/controller/DivinerAtomListController.php
··· 2 2 3 3 final class DivinerAtomListController extends DivinerController { 4 4 5 - private $key; 6 - 7 5 public function shouldAllowPublic() { 8 6 return true; 9 7 } 10 8 11 - public function willProcessRequest(array $data) { 12 - $this->key = idx($data, 'key', 'all'); 13 - } 9 + public function handleRequest(AphrontRequest $request) { 10 + $query_key = $request->getURIData('key'); 14 11 15 - public function processRequest() { 16 - $request = $this->getRequest(); 17 12 $controller = id(new PhabricatorApplicationSearchController()) 18 - ->setQueryKey($this->key) 13 + ->setQueryKey($query_key) 19 14 ->setSearchEngine(new DivinerAtomSearchEngine()) 20 15 ->setNavigation($this->buildSideNavView()); 21 16
+40 -11
src/applications/diviner/controller/DivinerBookController.php
··· 2 2 3 3 final class DivinerBookController extends DivinerController { 4 4 5 - private $bookName; 6 - 7 5 public function shouldAllowPublic() { 8 6 return true; 9 7 } 10 8 11 - public function willProcessRequest(array $data) { 12 - $this->bookName = $data['book']; 13 - } 9 + public function handleRequest(AphrontRequest $request) { 10 + $viewer = $request->getViewer(); 14 11 15 - public function processRequest() { 16 - $request = $this->getRequest(); 17 - $viewer = $request->getUser(); 12 + $book_name = $request->getURIData('book'); 18 13 19 14 $book = id(new DivinerBookQuery()) 20 15 ->setViewer($viewer) 21 - ->withNames(array($this->bookName)) 16 + ->withNames(array($book_name)) 22 17 ->executeOne(); 23 18 24 19 if (!$book) { 25 20 return new Aphront404Response(); 26 21 } 27 22 23 + $actions = $this->buildActionView($viewer, $book); 24 + 28 25 $crumbs = $this->buildApplicationCrumbs(); 29 26 $crumbs->setBorder(true); 30 - 31 27 $crumbs->addTextCrumb( 32 28 $book->getShortTitle(), 33 29 '/book/'.$book->getName().'/'); 34 30 31 + $action_button = id(new PHUIButtonView()) 32 + ->setTag('a') 33 + ->setText(pht('Actions')) 34 + ->setHref('#') 35 + ->setIconFont('fa-bars') 36 + ->addClass('phui-mobile-menu') 37 + ->setDropdownMenu($actions); 38 + 35 39 $header = id(new PHUIHeaderView()) 36 40 ->setHeader($book->getTitle()) 37 41 ->setUser($viewer) 38 42 ->setPolicyObject($book) 39 - ->setEpoch($book->getDateModified()); 43 + ->setEpoch($book->getDateModified()) 44 + ->addActionLink($action_button); 40 45 41 46 $document = new PHUIDocumentView(); 42 47 $document->setHeader($header); ··· 97 102 array( 98 103 'title' => $book->getTitle(), 99 104 )); 105 + } 106 + 107 + private function buildActionView( 108 + PhabricatorUser $user, 109 + DivinerLiveBook $book) { 110 + 111 + $can_edit = PhabricatorPolicyFilter::hasCapability( 112 + $user, 113 + $book, 114 + PhabricatorPolicyCapability::CAN_EDIT); 115 + 116 + $action_view = id(new PhabricatorActionListView()) 117 + ->setUser($user) 118 + ->setObject($book) 119 + ->setObjectURI($this->getRequest()->getRequestURI()); 120 + 121 + $action_view->addAction( 122 + id(new PhabricatorActionView()) 123 + ->setName(pht('Edit Book')) 124 + ->setIcon('fa-pencil') 125 + ->setHref('/book/'.$book->getName().'/edit/') 126 + ->setDisabled(!$can_edit)); 127 + 128 + return $action_view; 100 129 } 101 130 102 131 }
+117
src/applications/diviner/controller/DivinerBookEditController.php
··· 1 + <?php 2 + 3 + final class DivinerBookEditController extends DivinerController { 4 + 5 + public function handleRequest(AphrontRequest $request) { 6 + $viewer = $request->getViewer(); 7 + 8 + $book_name = $request->getURIData('book'); 9 + 10 + $book = id(new DivinerBookQuery()) 11 + ->setViewer($viewer) 12 + ->requireCapabilities( 13 + array( 14 + PhabricatorPolicyCapability::CAN_VIEW, 15 + PhabricatorPolicyCapability::CAN_EDIT, 16 + )) 17 + ->needProjectPHIDs(true) 18 + ->withNames(array($book_name)) 19 + ->executeOne(); 20 + 21 + if (!$book) { 22 + return new Aphront404Response(); 23 + } 24 + 25 + $view_uri = '/book/'.$book->getName().'/'; 26 + 27 + if ($request->isFormPost()) { 28 + $v_projects = $request->getArr('projectPHIDs'); 29 + $v_view = $request->getStr('viewPolicy'); 30 + $v_edit = $request->getStr('editPolicy'); 31 + 32 + $xactions = array(); 33 + $xactions[] = id(new DivinerLiveBookTransaction()) 34 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 35 + ->setMetadataValue( 36 + 'edge:type', 37 + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) 38 + ->setNewValue( 39 + array( 40 + '=' => array_fuse($v_projects), 41 + )); 42 + $xactions[] = id(new DivinerLiveBookTransaction()) 43 + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) 44 + ->setNewValue($v_view); 45 + $xactions[] = id(new DivinerLiveBookTransaction()) 46 + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) 47 + ->setNewValue($v_edit); 48 + 49 + id(new DivinerLiveBookEditor()) 50 + ->setContinueOnNoEffect(true) 51 + ->setContentSourceFromRequest($request) 52 + ->setActor($viewer) 53 + ->applyTransactions($book, $xactions); 54 + 55 + return id(new AphrontRedirectResponse())->setURI($view_uri); 56 + } 57 + 58 + $crumbs = $this->buildApplicationCrumbs(); 59 + $crumbs->addTextCrumb(pht('Edit Basics')); 60 + 61 + $title = pht('Edit %s', $book->getTitle()); 62 + 63 + $policies = id(new PhabricatorPolicyQuery()) 64 + ->setViewer($viewer) 65 + ->setObject($book) 66 + ->execute(); 67 + $view_capability = PhabricatorPolicyCapability::CAN_VIEW; 68 + $edit_capability = PhabricatorPolicyCapability::CAN_EDIT; 69 + 70 + $form = id(new AphrontFormView()) 71 + ->setUser($viewer) 72 + ->appendControl( 73 + id(new AphrontFormTokenizerControl()) 74 + ->setDatasource(new PhabricatorProjectDatasource()) 75 + ->setName('projectPHIDs') 76 + ->setLabel(pht('Projects')) 77 + ->setValue($book->getProjectPHIDs())) 78 + ->appendChild( 79 + id(new AphrontFormPolicyControl()) 80 + ->setName('viewPolicy') 81 + ->setPolicyObject($book) 82 + ->setCapability($view_capability) 83 + ->setPolicies($policies) 84 + ->setCaption($book->describeAutomaticCapability($view_capability))) 85 + ->appendChild( 86 + id(new AphrontFormPolicyControl()) 87 + ->setName('editPolicy') 88 + ->setPolicyObject($book) 89 + ->setCapability($edit_capability) 90 + ->setPolicies($policies) 91 + ->setCaption($book->describeAutomaticCapability($edit_capability))) 92 + ->appendChild( 93 + id(new AphrontFormSubmitControl()) 94 + ->setValue(pht('Save')) 95 + ->addCancelButton($view_uri)); 96 + 97 + $object_box = id(new PHUIObjectBoxView()) 98 + ->setHeaderText($title) 99 + ->setForm($form); 100 + 101 + $timeline = $this->buildTransactionTimeline( 102 + $book, 103 + new DivinerLiveBookTransactionQuery()); 104 + $timeline->setShouldTerminate(true); 105 + 106 + return $this->buildApplicationPage( 107 + array( 108 + $crumbs, 109 + $object_box, 110 + $timeline, 111 + ), 112 + array( 113 + 'title' => $title, 114 + )); 115 + } 116 + 117 + }
+3 -12
src/applications/diviner/controller/DivinerController.php
··· 3 3 abstract class DivinerController extends PhabricatorController { 4 4 5 5 protected function buildSideNavView() { 6 - $menu = $this->buildMenu(); 6 + $menu = $this->buildApplicationMenu(); 7 7 return AphrontSideNavFilterView::newFromMenu($menu); 8 8 } 9 9 10 10 public function buildApplicationMenu() { 11 - return $this->buildMenu(); 12 - } 13 - 14 - private function buildMenu() { 15 11 $menu = new PHUIListView(); 16 12 17 13 id(new DivinerAtomSearchEngine()) 18 - ->setViewer($this->getRequest()->getUser()) 14 + ->setViewer($this->getRequest()->getViewer()) 19 15 ->addNavigationItems($menu); 20 16 21 17 return $menu; ··· 24 20 protected function renderAtomList(array $symbols) { 25 21 assert_instances_of($symbols, 'DivinerLiveSymbol'); 26 22 27 - $request = $this->getRequest(); 28 - $user = $request->getUser(); 29 - 30 23 $list = array(); 31 24 foreach ($symbols as $symbol) { 32 - 33 25 switch ($symbol->getType()) { 34 26 case DivinerAtom::TYPE_FUNCTION: 35 27 $title = $symbol->getTitle().'()'; ··· 43 35 ->setTitle($title) 44 36 ->setHref($symbol->getURI()) 45 37 ->setSubtitle($symbol->getSummary()) 46 - ->setType(DivinerAtom::getAtomTypeNameString( 47 - $symbol->getType())); 38 + ->setType(DivinerAtom::getAtomTypeNameString($symbol->getType())); 48 39 49 40 $list[] = $item; 50 41 }
+6 -6
src/applications/diviner/controller/DivinerFindController.php
··· 6 6 return true; 7 7 } 8 8 9 - public function processRequest() { 10 - $request = $this->getRequest(); 11 - $viewer = $request->getUser(); 9 + public function handleRequest(AphrontRequest $request) { 10 + $viewer = $request->getViewer(); 12 11 13 - $book_name = $request->getStr('book'); 12 + $book_name = $request->getStr('book'); 14 13 $query_text = $request->getStr('name'); 15 14 16 15 $book = null; ··· 19 18 ->setViewer($viewer) 20 19 ->withNames(array($book_name)) 21 20 ->executeOne(); 21 + 22 22 if (!$book) { 23 23 return new Aphront404Response(); 24 24 } ··· 70 70 ->setTitle(pht('Documentation Not Found')) 71 71 ->appendChild( 72 72 pht( 73 - 'Unable to find the specified documentation. You may have '. 74 - 'followed a bad or outdated link.')) 73 + 'Unable to find the specified documentation. '. 74 + 'You may have followed a bad or outdated link.')) 75 75 ->addCancelButton($not_found_uri, pht('Read More Documentation')); 76 76 77 77 return id(new AphrontDialogResponse())->setDialog($dialog);
+10 -15
src/applications/diviner/controller/DivinerMainController.php
··· 6 6 return true; 7 7 } 8 8 9 - public function processRequest() { 10 - $request = $this->getRequest(); 11 - $viewer = $request->getUser(); 9 + public function handleRequest(AphrontRequest $request) { 10 + $viewer = $request->getViewer(); 12 11 13 12 $books = id(new DivinerBookQuery()) 14 13 ->setViewer($viewer) ··· 53 52 ->appendChild($list); 54 53 55 54 $document->appendChild($list); 56 - 57 55 } else { 58 56 $text = pht( 59 - "(NOTE) **Looking for Phabricator documentation?** If you're looking ". 60 - "for help and information about Phabricator, you can ". 61 - "[[ https://secure.phabricator.com/diviner/ | browse the public ". 62 - "Phabricator documentation ]] on the live site.\n\n". 63 - "Diviner is the documentation generator used to build the Phabricator ". 64 - "documentation.\n\n". 57 + "(NOTE) **Looking for Phabricator documentation?** ". 58 + "If you're looking for help and information about Phabricator, ". 59 + "you can [[https://secure.phabricator.com/diviner/ | ". 60 + "browse the public Phabricator documentation]] on the live site.\n\n". 61 + "Diviner is the documentation generator used to build the ". 62 + "Phabricator documentation.\n\n". 65 63 "You haven't generated any Diviner documentation books yet, so ". 66 64 "there's nothing to show here. If you'd like to generate your own ". 67 65 "local copy of the Phabricator documentation and have it appear ". 68 66 "here, run this command:\n\n". 69 - " phabricator/ $ ./bin/diviner generate\n\n". 70 - "Right now, Diviner isn't very useful for generating documentation ". 71 - "for projects other than Phabricator. If you're interested in using ". 72 - "it in your own projects, leave feedback for us on ". 73 - "[[ https://secure.phabricator.com/T4558 | T4558 ]]."); 67 + " %s\n\n", 68 + 'phabricator/ $ ./bin/diviner generate'); 74 69 75 70 $text = PhabricatorMarkupEngine::renderOneObject( 76 71 id(new PhabricatorMarkupOneOff())->setContent($text),
+23
src/applications/diviner/editor/DivinerLiveBookEditor.php
··· 1 + <?php 2 + 3 + final class DivinerLiveBookEditor 4 + extends PhabricatorApplicationTransactionEditor { 5 + 6 + public function getEditorApplicationClass() { 7 + return 'PhabricatorDivinerApplication'; 8 + } 9 + 10 + public function getEditorObjectsDescription() { 11 + return pht('Diviner Books'); 12 + } 13 + 14 + public function getTransactionTypes() { 15 + $types = parent::getTransactionTypes(); 16 + 17 + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 18 + $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 19 + 20 + return $types; 21 + } 22 + 23 + }
+8
src/applications/diviner/phid/DivinerAtomPHIDType.php
··· 12 12 return new DivinerLiveSymbol(); 13 13 } 14 14 15 + public function getTypeIcon() { 16 + return 'fa-cube'; 17 + } 18 + 19 + public function getPHIDTypeApplicationClass() { 20 + return 'PhabricatorDivinerApplication'; 21 + } 22 + 15 23 protected function buildQueryForObjects( 16 24 PhabricatorObjectQuery $query, 17 25 array $phids) {
+8
src/applications/diviner/phid/DivinerBookPHIDType.php
··· 12 12 return new DivinerLiveBook(); 13 13 } 14 14 15 + public function getTypeIcon() { 16 + return 'fa-book'; 17 + } 18 + 19 + public function getPHIDTypeApplicationClass() { 20 + return 'PhabricatorDivinerApplication'; 21 + } 22 + 15 23 protected function buildQueryForObjects( 16 24 PhabricatorObjectQuery $query, 17 25 array $phids) {
+5 -2
src/applications/diviner/publisher/DivinerLivePublisher.php
··· 8 8 if (!$this->book) { 9 9 $book_name = $this->getConfig('name'); 10 10 11 - $book = id(new DivinerLiveBook())->loadOneWhere('name = %s', $book_name); 11 + $book = id(new DivinerLiveBook())->loadOneWhere( 12 + 'name = %s', 13 + $book_name); 14 + 12 15 if (!$book) { 13 16 $book = id(new DivinerLiveBook()) 14 17 ->setName($book_name) 15 18 ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) 19 + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) 16 20 ->save(); 17 21 } 18 22 ··· 144 148 ->setContent(null) 145 149 ->save(); 146 150 } 147 - 148 151 } 149 152 } 150 153
+12 -2
src/applications/diviner/publisher/DivinerPublisher.php
··· 133 133 $created = array_keys($created); 134 134 } 135 135 136 - echo pht('Deleting %d documents.', count($deleted))."\n"; 136 + $console = PhutilConsole::getConsole(); 137 + 138 + $console->writeOut( 139 + "%s\n", 140 + pht( 141 + 'Deleting %s document(s).', 142 + new PhutilNumber(count($deleted)))); 137 143 $this->deleteDocumentsByHash($deleted); 138 144 139 - echo pht('Creating %d documents.', count($created))."\n"; 145 + $console->writeOut( 146 + "%s\n", 147 + pht( 148 + 'Creating %s document(s).', 149 + new PhutilNumber(count($created)))); 140 150 $this->createDocumentsByHash($created); 141 151 } 142 152
+8 -9
src/applications/diviner/query/DivinerAtomQuery.php
··· 79 79 return $this; 80 80 } 81 81 82 - 83 82 /** 84 83 * Include or exclude "ghosts", which are symbols which used to exist but do 85 84 * not exist currently (for example, a function which existed in an older ··· 137 136 foreach ($atoms as $key => $atom) { 138 137 $book = idx($books, $atom->getBookPHID()); 139 138 if (!$book) { 139 + $this->didRejectResult($atom); 140 140 unset($atoms[$key]); 141 141 continue; 142 142 } ··· 158 158 // Load all of the symbols this symbol extends, recursively. Commonly, 159 159 // this means all the ancestor classes and interfaces it extends and 160 160 // implements. 161 - 162 161 if ($this->needExtends) { 163 - 164 162 // First, load all the matching symbols by name. This does 99% of the 165 163 // work in most cases, assuming things are named at all reasonably. 166 - 167 164 $names = array(); 168 165 foreach ($atoms as $atom) { 169 166 if (!$atom->getAtom()) { ··· 303 300 304 301 if ($this->titles) { 305 302 $hashes = array(); 303 + 306 304 foreach ($this->titles as $title) { 307 305 $slug = DivinerAtomRef::normalizeTitleString($title); 308 306 $hash = PhabricatorHash::digestForIndex($slug); ··· 318 316 if ($this->contexts) { 319 317 $with_null = false; 320 318 $contexts = $this->contexts; 319 + 321 320 foreach ($contexts as $key => $value) { 322 321 if ($value === null) { 323 322 unset($contexts[$key]); ··· 373 372 } 374 373 375 374 if ($this->nameContains) { 376 - // NOTE: This CONVERT() call makes queries case-insensitive, since the 377 - // column has binary collation. Eventually, this should move into 375 + // NOTE: This `CONVERT()` call makes queries case-insensitive, since 376 + // the column has binary collation. Eventually, this should move into 378 377 // fulltext. 379 - 380 378 $where[] = qsprintf( 381 379 $conn_r, 382 380 'CONVERT(name USING utf8) LIKE %~', ··· 387 385 388 386 return $this->formatWhereClause($where); 389 387 } 390 - 391 388 392 389 /** 393 390 * Walk a list of atoms and collect all the node hashes of the atoms' ··· 413 410 foreach ($child_hashes as $hash) { 414 411 $hashes[$hash] = $hash; 415 412 } 413 + 416 414 if ($recurse_up) { 417 415 $hashes += $this->getAllChildHashes($symbol->getExtends(), true); 418 416 } ··· 420 418 421 419 return $hashes; 422 420 } 423 - 424 421 425 422 /** 426 423 * Attach child atoms to existing atoms. In recursive mode, also attach child ··· 452 449 $symbol_children[] = $children[$hash]; 453 450 } 454 451 } 452 + 455 453 $symbol->attachChildren($symbol_children); 454 + 456 455 if ($recurse_up) { 457 456 $this->attachAllChildren($symbol->getExtends(), $children, true); 458 457 }
+31
src/applications/diviner/query/DivinerBookQuery.php
··· 6 6 private $phids; 7 7 private $names; 8 8 9 + private $needProjectPHIDs; 10 + 9 11 public function withIDs(array $ids) { 10 12 $this->ids = $ids; 11 13 return $this; ··· 21 23 return $this; 22 24 } 23 25 26 + public function needProjectPHIDs($need_phids) { 27 + $this->needProjectPHIDs = $need_phids; 28 + return $this; 29 + } 30 + 24 31 protected function loadPage() { 25 32 $table = new DivinerLiveBook(); 26 33 $conn_r = $table->establishConnection('r'); ··· 34 41 $this->buildLimitClause($conn_r)); 35 42 36 43 return $table->loadAllFromArray($data); 44 + } 45 + 46 + protected function didFilterPage(array $books) { 47 + assert_instances_of($books, 'DivinerLiveBook'); 48 + 49 + if ($this->needProjectPHIDs) { 50 + $edge_query = id(new PhabricatorEdgeQuery()) 51 + ->withSourcePHIDs(mpull($books, 'getPHID')) 52 + ->withEdgeTypes( 53 + array( 54 + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 55 + )); 56 + $edge_query->execute(); 57 + 58 + foreach ($books as $book) { 59 + $project_phids = $edge_query->getDestinationPHIDs( 60 + array( 61 + $book->getPHID(), 62 + )); 63 + $book->attachProjectPHIDs($project_phids); 64 + } 65 + } 66 + 67 + return $books; 37 68 } 38 69 39 70 protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
+10
src/applications/diviner/query/DivinerLiveBookTransactionQuery.php
··· 1 + <?php 2 + 3 + final class DivinerLiveBookTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + public function getTemplateApplicationTransaction() { 7 + return new DivinerLiveBookTransaction(); 8 + } 9 + 10 + }
+2 -3
src/applications/diviner/renderer/DivinerDefaultRenderer.php
··· 202 202 } 203 203 204 204 if ($ref->getBook() != $this->getConfig('name')) { 205 - // If the ref is from a different book, we can't normalize it. Just return 206 - // it as-is if it has enough information to resolve. 205 + // If the ref is from a different book, we can't normalize it. 206 + // Just return it as-is if it has enough information to resolve. 207 207 if ($ref->getName() && $ref->getType()) { 208 208 return $ref; 209 209 } else { ··· 259 259 ), 260 260 $ref->getTitle()); 261 261 } 262 - 263 262 264 263 }
+9 -1
src/applications/diviner/search/DivinerAtomSearchIndexer.php
··· 27 27 PhabricatorSearchRelationship::RELATIONSHIP_BOOK, 28 28 $atom->getBookPHID(), 29 29 DivinerBookPHIDType::TYPECONST, 30 - $book->getDateCreated()); 30 + PhabricatorTime::getNow()); 31 + 32 + $doc->addRelationship( 33 + $atom->getGraphHash() 34 + ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED 35 + : PhabricatorSearchRelationship::RELATIONSHIP_OPEN, 36 + $atom->getBookPHID(), 37 + DivinerBookPHIDType::TYPECONST, 38 + PhabricatorTime::getNow()); 31 39 32 40 return $doc; 33 41 }
+5
src/applications/diviner/search/DivinerBookSearchIndexer.php
··· 18 18 PhabricatorSearchDocumentFieldType::FIELD_BODY, 19 19 $book->getPreface()); 20 20 21 + $this->indexTransactions( 22 + $doc, 23 + new DivinerLiveBookTransactionQuery(), 24 + array($phid)); 25 + 21 26 return $doc; 22 27 } 23 28
+50 -3
src/applications/diviner/storage/DivinerLiveBook.php
··· 3 3 final class DivinerLiveBook extends DivinerDAO 4 4 implements 5 5 PhabricatorPolicyInterface, 6 - PhabricatorDestructibleInterface { 6 + PhabricatorProjectInterface, 7 + PhabricatorDestructibleInterface, 8 + PhabricatorApplicationTransactionInterface { 7 9 8 10 protected $name; 9 11 protected $viewPolicy; 12 + protected $editPolicy; 10 13 protected $configurationData = array(); 14 + 15 + private $projectPHIDs = self::ATTACHABLE; 11 16 12 17 protected function getConfiguration() { 13 18 return array( ··· 63 68 return idx($spec, 'name', $group); 64 69 } 65 70 71 + public function attachProjectPHIDs(array $project_phids) { 72 + $this->projectPHIDs = $project_phids; 73 + return $this; 74 + } 75 + 76 + public function getProjectPHIDs() { 77 + return $this->assertAttached($this->projectPHIDs); 78 + } 79 + 80 + 66 81 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 67 82 83 + 68 84 public function getCapabilities() { 69 85 return array( 70 86 PhabricatorPolicyCapability::CAN_VIEW, 87 + PhabricatorPolicyCapability::CAN_EDIT, 71 88 ); 72 89 } 73 90 74 91 public function getPolicy($capability) { 75 - return PhabricatorPolicies::getMostOpenPolicy(); 92 + switch ($capability) { 93 + case PhabricatorPolicyCapability::CAN_VIEW: 94 + return $this->getViewPolicy(); 95 + case PhabricatorPolicyCapability::CAN_EDIT: 96 + return $this->getEditPolicy(); 97 + } 76 98 } 77 99 78 100 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 79 - return false; 101 + return false; 80 102 } 81 103 82 104 public function describeAutomaticCapability($capability) { 83 105 return null; 84 106 } 85 107 108 + 86 109 /* -( PhabricatorDestructibleInterface )----------------------------------- */ 110 + 87 111 88 112 public function destroyObjectPermanently( 89 113 PhabricatorDestructionEngine $engine) { ··· 100 124 101 125 $this->delete(); 102 126 $this->saveTransaction(); 127 + } 128 + 129 + 130 + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 131 + 132 + 133 + public function getApplicationTransactionEditor() { 134 + return new DivinerLiveBookEditor(); 135 + } 136 + 137 + public function getApplicationTransactionObject() { 138 + return $this; 139 + } 140 + 141 + public function getApplicationTransactionTemplate() { 142 + return new DivinerLiveBookTransaction(); 143 + } 144 + 145 + public function willRenderTimeline( 146 + PhabricatorApplicationTransactionView $timeline, 147 + AphrontRequest $request) { 148 + 149 + return $timeline; 103 150 } 104 151 105 152 }
+18
src/applications/diviner/storage/DivinerLiveBookTransaction.php
··· 1 + <?php 2 + 3 + final class DivinerLiveBookTransaction 4 + extends PhabricatorApplicationTransaction { 5 + 6 + public function getApplicationName() { 7 + return 'diviner'; 8 + } 9 + 10 + public function getApplicationTransactionType() { 11 + return DivinerBookPHIDType::TYPECONST; 12 + } 13 + 14 + public function getApplicationTransactionCommentObject() { 15 + return null; 16 + } 17 + 18 + }
+11 -14
src/applications/diviner/storage/DivinerLiveSymbol.php
··· 137 137 } 138 138 139 139 public function save() { 140 - 141 140 // NOTE: The identity hash is just a sanity check because the unique tuple 142 - // on this table is way way too long to fit into a normal UNIQUE KEY. We 143 - // don't use it directly, but its existence prevents duplicate records. 141 + // on this table is way way too long to fit into a normal `UNIQUE KEY`. 142 + // We don't use it directly, but its existence prevents duplicate records. 144 143 145 144 if (!$this->identityHash) { 146 145 $this->identityHash = PhabricatorHash::digestForIndex( ··· 159 158 160 159 public function getTitle() { 161 160 $title = parent::getTitle(); 161 + 162 162 if (!strlen($title)) { 163 163 $title = $this->getName(); 164 164 } 165 + 165 166 return $title; 166 167 } 167 168 168 169 public function setTitle($value) { 169 170 $this->writeField('title', $value); 171 + 170 172 if (strlen($value)) { 171 173 $slug = DivinerAtomRef::normalizeTitleString($value); 172 174 $hash = PhabricatorHash::digestForIndex($slug); ··· 174 176 } else { 175 177 $this->titleSlugHash = null; 176 178 } 179 + 177 180 return $this; 178 181 } 179 182 ··· 199 202 200 203 201 204 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 205 + 202 206 203 207 public function getCapabilities() { 204 208 return $this->getBook()->getCapabilities(); 205 209 } 206 210 207 - 208 211 public function getPolicy($capability) { 209 212 return $this->getBook()->getPolicy($capability); 210 213 } 211 - 212 214 213 215 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 214 216 return $this->getBook()->hasAutomaticCapability($capability, $viewer); ··· 219 221 } 220 222 221 223 222 - /* -( Markup Interface )--------------------------------------------------- */ 224 + /* -( PhabricatorMarkupInterface )------------------------------------------ */ 223 225 224 226 225 227 public function getMarkupFieldKey($field) { 226 228 return $this->getPHID().':'.$field.':'.$this->getGraphHash(); 227 229 } 228 230 229 - 230 231 public function newMarkupEngine($field) { 231 232 return PhabricatorMarkupEngine::getEngine('diviner'); 232 233 } 233 - 234 234 235 235 public function getMarkupText($field) { 236 236 if (!$this->getAtom()) { ··· 240 240 return $this->getAtom()->getDocblockText(); 241 241 } 242 242 243 - 244 - public function didMarkupText( 245 - $field, 246 - $output, 247 - PhutilMarkupEngine $engine) { 243 + public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { 248 244 return $output; 249 245 } 250 246 251 - 252 247 public function shouldUseMarkupCache($field) { 253 248 return true; 254 249 } 255 250 251 + 256 252 /* -( PhabricatorDestructibleInterface )----------------------------------- */ 253 + 257 254 258 255 public function destroyObjectPermanently( 259 256 PhabricatorDestructionEngine $engine) {
+9
src/applications/diviner/storage/DivinerSchemaSpec.php
··· 1 + <?php 2 + 3 + final class DivinerSchemaSpec extends PhabricatorConfigSchemaSpec { 4 + 5 + public function buildSchemata() { 6 + $this->buildEdgeSchemata(new DivinerLiveBook()); 7 + } 8 + 9 + }
+9 -9
src/applications/diviner/view/DivinerBookItemView.php
··· 43 43 44 44 $title = phutil_tag( 45 45 'span', 46 - array( 47 - 'class' => 'diviner-book-item-title', 48 - ), 46 + array( 47 + 'class' => 'diviner-book-item-title', 48 + ), 49 49 $this->title); 50 50 51 51 $subtitle = phutil_tag( 52 52 'span', 53 - array( 54 - 'class' => 'diviner-book-item-subtitle', 55 - ), 53 + array( 54 + 'class' => 'diviner-book-item-subtitle', 55 + ), 56 56 $this->subtitle); 57 57 58 58 $type = phutil_tag( 59 59 'span', 60 - array( 61 - 'class' => 'diviner-book-item-type', 62 - ), 60 + array( 61 + 'class' => 'diviner-book-item-type', 62 + ), 63 63 $this->type); 64 64 65 65 return array($title, $type, $subtitle);
+5 -3
src/applications/diviner/workflow/DivinerAtomizeWorkflow.php
··· 36 36 37 37 $atomizer_class = $args->getArg('atomizer'); 38 38 if (!$atomizer_class) { 39 - throw new Exception( 40 - pht('Specify an atomizer class with %s.', '--atomizer')); 39 + throw new PhutilArgumentUsageException( 40 + pht( 41 + 'Specify an atomizer class with %s.', 42 + '--atomizer')); 41 43 } 42 44 43 45 $symbols = id(new PhutilSymbolLoader()) ··· 46 48 ->setAncestorClass('DivinerAtomizer') 47 49 ->selectAndLoadSymbols(); 48 50 if (!$symbols) { 49 - throw new Exception( 51 + throw new PhutilArgumentUsageException( 50 52 pht( 51 53 "Atomizer class '%s' must be a concrete subclass of %s.", 52 54 $atomizer_class,
+38 -10
src/applications/diviner/workflow/DivinerGenerateWorkflow.php
··· 50 50 } else { 51 51 $cwd = getcwd(); 52 52 $this->log(pht('FINDING DOCUMENTATION BOOKS')); 53 + 53 54 $books = id(new FileFinder($cwd)) 54 55 ->withType('f') 55 56 ->withSuffix('book') ··· 92 93 // amount of work we can, so that regenerating documentation after minor 93 94 // changes is quick. 94 95 // 95 - // = ATOM CACHE = 96 + // = Atom Cache = 96 97 // 97 98 // In the first stage, we find all the direct changes to source code since 98 99 // the last run. This stage relies on two data structures: ··· 118 119 // its methods). The File Hash Map contains an exhaustive list of all atoms 119 120 // with type "file", but not child atoms of those top-level atoms.) 120 121 // 121 - // = GRAPH CACHE = 122 + // = Graph Cache = 122 123 // 123 124 // We now know which atoms exist, and can compare the Atom Map to some 124 125 // existing cache to figure out what has changed. However, this isn't ··· 176 177 ->setConcreteOnly(true) 177 178 ->setAncestorClass('DivinerPublisher') 178 179 ->selectAndLoadSymbols(); 180 + 179 181 if (!$symbols) { 180 - throw new Exception( 182 + throw new PhutilArgumentUsageException( 181 183 pht( 182 184 "Publisher class '%s' must be a concrete subclass of %s.", 183 185 $publisher_class, ··· 188 190 $this->publishDocumentation($args->getArg('clean'), $publisher); 189 191 } 190 192 193 + 191 194 /* -( Atom Cache )--------------------------------------------------------- */ 195 + 192 196 193 197 private function buildAtomCache() { 194 198 $this->log(pht('BUILDING ATOM CACHE')); 195 199 196 200 $file_hashes = $this->findFilesInProject(); 197 - $this->log(pht('Found %d file(s) in project.', count($file_hashes))); 201 + $this->log( 202 + pht( 203 + 'Found %s file(s) in project.', 204 + new PhutilNumber(count($file_hashes)))); 198 205 199 206 $this->deleteDeadAtoms($file_hashes); 200 207 $atomize = $this->getFilesToAtomize($file_hashes); 201 - $this->log(pht('Found %d unatomized, uncached file(s).', count($atomize))); 208 + $this->log( 209 + pht( 210 + 'Found %s unatomized, uncached file(s).', 211 + new PhutilNumber(count($atomize)))); 202 212 203 213 $file_atomizers = $this->getAtomizersForFiles($atomize); 204 - $this->log(pht('Found %d file(s) to atomize.', count($file_atomizers))); 214 + $this->log( 215 + pht( 216 + 'Found %s file(s) to atomize.', 217 + new PhutilNumber(count($file_atomizers)))); 218 + 205 219 $futures = $this->buildAtomizerFutures($file_atomizers); 206 - $this->log(pht('Atomizing %d file(s).', count($file_atomizers))); 220 + $this->log( 221 + pht( 222 + 'Atomizing %s file(s).', 223 + new PhutilNumber(count($file_atomizers)))); 207 224 208 225 if ($futures) { 209 226 $this->resolveAtomizerFutures($futures, $file_hashes); ··· 344 361 ->setTotal(count($futures)); 345 362 $futures = id(new FutureIterator($futures)) 346 363 ->limit(4); 364 + 347 365 foreach ($futures as $key => $future) { 348 366 try { 349 367 $atoms = $future->resolveJSON(); ··· 396 414 397 415 /* -( Graph Cache )-------------------------------------------------------- */ 398 416 417 + 399 418 private function buildGraphCache() { 400 419 $this->log(pht('BUILDING GRAPH CACHE')); 401 420 ··· 407 426 $dirty_nhashes = array(); 408 427 409 428 $del_atoms = array_diff_key($symbol_map, $atoms); 410 - $this->log(pht('Found %d obsolete atom(s) in graph.', count($del_atoms))); 429 + $this->log( 430 + pht( 431 + 'Found %s obsolete atom(s) in graph.', 432 + new PhutilNumber(count($del_atoms)))); 411 433 412 434 foreach ($del_atoms as $nhash => $shash) { 413 435 $atom_cache->deleteSymbol($nhash); ··· 418 440 } 419 441 420 442 $new_atoms = array_diff_key($atoms, $symbol_map); 421 - $this->log(pht('Found %d new atom(s) in graph.', count($new_atoms))); 443 + $this->log( 444 + pht( 445 + 'Found %s new atom(s) in graph.', 446 + new PhutilNumber(count($new_atoms)))); 422 447 423 448 foreach ($new_atoms as $nhash => $ignored) { 424 449 $shash = $this->computeSymbolHash($nhash); ··· 454 479 } 455 480 } 456 481 457 - $this->log(pht('Found %d affected atoms.', count($dirty_nhashes))); 482 + $this->log( 483 + pht( 484 + 'Found %s affected atoms.', 485 + new PhutilNumber(count($dirty_nhashes)))); 458 486 459 487 foreach ($dirty_nhashes as $nhash => $ignored) { 460 488 $atom_cache->addGraph($nhash, $this->computeGraphHash($nhash));
+4 -4
src/applications/herald/adapter/HeraldAdapter.php
··· 850 850 case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: 851 851 $standard = array( 852 852 self::ACTION_NOTHING => pht('Do nothing'), 853 - self::ACTION_ADD_CC => pht('Add emails to CC'), 854 - self::ACTION_REMOVE_CC => pht('Remove emails from CC'), 853 + self::ACTION_ADD_CC => pht('Add Subscribers'), 854 + self::ACTION_REMOVE_CC => pht('Remove Subscribers'), 855 855 self::ACTION_EMAIL => pht('Send an email to'), 856 856 self::ACTION_AUDIT => pht('Trigger an Audit by'), 857 857 self::ACTION_FLAG => pht('Mark with flag'), ··· 868 868 case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: 869 869 $standard = array( 870 870 self::ACTION_NOTHING => pht('Do nothing'), 871 - self::ACTION_ADD_CC => pht('Add me to CC'), 872 - self::ACTION_REMOVE_CC => pht('Remove me from CC'), 871 + self::ACTION_ADD_CC => pht('Add me as a subscriber'), 872 + self::ACTION_REMOVE_CC => pht('Remove me as a subscriber'), 873 873 self::ACTION_EMAIL => pht('Send me an email'), 874 874 self::ACTION_AUDIT => pht('Trigger an Audit by me'), 875 875 self::ACTION_FLAG => pht('Mark with flag'),
+2
src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
··· 3 3 final class PhabricatorMailImplementationPHPMailerAdapter 4 4 extends PhabricatorMailImplementationAdapter { 5 5 6 + private $mailer; 7 + 6 8 /** 7 9 * @phutil-external-symbol class PHPMailer 8 10 */
+2 -2
src/applications/metamta/replyhandler/PhabricatorMailTarget.php
··· 55 55 return $this->viewer; 56 56 } 57 57 58 - public function sendMail(PhabricatorMetaMTAMail $mail) { 58 + public function willSendMail(PhabricatorMetaMTAMail $mail) { 59 59 $viewer = $this->getViewer(); 60 60 61 61 $mail->addPHIDHeaders('X-Phabricator-To', $this->rawToPHIDs); ··· 92 92 $mail->addCCs($cc); 93 93 } 94 94 95 - return $mail->save(); 95 + return $mail; 96 96 } 97 97 98 98 private function getRecipientsSummary(
+10 -6
src/applications/passphrase/controller/PassphraseCredentialEditController.php
··· 47 47 $is_new = true; 48 48 49 49 // Prefill username if provided. 50 - $credential->setUsername($request->getStr('username')); 50 + $credential->setUsername((string)$request->getStr('username')); 51 51 52 52 if (!$request->getStr('isInitialized')) { 53 53 $type->didInitializeNewCredential($viewer, $credential); ··· 151 151 $credential->openTransaction(); 152 152 153 153 if (!$credential->getIsLocked()) { 154 - $xactions[] = id(new PassphraseCredentialTransaction()) 154 + if ($type->shouldRequireUsername()) { 155 + $xactions[] = id(new PassphraseCredentialTransaction()) 155 156 ->setTransactionType($type_username) 156 157 ->setNewValue($v_username); 157 - 158 + } 158 159 // If some value other than a sequence of bullets was provided for 159 160 // the credential, update it. In particular, note that we are 160 161 // explicitly allowing empty secrets: one use case is HTTP auth where ··· 263 264 pht('This credential is permanently locked and can not be edited.')); 264 265 } 265 266 266 - $form 267 + if ($type->shouldRequireUsername()) { 268 + $form 267 269 ->appendChild( 268 270 id(new AphrontFormTextControl()) 269 271 ->setName('username') 270 272 ->setLabel(pht('Login/Username')) 271 273 ->setValue($v_username) 272 274 ->setDisabled($credential_is_locked) 273 - ->setError($e_username)) 274 - ->appendChild( 275 + ->setError($e_username)); 276 + } 277 + $form 278 + ->appendChild( 275 279 $secret_control 276 280 ->setName('secret') 277 281 ->setLabel($type->getSecretLabel())
+5 -3
src/applications/passphrase/controller/PassphraseCredentialViewController.php
··· 182 182 pht('Editable By'), 183 183 $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); 184 184 185 - $properties->addProperty( 186 - pht('Username'), 187 - $credential->getUsername()); 185 + if ($type->shouldRequireUsername()) { 186 + $properties->addProperty( 187 + pht('Username'), 188 + $credential->getUsername()); 189 + } 188 190 189 191 $used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 190 192 $credential->getPHID(),
+4
src/applications/passphrase/credentialtype/PassphraseCredentialType.php
··· 131 131 return $secret; 132 132 } 133 133 134 + public function shouldRequireUsername() { 135 + return true; 136 + } 137 + 134 138 }
+37
src/applications/passphrase/credentialtype/PassphraseNoteCredentialType.php
··· 1 + <?php 2 + 3 + final class PassphraseNoteCredentialType 4 + extends PassphraseCredentialType { 5 + 6 + const CREDENTIAL_TYPE = 'note'; 7 + const PROVIDES_TYPE = 'provides/note'; 8 + 9 + public function getCredentialType() { 10 + return self::CREDENTIAL_TYPE; 11 + } 12 + 13 + public function getProvidesType() { 14 + return self::PROVIDES_TYPE; 15 + } 16 + 17 + public function getCredentialTypeName() { 18 + return pht('Note'); 19 + } 20 + 21 + public function getCredentialTypeDescription() { 22 + return pht('Store a plaintext note.'); 23 + } 24 + 25 + public function getSecretLabel() { 26 + return pht('Note'); 27 + } 28 + 29 + public function newSecretControl() { 30 + return id(new AphrontFormTextAreaControl()); 31 + } 32 + 33 + public function shouldRequireUsername() { 34 + return false; 35 + } 36 + 37 + }
+4
src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
··· 174 174 } 175 175 break; 176 176 case PassphraseCredentialTransaction::TYPE_USERNAME: 177 + $credential_type = $object->getCredentialTypeImplementation(); 178 + if (!$credential_type->shouldRequireUsername()) { 179 + break; 180 + } 177 181 $missing = $this->validateIsEmptyTextField( 178 182 $object->getUsername(), 179 183 $xactions);
+3 -2
src/applications/phriction/controller/PhrictionMoveController.php
··· 36 36 // about it. 37 37 if (strlen($v_slug)) { 38 38 $normal_slug = PhabricatorSlug::normalize($v_slug); 39 - if ($normal_slug !== $v_slug) { 39 + $no_slash_slug = rtrim($normal_slug, '/'); 40 + if ($normal_slug !== $v_slug && $no_slash_slug !== $v_slug) { 40 41 return $this->newDialog() 41 42 ->setTitle(pht('Adjust Path')) 42 43 ->appendParagraph( 43 44 pht( 44 45 'The path you entered (%s) is not a valid wiki document '. 45 - 'path. Paths may not contain special characters.', 46 + 'path. Paths may not contain spaces or special characters.', 46 47 phutil_tag('strong', array(), $v_slug))) 47 48 ->appendParagraph( 48 49 pht(
+4
src/applications/phriction/editor/PhrictionTransactionEditor.php
··· 391 391 pht("A document's content changes."), 392 392 PhrictionTransaction::MAILTAG_DELETE => 393 393 pht('A document is deleted.'), 394 + PhrictionTransaction::MAILTAG_SUBSCRIBERS => 395 + pht('A document\'s subscribers change.'), 396 + PhrictionTransaction::MAILTAG_OTHER => 397 + pht('Other document activity not listed above occurs.'), 394 398 ); 395 399 } 396 400
+11 -4
src/applications/phriction/storage/PhrictionTransaction.php
··· 9 9 const TYPE_MOVE_TO = 'move-to'; 10 10 const TYPE_MOVE_AWAY = 'move-away'; 11 11 12 - const MAILTAG_TITLE = 'phriction-title'; 13 - const MAILTAG_CONTENT = 'phriction-content'; 14 - const MAILTAG_DELETE = 'phriction-delete'; 12 + const MAILTAG_TITLE = 'phriction-title'; 13 + const MAILTAG_CONTENT = 'phriction-content'; 14 + const MAILTAG_DELETE = 'phriction-delete'; 15 + const MAILTAG_SUBSCRIBERS = 'phriction-subscribers'; 16 + const MAILTAG_OTHER = 'phriction-other'; 15 17 16 18 public function getApplicationName() { 17 19 return 'phriction'; ··· 280 282 case self::TYPE_DELETE: 281 283 $tags[] = self::MAILTAG_DELETE; 282 284 break; 283 - 285 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 286 + $tags[] = self::MAILTAG_SUBSCRIBERS; 287 + break; 288 + default: 289 + $tags[] = self::MAILTAG_OTHER; 290 + break; 284 291 } 285 292 return $tags; 286 293 }
+2
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 433 433 pht('Project membership changes.'), 434 434 PhabricatorProjectTransaction::MAILTAG_WATCHERS => 435 435 pht('Project watcher list changes.'), 436 + PhabricatorProjectTransaction::MAILTAG_SUBSCRIBERS => 437 + pht('Project subscribers change.'), 436 438 PhabricatorProjectTransaction::MAILTAG_OTHER => 437 439 pht('Other project activity not listed above occurs.'), 438 440 );
+8 -4
src/applications/project/storage/PhabricatorProjectTransaction.php
··· 14 14 // NOTE: This is deprecated, members are just a normal edge now. 15 15 const TYPE_MEMBERS = 'project:members'; 16 16 17 - const MAILTAG_METADATA = 'project-metadata'; 18 - const MAILTAG_MEMBERS = 'project-members'; 19 - const MAILTAG_WATCHERS = 'project-watchers'; 20 - const MAILTAG_OTHER = 'project-other'; 17 + const MAILTAG_METADATA = 'project-metadata'; 18 + const MAILTAG_MEMBERS = 'project-members'; 19 + const MAILTAG_SUBSCRIBERS = 'project-subscribers'; 20 + const MAILTAG_WATCHERS = 'project-watchers'; 21 + const MAILTAG_OTHER = 'project-other'; 21 22 22 23 public function getApplicationName() { 23 24 return 'project'; ··· 368 369 case self::TYPE_ICON: 369 370 case self::TYPE_COLOR: 370 371 $tags[] = self::MAILTAG_METADATA; 372 + break; 373 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 374 + $tags[] = self::MAILTAG_SUBSCRIBERS; 371 375 break; 372 376 case PhabricatorTransactions::TYPE_EDGE: 373 377 $type = $this->getMetadata('edge:type');
+3 -9
src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
··· 166 166 protected function shouldSendMail( 167 167 PhabricatorLiskDAO $object, 168 168 array $xactions) { 169 - return true; 170 - } 171 - 172 - protected function sendMail( 173 - PhabricatorLiskDAO $object, 174 - array $xactions) { 175 169 176 170 // Avoid sending emails that only talk about commit discovery. 177 171 $types = array_unique(mpull($xactions, 'getTransactionType')); 178 172 if ($types === array(ReleephRequestTransaction::TYPE_DISCOVERY)) { 179 - return null; 173 + return false; 180 174 } 181 175 182 176 // Don't email people when we discover that something picks or reverts OK. 183 177 if ($types === array(ReleephRequestTransaction::TYPE_PICK_STATUS)) { 184 178 if (!mfilter($xactions, 'isBoringPickStatus', true /* negate */)) { 185 179 // If we effectively call "isInterestingPickStatus" and get nothing... 186 - return null; 180 + return false; 187 181 } 188 182 } 189 183 190 - return parent::sendMail($object, $xactions); 184 + return true; 191 185 } 192 186 193 187 protected function buildReplyHandler(PhabricatorLiskDAO $object) {
+16 -9
src/applications/repository/customfield/PhabricatorCommitBranchesField.php
··· 29 29 'callsign' => $this->getObject()->getRepository()->getCallsign(), 30 30 ); 31 31 32 - $branches_raw = id(new ConduitCall('diffusion.branchquery', $params)) 33 - ->setUser($this->getViewer()) 34 - ->execute(); 32 + try { 33 + $branches_raw = id(new ConduitCall('diffusion.branchquery', $params)) 34 + ->setUser($this->getViewer()) 35 + ->execute(); 36 + 37 + $branches = DiffusionRepositoryRef::loadAllFromDictionaries( 38 + $branches_raw); 39 + if (!$branches) { 40 + return; 41 + } 35 42 36 - $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches_raw); 37 - if (!$branches) { 38 - return; 43 + $branch_names = mpull($branches, 'getShortName'); 44 + sort($branch_names); 45 + $branch_text = implode(', ', $branch_names); 46 + } catch (Exception $ex) { 47 + $branch_text = pht('<%s: %s>', get_class($ex), $ex->getMessage()); 39 48 } 40 - $branch_names = mpull($branches, 'getShortName'); 41 - sort($branch_names); 42 49 43 - $body->addTextSection(pht('BRANCHES'), implode(', ', $branch_names)); 50 + $body->addTextSection(pht('BRANCHES'), $branch_text); 44 51 } 45 52 46 53 }
+24 -16
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 1040 1040 // Hook for edges or other properties that may need (re-)loading 1041 1041 $object = $this->willPublish($object, $xactions); 1042 1042 1043 - $mailed = array(); 1043 + $messages = array(); 1044 1044 if (!$this->getDisableEmail()) { 1045 1045 if ($this->shouldSendMail($object, $xactions)) { 1046 - $mailed = $this->sendMail($object, $xactions); 1046 + $messages = $this->buildMail($object, $xactions); 1047 1047 } 1048 1048 } 1049 1049 ··· 1055 1055 } 1056 1056 1057 1057 if ($this->shouldPublishFeedStory($object, $xactions)) { 1058 - $this->publishFeedStory( 1059 - $object, 1060 - $xactions, 1061 - $mailed); 1058 + $mailed = array(); 1059 + foreach ($messages as $mail) { 1060 + foreach ($mail->buildRecipientList() as $phid) { 1061 + $mailed[$phid] = true; 1062 + } 1063 + } 1064 + 1065 + $this->publishFeedStory($object, $xactions, $mailed); 1066 + } 1067 + 1068 + // NOTE: This actually sends the mail. We do this last to reduce the chance 1069 + // that we send some mail, hit an exception, then send the mail again when 1070 + // retrying. 1071 + foreach ($messages as $mail) { 1072 + $mail->save(); 1062 1073 } 1063 1074 1064 1075 return $xactions; ··· 2241 2252 /** 2242 2253 * @task mail 2243 2254 */ 2244 - protected function sendMail( 2255 + private function buildMail( 2245 2256 PhabricatorLiskDAO $object, 2246 2257 array $xactions) { 2247 2258 ··· 2255 2266 // Set this explicitly before we start swapping out the effective actor. 2256 2267 $this->setActingAsPHID($this->getActingAsPHID()); 2257 2268 2258 - 2259 - $mailed = array(); 2269 + $messages = array(); 2260 2270 foreach ($targets as $target) { 2261 2271 $original_actor = $this->getActor(); 2262 2272 ··· 2270 2280 // Reload handles for the new viewer. 2271 2281 $this->loadHandles($xactions); 2272 2282 2273 - $mail = $this->sendMailToTarget($object, $xactions, $target); 2283 + $mail = $this->buildMailForTarget($object, $xactions, $target); 2274 2284 } catch (Exception $ex) { 2275 2285 $caught = $ex; 2276 2286 } ··· 2283 2293 } 2284 2294 2285 2295 if ($mail) { 2286 - foreach ($mail->buildRecipientList() as $phid) { 2287 - $mailed[$phid] = true; 2288 - } 2296 + $messages[] = $mail; 2289 2297 } 2290 2298 } 2291 2299 2292 - return array_keys($mailed); 2300 + return $messages; 2293 2301 } 2294 2302 2295 - private function sendMailToTarget( 2303 + private function buildMailForTarget( 2296 2304 PhabricatorLiskDAO $object, 2297 2305 array $xactions, 2298 2306 PhabricatorMailTarget $target) { ··· 2354 2362 $mail->setParentMessageID($this->getParentMessageID()); 2355 2363 } 2356 2364 2357 - return $target->sendMail($mail); 2365 + return $target->willSendMail($mail); 2358 2366 } 2359 2367 2360 2368 private function addMailProjectMetadata(
+46
src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
··· 1124 1124 '%s changed package owners, added: %4$s; removed: %6$s.', 1125 1125 ), 1126 1126 1127 + 'Found %s book(s).' => array( 1128 + 'Found %s book.', 1129 + 'Found %s books.', 1130 + ), 1131 + 'Found %s file(s) in project.' => array( 1132 + 'Found %s file in project.', 1133 + 'Found %s files in project.', 1134 + ), 1135 + 'Found %s unatomized, uncached file(s).' => array( 1136 + 'Found %s unatomized, uncached file.', 1137 + 'Found %s unatomized, uncached files.', 1138 + ), 1139 + 'Found %s file(s) to atomize.' => array( 1140 + 'Found %s file to atomize.', 1141 + 'Found %s files to atomize.', 1142 + ), 1143 + 'Atomizing %s file(s).' => array( 1144 + 'Atomizing %s file.', 1145 + 'Atomizing %s files.', 1146 + ), 1147 + 'Creating %s document(s).' => array( 1148 + 'Creating %s document.', 1149 + 'Creating %s documents.', 1150 + ), 1151 + 'Deleting %s document(s).' => array( 1152 + 'Deleting %s document.', 1153 + 'Deleting %s documents.', 1154 + ), 1155 + 'Found %s obsolete atom(s) in graph.' => array( 1156 + 'Found %s obsolete atom in graph.', 1157 + 'Found %s obsolete atoms in graph.', 1158 + ), 1159 + 'Found %s new atom(s) in graph.' => array( 1160 + 'Found %s new atom in graph.', 1161 + 'Found %s new atoms in graph.', 1162 + ), 1163 + 'This call takes %s parameter(s), but only %s are documented.' => array( 1164 + array( 1165 + 'This call takes %s parameter, but only %s is documented.', 1166 + 'This call takes %s parameter, but only %s are documented.', 1167 + ), 1168 + array( 1169 + 'This call takes %s parameters, but only %s is documented.', 1170 + 'This call takes %s parameters, but only %s are documented.', 1171 + ), 1172 + ), 1127 1173 ); 1128 1174 } 1129 1175
+11
src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
··· 872 872 * @task order 873 873 */ 874 874 public function getOrderableColumns() { 875 + $cache = PhabricatorCaches::getRequestCache(); 876 + $class = get_class($this); 877 + $cache_key = 'query.orderablecolumns.'.$class; 878 + 879 + $columns = $cache->getKey($cache_key); 880 + if ($columns !== null) { 881 + return $columns; 882 + } 883 + 875 884 $columns = array( 876 885 'id' => array( 877 886 'table' => $this->getPrimaryTableAlias(), ··· 908 917 ); 909 918 } 910 919 } 920 + 921 + $cache->setKey($cache_key, $columns); 911 922 912 923 return $columns; 913 924 }
+5 -14
src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
··· 35 35 private $workspace = array(); 36 36 private $inFlightPHIDs = array(); 37 37 private $policyFilteredPHIDs = array(); 38 - private $canUseApplication; 39 38 40 39 /** 41 40 * Should we continue or throw an exception when a query result is filtered ··· 679 678 * execute the query. 680 679 */ 681 680 public function canViewerUseQueryApplication() { 682 - if ($this->canUseApplication === null) { 683 - $class = $this->getQueryApplicationClass(); 684 - if (!$class) { 685 - $this->canUseApplication = true; 686 - } else { 687 - $result = id(new PhabricatorApplicationQuery()) 688 - ->setViewer($this->getViewer()) 689 - ->withClasses(array($class)) 690 - ->execute(); 691 - 692 - $this->canUseApplication = (bool)$result; 693 - } 681 + $class = $this->getQueryApplicationClass(); 682 + if (!$class) { 683 + return true; 694 684 } 695 685 696 - return $this->canUseApplication; 686 + $viewer = $this->getViewer(); 687 + return PhabricatorApplication::isClassInstalledForViewer($class, $viewer); 697 688 } 698 689 699 690 }