1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-09 14:21:02 +01:00

(stable) Promote 2018 Week 50

This commit is contained in:
epriestley 2018-12-14 23:19:35 -08:00
commit 61a5a48115
90 changed files with 1402 additions and 894 deletions

View file

@ -9,7 +9,7 @@ return array(
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => 'cff4ff6f',
'core.pkg.css' => '9d1148a4',
'core.pkg.js' => '4bde473b',
'differential.pkg.css' => '06dc617c',
'differential.pkg.js' => 'ef0b989b',
@ -127,7 +127,7 @@ return array(
'rsrc/css/phui/calendar/phui-calendar-list.css' => '576be600',
'rsrc/css/phui/calendar/phui-calendar-month.css' => '21154caf',
'rsrc/css/phui/calendar/phui-calendar.css' => 'f1ddf11c',
'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '628f59de',
'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '7a7c22af',
'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77',
'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3',
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6',
@ -214,7 +214,7 @@ return array(
'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313',
'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d',
'rsrc/externals/javelin/core/init.js' => '8d83d2a1',
'rsrc/externals/javelin/core/init_node.js' => 'c234aded',
'rsrc/externals/javelin/core/init_node.js' => 'f7732951',
'rsrc/externals/javelin/core/install.js' => '05270951',
'rsrc/externals/javelin/core/util.js' => '93cc50d6',
'rsrc/externals/javelin/docs/Base.js' => '74676256',
@ -469,6 +469,7 @@ return array(
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a',
'rsrc/js/core/behavior-line-linker.js' => '66a62306',
'rsrc/js/core/behavior-linked-container.js' => '291da458',
'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0',
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
@ -616,6 +617,7 @@ return array(
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => '6b31879a',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-linked-container' => '291da458',
'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
'javelin-behavior-maniphest-subpriority-editor' => '71237763',
@ -832,7 +834,7 @@ return array(
'phui-lightbox-css' => '0a035e40',
'phui-list-view-css' => '38f8c9bd',
'phui-object-box-css' => '9cff003c',
'phui-oi-big-ui-css' => '628f59de',
'phui-oi-big-ui-css' => '7a7c22af',
'phui-oi-color-css' => 'cd2b9b77',
'phui-oi-drag-ui-css' => '08f4ccc3',
'phui-oi-flush-ui-css' => '9d9685d6',
@ -1027,6 +1029,10 @@ return array(
'javelin-uri',
'phabricator-notification',
),
'291da458' => array(
'javelin-behavior',
'javelin-dom',
),
'2926fff2' => array(
'javelin-behavior',
'javelin-dom',
@ -1348,9 +1354,6 @@ return array(
'javelin-magical-init',
'javelin-util',
),
'628f59de' => array(
'phui-oi-list-view-css',
),
'62dfea03' => array(
'javelin-install',
'javelin-util',
@ -1508,6 +1511,9 @@ return array(
'owners-path-editor',
'javelin-behavior',
),
'7a7c22af' => array(
'phui-oi-list-view-css',
),
'7cbe244b' => array(
'javelin-install',
'javelin-util',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_user.phabricator_session
ADD phid VARBINARY(64) NOT NULL;

View file

@ -0,0 +1,18 @@
<?php
$table = new PhabricatorAuthSession();
$iterator = new LiskMigrationIterator($table);
$conn = $table->establishConnection('w');
foreach ($iterator as $session) {
if (strlen($session->getPHID())) {
continue;
}
queryfx(
$conn,
'UPDATE %R SET phid = %s WHERE id = %d',
$table,
$session->generatePHID(),
$session->getID());
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_user.phabricator_session
ADD UNIQUE KEY `key_phid` (phid);

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_user.phabricator_session
CHANGE sessionKey sessionKey VARBINARY(64) NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_user.user_log
CHANGE session session VARBINARY(64);

View file

@ -12,9 +12,9 @@ foreach ($commits as $commit) {
continue;
}
$data = $commit->loadOneRelative(
new PhabricatorRepositoryCommitData(),
'commitID');
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
continue;

View file

@ -8,7 +8,9 @@ $commit_table->establishConnection('w');
$edges = 0;
foreach (new LiskMigrationIterator($commit_table) as $commit) {
$data = $commit->loadOneRelative($data_table, 'commitID');
$data = $data_table->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
continue;
}

View file

@ -1,22 +1,7 @@
<?php
$table = new PhabricatorUser();
$table->openTransaction();
$conn = $table->establishConnection('w');
$sessions = queryfx_all(
$conn,
'SELECT userPHID, type, sessionKey FROM %T FOR UPDATE',
PhabricatorUser::SESSION_TABLE);
foreach ($sessions as $session) {
queryfx(
$conn,
'UPDATE %T SET sessionKey = %s WHERE userPHID = %s AND type = %s',
PhabricatorUser::SESSION_TABLE,
PhabricatorHash::weakDigest($session['sessionKey']),
$session['userPHID'],
$session['type']);
}
$table->saveTransaction();
// See T13225. Long ago, this upgraded session key storage from unhashed to
// HMAC-SHA1 here. We later upgraded storage to HMAC-SHA256, so this is initial
// upgrade is now fairly pointless. Dropping this migration entirely only logs
// users out of installs that waited more than 5 years to upgrade, which seems
// like a reasonable behavior.

View file

@ -1633,7 +1633,6 @@ phutil_register_library_map(array(
'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php',
'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php',
'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php',
'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php',
@ -1760,6 +1759,7 @@ phutil_register_library_map(array(
'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php',
'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php',
'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php',
'ManiphestTaskSubtaskController' => 'applications/maniphest/controller/ManiphestTaskSubtaskController.php',
'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php',
'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php',
'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php',
@ -2084,6 +2084,7 @@ phutil_register_library_map(array(
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php',
'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php',
'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php',
'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php',
@ -2295,6 +2296,7 @@ phutil_register_library_map(array(
'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php',
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
'PhabricatorAuthSessionPHIDType' => 'applications/auth/phid/PhabricatorAuthSessionPHIDType.php',
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php',
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
@ -3503,7 +3505,6 @@ phutil_register_library_map(array(
'PhabricatorNotificationServersConfigType' => 'applications/notification/config/PhabricatorNotificationServersConfigType.php',
'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php',
'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php',
'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php',
'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php',
'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php',
'PhabricatorNotificationsSetting' => 'applications/settings/setting/PhabricatorNotificationsSetting.php',
@ -4596,6 +4597,7 @@ phutil_register_library_map(array(
'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php',
'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php',
@ -4622,6 +4624,7 @@ phutil_register_library_map(array(
'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php',
'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php',
'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php',
'PhabricatorUserNotifyTransaction' => 'applications/people/xaction/PhabricatorUserNotifyTransaction.php',
'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php',
'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php',
@ -4643,6 +4646,7 @@ phutil_register_library_map(array(
'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
'PhabricatorUserTransactionEditor' => 'applications/people/editor/PhabricatorUserTransactionEditor.php',
'PhabricatorUserTransactionType' => 'applications/people/xaction/PhabricatorUserTransactionType.php',
'PhabricatorUserUsernameTransaction' => 'applications/people/xaction/PhabricatorUserUsernameTransaction.php',
'PhabricatorUsersEditField' => 'applications/transactions/editfield/PhabricatorUsersEditField.php',
'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php',
'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php',
@ -7197,7 +7201,6 @@ phutil_register_library_map(array(
'Phobject',
'AphrontDatabaseTableRefInterface',
),
'LiskDAOSet' => 'Phobject',
'LiskDAOTestCase' => 'PhabricatorTestCase',
'LiskEphemeralObjectException' => 'Exception',
'LiskFixtureTestCase' => 'PhabricatorTestCase',
@ -7347,6 +7350,7 @@ phutil_register_library_map(array(
'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase',
'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubtaskController' => 'ManiphestController',
'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskTestCase' => 'PhabricatorTestCase',
'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField',
@ -7700,6 +7704,7 @@ phutil_register_library_map(array(
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAnchorView' => 'AphrontView',
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow',
@ -7945,6 +7950,7 @@ phutil_register_library_map(array(
'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthSessionInfo' => 'Phobject',
'PhabricatorAuthSessionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
@ -9320,7 +9326,6 @@ phutil_register_library_map(array(
'PhabricatorNotificationServersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorNotificationStatusView' => 'AphrontTagView',
'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory',
'PhabricatorNotificationUIExample' => 'PhabricatorUIExample',
'PhabricatorNotificationsApplication' => 'PhabricatorApplication',
'PhabricatorNotificationsSetting' => 'PhabricatorInternalSetting',
@ -10642,6 +10647,7 @@ phutil_register_library_map(array(
'PhabricatorConduitResultInterface',
'PhabricatorAuthPasswordHashInterface',
),
'PhabricatorUserApproveTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserCache' => 'PhabricatorUserDAO',
@ -10674,6 +10680,7 @@ phutil_register_library_map(array(
'PhabricatorUserLogView' => 'AphrontView',
'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotifyTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorUserPreferences' => array(
'PhabricatorUserDAO',
@ -10700,6 +10707,7 @@ phutil_register_library_map(array(
'PhabricatorUserTransaction' => 'PhabricatorModularTransaction',
'PhabricatorUserTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorUserUsernameTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUsersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField',

View file

@ -0,0 +1,81 @@
<?php
final class PhabricatorAphlictManagementNotifyWorkflow
extends PhabricatorAphlictManagementWorkflow {
protected function didConstruct() {
$this
->setName('notify')
->setSynopsis(pht('Send a notification to a user.'))
->setArguments(
array(
array(
'name' => 'user',
'param' => 'username',
'help' => pht('User to notify.'),
),
array(
'name' => 'message',
'param' => 'text',
'help' => pht('Message to send.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$username = $args->getArg('user');
if (!strlen($username)) {
throw new PhutilArgumentUsageException(
pht(
'Specify a user to notify with "--user".'));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($username))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht(
'No user with username "%s" exists.',
$username));
}
$message = $args->getArg('message');
if (!strlen($message)) {
throw new PhutilArgumentUsageException(
pht(
'Specify a message to send with "--message".'));
}
$application_phid = id(new PhabricatorNotificationsApplication())
->getPHID();
$content_source = $this->newContentSource();
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserNotifyTransaction::TRANSACTIONTYPE)
->setNewValue($message)
->setForceNotifyPHIDs(array($user->getPHID()));
$editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
->setActingAsPHID($application_phid)
->setContentSource($content_source);
$editor->applyTransactions($user, $xactions);
echo tsprintf(
"%s\n",
pht('Sent notification.'));
return 0;
}
}

View file

@ -251,47 +251,16 @@ final class PhabricatorAuditEditor
case PhabricatorTransactions::TYPE_COMMENT:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
$author_phid = $object->getAuthorPHID();
$actor_is_author = ($actor_phid == $author_phid);
$query_template = id(new DiffusionDiffInlineCommentQuery())
->withCommitPHIDs(array($object->getPHID()));
$state_map = PhabricatorTransactions::getInlineStateMap();
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
$query = id(new DiffusionDiffInlineCommentQuery())
->setViewer($this->getActor())
->withCommitPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map));
$inlines = array();
$inlines[] = id(clone $query)
->withAuthorPHIDs(array($actor_phid))
->withHasTransaction(false)
->execute();
if ($actor_is_author) {
$inlines[] = id(clone $query)
->withHasTransaction(true)
->execute();
if ($state_xaction) {
$xactions[] = $state_xaction;
}
$inlines = array_mergev($inlines);
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}

View file

@ -16,8 +16,9 @@ final class PhabricatorAuthTerminateSessionController
$query->withIDs(array($id));
}
$current_key = PhabricatorHash::weakDigest(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
$current_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
$sessions = $query->execute();
foreach ($sessions as $key => $session) {

View file

@ -56,7 +56,8 @@ final class PhabricatorAuthUnlinkController
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$viewer,
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())->setURI($this->getDoneURI());
}

View file

@ -109,36 +109,49 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$session_table = new PhabricatorAuthSession();
$user_table = new PhabricatorUser();
$conn_r = $session_table->establishConnection('r');
$session_key = PhabricatorHash::weakDigest($session_token);
$conn = $session_table->establishConnection('r');
$cache_parts = $this->getUserCacheQueryParts($conn_r);
// TODO: See T13225. We're moving sessions to a more modern digest
// algorithm, but still accept older cookies for compatibility.
$session_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_token));
$weak_key = PhabricatorHash::weakDigest($session_token);
$cache_parts = $this->getUserCacheQueryParts($conn);
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
$info = queryfx_one(
$conn_r,
$conn,
'SELECT
s.id AS s_id,
s.phid AS s_phid,
s.sessionExpires AS s_sessionExpires,
s.sessionStart AS s_sessionStart,
s.highSecurityUntil AS s_highSecurityUntil,
s.isPartial AS s_isPartial,
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
IF(s.sessionKey = %P, 1, 0) as s_weak,
u.*
%Q
FROM %T u JOIN %T s ON u.phid = s.userPHID
AND s.type = %s AND s.sessionKey = %P %Q',
FROM %R u JOIN %R s ON u.phid = s.userPHID
AND s.type = %s AND s.sessionKey IN (%P, %P) %Q',
new PhutilOpaqueEnvelope($weak_key),
$cache_selects,
$user_table->getTableName(),
$session_table->getTableName(),
$user_table,
$session_table,
$session_type,
new PhutilOpaqueEnvelope($session_key),
new PhutilOpaqueEnvelope($weak_key),
$cache_joins);
if (!$info) {
return null;
}
// TODO: Remove this, see T13225.
$is_weak = (bool)$info['s_weak'];
unset($info['s_weak']);
$session_dict = array(
'userPHID' => $info['phid'],
'sessionKey' => $session_key,
@ -201,6 +214,19 @@ final class PhabricatorAuthSessionEngine extends Phobject {
unset($unguarded);
}
// TODO: Remove this, see T13225.
if ($is_weak) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn_w = $session_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET sessionKey = %P WHERE id = %d',
$session->getTableName(),
new PhutilOpaqueEnvelope($session_key),
$session->getID());
unset($unguarded);
}
$user->attachSession($session);
return $user;
}
@ -240,7 +266,8 @@ final class PhabricatorAuthSessionEngine extends Phobject {
// This has a side effect of validating the session type.
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
$digest_key = PhabricatorHash::weakDigest($session_key);
$digest_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_key));
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
// write.
@ -298,7 +325,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
*/
public function terminateLoginSessions(
PhabricatorUser $user,
$except_session = null) {
PhutilOpaqueEnvelope $except_session = null) {
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($user)
@ -306,7 +333,8 @@ final class PhabricatorAuthSessionEngine extends Phobject {
->execute();
if ($except_session !== null) {
$except_session = PhabricatorHash::weakDigest($except_session);
$except_session = PhabricatorAuthSession::newSessionDigest(
$except_session);
}
foreach ($sessions as $key => $session) {

View file

@ -2,6 +2,8 @@
final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
const DIGEST_TEMPORARY_KEY = 'mfa.totp.sync';
public function getFactorKey() {
return 'totp';
}
@ -34,12 +36,16 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
// (We store and verify the hash of the key, not the key itself, to limit
// how useful the data in the table is to an attacker.)
$token_code = PhabricatorHash::digestWithNamedKey(
$key,
self::DIGEST_TEMPORARY_KEY);
$temporary_token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($totp_token_type))
->withExpired(false)
->withTokenCodes(array(PhabricatorHash::weakDigest($key)))
->withTokenCodes(array($token_code))
->executeOne();
if (!$temporary_token) {
// If we don't have a matching token, regenerate the key below.
@ -53,12 +59,16 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
// Mark this key as one we generated, so the user is allowed to submit
// a response for it.
$token_code = PhabricatorHash::digestWithNamedKey(
$key,
self::DIGEST_TEMPORARY_KEY);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthTemporaryToken())
->setTokenResource($user->getPHID())
->setTokenType($totp_token_type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::weakDigest($key))
->setTokenCode($token_code)
->save();
unset($unguarded);
}

View file

@ -0,0 +1,34 @@
<?php
final class PhabricatorAuthSessionPHIDType
extends PhabricatorPHIDType {
const TYPECONST = 'SSSN';
public function getTypeName() {
return pht('Session');
}
public function newObject() {
return new PhabricatorAuthSession();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorAuthApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorAuthSessionQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
return;
}
}

View file

@ -4,6 +4,7 @@ final class PhabricatorAuthSessionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $identityPHIDs;
private $sessionKeys;
private $sessionTypes;
@ -28,19 +29,17 @@ final class PhabricatorAuthSessionQuery
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthSession();
}
protected function loadPage() {
$table = new PhabricatorAuthSession();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $sessions) {
@ -65,8 +64,8 @@ final class PhabricatorAuthSessionQuery
return $sessions;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
@ -75,6 +74,13 @@ final class PhabricatorAuthSessionQuery
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->identityPHIDs !== null) {
$where[] = qsprintf(
$conn,
@ -85,7 +91,8 @@ final class PhabricatorAuthSessionQuery
if ($this->sessionKeys !== null) {
$hashes = array();
foreach ($this->sessionKeys as $session_key) {
$hashes[] = PhabricatorHash::weakDigest($session_key);
$hashes[] = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_key));
}
$where[] = qsprintf(
$conn,
@ -100,9 +107,7 @@ final class PhabricatorAuthSessionQuery
$this->sessionTypes);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
return $where;
}
public function getQueryApplicationClass() {

View file

@ -6,6 +6,8 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
const TYPE_WEB = 'web';
const TYPE_CONDUIT = 'conduit';
const SESSION_DIGEST_KEY = 'session.digest';
protected $userPHID;
protected $type;
protected $sessionKey;
@ -17,12 +19,19 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
private $identityObject = self::ATTACHABLE;
public static function newSessionDigest(PhutilOpaqueEnvelope $session_token) {
return PhabricatorHash::digestWithNamedKey(
$session_token->openEnvelope(),
self::SESSION_DIGEST_KEY);
}
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'type' => 'text32',
'sessionKey' => 'bytes40',
'sessionKey' => 'text64',
'sessionStart' => 'epoch',
'sessionExpires' => 'epoch',
'highSecurityUntil' => 'epoch?',
@ -74,6 +83,10 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
}
}
public function getPHIDType() {
return PhabricatorAuthSessionPHIDType::TYPECONST;
}
public function isHighSecuritySession() {
$until = $this->getHighSecurityUntil();

View file

@ -279,36 +279,17 @@ final class DifferentialRevisionEditEngine
$object);
$inlines = msort($inlines, 'getID');
foreach ($inlines as $inline) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_INLINE)
->attachComment($inline);
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer);
$viewer_phid = $viewer->getPHID();
$viewer_is_author = ($object->getAuthorPHID() == $viewer_phid);
if ($viewer_is_author) {
$state_map = PhabricatorTransactions::getInlineStateMap();
$query_template = id(new DifferentialDiffInlineCommentQuery())
->withRevisionPHIDs(array($object->getPHID()));
$inlines = id(new DifferentialDiffInlineCommentQuery())
->setViewer($viewer)
->withRevisionPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if ($inlines) {
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
}
}
$xactions = $editor->newAutomaticInlineTransactions(
$object,
$inlines,
DifferentialTransaction::TYPE_INLINE,
$query_template);
return $xactions;
}

View file

@ -247,50 +247,16 @@ final class DifferentialTransactionEditor
case DifferentialTransaction::TYPE_INLINE:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
$author_phid = $object->getAuthorPHID();
$actor_is_author = ($actor_phid == $author_phid);
$query_template = id(new DifferentialDiffInlineCommentQuery())
->withRevisionPHIDs(array($object->getPHID()));
$state_map = PhabricatorTransactions::getInlineStateMap();
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
$query = id(new DifferentialDiffInlineCommentQuery())
->setViewer($this->getActor())
->withRevisionPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map));
$inlines = array();
// We're going to undraft any "done" marks on your own inlines.
$inlines[] = id(clone $query)
->withAuthorPHIDs(array($actor_phid))
->withHasTransaction(false)
->execute();
// If you're the author, we also undraft any "done" marks on other
// inlines.
if ($actor_is_author) {
$inlines[] = id(clone $query)
->withHasTransaction(true)
->execute();
if ($state_xaction) {
$results[] = $state_xaction;
}
$inlines = array_mergev($inlines);
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$results[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}

View file

@ -170,36 +170,17 @@ final class DiffusionCommitEditEngine
$raw = true);
$inlines = msort($inlines, 'getID');
foreach ($inlines as $inline) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorAuditActionConstants::INLINE)
->attachComment($inline);
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer);
$viewer_phid = $viewer->getPHID();
$viewer_is_author = ($object->getAuthorPHID() == $viewer_phid);
if ($viewer_is_author) {
$state_map = PhabricatorTransactions::getInlineStateMap();
$query_template = id(new DiffusionDiffInlineCommentQuery())
->withCommitPHIDs(array($object->getPHID()));
$inlines = id(new DiffusionDiffInlineCommentQuery())
->setViewer($viewer)
->withCommitPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if ($inlines) {
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
}
}
$xactions = $editor->newAutomaticInlineTransactions(
$object,
$inlines,
PhabricatorAuditActionConstants::INLINE,
$query_template);
return $xactions;
}

View file

@ -212,11 +212,8 @@ final class DiffusionRepositoryEditEngine
->setObject($object)
->execute();
$track_value = $object->getDetail('branch-filter', array());
$track_value = array_keys($track_value);
$autoclose_value = $object->getDetail('close-commits-filter', array());
$autoclose_value = array_keys($autoclose_value);
$track_value = $object->getTrackOnlyRules();
$autoclose_value = $object->getAutocloseOnlyRules();
$automation_instructions = pht(
"Configure **Repository Automation** to allow Phabricator to ".

View file

@ -23,8 +23,8 @@ final class DiffusionRepositoryBranchesManagementPanel
$has_any =
$repository->getDetail('default-branch') ||
$repository->getDetail('branch-filter') ||
$repository->getDetail('close-commits-filter');
$repository->getTrackOnlyRules() ||
$repository->getAutocloseOnlyRules();
if ($has_any) {
return 'fa-code-fork';
@ -74,17 +74,21 @@ final class DiffusionRepositoryBranchesManagementPanel
->setViewer($viewer);
$default_branch = nonempty(
$repository->getHumanReadableDetail('default-branch'),
$repository->getDetail('default-branch'),
phutil_tag('em', array(), $repository->getDefaultBranch()));
$view->addProperty(pht('Default Branch'), $default_branch);
$track_only_rules = $repository->getTrackOnlyRules();
$track_only_rules = implode(', ', $track_only_rules);
$track_only = nonempty(
$repository->getHumanReadableDetail('branch-filter', array()),
$track_only_rules,
phutil_tag('em', array(), pht('Track All Branches')));
$view->addProperty(pht('Track Only'), $track_only);
$autoclose_rules = $repository->getAutocloseOnlyRules();
$autoclose_rules = implode(', ', $autoclose_rules);
$autoclose_only = nonempty(
$repository->getHumanReadableDetail('close-commits-filter', array()),
$autoclose_rules,
phutil_tag('em', array(), pht('Autoclose On All Branches')));
$autoclose_disabled = false;

View file

@ -68,7 +68,7 @@ final class DiffusionRepositorySubversionManagementPanel
->setViewer($viewer);
$default_branch = nonempty(
$repository->getHumanReadableDetail('svn-subpath'),
$repository->getDetail('svn-subpath'),
phutil_tag('em', array(), pht('Import Entire Repository')));
$view->addProperty(pht('Import Only'), $default_branch);

View file

@ -43,12 +43,13 @@ final class DiffusionPathChangeQuery extends Phobject {
$conn_r = $repository->establishConnection('r');
$limit = '';
if ($this->limit) {
$limit = qsprintf(
$conn_r,
'LIMIT %d',
$this->limit + 1);
} else {
$limit = qsprintf($conn_r, '');
}
$raw_changes = queryfx_all(

View file

@ -34,6 +34,7 @@ final class DrydockConsoleController extends DrydockController {
->setHeader(pht('Blueprints'))
->setImageIcon('fa-map-o')
->setHref($this->getApplicationURI('blueprint/'))
->setClickable(true)
->addAttribute(
pht(
'Configure blueprints so Drydock can build resources, like '.
@ -44,6 +45,7 @@ final class DrydockConsoleController extends DrydockController {
->setHeader(pht('Resources'))
->setImageIcon('fa-map')
->setHref($this->getApplicationURI('resource/'))
->setClickable(true)
->addAttribute(
pht('View and manage resources Drydock has built, like hosts.')));
@ -52,6 +54,7 @@ final class DrydockConsoleController extends DrydockController {
->setHeader(pht('Leases'))
->setImageIcon('fa-link')
->setHref($this->getApplicationURI('lease/'))
->setClickable(true)
->addAttribute(pht('Manage leases on resources.')));
$menu->addItem(
@ -59,6 +62,7 @@ final class DrydockConsoleController extends DrydockController {
->setHeader(pht('Repository Operations'))
->setImageIcon('fa-fighter-jet')
->setHref($this->getApplicationURI('operation/'))
->setClickable(true)
->addAttribute(pht('Review the repository operation queue.')));
$crumbs = $this->buildApplicationCrumbs();

View file

@ -12,11 +12,8 @@ final class PhabricatorFactUpdateIterator extends PhutilBufferedIterator {
private $position;
private $ignoreUpdatesDuration = 15;
private $set;
public function __construct(LiskDAO $object) {
$this->set = new LiskDAOSet();
$this->object = $object->putInSet($this->set);
$this->object = $object;
}
public function setPosition($position) {
@ -41,8 +38,6 @@ final class PhabricatorFactUpdateIterator extends PhutilBufferedIterator {
}
protected function loadPage() {
$this->set->clearSet();
if ($this->object->hasProperty('dateModified')) {
if ($this->cursor) {
list($after_epoch, $after_id) = explode(':', $this->cursor);

View file

@ -34,7 +34,15 @@ final class PhabricatorFeedQuery
}
protected function willFilterPage(array $data) {
return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
$stories = PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
foreach ($stories as $key => $story) {
if (!$story->isVisibleInFeed()) {
unset($stories[$key]);
}
}
return $stories;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {

View file

@ -418,6 +418,14 @@ abstract class PhabricatorFeedStory
return array();
}
public function isVisibleInFeed() {
return true;
}
public function isVisibleInNotifications() {
return true;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */

View file

@ -243,9 +243,9 @@ final class HeraldEngine extends Phobject {
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %LQ',
HeraldRule::TABLE_RULE_APPLIED,
implode(', ', $sql));
$sql);
}
}
}

View file

@ -52,6 +52,7 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication {
'task/' => array(
$this->getEditRoutePattern('edit/')
=> 'ManiphestTaskEditController',
'subtask/(?P<id>[1-9]\d*)/' => 'ManiphestTaskSubtaskController',
),
'subpriority/' => 'ManiphestSubpriorityController',
),

View file

@ -338,6 +338,8 @@ dictionary with these keys:
- `tag` //Optional string.// Tag text for this subtype.
- `color` //Optional string.// Display color for this subtype.
- `icon` //Optional string.// Icon for the subtype.
- `children` //Optional map.// Configure options shown to the user when
they "Create Subtask". See below.
Each subtype must have a unique key, and you must define a subtype with
the key "%s", which is used as a default subtype.
@ -345,6 +347,54 @@ the key "%s", which is used as a default subtype.
The tag text (`tag`) is used to set the text shown in the subtype tag on list
views and workboards. If you do not configure it, the default subtype will have
no subtype tag and other subtypes will use their name as tag text.
The `children` key allows you to configure which options are presented to the
user when they "Create Subtask" from a task of this subtype. You can specify
these keys:
- `subtypes`: //Optional list<string>.// Show users creation forms for these
task subtypes.
- `forms`: //Optional list<string|int>.// Show users these specific forms,
in order.
If you don't specify either constraint, users will be shown creation forms
for the same subtype.
For example, if you have a "quest" subtype and do not configure `children`,
users who click "Create Subtask" will be presented with all create forms for
"quest" tasks.
If you want to present them with forms for a different task subtype or set of
subtypes instead, use `subtypes`:
```
{
...
"children": {
"subtypes": ["objective", "boss", "reward"]
}
...
}
```
If you want to present them with specific forms, use `forms` and specify form
IDs:
```
{
...
"children": {
"forms": [12, 16]
}
...
}
```
When specifying forms by ID explicitly, the order you specify the forms in will
be used when presenting options to the user.
If only one option would be presented, the user will be taken directly to the
appropriate form instead of being prompted to choose a form.
EOTEXT
,
$subtype_default_key));

View file

@ -281,29 +281,39 @@ final class ManiphestTaskDetailController extends ManiphestController {
->setDisabled(!$can_edit)
->setWorkflow($workflow_edit));
$edit_config = $edit_engine->loadDefaultEditConfiguration($task);
$can_create = (bool)$edit_config;
$subtype_map = $task->newEditEngineSubtypeMap();
$subtask_options = $subtype_map->getCreateFormsForSubtype(
$edit_engine,
$task);
if ($can_create) {
$form_key = $edit_config->getIdentifier();
$edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
// If no forms are available, we want to show the user an error.
// If one form is available, we take them user directly to the form.
// If two or more forms are available, we give the user a choice.
// The "subtask" controller handles the first case (no forms) and the
// third case (more than one form). In the case of one form, we link
// directly to the form.
$subtask_uri = "/task/subtask/{$id}/";
$subtask_workflow = true;
if (count($subtask_options) == 1) {
$subtask_form = head($subtask_options);
$form_key = $subtask_form->getIdentifier();
$subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
->setQueryParam('parent', $id)
->setQueryParam('template', $id)
->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
$edit_uri = $this->getApplicationURI($edit_uri);
} else {
// TODO: This will usually give us a somewhat-reasonable error page, but
// could be a bit cleaner.
$edit_uri = "/task/edit/{$id}/";
$edit_uri = $this->getApplicationURI($edit_uri);
$subtask_workflow = false;
}
$subtask_uri = $this->getApplicationURI($subtask_uri);
$subtask_item = id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($edit_uri)
->setHref($subtask_uri)
->setIcon('fa-level-down')
->setDisabled(!$can_create)
->setWorkflow(!$can_create);
->setDisabled(!$subtask_options)
->setWorkflow($subtask_workflow);
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,

View file

@ -0,0 +1,71 @@
<?php
final class ManiphestTaskSubtaskController
extends ManiphestController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$cancel_uri = $task->getURI();
$edit_engine = id(new ManiphestEditEngine())
->setViewer($viewer)
->setTargetObject($task);
$subtype_map = $task->newEditEngineSubtypeMap();
$subtype_options = $subtype_map->getCreateFormsForSubtype(
$edit_engine,
$task);
if (!$subtype_options) {
return $this->newDialog()
->setTitle(pht('No Forms'))
->appendParagraph(
pht(
'You do not have access to any forms which can be used to '.
'create a subtask.'))
->addCancelButton($cancel_uri, pht('Close'));
}
$menu = id(new PHUIObjectItemListView())
->setUser($viewer)
->setBig(true)
->setFlush(true);
foreach ($subtype_options as $form_key => $subtype_form) {
$subtype_key = $subtype_form->getSubtype();
$subtype = $subtype_map->getSubtype($subtype_key);
$subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
->setQueryParam('parent', $id)
->setQueryParam('template', $id)
->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
$subtask_uri = $this->getApplicationURI($subtask_uri);
$item = id(new PHUIObjectItemView())
->setHeader($subtype_form->getDisplayName())
->setHref($subtask_uri)
->setClickable(true)
->setImageIcon($subtype->newIconView())
->addAttribute($subtype->getName());
$menu->addItem($item);
}
return $this->newDialog()
->setTitle(pht('Choose Subtype'))
->appendChild($menu)
->addCancelButton($cancel_uri);
}
}

View file

@ -169,7 +169,9 @@ EODOCS
->setConduitDocumentation($column_documentation)
->setAliases(array('columnPHID', 'columns', 'columnPHIDs'))
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setIsFormField(false)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setCommentActionLabel(pht('Move on Workboard'))
->setCommentActionOrder(2000)
->setColumnMap($column_map),

View file

@ -641,9 +641,9 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
if ($this->hasOpenSubtasks !== null) {
if ($this->hasOpenSubtasks) {
$join_type = 'JOIN';
$join_type = qsprintf($conn, 'JOIN');
} else {
$join_type = 'LEFT JOIN';
$join_type = qsprintf($conn, 'LEFT JOIN');
}
$joins[] = qsprintf(

View file

@ -160,15 +160,6 @@ final class PhabricatorNotificationBuilder extends Phobject {
'href' => $story->getURI(),
'icon' => $story->getImageURI(),
);
} else if ($story instanceof PhabricatorNotificationTestFeedStory) {
$dict[] = array(
'showAnyNotification' => $web_ready,
'showDesktopNotification' => $desktop_ready,
'title' => pht('Test Notification'),
'body' => $story->renderText(),
'href' => null,
'icon' => PhabricatorUser::getDefaultProfileImageURI(),
);
} else {
$dict[] = array(
'showWebNotification' => false,

View file

@ -6,34 +6,31 @@ final class PhabricatorNotificationTestController
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$story_type = 'PhabricatorNotificationTestFeedStory';
$story_data = array(
'title' => pht(
'This is a test notification, sent at %s.',
phabricator_datetime(time(), $viewer)),
);
$viewer_phid = $viewer->getPHID();
// NOTE: Because we don't currently show you your own notifications, make
// sure this comes from a different PHID.
$application_phid = id(new PhabricatorNotificationsApplication())
->getPHID();
// TODO: When it's easier to get these buttons to render as forms, this
// would be slightly nicer as a more standard isFormPost() check.
if ($request->validateCSRF()) {
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($application_phid)
->setRelatedPHIDs(array($viewer_phid))
->setPrimaryObjectPHID($viewer_phid)
->setSubscribedPHIDs(array($viewer_phid))
->setNotifyAuthor(true)
->publish();
$message_text = pht(
'This is a test notification, sent at %s.',
phabricator_datetime(time(), $viewer));
// NOTE: Currently, the FeedStoryPublisher explicitly filters out
// notifications about your own actions. Send this notification from
// a different actor to get around this.
$application_phid = id(new PhabricatorNotificationsApplication())
->getPHID();
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserNotifyTransaction::TRANSACTIONTYPE)
->setNewValue($message_text)
->setForceNotifyPHIDs(array($viewer->getPHID()));
$editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
->setActingAsPHID($application_phid)
->setContentSourceFromRequest($request);
$editor->applyTransactions($viewer, $xactions);
}
return id(new AphrontAjaxResponse());

View file

@ -1,27 +0,0 @@
<?php
final class PhabricatorNotificationTestFeedStory extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getAuthorPHID();
}
public function renderView() {
$data = $this->getStoryData();
$author_phid = $data->getAuthorPHID();
$view = $this->newStoryView();
$view->setTitle($data->getValue('title'));
$view->setImage($this->getHandle($author_phid)->getImageURI());
return $view;
}
public function renderText() {
$data = $this->getStoryData();
return $data->getValue('title');
}
}

View file

@ -53,13 +53,13 @@ final class PhabricatorNotificationQuery
$data = queryfx_all(
$conn,
'SELECT story.*, notif.hasViewed FROM %T notif
JOIN %T story ON notif.chronologicalKey = story.chronologicalKey
'SELECT story.*, notif.hasViewed FROM %R notif
JOIN %R story ON notif.chronologicalKey = story.chronologicalKey
%Q
ORDER BY notif.chronologicalKey DESC
%Q',
$notification_table->getTableName(),
$story_table->getTableName(),
$notification_table,
$story_table,
$this->buildWhereClause($conn),
$this->buildLimitClause($conn));
@ -93,7 +93,7 @@ final class PhabricatorNotificationQuery
(int)!$this->unread);
}
if ($this->keys) {
if ($this->keys !== null) {
$where[] = qsprintf(
$conn,
'notif.chronologicalKey IN (%Ls)',
@ -103,6 +103,16 @@ final class PhabricatorNotificationQuery
return $where;
}
protected function willFilterPage(array $stories) {
foreach ($stories as $key => $story) {
if (!$story->isVisibleInNotifications()) {
unset($stories[$key]);
}
}
return $stories;
}
protected function getResultCursor($item) {
return $item->getChronologicalKey();
}

View file

@ -24,30 +24,18 @@ final class PhabricatorPeopleApproveController
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(PhabricatorUserApproveTransaction::TRANSACTIONTYPE)
->setNewValue(true);
id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
->approveUser($user, true);
$title = pht(
'Phabricator Account "%s" Approved',
$user->getUsername());
$body = sprintf(
"%s\n\n %s\n\n",
pht(
'Your Phabricator account (%s) has been approved by %s. You can '.
'login here:',
$user->getUsername(),
$viewer->getUsername()),
PhabricatorEnv::getProductionURI('/'));
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($user->getPHID()))
->addCCs(array($viewer->getPHID()))
->setSubject('[Phabricator] '.$title)
->setForceDelivery(true)
->setBody($body)
->saveAndSend();
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
}

View file

@ -36,12 +36,18 @@ final class PhabricatorPeopleProfileManageController
$crumbs->addTextCrumb(pht('Manage'));
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$user,
new PhabricatorPeopleTransactionQuery());
$timeline->setShouldTerminate(true);
$manage = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setCurtain($curtain)
->addPropertySection(pht('Details'), $properties);
->addPropertySection(pht('Details'), $properties)
->setMainColumn($timeline);
return $this->newPage()
->setTitle(
@ -51,10 +57,7 @@ final class PhabricatorPeopleProfileManageController
))
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild(
array(
$manage,
));
->appendChild($manage);
}
private function buildPropertyView(PhabricatorUser $user) {

View file

@ -74,6 +74,10 @@ final class PhabricatorPeopleProfileViewController
->setTitle($user->getUsername())
->setNavigation($nav)
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$user->getPHID(),
))
->appendChild(
array(
$home,

View file

@ -22,36 +22,29 @@ final class PhabricatorPeopleRenameController
$request,
$done_uri);
$errors = array();
$v_username = $user->getUsername();
$e_username = true;
$validation_exception = null;
$username = $user->getUsername();
if ($request->isFormPost()) {
$v_username = $request->getStr('username');
$username = $request->getStr('username');
$xactions = array();
if (!strlen($v_username)) {
$e_username = pht('Required');
$errors[] = pht('New username is required.');
} else if ($v_username == $user->getUsername()) {
$e_username = pht('Invalid');
$errors[] = pht('New username must be different from old username.');
} else if (!PhabricatorUser::validateUsername($v_username)) {
$e_username = pht('Invalid');
$errors[] = PhabricatorUser::describeValidUsername();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserUsernameTransaction::TRANSACTIONTYPE)
->setNewValue($username);
$editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
if (!$errors) {
try {
id(new PhabricatorUserEditor())
->setActor($viewer)
->changeUsername($user, $v_username);
return id(new AphrontRedirectResponse())->setURI($done_uri);
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_username = pht('Not Unique');
$errors[] = pht('Another user already has that username.');
}
}
}
$inst1 = pht(
@ -87,18 +80,13 @@ final class PhabricatorPeopleRenameController
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('New Username'))
->setValue($v_username)
->setName('username')
->setError($e_username));
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
->setValue($username)
->setName('username'));
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Change Username'))
->appendChild($errors)
->setValidationException($validation_exception)
->appendParagraph($inst1)
->appendParagraph($inst2)
->appendParagraph($inst3)

View file

@ -75,6 +75,16 @@ final class PhabricatorUserEditEngine
->setConduitDescription(pht('Disable or enable the user.'))
->setConduitTypeDescription(pht('True to disable the user.'))
->setValue($object->getIsDisabled()),
id(new PhabricatorBoolEditField())
->setKey('approved')
->setOptions(pht('Approved'), pht('Unapproved'))
->setLabel(pht('Approved'))
->setDescription(pht('Approve the user.'))
->setTransactionType(PhabricatorUserApproveTransaction::TRANSACTIONTYPE)
->setIsFormField(false)
->setConduitDescription(pht('Approve or reject the user.'))
->setConduitTypeDescription(pht('True to approve the user.'))
->setValue($object->getIsApproved()),
);
}

View file

@ -129,53 +129,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor {
}
/**
* @task edit
*/
public function changeUsername(PhabricatorUser $user, $username) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
throw new Exception(pht('Username is invalid! %s', $valid));
}
$old_username = $user->getUsername();
$user->openTransaction();
$user->reload();
$user->setUsername($username);
try {
$user->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$user->setUsername($old_username);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_CHANGE_USERNAME);
$log->setOldValue($old_username);
$log->setNewValue($username);
$log->save();
$user->saveTransaction();
// The SSH key cache currently includes usernames, so dirty it. See T12554
// for discussion.
PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
$user->sendUsernameChangeEmail($actor, $old_username);
}
/* -( Editing Roles )------------------------------------------------------ */
@ -293,45 +246,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor {
return $this;
}
/**
* @task role
*/
public function approveUser(PhabricatorUser $user, $approve) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsApproved() == $approve) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_APPROVE);
$log->setOldValue($user->getIsApproved());
$log->setNewValue($approve);
$user->setIsApproved($approve);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/* -( Adding, Removing and Changing Email )-------------------------------- */

View file

@ -11,4 +11,18 @@ final class PhabricatorUserTransactionEditor
return pht('Users');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array();
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
}

View file

@ -163,14 +163,7 @@ final class PhabricatorPeopleQuery
}
protected function loadPage() {
$table = new PhabricatorUser();
$data = $this->loadStandardPageRows($table);
if ($this->needPrimaryEmail) {
$table->putInSet(new LiskDAOSet());
}
return $table->loadAllFromArray($data);
return $this->loadStandardPage($this->newResultObject());
}
protected function didFilterPage(array $users) {

View file

@ -458,14 +458,9 @@ final class PhabricatorUser
}
public function loadPrimaryEmail() {
$email = new PhabricatorUserEmail();
$conn = $email->establishConnection('r');
return $this->loadOneRelative(
$email,
'userPHID',
'getPHID',
qsprintf($conn, '(isPrimary = 1)'));
return id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND isPrimary = 1',
$this->getPHID());
}

View file

@ -150,7 +150,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO
'actorPHID' => 'phid?',
'action' => 'text64',
'remoteAddr' => 'text64',
'session' => 'bytes40?',
'session' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'actorPHID' => array(

View file

@ -0,0 +1,96 @@
<?php
final class PhabricatorUserApproveTransaction
extends PhabricatorUserTransactionType {
const TRANSACTIONTYPE = 'user.approve';
public function generateOldValue($object) {
return (bool)$object->getIsApproved();
}
public function generateNewValue($object, $value) {
return (bool)$value;
}
public function applyInternalEffects($object, $value) {
$object->setIsApproved((int)$value);
}
public function applyExternalEffects($object, $value) {
$user = $object;
$this->newUserLog(PhabricatorUserLog::ACTION_APPROVE)
->setOldValue((bool)$user->getIsApproved())
->setNewValue((bool)$value)
->save();
$actor = $this->getActor();
$title = pht(
'Phabricator Account "%s" Approved',
$user->getUsername());
$body = sprintf(
"%s\n\n %s\n\n",
pht(
'Your Phabricator account (%s) has been approved by %s. You can '.
'login here:',
$user->getUsername(),
$actor->getUsername()),
PhabricatorEnv::getProductionURI('/'));
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($user->getPHID()))
->addCCs(array($actor->getPHID()))
->setSubject('[Phabricator] '.$title)
->setForceDelivery(true)
->setBody($body)
->saveAndSend();
}
public function getTitle() {
$new = $this->getNewValue();
if ($new) {
return pht(
'%s approved this user.',
$this->renderAuthor());
} else {
return pht(
'%s rejected this user.',
$this->renderAuthor());
}
}
public function shouldHideForFeed() {
return true;
}
public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array();
foreach ($xactions as $xaction) {
$is_approved = (bool)$object->getIsApproved();
if ((bool)$xaction->getNewValue() === $is_approved) {
continue;
}
if (!$actor->getIsAdmin()) {
$errors[] = $this->newInvalidError(
pht('You must be an administrator to approve users.'));
}
}
return $errors;
}
public function getRequiredCapabilities(
$object,
PhabricatorApplicationTransaction $xaction) {
// Unlike normal user edits, approvals require admin permissions, which
// is enforced by validateTransactions().
return null;
}
}

View file

@ -15,7 +15,9 @@ final class PhabricatorUserDisableTransaction
public function applyInternalEffects($object, $value) {
$object->setIsDisabled((int)$value);
}
public function applyExternalEffects($object, $value) {
$this->newUserLog(PhabricatorUserLog::ACTION_DISABLE)
->setOldValue((bool)$object->getIsDisabled())
->setNewValue((bool)$value)
@ -35,19 +37,10 @@ final class PhabricatorUserDisableTransaction
}
}
public function getTitleForFeed() {
$new = $this->getNewValue();
if ($new) {
return pht(
'%s disabled %s.',
$this->renderAuthor(),
$this->renderObject());
} else {
return pht(
'%s enabled %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function shouldHideForFeed() {
// Don't publish feed stories about disabling users, since this can be
// a sensitive action.
return true;
}
public function validateTransactions($object, array $xactions) {

View file

@ -0,0 +1,38 @@
<?php
final class PhabricatorUserNotifyTransaction
extends PhabricatorUserTransactionType {
const TRANSACTIONTYPE = 'notify';
public function generateOldValue($object) {
return null;
}
public function generateNewValue($object, $value) {
return $value;
}
public function getTitle() {
return pht(
'%s sent this user a test notification.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return $this->getNewValue();
}
public function shouldHideForNotifications() {
return false;
}
public function shouldHideForFeed() {
return true;
}
public function shouldHideForMail() {
return true;
}
}

View file

@ -0,0 +1,92 @@
<?php
final class PhabricatorUserUsernameTransaction
extends PhabricatorUserTransactionType {
const TRANSACTIONTYPE = 'user.rename';
public function generateOldValue($object) {
return $object->getUsername();
}
public function generateNewValue($object, $value) {
return $value;
}
public function applyInternalEffects($object, $value) {
$object->setUsername($value);
}
public function applyExternalEffects($object, $value) {
$user = $object;
$this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME)
->setOldValue($this->getOldValue())
->setNewValue($value)
->save();
// The SSH key cache currently includes usernames, so dirty it. See T12554
// for discussion.
PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
$user->sendUsernameChangeEmail($this->getActor(), $this->getOldValue());
}
public function getTitle() {
return pht(
'%s renamed this user from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
$old = $xaction->getOldValue();
if ($old === $new) {
continue;
}
if (!$actor->getIsAdmin()) {
$errors[] = $this->newInvalidError(
pht('You must be an administrator to rename users.'));
}
if (!strlen($new)) {
$errors[] = $this->newRequiredError(
pht('New username is required.'), $xaction);
} else if (!PhabricatorUser::validateUsername($new)) {
$errors[] = $this->newInvalidError(
PhabricatorUser::describeValidUsername(), $xaction);
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($new))
->executeOne();
if ($user) {
$errors[] = $this->newInvalidError(
pht('Another user already has that username.'), $xaction);
}
}
return $errors;
}
public function getRequiredCapabilities(
$object,
PhabricatorApplicationTransaction $xaction) {
// Unlike normal user edits, renames require admin permissions, which
// is enforced by validateTransactions().
return null;
}
}

View file

@ -41,12 +41,19 @@ final class PhrictionEditConduitAPIMethod extends PhrictionConduitAPIMethod {
}
$xactions = array();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('title'));
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('content'));
if ($request->getValue('title')) {
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('title'));
}
if ($request->getValue('content')) {
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue($request->getValue('content'));
}
$editor = id(new PhrictionTransactionEditor())
->setActor($request->getUser())

View file

@ -2,6 +2,14 @@
abstract class PhrictionController extends PhabricatorController {
private $showingWelcomeDocument = false;
public function setShowingWelcomeDocument($show_welcome) {
$this->showingWelcomeDocument = $show_welcome;
return $this;
}
public function buildSideNavView($for_app = false) {
$user = $this->getRequest()->getUser();
@ -37,12 +45,14 @@ abstract class PhrictionController extends PhabricatorController {
->setIcon('fa-home'));
}
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('New Document'))
->setHref('/phriction/new/?slug='.$this->getDocumentSlug())
->setWorkflow(true)
->setIcon('fa-plus-square'));
if (!$this->showingWelcomeDocument) {
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('New Document'))
->setHref('/phriction/new/?slug='.$this->getDocumentSlug())
->setWorkflow(true)
->setIcon('fa-plus-square'));
}
return $crumbs;
}

View file

@ -9,6 +9,7 @@ final class PhrictionDocumentController
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$this->slug = $request->getURIData('slug');
@ -35,15 +36,15 @@ final class PhrictionDocumentController
->needContent(true)
->executeOne();
if (!$document) {
$document = PhrictionDocument::initializeNewDocument($viewer, $slug);
if ($slug == '/') {
$title = pht('Welcome to Phriction');
$subtitle = pht('Phriction is a simple and easy to use wiki for '.
'keeping track of documents and their changes.');
$page_title = pht('Welcome');
$create_text = pht('Edit this Document');
$this->setShowingWelcomeDocument(true);
} else {
$title = pht('No Document Here');

View file

@ -30,19 +30,16 @@ final class ReleephGetBranchesConduitAPIMethod extends ReleephConduitAPIMethod {
foreach ($projects as $project) {
$repository = $project->getRepository();
$branches = $project->loadRelatives(
id(new ReleephBranch()),
'releephProjectID',
'getID',
'isActive = 1');
$branches = id(new ReleephBranch())->loadAllWhere(
'releephProjectID = %d AND isActive = 1',
$project->getID());
foreach ($branches as $branch) {
$full_branch_name = $branch->getName();
$cut_point_commit = $branch->loadOneRelative(
id(new PhabricatorRepositoryCommit()),
'phid',
'getCutPointCommitPHID');
$cut_point_commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'phid = %s',
$branch->getCutPointCommitPHID());
$results[] = array(
'project' => $project->getName(),

View file

@ -22,16 +22,17 @@ final class ReleephDiffSizeFieldSpecification
}
$diff_rev = $requested_object;
$diffs = $diff_rev->loadRelatives(
new DifferentialDiff(),
'revisionID',
'getID',
'creationMethod <> "commit"');
$diffs = id(new DifferentialDiff())->loadAllWhere(
'revisionID = %d AND creationMethod != %s',
$diff_rev->getID(),
'commit');
$all_changesets = array();
$most_recent_changesets = null;
foreach ($diffs as $diff) {
$changesets = $diff->loadRelatives(new DifferentialChangeset(), 'diffID');
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$diff->getID());
$all_changesets += $changesets;
$most_recent_changesets = $changesets;
}

View file

@ -257,18 +257,17 @@ final class ReleephRequest extends ReleephDAO
/* -( Loading external objects )------------------------------------------- */
public function loadPhabricatorRepositoryCommit() {
return $this->loadOneRelative(
new PhabricatorRepositoryCommit(),
'phid',
'getRequestCommitPHID');
return id(new PhabricatorRepositoryCommit())->loadOneWhere(
'phid = %s',
$this->getRequestCommitPHID());
}
public function loadPhabricatorRepositoryCommitData() {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
return $commit->loadOneRelative(
new PhabricatorRepositoryCommitData(),
'commitID');
return id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
}
}

View file

@ -15,11 +15,11 @@ final class PhabricatorRepositoryManagementThawWorkflow
array(
array(
'name' => 'demote',
'param' => 'device/service',
'param' => 'device|service',
'help' => pht(
'Demote a device (or all devices in a service) discarding '.
'local changes. Clears stuck write locks and recovers from '.
'lost leaders.'),
'unsynchronized changes. Clears stuck write locks and recovers '.
'from lost leaders.'),
),
array(
'name' => 'promote',

View file

@ -239,20 +239,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return idx($this->details, $key, $default);
}
public function getHumanReadableDetail($key, $default = null) {
$value = $this->getDetail($key, $default);
switch ($key) {
case 'branch-filter':
case 'close-commits-filter':
$value = array_keys($value);
$value = implode(', ', $value);
break;
}
return $value;
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
@ -1202,6 +1188,26 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return null;
}
public function getAutocloseOnlyRules() {
return array_keys($this->getDetail('close-commits-filter', array()));
}
public function setAutocloseOnlyRules(array $rules) {
$rules = array_fill_keys($rules, true);
$this->setDetail('close-commits-filter', $rules);
return $this;
}
public function getTrackOnlyRules() {
return array_keys($this->getDetail('branch-filter', array()));
}
public function setTrackOnlyRules(array $rules) {
$rules = array_fill_keys($rules, true);
$this->setDetail('branch-filter', $rules);
return $this;
}
/* -( Repository URI Management )------------------------------------------ */

View file

@ -37,11 +37,7 @@ final class PhabricatorRepositoryTestCase
$repo->shouldTrackBranch('imaginary'),
pht('Track all branches by default.'));
$repo->setDetail(
'branch-filter',
array(
'master' => true,
));
$repo->setTrackOnlyRules(array('master'));
$this->assertTrue(
$repo->shouldTrackBranch('master'),

View file

@ -114,40 +114,36 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker
PhabricatorRepositoryCommit $commit,
array $changes) {
$conn = $repository->establishConnection('w');
$repository_id = (int)$repository->getID();
$commit_id = (int)$commit->getID();
// NOTE: This SQL is being built manually instead of with qsprintf()
// because some SVN changes affect an enormous number of paths (millions)
// and this showed up as significantly slow on a profile at some point.
$changes_sql = array();
foreach ($changes as $change) {
$values = array(
$changes_sql[] = qsprintf(
$conn,
'(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)',
$repository_id,
(int)$change->getPathID(),
$commit_id,
nonempty((int)$change->getTargetPathID(), 'null'),
nonempty((int)$change->getTargetCommitID(), 'null'),
nonempty((int)$change->getTargetPathID(), null),
nonempty((int)$change->getTargetCommitID(), null),
(int)$change->getChangeType(),
(int)$change->getFileType(),
(int)$change->getIsDirect(),
(int)$change->getCommitSequence(),
);
$changes_sql[] = '('.implode(', ', $values).')';
(int)$change->getCommitSequence());
}
$conn_w = $repository->establishConnection('w');
queryfx(
$conn_w,
$conn,
'DELETE FROM %T WHERE commitID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit_id);
foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) {
queryfx(
$conn_w,
$conn,
'INSERT INTO %T
(repositoryID, pathID, commitID, targetPathID, targetCommitID,
changeType, fileType, isDirect, commitSequence)

View file

@ -6,11 +6,11 @@ final class PhabricatorRepositoryAutocloseOnlyTransaction
const TRANSACTIONTYPE = 'repo:autoclose-only';
public function generateOldValue($object) {
return array_keys($object->getDetail('close-commits-filter', array()));
return $object->getAutocloseOnlyRules();
}
public function applyInternalEffects($object, $value) {
$object->setDetail('close-commits-filter', array_fill_keys($value, true));
$object->setAutocloseOnlyRules($value);
}
public function getTitle() {

View file

@ -6,11 +6,11 @@ final class PhabricatorRepositoryTrackOnlyTransaction
const TRANSACTIONTYPE = 'repo:track-only';
public function generateOldValue($object) {
return array_keys($object->getDetail('branch-filter', array()));
return $object->getTrackOnlyRules();
}
public function applyInternalEffects($object, $value) {
$object->setDetail('branch-filter', array_fill_keys($value, true));
$object->setTrackOnlyRules($value);
}
public function getTitle() {

View file

@ -63,7 +63,7 @@ final class PhabricatorSettingsTimezoneController
$server_offset = $viewer->getTimeZoneOffset();
if ($client_offset == $server_offset || $did_calibrate) {
if (($client_offset == $server_offset) || $did_calibrate) {
return $this->newDialog()
->setTitle(pht('Timezone Calibrated'))
->appendParagraph(
@ -113,12 +113,13 @@ final class PhabricatorSettingsTimezoneController
}
private function formatOffset($offset) {
$offset = $offset / 60;
if ($offset >= 0) {
return pht('UTC-%d', $offset);
$hours = $offset / 60;
// Non-integer number of hours off UTC?
if ($offset % 60) {
$minutes = abs($offset % 60);
return pht('UTC%+d:%02d', $hours, $minutes);
} else {
return pht('UTC+%d', -$offset);
return pht('UTC%+d', $hours);
}
}

View file

@ -193,7 +193,8 @@ final class PhabricatorMultiFactorSettingsPanel
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$config->getID()));

View file

@ -143,7 +143,7 @@ final class PhabricatorNotificationsSettingsPanel
->setCaption(
pht(
'Phabricator can send real-time notifications to your web browser '.
'or to your desktop. Select where you\'d want to receive these '.
'or to your desktop. Select where you want to receive these '.
'real-time updates.'))
->initBehavior(
'desktop-notifications-control',

View file

@ -121,7 +121,8 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())->setURI($next);
}

View file

@ -44,8 +44,9 @@ final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel {
->withPHIDs($identity_phids)
->execute();
$current_key = PhabricatorHash::weakDigest(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
$current_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
$rows = array();
$rowc = array();

View file

@ -358,7 +358,7 @@ abstract class PhabricatorEditEngine
return $this->editEngineConfiguration;
}
private function newConfigurationQuery() {
public function newConfigurationQuery() {
return id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($this->getViewer())
->withEngineKeys(array($this->getEngineKey()));

View file

@ -11,6 +11,8 @@ final class PhabricatorEditEngineSubtype
private $icon;
private $tagText;
private $color;
private $childSubtypes = array();
private $childIdentifiers = array();
public function setKey($key) {
$this->key = $key;
@ -57,6 +59,24 @@ final class PhabricatorEditEngineSubtype
return $this->color;
}
public function setChildSubtypes(array $child_subtypes) {
$this->childSubtypes = $child_subtypes;
return $this;
}
public function getChildSubtypes() {
return $this->childSubtypes;
}
public function setChildFormIdentifiers(array $child_identifiers) {
$this->childIdentifiers = $child_identifiers;
return $this;
}
public function getChildFormIdentifiers() {
return $this->childIdentifiers;
}
public function hasTagView() {
return (bool)strlen($this->getTagText());
}
@ -118,6 +138,7 @@ final class PhabricatorEditEngineSubtype
'tag' => 'optional string',
'color' => 'optional string',
'icon' => 'optional string',
'children' => 'optional map<string, wild>',
));
$key = $value['key'];
@ -141,6 +162,27 @@ final class PhabricatorEditEngineSubtype
'no name. Subtypes must have a name.',
$key));
}
$children = idx($value, 'children');
if ($children) {
PhutilTypeSpec::checkMap(
$children,
array(
'subtypes' => 'optional list<string>',
'forms' => 'optional list<string|int>',
));
$child_subtypes = idx($children, 'subtypes');
$child_forms = idx($children, 'forms');
if ($child_subtypes && $child_forms) {
throw new Exception(
pht(
'Subtype configuration is invalid: subtype with key "%s" '.
'specifies both child subtypes and child forms. Specify one '.
'or the other, but not both.'));
}
}
}
if (!isset($map[self::SUBTYPE_DEFAULT])) {
@ -179,10 +221,27 @@ final class PhabricatorEditEngineSubtype
$subtype->setColor($color);
}
$children = idx($entry, 'children', array());
$child_subtypes = idx($children, 'subtypes');
$child_forms = idx($children, 'forms');
if ($child_subtypes) {
$subtype->setChildSubtypes($child_subtypes);
}
if ($child_forms) {
$subtype->setChildFormIdentifiers($child_forms);
}
$map[$key] = $subtype;
}
return new PhabricatorEditEngineSubtypeMap($map);
}
public function newIconView() {
return id(new PHUIIconView())
->setIcon($this->getIcon(), $this->getColor());
}
}

View file

@ -39,4 +39,45 @@ final class PhabricatorEditEngineSubtypeMap
return $this->subtypes[$subtype_key];
}
public function getCreateFormsForSubtype(
PhabricatorEditEngine $edit_engine,
PhabricatorEditEngineSubtypeInterface $object) {
$subtype_key = $object->getEditEngineSubtype();
$subtype = $this->getSubtype($subtype_key);
$select_identifiers = $subtype->getChildFormIdentifiers();
$select_subtypes = $subtype->getChildSubtypes();
$query = $edit_engine->newConfigurationQuery()
->withIsDisabled(false);
if ($select_identifiers) {
$query->withIdentifiers($select_identifiers);
} else {
// If we're selecting by subtype rather than selecting specific forms,
// only select create forms.
$query->withIsDefault(true);
if ($select_subtypes) {
$query->withSubtypes($select_subtypes);
} else {
$query->withSubtypes(array($subtype_key));
}
}
$forms = $query->execute();
$forms = mpull($forms, null, 'getIdentifier');
// If we're selecting by ID, respect the order specified in the
// constraint. Otherwise, use the create form sort order.
if ($select_identifiers) {
$forms = array_select_keys($forms, $select_identifiers) + $forms;
} else {
$forms = msort($forms, 'getCreateSortKey');
}
return $forms;
}
}

View file

@ -3378,9 +3378,28 @@ abstract class PhabricatorApplicationTransactionEditor
PhabricatorLiskDAO $object,
array $xactions) {
return array_unique(array_merge(
$this->getMailTo($object),
$this->getMailCC($object)));
// If some transactions are forcing notification delivery, add the forced
// recipients to the notify list.
$force_list = array();
foreach ($xactions as $xaction) {
$force_phids = $xaction->getForceNotifyPHIDs();
if (!$force_phids) {
continue;
}
foreach ($force_phids as $force_phid) {
$force_list[] = $force_phid;
}
}
$to_list = $this->getMailTo($object);
$cc_list = $this->getMailCC($object);
$full_list = array_merge($force_list, $to_list, $cc_list);
$full_list = array_fuse($full_list);
return array_keys($full_list);
}
@ -3409,7 +3428,19 @@ abstract class PhabricatorApplicationTransactionEditor
array $xactions,
array $mailed_phids) {
$xactions = mfilter($xactions, 'shouldHideForFeed', true);
// Remove transactions which don't publish feed stories or notifications.
// These never show up anywhere, so we don't need to do anything with them.
foreach ($xactions as $key => $xaction) {
if (!$xaction->shouldHideForFeed()) {
continue;
}
if (!$xaction->shouldHideForNotifications()) {
continue;
}
unset($xactions[$key]);
}
if (!$xactions) {
return;
@ -4594,4 +4625,87 @@ abstract class PhabricatorApplicationTransactionEditor
return $mail;
}
public function newAutomaticInlineTransactions(
PhabricatorLiskDAO $object,
array $inlines,
$transaction_type,
PhabricatorCursorPagedPolicyAwareQuery $query_template) {
$xactions = array();
foreach ($inlines as $inline) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType($transaction_type)
->attachComment($inline);
}
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
if ($state_xaction) {
$xactions[] = $state_xaction;
}
return $xactions;
}
protected function newInlineStateTransaction(
PhabricatorLiskDAO $object,
PhabricatorCursorPagedPolicyAwareQuery $query_template) {
$actor_phid = $this->getActingAsPHID();
$author_phid = $object->getAuthorPHID();
$actor_is_author = ($actor_phid == $author_phid);
$state_map = PhabricatorTransactions::getInlineStateMap();
$query = id(clone $query_template)
->setViewer($this->getActor())
->withFixedStates(array_keys($state_map));
$inlines = array();
$inlines[] = id(clone $query)
->withAuthorPHIDs(array($actor_phid))
->withHasTransaction(false)
->execute();
if ($actor_is_author) {
$inlines[] = id(clone $query)
->withHasTransaction(true)
->execute();
}
$inlines = array_mergev($inlines);
if (!$inlines) {
return null;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
// See PHI995. Copy some information about the inlines into the transaction
// so we can tailor rendering behavior. In particular, we don't want to
// render transactions about users marking their own inlines as "Done".
$inline_details = array();
foreach ($inlines as $inline) {
$inline_details[$inline->getPHID()] = array(
'authorPHID' => $inline->getAuthorPHID(),
);
}
return $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setMetadataValue('inline.details', $inline_details)
->setOldValue($old_value)
->setNewValue($new_value);
}
}

View file

@ -36,12 +36,7 @@ class PhabricatorApplicationTransactionFeedStory
// time because it's cheap and gets us better results when things change
// by letting the changes apply retroactively.
$xaction_phids = $this->getValue('transactionPHIDs');
$xactions = array();
foreach ($xaction_phids as $xaction_phid) {
$xactions[] = $this->getObject($xaction_phid);
}
$xactions = $this->getTransactions();
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHideForFeed()) {
@ -52,7 +47,7 @@ class PhabricatorApplicationTransactionFeedStory
if ($xactions) {
$primary_phid = head($xactions)->getPHID();
} else {
$primary_phid = head($xaction_phids);
$primary_phid = head($this->getValue('transactionPHIDs'));
}
$this->primaryTransactionPHID = $primary_phid;
@ -61,6 +56,41 @@ class PhabricatorApplicationTransactionFeedStory
return $this->primaryTransactionPHID;
}
public function isVisibleInNotifications() {
$xactions = $this->getTransactions();
foreach ($xactions as $key => $xaction) {
if (!$xaction->shouldHideForNotifications()) {
return true;
}
}
return false;
}
public function isVisibleInFeed() {
$xactions = $this->getTransactions();
foreach ($xactions as $key => $xaction) {
if (!$xaction->shouldHideForFeed()) {
return true;
}
}
return false;
}
private function getTransactions() {
$xaction_phids = $this->getValue('transactionPHIDs');
$xactions = array();
foreach ($xaction_phids as $xaction_phid) {
$xactions[] = $this->getObject($xaction_phid);
}
return $xactions;
}
public function getPrimaryTransaction() {
return $this->getObject($this->getPrimaryTransactionPHID());
}

View file

@ -115,14 +115,6 @@ final class PhabricatorEditEngineConfigurationSearchEngine
->setHeader($config->getDisplayName());
$id = $config->getID();
if ($id) {
$item->addIcon('fa-file-text-o bluegrey', pht('Form %d', $id));
$key = $id;
} else {
$item->addIcon('fa-file-text bluegrey', pht('Builtin'));
$key = $config->getBuiltinKey();
}
$item->setHref("/transactions/editengine/{$engine_key}/view/{$key}/");
if ($config->getIsDefault()) {
$item->addAttribute(pht('Default Create Form'));
@ -139,6 +131,31 @@ final class PhabricatorEditEngineConfigurationSearchEngine
$item->setStatusIcon('fa-file-text-o green', pht('Enabled'));
}
$subtype_key = $config->getSubtype();
if ($subtype_key !== PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) {
$engine = $config->getEngine();
if ($engine->supportsSubtypes()) {
$map = $engine->newSubtypeMap();
if ($map->isValidSubtype($subtype_key)) {
$subtype = $map->getSubtype($subtype_key);
$icon = $subtype->getIcon();
$color = $subtype->getColor();
$item->addIcon("{$icon} {$color}", $subtype->getName());
}
}
}
if ($id) {
$item->setObjectName(pht('Form %d', $id));
$key = $id;
} else {
$item->addIcon('fa-file-text bluegrey', pht('Builtin'));
$key = $config->getBuiltinKey();
}
$item->setHref("/transactions/editengine/{$engine_key}/view/{$key}/");
$list->addItem($item);
}

View file

@ -664,6 +664,16 @@ abstract class PhabricatorApplicationTransaction
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
if (!$done && !$undone) {
return true;
}
break;
}
return false;
@ -749,6 +759,10 @@ abstract class PhabricatorApplicationTransaction
return $this->shouldHide();
}
public function shouldHideForNotifications() {
return $this->shouldHideForFeed();
}
public function getTitleForMail() {
return id(clone $this)->setRenderingTarget('text')->getTitle();
}
@ -1007,15 +1021,7 @@ abstract class PhabricatorApplicationTransaction
}
case PhabricatorTransactions::TYPE_INLINESTATE:
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
$done++;
} else {
$undone++;
}
}
list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
if ($done && $undone) {
return pht(
'%s marked %s inline comment(s) as done and %s inline comment(s) '.
@ -1582,6 +1588,49 @@ abstract class PhabricatorApplicationTransaction
return $moves;
}
private function getInterestingInlineStateChangeCounts() {
// See PHI995. Newer inline state transactions have additional details
// which we use to tailor the rendering behavior. These details are not
// present on older transactions.
$details = $this->getMetadataValue('inline.details', array());
$new = $this->getNewValue();
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
$is_done = ($state == PhabricatorInlineCommentInterface::STATE_DONE);
// See PHI995. If you're marking your own inline comments as "Done",
// don't count them when rendering a timeline story. In the case where
// you're only affecting your own comments, this will hide the
// "alice marked X comments as done" story entirely.
// Usually, this happens when you pre-mark inlines as "done" and submit
// them yourself. We'll still generate an "alice added inline comments"
// story (in most cases/contexts), but the state change story is largely
// just clutter and slightly confusing/misleading.
$inline_details = idx($details, $phid, array());
$inline_author_phid = idx($inline_details, 'authorPHID');
if ($inline_author_phid) {
if ($inline_author_phid == $this->getAuthorPHID()) {
if ($is_done) {
continue;
}
}
}
if ($is_done) {
$done++;
} else {
$undone++;
}
}
return array($done, $undone);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
@ -1617,6 +1666,15 @@ abstract class PhabricatorApplicationTransaction
return null;
}
public function setForceNotifyPHIDs(array $phids) {
$this->setMetadataValue('notify.force', $phids);
return $this;
}
public function getForceNotifyPHIDs() {
return $this->getMetadataValue('notify.force', array());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -108,6 +108,17 @@ abstract class PhabricatorModularTransaction
return parent::shouldHideForMail($xactions);
}
final public function shouldHideForNotifications() {
$hide = $this->getTransactionImplementation()->shouldHideForNotifications();
// Returning "null" means "use the default behavior".
if ($hide === null) {
return parent::shouldHideForNotifications();
}
return $hide;
}
/* final */ public function getIcon() {
$icon = $this->getTransactionImplementation()->getIcon();
if ($icon !== null) {

View file

@ -59,6 +59,10 @@ abstract class PhabricatorModularTransactionType
return false;
}
public function shouldHideForNotifications() {
return null;
}
public function getIcon() {
return null;
}

View file

@ -193,8 +193,6 @@ abstract class LiskDAO extends Phobject
private static $connections = array();
private $inSet = null;
protected $id;
protected $phid;
protected $dateCreated;
@ -659,179 +657,11 @@ abstract class LiskDAO extends Phobject
} else {
$result[] = $obj->loadFromArray($row);
}
if ($this->inSet) {
$this->inSet->addToSet($obj);
}
}
return $result;
}
/**
* This method helps to prevent the 1+N queries problem. It happens when you
* execute a query for each row in a result set. Like in this code:
*
* COUNTEREXAMPLE, name=Easy to write but expensive to execute
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* foreach ($diffs as $diff) {
* $changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID = %d',
* $diff->getID());
* // Do something with $changesets.
* }
*
* One can solve this problem by reading all the dependent objects at once and
* assigning them later:
*
* COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID IN (%Ld)',
* mpull($diffs, 'getID'));
* $all_changesets = mgroup($all_changesets, 'getDiffID');
* foreach ($diffs as $diff) {
* $changesets = idx($all_changesets, $diff->getID(), array());
* // Do something with $changesets.
* }
*
* The method @{method:loadRelatives} abstracts this approach which allows
* writing a code which is simple and efficient at the same time:
*
* name=Easy to write and cheap to execute
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* // Do something with $changesets.
* }
*
* This will load dependent objects for all diffs in the first call of
* @{method:loadRelatives} and use this result for all following calls.
*
* The method supports working with set of sets, like in this code:
*
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* foreach ($changesets as $changeset) {
* $hunks = $changeset->loadRelatives(
* new DifferentialHunk(),
* 'changesetID');
* // Do something with hunks.
* }
* }
*
* This code will execute just three queries - one to load all diffs, one to
* load all their related changesets and one to load all their related hunks.
* You can try to write an equivalent code without using this method as
* a homework.
*
* The method also supports retrieving referenced objects, for example authors
* of all diffs (using shortcut @{method:loadOneRelative}):
*
* foreach ($diffs as $diff) {
* $author = $diff->loadOneRelative(
* new PhabricatorUser(),
* 'phid',
* 'getAuthorPHID');
* // Do something with author.
* }
*
* It is also possible to specify additional conditions for the `WHERE`
* clause. Similarly to @{method:loadAllWhere}, you can specify everything
* after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
* allowed to pass only a constant string (`%` doesn't have a special
* meaning). This is intentional to avoid mistakes with using data from one
* row in retrieving other rows. Example of a correct usage:
*
* $status = $author->loadOneRelative(
* new PhabricatorCalendarEvent(),
* 'userPHID',
* 'getPHID',
* '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return list Objects of type $object.
*
* @task load
*/
public function loadRelatives(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
if (!$this->inSet) {
id(new LiskDAOSet())->addToSet($this);
}
$relatives = $this->inSet->loadRelatives(
$object,
$foreign_column,
$key_method,
$where);
return idx($relatives, $this->$key_method(), array());
}
/**
* Load referenced row. See @{method:loadRelatives} for details.
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return LiskDAO Object of type $object or null if there's no such object.
*
* @task load
*/
final public function loadOneRelative(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
$relatives = $this->loadRelatives(
$object,
$foreign_column,
$key_method,
$where);
if (!$relatives) {
return null;
}
if (count($relatives) > 1) {
throw new AphrontCountQueryException(
pht(
'More than one result from %s!',
__FUNCTION__.'()'));
}
return reset($relatives);
}
final public function putInSet(LiskDAOSet $set) {
$this->inSet = $set;
return $this;
}
final protected function getInSet() {
return $this->inSet;
}
/* -( Examining Objects )-------------------------------------------------- */

View file

@ -1,101 +0,0 @@
<?php
/**
* You usually don't need to use this class directly as it is controlled by
* @{class:LiskDAO}. You can create it if you want to work with objects of same
* type from different sources as with one set. Let's say you want to get
* e-mails of all users involved in a revision:
*
* $users = new LiskDAOSet();
* $users->addToSet($author);
* foreach ($reviewers as $reviewer) {
* $users->addToSet($reviewer);
* }
* foreach ($ccs as $cc) {
* $users->addToSet($cc);
* }
* // Preload e-mails of all involved users and return e-mails of author.
* $author_emails = $author->loadRelatives(
* new PhabricatorUserEmail(),
* 'userPHID',
* 'getPHID');
*/
final class LiskDAOSet extends Phobject {
private $daos = array();
private $relatives = array();
private $subsets = array();
public function addToSet(LiskDAO $dao) {
if ($this->relatives) {
throw new Exception(
pht(
"Don't call %s after loading data!",
__FUNCTION__.'()'));
}
$this->daos[] = $dao;
$dao->putInSet($this);
return $this;
}
/**
* The main purpose of this method is to break cyclic dependency.
* It removes all objects from this set and all subsets created by it.
*/
public function clearSet() {
$this->daos = array();
$this->relatives = array();
foreach ($this->subsets as $set) {
$set->clearSet();
}
$this->subsets = array();
return $this;
}
/**
* See @{method:LiskDAO::loadRelatives}.
*/
public function loadRelatives(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
$relatives = &$this->relatives[
get_class($object)."-{$foreign_column}-{$key_method}-{$where}"];
if ($relatives === null) {
$ids = array();
foreach ($this->daos as $dao) {
$id = $dao->$key_method();
if ($id !== null) {
$ids[$id] = $id;
}
}
if (!$ids) {
$relatives = array();
} else {
$set = new LiskDAOSet();
$this->subsets[] = $set;
$conn = $object->establishConnection('r');
if (strlen($where)) {
$where_clause = qsprintf($conn, 'AND %Q', $where);
} else {
$where_clause = qsprintf($conn, '');
}
$relatives = $object->putInSet($set)->loadAllWhere(
'%C IN (%Ls) %Q',
$foreign_column,
$ids,
$where_clause);
$relatives = mgroup($relatives, 'get'.$foreign_column);
}
}
return $relatives;
}
}

View file

@ -17,11 +17,9 @@ final class LiskMigrationIterator extends PhutilBufferedIterator {
private $object;
private $cursor;
private $set;
public function __construct(LiskDAO $object) {
$this->set = new LiskDAOSet();
$this->object = $object->putInSet($this->set);
$this->object = $object;
}
protected function didRewind() {
@ -33,15 +31,15 @@ final class LiskMigrationIterator extends PhutilBufferedIterator {
}
protected function loadPage() {
$this->set->clearSet();
$results = $this->object->loadAllWhere(
'id > %d ORDER BY id ASC LIMIT %d',
$this->cursor,
$this->getPageSize());
if ($results) {
$this->cursor = last($results)->getID();
}
return $results;
}

View file

@ -187,6 +187,16 @@ final class PhabricatorHash extends Phobject {
}
public static function digestHMACSHA256($message, $key) {
if (!is_string($message)) {
throw new Exception(
pht('HMAC-SHA256 can only digest strings.'));
}
if (!is_string($key)) {
throw new Exception(
pht('HMAC-SHA256 keys must be strings.'));
}
if (!strlen($key)) {
throw new Exception(
pht('HMAC-SHA256 requires a nonempty key.'));
@ -194,7 +204,9 @@ final class PhabricatorHash extends Phobject {
$result = hash_hmac('sha256', $message, $key, $raw_output = false);
if ($result === false) {
// Although "hash_hmac()" is documented as returning `false` when it fails,
// it can also return `null` if you pass an object as the "$message".
if ($result === false || $result === null) {
throw new Exception(
pht('Unable to compute HMAC-SHA256 digest of message.'));
}

View file

@ -404,7 +404,7 @@ final class AphrontDialogView
$header),
phutil_tag('div',
array(
'class' => 'aphront-dialog-body phabricator-remarkup grouped',
'class' => 'aphront-dialog-body grouped',
),
$children),
$tail,

View file

@ -28,6 +28,7 @@ final class PHUIObjectItemView extends AphrontTagView {
private $sideColumn;
private $coverImage;
private $description;
private $clickable;
private $selectableName;
private $selectableValue;
@ -179,6 +180,15 @@ final class PHUIObjectItemView extends AphrontTagView {
return $this;
}
public function setClickable($clickable) {
$this->clickable = $clickable;
return $this;
}
public function getClickable() {
return $this->clickable;
}
public function setEpoch($epoch) {
$date = phabricator_datetime($epoch, $this->getUser());
$this->addIcon('none', $date);
@ -332,6 +342,13 @@ final class PHUIObjectItemView extends AphrontTagView {
$item_classes[] = 'phui-oi-with-image-icon';
}
if ($this->getClickable()) {
Javelin::initBehavior('linked-container');
$item_classes[] = 'phui-oi-linked-container';
$sigils[] = 'linked-container';
}
return array(
'class' => $item_classes,
'sigil' => $sigils,
@ -443,15 +460,6 @@ final class PHUIObjectItemView extends AphrontTagView {
),
$spec['label']);
if (isset($spec['attributes']['href'])) {
$icon_href = phutil_tag(
'a',
array('href' => $spec['attributes']['href']),
array($icon, $label));
} else {
$icon_href = array($icon, $label);
}
$classes = array();
$classes[] = 'phui-oi-icon';
if (isset($spec['attributes']['class'])) {
@ -463,7 +471,10 @@ final class PHUIObjectItemView extends AphrontTagView {
array(
'class' => implode(' ', $classes),
),
$icon_href);
array(
$icon,
$label,
));
}
$icons[] = phutil_tag(

View file

@ -59,3 +59,16 @@
background-color: {$hoverblue};
border-radius: 3px;
}
.device-desktop .phui-oi-linked-container {
cursor: pointer;
}
.device-desktop .phui-oi-linked-container:hover {
background-color: {$hoverblue};
border-radius: 3px;
}
.device-desktop .phui-oi-linked-container a:hover {
text-decoration: none;
}

View file

@ -49,7 +49,7 @@ JX.require = function(thing) {
}
vm.createScript(content, path)
.runInNewContext(sandbox, path);
.runInNewContext(sandbox);
};
exports.JX = JX;

View file

@ -0,0 +1,47 @@
/**
* @provides javelin-behavior-linked-container
* @requires javelin-behavior javelin-dom
*/
JX.behavior('linked-container', function() {
JX.Stratcom.listen(
'click',
'linked-container',
function(e) {
// If the user clicked some link inside the container, bail out and just
// click the link.
if (e.getNode('tag:a')) {
return;
}
// If this is some sort of unusual click, bail out. Note that we'll
// handle "Left Click" and "Command + Left Click" differently, below.
if (!e.isLeftButton()) {
return;
}
var container = e.getNode('linked-container');
// Find the first link in the container. We're going to pretend the user
// clicked it.
var link = JX.DOM.scry(container, 'a')[0];
if (!link) {
return;
}
// If the click is a "Command + Left Click", change the target of the
// link so we open it in a new tab.
var is_command = !!e.getRawEvent().metaKey;
if (is_command) {
var old_target = link.target;
link.target = '_blank';
link.click();
link.target = old_target;
} else {
link.click();
}
});
});