1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-27 09:12:41 +01:00

(stable) Promote 2016 Week 47

This commit is contained in:
epriestley 2016-11-18 15:23:02 -08:00
commit 10c4dedd18
126 changed files with 3139 additions and 1185 deletions

View file

@ -9,8 +9,8 @@ return array(
'names' => array(
'conpherence.pkg.css' => '0b64e988',
'conpherence.pkg.js' => '6249a1cf',
'core.pkg.css' => 'a729d20e',
'core.pkg.js' => '1a77dddf',
'core.pkg.css' => '347113ea',
'core.pkg.js' => '40e98735',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => 'a4ba74b5',
'differential.pkg.js' => '634399e9',
@ -21,8 +21,7 @@ return array(
'maniphest.pkg.js' => '949a7498',
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
'rsrc/css/aphront/dark-console.css' => 'f54bf286',
'rsrc/css/aphront/dialog-view.css' => 'ea3745f5',
'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d',
'rsrc/css/aphront/dialog-view.css' => '49b2a8a3',
'rsrc/css/aphront/list-filter-view.css' => '5d6f0526',
'rsrc/css/aphront/multi-column.css' => '84cc6640',
'rsrc/css/aphront/notification.css' => '3f6c89c9',
@ -84,7 +83,7 @@ return array(
'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b',
'rsrc/css/application/paste/paste.css' => '1898e534',
'rsrc/css/application/people/people-profile.css' => '2473d929',
'rsrc/css/application/phame/phame.css' => '8efb0729',
'rsrc/css/application/phame/phame.css' => '654dd9ef',
'rsrc/css/application/pholio/pholio-edit.css' => '07676f51',
'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
'rsrc/css/application/pholio/pholio.css' => 'ca89d380',
@ -133,14 +132,15 @@ return array(
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
'rsrc/css/phui/phui-cms.css' => 'be43c8a8',
'rsrc/css/phui/phui-comment-form.css' => '4ecc56ef',
'rsrc/css/phui/phui-comment-panel.css' => '85113e6a',
'rsrc/css/phui/phui-crumbs-view.css' => '195ac419',
'rsrc/css/phui/phui-curtain-view.css' => '947bf1a4',
'rsrc/css/phui/phui-document-pro.css' => 'ca1fed81',
'rsrc/css/phui/phui-document-pro.css' => 'c354e312',
'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf',
'rsrc/css/phui/phui-document.css' => 'c32e8dec',
'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9',
'rsrc/css/phui/phui-fontkit.css' => '9cda225e',
'rsrc/css/phui/phui-form-view.css' => '91adabe4',
'rsrc/css/phui/phui-form-view.css' => '3fadd537',
'rsrc/css/phui/phui-form.css' => 'b8fb087a',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
'rsrc/css/phui/phui-header-view.css' => '6ec8f155',
@ -149,8 +149,9 @@ return array(
'rsrc/css/phui/phui-icon.css' => '417f80fb',
'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c',
'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
'rsrc/css/phui/phui-info-view.css' => '28efab79',
'rsrc/css/phui/phui-info-view.css' => 'ec92802a',
'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0',
'rsrc/css/phui/phui-lightbox.css' => 'e17ce2bd',
'rsrc/css/phui/phui-list.css' => '9da2aa00',
'rsrc/css/phui/phui-object-box.css' => '6b487c57',
'rsrc/css/phui/phui-object-item-list-view.css' => '87278fa0',
@ -165,8 +166,8 @@ return array(
'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2',
'rsrc/css/phui/phui-timeline-view.css' => 'bc523970',
'rsrc/css/phui/phui-two-column-view.css' => 'bbe32c23',
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7',
'rsrc/css/phui/workboards/phui-workboard.css' => 'e09eb53a',
'rsrc/css/phui/workboards/phui-workboard-color.css' => '207828dd',
'rsrc/css/phui/workboards/phui-workboard.css' => '60d09514',
'rsrc/css/phui/workboards/phui-workcard.css' => '0c62d7c5',
'rsrc/css/phui/workboards/phui-workpanel.css' => '92197373',
'rsrc/css/sprite-login.css' => '6dbbbd97',
@ -261,7 +262,7 @@ return array(
'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a',
'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '8d3bc1b2',
'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838',
'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '185bbd53',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '013ffff9',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
@ -323,12 +324,6 @@ return array(
'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264',
'rsrc/image/icon/fatcow/source/tablet.png' => '49396799',
'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d',
'rsrc/image/icon/lightbox/close-2.png' => 'cc40e7c8',
'rsrc/image/icon/lightbox/close-hover-2.png' => 'fb5d6d9e',
'rsrc/image/icon/lightbox/left-arrow-2.png' => '8426133b',
'rsrc/image/icon/lightbox/left-arrow-hover-2.png' => '701e5ee3',
'rsrc/image/icon/lightbox/right-arrow-2.png' => '6d5519a0',
'rsrc/image/icon/lightbox/right-arrow-hover-2.png' => '3a04aa21',
'rsrc/image/icon/subscribe.png' => 'd03ed5a5',
'rsrc/image/icon/tango/attachment.png' => 'ecc8022e',
'rsrc/image/icon/tango/edit.png' => '929a1363',
@ -477,7 +472,7 @@ return array(
'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5',
'rsrc/js/core/Busy.js' => '59a7976a',
'rsrc/js/core/DragAndDropFileUpload.js' => '58dea2fa',
'rsrc/js/core/DraggableList.js' => '5a13c79f',
'rsrc/js/core/DraggableList.js' => 'bea6e7f4',
'rsrc/js/core/Favicon.js' => '1fe2510c',
'rsrc/js/core/FileUpload.js' => '680ea2c8',
'rsrc/js/core/Hovercard.js' => '1bd28176',
@ -500,7 +495,7 @@ return array(
'rsrc/js/core/behavior-device.js' => 'bb1dd507',
'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22',
'rsrc/js/core/behavior-error-log.js' => '6882e80a',
'rsrc/js/core/behavior-fancy-datepicker.js' => '568931f3',
'rsrc/js/core/behavior-fancy-datepicker.js' => 'a9210d03',
'rsrc/js/core/behavior-file-tree.js' => '88236f00',
'rsrc/js/core/behavior-form.js' => '5c54cbf3',
'rsrc/js/core/behavior-gesture.js' => '3ab51e2c',
@ -510,7 +505,7 @@ return array(
'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64',
'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'ec949017',
'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => 'e0ec7f2f',
@ -550,7 +545,7 @@ return array(
'almanac-css' => 'dbb9b3af',
'aphront-bars' => '231ac33c',
'aphront-dark-console-css' => 'f54bf286',
'aphront-dialog-view-css' => 'ea3745f5',
'aphront-dialog-view-css' => '49b2a8a3',
'aphront-list-filter-view-css' => '5d6f0526',
'aphront-multi-column-view-css' => '84cc6640',
'aphront-panel-view-css' => '8427b78d',
@ -649,14 +644,14 @@ return array(
'javelin-behavior-editengine-reorder-fields' => 'b59e1e96',
'javelin-behavior-error-log' => '6882e80a',
'javelin-behavior-event-all-day' => 'b41537c9',
'javelin-behavior-fancy-datepicker' => '568931f3',
'javelin-behavior-fancy-datepicker' => 'a9210d03',
'javelin-behavior-global-drag-and-drop' => '960f6a39',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => 'a464fe03',
'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8499b6ab',
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => 'f8ba29d7',
'javelin-behavior-lightbox-attachments' => 'ec949017',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
@ -753,7 +748,7 @@ return array(
'javelin-tokenizer' => '8d3bc1b2',
'javelin-typeahead' => '70baed2f',
'javelin-typeahead-composite-source' => '503e17fd',
'javelin-typeahead-normalizer' => 'e6e25838',
'javelin-typeahead-normalizer' => '185bbd53',
'javelin-typeahead-ondemand-source' => '013ffff9',
'javelin-typeahead-preloaded-source' => '54f314a0',
'javelin-typeahead-source' => '0fcf201c',
@ -772,7 +767,6 @@ return array(
'javelin-workboard-column' => '21df4ff5',
'javelin-workboard-controller' => '55baf5ed',
'javelin-workflow' => '1e911d0f',
'lightbox-attachment-css' => '7acac05d',
'maniphest-batch-editor' => 'b0f0b6d5',
'maniphest-report-css' => '9b9580b7',
'maniphest-task-edit-css' => 'fda62a9b',
@ -792,7 +786,7 @@ return array(
'phabricator-countdown-css' => '16c52f5c',
'phabricator-dashboard-css' => 'bc6f2127',
'phabricator-drag-and-drop-file-upload' => '58dea2fa',
'phabricator-draggable-list' => '5a13c79f',
'phabricator-draggable-list' => 'bea6e7f4',
'phabricator-fatal-config-template-css' => '8f18fa41',
'phabricator-favicon' => '1fe2510c',
'phabricator-feed-css' => 'ecd4ec57',
@ -830,7 +824,7 @@ return array(
'phabricator-uiexample-reactor-sendclass' => '1def2711',
'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee',
'phabricator-zindex-css' => 'd1270942',
'phame-css' => '8efb0729',
'phame-css' => '654dd9ef',
'pholio-css' => 'ca89d380',
'pholio-edit-css' => '07676f51',
'pholio-inline-comments-css' => '8e545e49',
@ -853,16 +847,17 @@ return array(
'phui-chart-css' => '6bf6f78e',
'phui-cms-css' => 'be43c8a8',
'phui-comment-form-css' => '4ecc56ef',
'phui-comment-panel-css' => '85113e6a',
'phui-crumbs-view-css' => '195ac419',
'phui-curtain-view-css' => '947bf1a4',
'phui-document-summary-view-css' => '9ca48bdf',
'phui-document-view-css' => 'c32e8dec',
'phui-document-view-pro-css' => 'ca1fed81',
'phui-document-view-pro-css' => 'c354e312',
'phui-feed-story-css' => '44a9c8e9',
'phui-font-icon-base-css' => '870a7360',
'phui-fontkit-css' => '9cda225e',
'phui-form-css' => 'b8fb087a',
'phui-form-view-css' => '91adabe4',
'phui-form-view-css' => '3fadd537',
'phui-head-thing-view-css' => 'fd311e5f',
'phui-header-view-css' => '6ec8f155',
'phui-hovercard' => '1bd28176',
@ -871,9 +866,10 @@ return array(
'phui-icon-view-css' => '417f80fb',
'phui-image-mask-css' => 'a8498f9c',
'phui-info-panel-css' => '27ea50a1',
'phui-info-view-css' => '28efab79',
'phui-info-view-css' => 'ec92802a',
'phui-inline-comment-view-css' => '5953c28e',
'phui-invisible-character-view-css' => '6993d9f0',
'phui-lightbox-css' => 'e17ce2bd',
'phui-list-view-css' => '9da2aa00',
'phui-object-box-css' => '6b487c57',
'phui-object-item-list-view-css' => '87278fa0',
@ -889,8 +885,8 @@ return array(
'phui-theme-css' => '798c69b8',
'phui-timeline-view-css' => 'bc523970',
'phui-two-column-view-css' => 'bbe32c23',
'phui-workboard-color-css' => 'ac6fe6a7',
'phui-workboard-view-css' => 'e09eb53a',
'phui-workboard-color-css' => '207828dd',
'phui-workboard-view-css' => '60d09514',
'phui-workcard-view-css' => '0c62d7c5',
'phui-workpanel-view-css' => '92197373',
'phuix-action-list-view' => 'b5c256b8',
@ -1043,6 +1039,9 @@ return array(
'javelin-workflow',
'javelin-workboard-controller',
),
'185bbd53' => array(
'javelin-install',
),
'1aa4c968' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1353,13 +1352,6 @@ return array(
'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board',
),
'568931f3' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'58dea2fa' => array(
'javelin-install',
'javelin-util',
@ -1379,14 +1371,6 @@ return array(
'javelin-vector',
'javelin-dom',
),
'5a13c79f' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'javelin-vector',
'javelin-magical-init',
),
'5c54cbf3' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1597,6 +1581,9 @@ return array(
'javelin-dom',
'javelin-stratcom',
),
'85113e6a' => array(
'phui-timeline-view-css',
),
'85ee8ce6' => array(
'aphront-dialog-view-css',
),
@ -1808,6 +1795,13 @@ return array(
'javelin-uri',
'phabricator-keyboard-shortcut',
),
'a9210d03' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'a9f88de2' => array(
'javelin-behavior',
'javelin-dom',
@ -1939,6 +1933,14 @@ return array(
'javelin-util',
'javelin-request',
),
'bea6e7f4' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'javelin-vector',
'javelin-magical-init',
),
'bee502c8' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2118,9 +2120,6 @@ return array(
'javelin-workflow',
'javelin-magical-init',
),
'e6e25838' => array(
'javelin-install',
),
'e9581f08' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2128,6 +2127,15 @@ return array(
'javelin-dom',
'phabricator-draggable-list',
),
'ec949017' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'edd1ba66' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2188,14 +2196,6 @@ return array(
'javelin-install',
'javelin-dom',
),
'f8ba29d7' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phabricator-busy',
),
'fb20ac8d' => array(
'javelin-behavior',
'javelin-aphlict',
@ -2288,7 +2288,8 @@ return array(
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'lightbox-attachment-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',

View file

@ -111,7 +111,8 @@ return array(
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'lightbox-attachment-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_phame.phame_post
ADD subtitle VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_phame.phame_post
ADD headerImagePHID VARBINARY(64);

View file

@ -34,7 +34,13 @@ try {
'name' => 'host',
'param' => 'hostname',
'help' => pht(
'Connect to __host__ instead of the default host.'),
'Operate on the database server identified by __hostname__.'),
),
array(
'name' => 'ref',
'param' => 'ref',
'help' => pht(
'Operate on the database identified by __ref__.'),
),
array(
'name' => 'user',
@ -81,118 +87,147 @@ try {
// First, test that the Phabricator configuration is set up correctly. After
// we know this works we'll test any administrative credentials specifically.
$refs = PhabricatorDatabaseRef::getActiveDatabaseRefs();
if (!$refs) {
throw new PhutilArgumentUsageException(
pht('No databases are configured.'));
}
$host = $args->getArg('host');
if (strlen($host)) {
$ref = null;
$ref_key = $args->getArg('ref');
if (strlen($host) || strlen($ref_key)) {
if ($host && $ref_key) {
throw new PhutilArgumentUsageException(
pht(
'Use "--host" or "--ref" to select a database, but not both.'));
}
$refs = PhabricatorDatabaseRef::getLiveRefs();
// Include the master in case the user is just specifying a redundant
// "--host" flag for no reason and does not actually have a database
// cluster configured.
$refs[] = PhabricatorDatabaseRef::getMasterDatabaseRef();
$refs = PhabricatorDatabaseRef::getActiveDatabaseRefs();
$possible_refs = array();
foreach ($refs as $possible_ref) {
if ($possible_ref->getHost() == $host) {
$ref = $possible_ref;
if ($host && ($possible_ref->getHost() == $host)) {
$possible_refs[] = $possible_ref;
break;
}
if ($ref_key && ($possible_ref->getRefKey() == $ref_key)) {
$possible_refs[] = $possible_ref;
break;
}
}
if (!$ref) {
if (!$possible_refs) {
if ($host) {
throw new PhutilArgumentUsageException(
pht(
'There is no configured database on host "%s". This command can '.
'only interact with configured databases.',
$host));
} else {
throw new PhutilArgumentUsageException(
pht(
'There is no configured database with ref "%s". This command can '.
'only interact with configured databases.',
$ref_key));
}
}
if (count($possible_refs) > 1) {
throw new PhutilArgumentUsageException(
pht(
'There is no configured database on host "%s". This command can '.
'only interact with configured databases.',
'Host "%s" identifies more than one database. Use "--ref" to select '.
'a specific database.',
$host));
}
} else {
$ref = PhabricatorDatabaseRef::getMasterDatabaseRef();
if (!$ref) {
throw new Exception(
pht('No database master is configured.'));
$refs = $possible_refs;
}
$apis = array();
foreach ($refs as $ref) {
$default_user = $ref->getUser();
$default_host = $ref->getHost();
$default_port = $ref->getPort();
$test_api = id(new PhabricatorStorageManagementAPI())
->setUser($default_user)
->setHost($default_host)
->setPort($default_port)
->setPassword($ref->getPass())
->setNamespace($args->getArg('namespace'));
try {
queryfx(
$test_api->getConn(null),
'SELECT 1');
} catch (AphrontQueryException $ex) {
$message = phutil_console_format(
"**%s**\n\n%s\n\n%s\n\n%s\n\n**%s**: %s\n",
pht('MySQL Credentials Not Configured'),
pht(
'Unable to connect to MySQL using the configured credentials. '.
'You must configure standard credentials before you can upgrade '.
'storage. Run these commands to set up credentials:'),
" phabricator/ $ ./bin/config set mysql.host __host__\n".
" phabricator/ $ ./bin/config set mysql.user __username__\n".
" phabricator/ $ ./bin/config set mysql.pass __password__",
pht(
'These standard credentials are separate from any administrative '.
'credentials provided to this command with __%s__ or '.
'__%s__, and must be configured correctly before you can proceed.',
'--user',
'--password'),
pht('Raw MySQL Error'),
$ex->getMessage());
echo phutil_console_wrap($message);
exit(1);
}
}
$default_user = $ref->getUser();
$default_host = $ref->getHost();
$default_port = $ref->getPort();
if ($args->getArg('password') === null) {
// This is already a PhutilOpaqueEnvelope.
$password = $ref->getPass();
} else {
// Put this in a PhutilOpaqueEnvelope.
$password = new PhutilOpaqueEnvelope($args->getArg('password'));
PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password'));
}
$test_api = id(new PhabricatorStorageManagementAPI())
->setUser($default_user)
->setHost($default_host)
->setPort($default_port)
->setPassword($ref->getPass())
->setNamespace($args->getArg('namespace'));
$selected_user = $args->getArg('user');
if ($selected_user === null) {
$selected_user = $default_user;
}
try {
queryfx(
$test_api->getConn(null),
'SELECT 1');
} catch (AphrontQueryException $ex) {
$message = phutil_console_format(
"**%s**\n\n%s\n\n%s\n\n%s\n\n**%s**: %s\n",
pht('MySQL Credentials Not Configured'),
pht(
'Unable to connect to MySQL using the configured credentials. '.
'You must configure standard credentials before you can upgrade '.
'storage. Run these commands to set up credentials:'),
" phabricator/ $ ./bin/config set mysql.host __host__\n".
" phabricator/ $ ./bin/config set mysql.user __username__\n".
" phabricator/ $ ./bin/config set mysql.pass __password__",
pht(
'These standard credentials are separate from any administrative '.
'credentials provided to this command with __%s__ or '.
'__%s__, and must be configured correctly before you can proceed.',
'--user',
'--password'),
pht('Raw MySQL Error'),
$ex->getMessage());
echo phutil_console_wrap($message);
exit(1);
}
$api = id(new PhabricatorStorageManagementAPI())
->setUser($selected_user)
->setHost($default_host)
->setPort($default_port)
->setPassword($password)
->setNamespace($args->getArg('namespace'))
->setDisableUTF8MB4($args->getArg('disable-utf8mb4'));
PhabricatorEnv::overrideConfig('mysql.user', $api->getUser());
if ($args->getArg('password') === null) {
// This is already a PhutilOpaqueEnvelope.
$password = $ref->getPass();
} else {
// Put this in a PhutilOpaqueEnvelope.
$password = new PhutilOpaqueEnvelope($args->getArg('password'));
PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password'));
}
try {
queryfx(
$api->getConn(null),
'SELECT 1');
} catch (AphrontQueryException $ex) {
$message = phutil_console_format(
"**%s**\n\n%s\n\n**%s**: %s\n",
pht('Bad Administrative Credentials'),
pht(
'Unable to connect to MySQL using the administrative credentials '.
'provided with the __%s__ and __%s__ flags. Check that '.
'you have entered them correctly.',
'--user',
'--password'),
pht('Raw MySQL Error'),
$ex->getMessage());
echo phutil_console_wrap($message);
exit(1);
}
$selected_user = $args->getArg('user');
if ($selected_user === null) {
$selected_user = $default_user;
}
$api = id(new PhabricatorStorageManagementAPI())
->setUser($selected_user)
->setHost($default_host)
->setPort($default_port)
->setPassword($password)
->setNamespace($args->getArg('namespace'))
->setDisableUTF8MB4($args->getArg('disable-utf8mb4'));
PhabricatorEnv::overrideConfig('mysql.user', $api->getUser());
try {
queryfx(
$api->getConn(null),
'SELECT 1');
} catch (AphrontQueryException $ex) {
$message = phutil_console_format(
"**%s**\n\n%s\n\n**%s**: %s\n",
pht('Bad Administrative Credentials'),
pht(
'Unable to connect to MySQL using the administrative credentials '.
'provided with the __%s__ and __%s__ flags. Check that '.
'you have entered them correctly.',
'--user',
'--password'),
pht('Raw MySQL Error'),
$ex->getMessage());
echo phutil_console_wrap($message);
exit(1);
$api->setRef($ref);
$apis[] = $api;
}
$workflows = id(new PhutilClassMapQuery())
@ -202,7 +237,7 @@ $workflows = id(new PhutilClassMapQuery())
$patches = PhabricatorSQLPatchList::buildAllPatches();
foreach ($workflows as $workflow) {
$workflow->setAPI($api);
$workflow->setAPIs($apis);
$workflow->setPatches($patches);
}

View file

@ -1917,6 +1917,8 @@ phutil_register_library_map(array(
'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php',
'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php',
'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php',
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
@ -1941,6 +1943,7 @@ phutil_register_library_map(array(
'PhabricatorAuthSessionEngineExtension' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtension.php',
'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php',
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
@ -2158,6 +2161,9 @@ phutil_register_library_map(array(
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
'PhabricatorCalendarInviteeDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php',
'PhabricatorCalendarInviteeUserDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php',
'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php',
'PhabricatorCalendarManagementReloadWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementReloadWorkflow.php',
'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php',
@ -2647,6 +2653,7 @@ phutil_register_library_map(array(
'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php',
'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php',
'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php',
'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
@ -2728,6 +2735,10 @@ phutil_register_library_map(array(
'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
'PhabricatorGuidanceContext' => 'applications/guides/guidance/PhabricatorGuidanceContext.php',
'PhabricatorGuidanceEngine' => 'applications/guides/guidance/PhabricatorGuidanceEngine.php',
'PhabricatorGuidanceEngineExtension' => 'applications/guides/guidance/PhabricatorGuidanceEngineExtension.php',
'PhabricatorGuidanceMessage' => 'applications/guides/guidance/PhabricatorGuidanceMessage.php',
'PhabricatorGuideApplication' => 'applications/guides/application/PhabricatorGuideApplication.php',
'PhabricatorGuideController' => 'applications/guides/controller/PhabricatorGuideController.php',
'PhabricatorGuideInstallModule' => 'applications/guides/module/PhabricatorGuideInstallModule.php',
@ -2887,6 +2898,7 @@ phutil_register_library_map(array(
'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
'PhabricatorMarkupEngineTestCase' => 'infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php',
'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php',
'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php',
@ -3218,6 +3230,7 @@ phutil_register_library_map(array(
'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php',
'PhabricatorPeopleCreateGuidanceContext' => 'applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php',
'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php',
'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php',
'PhabricatorPeopleDetailsProfilePanel' => 'applications/people/profilepanel/PhabricatorPeopleDetailsProfilePanel.php',
@ -4075,6 +4088,7 @@ phutil_register_library_map(array(
'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php',
'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php',
'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php',
'PhamePostHeaderPictureController' => 'applications/phame/controller/post/PhamePostHeaderPictureController.php',
'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php',
'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php',
'PhamePostListView' => 'applications/phame/view/PhamePostListView.php',
@ -6436,7 +6450,7 @@ phutil_register_library_map(array(
'PHUIInfoExample' => 'PhabricatorUIExample',
'PHUIInfoPanelExample' => 'PhabricatorUIExample',
'PHUIInfoPanelView' => 'AphrontView',
'PHUIInfoView' => 'AphrontView',
'PHUIInfoView' => 'AphrontTagView',
'PHUIInvisibleCharacterTestCase' => 'PhabricatorTestCase',
'PHUIInvisibleCharacterView' => 'AphrontView',
'PHUIListExample' => 'PhabricatorUIExample',
@ -6730,6 +6744,8 @@ phutil_register_library_map(array(
'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController',
@ -6762,6 +6778,7 @@ phutil_register_library_map(array(
'PhabricatorAuthSessionEngineExtension' => 'Phobject',
'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthSessionInfo' => 'Phobject',
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
@ -7032,6 +7049,9 @@ phutil_register_library_map(array(
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarInviteeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementReloadWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow',
@ -7604,6 +7624,7 @@ phutil_register_library_map(array(
'PhabricatorFileImageProxyController' => 'PhabricatorFileController',
'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
'PhabricatorFileInfoController' => 'PhabricatorFileController',
'PhabricatorFileLightboxController' => 'PhabricatorFileController',
'PhabricatorFileLinkView' => 'AphrontView',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
@ -7687,6 +7708,10 @@ phutil_register_library_map(array(
'PhabricatorGlobalLock' => 'PhutilLock',
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGuidanceContext' => 'Phobject',
'PhabricatorGuidanceEngine' => 'Phobject',
'PhabricatorGuidanceEngineExtension' => 'Phobject',
'PhabricatorGuidanceMessage' => 'Phobject',
'PhabricatorGuideApplication' => 'PhabricatorApplication',
'PhabricatorGuideController' => 'PhabricatorController',
'PhabricatorGuideInstallModule' => 'PhabricatorGuideModule',
@ -7851,6 +7876,7 @@ phutil_register_library_map(array(
'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
'PhabricatorMarkupEngine' => 'Phobject',
'PhabricatorMarkupEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorMarkupOneOff' => array(
'Phobject',
'PhabricatorMarkupInterface',
@ -8256,6 +8282,7 @@ phutil_register_library_map(array(
'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
'PhabricatorPeopleCreateGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleDetailsProfilePanel' => 'PhabricatorProfilePanel',
@ -9304,6 +9331,7 @@ phutil_register_library_map(array(
'PhamePostEditEngine' => 'PhabricatorEditEngine',
'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor',
'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine',
'PhamePostHeaderPictureController' => 'PhamePostController',
'PhamePostHistoryController' => 'PhamePostController',
'PhamePostListController' => 'PhamePostController',
'PhamePostListView' => 'AphrontTagView',

View file

@ -94,58 +94,12 @@ final class PhabricatorAuthListController
$crumbs->addTextCrumb(pht('Auth Providers'));
$crumbs->setBorder(true);
$domains_key = 'auth.email-domains';
$domains_link = $this->renderConfigLink($domains_key);
$domains_value = PhabricatorEnv::getEnvConfig($domains_key);
$guidance_context = new PhabricatorAuthProvidersGuidanceContext();
$approval_key = 'auth.require-approval';
$approval_link = $this->renderConfigLink($approval_key);
$approval_value = PhabricatorEnv::getEnvConfig($approval_key);
$issues = array();
if ($domains_value) {
$issues[] = pht(
'Phabricator is configured with an email domain whitelist (in %s), so '.
'only users with a verified email address at one of these %s '.
'allowed domain(s) will be able to register an account: %s',
$domains_link,
phutil_count($domains_value),
phutil_tag('strong', array(), implode(', ', $domains_value)));
} else {
$issues[] = pht(
'Anyone who can browse to this Phabricator install will be able to '.
'register an account. To add email domain restrictions, configure '.
'%s.',
$domains_link);
}
if ($approval_value) {
$issues[] = pht(
'Administrative approvals are enabled (in %s), so all new users must '.
'have their accounts approved by an administrator.',
$approval_link);
} else {
$issues[] = pht(
'Administrative approvals are disabled, so users who register will '.
'be able to use their accounts immediately. To enable approvals, '.
'configure %s.',
$approval_link);
}
if (!$domains_value && !$approval_value) {
$severity = PHUIInfoView::SEVERITY_WARNING;
$issues[] = pht(
'You can safely ignore this warning if the install itself has '.
'access controls (for example, it is deployed on a VPN) or if all of '.
'the configured providers have access controls (for example, they are '.
'all private LDAP or OAuth servers).');
} else {
$severity = PHUIInfoView::SEVERITY_NOTICE;
}
$warning = id(new PHUIInfoView())
->setSeverity($severity)
->setErrors($issues);
$guidance = id(new PhabricatorGuidanceEngine())
->setViewer($viewer)
->setGuidanceContext($guidance_context)
->newInfoView();
$button = id(new PHUIButtonView())
->setTag('a')
@ -170,7 +124,7 @@ final class PhabricatorAuthListController
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$warning,
$guidance,
$list,
));
@ -180,14 +134,4 @@ final class PhabricatorAuthListController
->appendChild($view);
}
private function renderConfigLink($key) {
return phutil_tag(
'a',
array(
'href' => '/config/edit/'.$key.'/',
'target' => '_blank',
),
$key);
}
}

View file

@ -0,0 +1,36 @@
<?php
final class PhabricatorAuthSessionInfo extends Phobject {
private $sessionType;
private $identityPHID;
private $isPartial;
public function setSessionType($session_type) {
$this->sessionType = $session_type;
return $this;
}
public function getSessionType() {
return $this->sessionType;
}
public function setIdentityPHID($identity_phid) {
$this->identityPHID = $identity_phid;
return $this;
}
public function getIdentityPHID() {
return $this->identityPHID;
}
public function setIsPartial($is_partial) {
$this->isPartial = $is_partial;
return $this;
}
public function getIsPartial() {
return $this->isPartial;
}
}

View file

@ -270,6 +270,16 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$log->save();
unset($unguarded);
$info = id(new PhabricatorAuthSessionInfo())
->setSessionType($session_type)
->setIdentityPHID($identity_phid)
->setIsPartial($partial);
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didEstablishSession($info);
}
return $session_key;
}
@ -837,6 +847,11 @@ final class PhabricatorAuthSessionEngine extends Phobject {
// Switch to the user's translation.
PhabricatorEnv::setLocaleCode($user->getTranslation());
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->willServeRequestForUser($user);
}
}
}

View file

@ -16,6 +16,14 @@ abstract class PhabricatorAuthSessionEngineExtension
abstract public function getExtensionName();
public function didEstablishSession(PhabricatorAuthSessionInfo $info) {
return;
}
public function willServeRequestForUser(PhabricatorUser $user) {
return;
}
public function didLogout(PhabricatorUser $user, array $sessions) {
return;
}

View file

@ -0,0 +1,4 @@
<?php
final class PhabricatorAuthProvidersGuidanceContext
extends PhabricatorGuidanceContext {}

View file

@ -0,0 +1,88 @@
<?php
final class PhabricatorAuthProvidersGuidanceEngineExtension
extends PhabricatorGuidanceEngineExtension {
const GUIDANCEKEY = 'core.auth.providers';
public function canGenerateGuidance(PhabricatorGuidanceContext $context) {
return ($context instanceof PhabricatorAuthProvidersGuidanceContext);
}
public function generateGuidance(PhabricatorGuidanceContext $context) {
$domains_key = 'auth.email-domains';
$domains_link = $this->renderConfigLink($domains_key);
$domains_value = PhabricatorEnv::getEnvConfig($domains_key);
$approval_key = 'auth.require-approval';
$approval_link = $this->renderConfigLink($approval_key);
$approval_value = PhabricatorEnv::getEnvConfig($approval_key);
$results = array();
if ($domains_value) {
$message = pht(
'Phabricator is configured with an email domain whitelist (in %s), so '.
'only users with a verified email address at one of these %s '.
'allowed domain(s) will be able to register an account: %s',
$domains_link,
phutil_count($domains_value),
phutil_tag('strong', array(), implode(', ', $domains_value)));
$results[] = $this->newGuidance('core.auth.email-domains.on')
->setMessage($message);
} else {
$message = pht(
'Anyone who can browse to this Phabricator install will be able to '.
'register an account. To add email domain restrictions, configure '.
'%s.',
$domains_link);
$results[] = $this->newGuidance('core.auth.email-domains.off')
->setMessage($message);
}
if ($approval_value) {
$message = pht(
'Administrative approvals are enabled (in %s), so all new users must '.
'have their accounts approved by an administrator.',
$approval_link);
$results[] = $this->newGuidance('core.auth.require-approval.on')
->setMessage($message);
} else {
$message = pht(
'Administrative approvals are disabled, so users who register will '.
'be able to use their accounts immediately. To enable approvals, '.
'configure %s.',
$approval_link);
$results[] = $this->newGuidance('core.auth.require-approval.off')
->setMessage($message);
}
if (!$domains_value && !$approval_value) {
$message = pht(
'You can safely ignore these warnings if the install itself has '.
'access controls (for example, it is deployed on a VPN) or if all of '.
'the configured providers have access controls (for example, they are '.
'all private LDAP or OAuth servers).');
$results[] = $this->newWarning('core.auth.warning')
->setMessage($message);
}
return $results;
}
private function renderConfigLink($key) {
return phutil_tag(
'a',
array(
'href' => '/config/edit/'.$key.'/',
'target' => '_blank',
),
$key);
}
}

View file

@ -309,16 +309,48 @@ final class PhabricatorCalendarEventViewController
$status_declined => 'red',
);
$viewer_phid = $viewer->getPHID();
$is_rsvp_invited = $event->isRSVPInvited($viewer_phid);
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
$head = array();
$tail = array();
foreach ($invitees as $invitee) {
$item = new PHUIStatusItemView();
$invitee_phid = $invitee->getInviteePHID();
$status = $invitee->getStatus();
$target = $viewer->renderHandle($invitee_phid);
$icon = $icon_map[$status];
$icon_color = $icon_color_map[$status];
$is_user = (phid_get_type($invitee_phid) == $type_user);
if (!$is_user) {
$icon = 'fa-users';
$icon_color = 'blue';
} else {
$icon = $icon_map[$status];
$icon_color = $icon_color_map[$status];
}
// Highlight invited groups which you're a member of if you have
// not RSVP'd to an event yet.
if ($is_rsvp_invited) {
if ($invitee_phid != $viewer_phid) {
if ($event->hasRSVPAuthority($viewer_phid, $invitee_phid)) {
$item->setHighlighted(true);
}
}
}
$item->setIcon($icon, $icon_color)
->setTarget($target);
if ($is_user) {
$tail[] = $item;
} else {
$head[] = $item;
}
}
foreach (array_merge($head, $tail) as $item) {
$invitee_list->addItem($item);
}
} else {
@ -511,6 +543,7 @@ final class PhabricatorCalendarEventViewController
$event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withIDs(array($id))
->needRSVPs(array($viewer->getPHID()))
->executeOne();
if (!$event) {
return null;
@ -586,10 +619,8 @@ final class PhabricatorCalendarEventViewController
$viewer = $this->getViewer();
$id = $event->getID();
$invite_status = $event->getUserInviteStatus($viewer->getPHID());
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
$is_invite_pending = ($invite_status == $status_invited);
if (!$is_invite_pending) {
$is_pending = $event->isRSVPInvited($viewer->getPHID());
if (!$is_pending) {
return array();
}

View file

@ -82,6 +82,9 @@ final class PhabricatorCalendarExportICSController
$saved->setParameter('rangeEnd', null);
$saved->setParameter('upcoming', null);
// The "month" and "day" display modes imply time ranges.
$saved->setParameter('display', 'list');
$query = $engine->buildQueryFromSavedQuery($saved);
$events = $query

View file

@ -108,6 +108,8 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('New event name.'))
->setValue($object->getName()),
id(new PhabricatorBoolEditField())
->setIsLockable(false)
->setIsDefaultable(false)
->setKey('isAllDay')
->setOptions(pht('Normal Event'), pht('All Day Event'))
->setAsCheckbox(true)
@ -151,6 +153,8 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('True to cancel the event.'))
->setValue($object->getIsCancelled()),
id(new PhabricatorUsersEditField())
->setIsLockable(false)
->setIsDefaultable(false)
->setKey('hostPHID')
->setAliases(array('host'))
->setLabel(pht('Host'))
@ -162,6 +166,8 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('New event host.'))
->setSingleValue($object->getHostPHID()),
id(new PhabricatorDatasourceEditField())
->setIsLockable(false)
->setIsDefaultable(false)
->setIsHidden($is_future)
->setKey('inviteePHIDs')
->setAliases(array('invite', 'invitee', 'invitees', 'inviteePHID'))
@ -203,6 +209,8 @@ final class PhabricatorCalendarEventEditEngine
id(new PhabricatorBoolEditField())
->setIsHidden(true)
->setIsLockable(false)
->setIsDefaultable(false)
->setKey('isRecurring')
->setLabel(pht('Recurring'))
->setOptions(pht('One-Time Event'), pht('Recurring Event'))
@ -213,6 +221,8 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('Mark the event as a recurring event.'))
->setValue(true),
id(new PhabricatorSelectEditField())
->setIsLockable(false)
->setIsDefaultable(false)
->setKey('frequency')
->setLabel(pht('Frequency'))
->setOptions($frequency_options)

View file

@ -43,7 +43,7 @@ final class PhabricatorCalendarEventInviteesPolicyRule
if (!isset($this->sourcePHIDs[$viewer_phid])) {
$source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$viewer_phid,
PhabricatorProjectMemberOfProjectEdgeType::EDGECONST);
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
$source_phids[] = $viewer_phid;
$this->sourcePHIDs[$viewer_phid] = $source_phids;
}

View file

@ -20,6 +20,7 @@ final class PhabricatorCalendarEventQuery
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $isImported;
private $needRSVPs;
private $generateGhosts = false;
@ -109,6 +110,11 @@ final class PhabricatorCalendarEventQuery
return $this;
}
public function needRSVPs(array $phids) {
$this->needRSVPs = $phids;
return $this;
}
protected function getDefaultOrderVector() {
return array('start', 'id');
}
@ -166,7 +172,6 @@ final class PhabricatorCalendarEventQuery
}
$raw_limit = $this->getRawResultLimit();
if (!$raw_limit && !$this->rangeEnd) {
throw new Exception(
pht(
@ -614,6 +619,70 @@ final class PhabricatorCalendarEventQuery
$events = msort($events, 'getStartDateTimeEpoch');
if ($this->needRSVPs) {
$rsvp_phids = $this->needRSVPs;
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$project_phids = array();
foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) {
$invitee_phid = $invitee->getInviteePHID();
if (phid_get_type($invitee_phid) == $project_type) {
$project_phids[] = $invitee_phid;
}
}
}
if ($project_phids) {
$member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes(array($member_type))
->withDestinationPHIDs($rsvp_phids);
$edges = $query->execute();
$project_map = array();
foreach ($edges as $src => $types) {
foreach ($types as $type => $dsts) {
foreach ($dsts as $dst => $edge) {
$project_map[$dst][] = $src;
}
}
}
} else {
$project_map = array();
}
$membership_map = array();
foreach ($rsvp_phids as $rsvp_phid) {
$membership_map[$rsvp_phid] = array();
$membership_map[$rsvp_phid][] = $rsvp_phid;
$project_phids = idx($project_map, $rsvp_phid);
if ($project_phids) {
foreach ($project_phids as $project_phid) {
$membership_map[$rsvp_phid][] = $project_phid;
}
}
}
foreach ($events as $event) {
$invitees = $event->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
$rsvp_map = array();
foreach ($rsvp_phids as $rsvp_phid) {
$membership_phids = $membership_map[$rsvp_phid];
$rsvps = array_select_keys($invitees, $membership_phids);
$rsvp_map[$rsvp_phid] = $rsvps;
}
$event->attachRSVPs($rsvp_map);
}
}
return $events;
}

View file

@ -16,7 +16,10 @@ final class PhabricatorCalendarEventSearchEngine
}
public function newQuery() {
return new PhabricatorCalendarEventQuery();
$viewer = $this->requireViewer();
return id(new PhabricatorCalendarEventQuery())
->needRSVPs(array($viewer->getPHID()));
}
protected function shouldShowOrderField() {
@ -33,7 +36,7 @@ final class PhabricatorCalendarEventSearchEngine
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Invited'))
->setKey('invitedPHIDs')
->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
->setDatasource(new PhabricatorCalendarInviteeDatasource()),
id(new PhabricatorSearchDateControlField())
->setLabel(pht('Occurs After'))
->setKey('rangeStart'),
@ -79,6 +82,18 @@ final class PhabricatorCalendarEventSearchEngine
);
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = parent::buildQueryFromSavedQuery($saved);
// If this is an export query for generating an ".ics" file, don't
// build ghost events.
if ($saved->getParameter('export')) {
$query->setGenerateGhosts(false);
}
return $query;
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
$viewer = $this->requireViewer();
@ -122,13 +137,7 @@ final class PhabricatorCalendarEventSearchEngine
$query->withImportSourcePHIDs($map['importSourcePHIDs']);
}
// Generate ghosts (and ignore stub events) if we aren't querying for
// specific events or exporting.
if (!empty($map['export'])) {
// This is a specific mode enabled by event exports.
$query
->withIsStub(false);
} else if (!$map['ids'] && !$map['phids']) {
if (!$map['ids'] && !$map['phids']) {
$query
->withIsStub(false)
->setGenerateGhosts(true);
@ -361,10 +370,14 @@ final class PhabricatorCalendarEventSearchEngine
$month_view->setUser($viewer);
$viewer_phid = $viewer->getPHID();
foreach ($events as $event) {
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$is_invited = $event->isRSVPInvited($viewer_phid);
$is_attending = $event->getIsUserAttending($viewer_phid);
$event_view = id(new AphrontCalendarEventView())
->setHostPHID($event->getHostPHID())
->setEpochRange($epoch_min, $epoch_max)
@ -373,6 +386,8 @@ final class PhabricatorCalendarEventSearchEngine
->setURI($event->getURI())
->setIsAllDay($event->getIsAllDay())
->setIcon($event->getDisplayIcon($viewer))
->setViewerIsInvited($is_invited || $is_attending)
->setDatetimeSummary($event->renderEventDate($viewer, true))
->setIconColor($event->getDisplayIconColor($viewer));
$month_view->addEvent($event_view);
@ -441,6 +456,7 @@ final class PhabricatorCalendarEventSearchEngine
->setIconColor($status_color)
->setName($event->getName())
->setURI($event->getURI())
->setDatetimeSummary($event->renderEventDate($viewer, true))
->setIsCancelled($event->getIsCancelled());
$day_view->addEvent($event_view);

View file

@ -50,6 +50,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
private $parentEvent = self::ATTACHABLE;
private $invitees = self::ATTACHABLE;
private $importSource = self::ATTACHABLE;
private $rsvps = self::ATTACHABLE;
private $viewerTimezone;
@ -537,16 +538,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $is_attending;
}
public function getIsUserInvited($phid) {
$uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
$declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
$status = $this->getUserInviteStatus($phid);
if ($status == $uninvited_status || $status == $declined_status) {
return false;
}
return true;
}
public function getIsGhostEvent() {
return $this->isGhostEvent;
}
@ -590,13 +581,15 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$start = $this->newStartDateTime();
$end = $this->newEndDateTime();
if ($show_end) {
$min_date = $start->newPHPDateTime();
$max_date = $end->newPHPDateTime();
$min_date = $start->newPHPDateTime();
$max_date = $end->newPHPDateTime();
if ($this->getIsAllDay()) {
// Subtract one second since the stored date is exclusive.
$max_date = $max_date->modify('-1 second');
}
if ($show_end) {
$min_day = $min_date->format('Y m d');
$max_day = $max_date->format('Y m d');
@ -605,8 +598,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$show_end_date = false;
}
$min_epoch = $start->getEpoch();
$max_epoch = $end->getEpoch();
$min_epoch = $min_date->format('U');
$max_epoch = $max_date->format('U');
if ($this->getIsAllDay()) {
if ($show_end_date) {
@ -643,14 +636,19 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
if ($viewer->isLoggedIn()) {
$status = $this->getUserInviteStatus($viewer->getPHID());
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'fa-check-circle';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'fa-user-plus';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'fa-times';
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'fa-users';
} else {
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'fa-check-circle';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'fa-user-plus';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'fa-times-circle';
}
}
}
@ -671,7 +669,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
if ($viewer->isLoggedIn()) {
$status = $this->getUserInviteStatus($viewer->getPHID());
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'green';
}
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'green';
@ -1121,6 +1124,52 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $phids;
}
public function getRSVPs($phid) {
return $this->assertAttachedKey($this->rsvps, $phid);
}
public function attachRSVPs(array $rsvps) {
$this->rsvps = $rsvps;
return $this;
}
public function isRSVPInvited($phid) {
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
return ($this->getRSVPStatus($phid) == $status_invited);
}
public function hasRSVPAuthority($phid, $other_phid) {
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getInviteePHID() == $other_phid) {
return true;
}
}
return false;
}
public function getRSVPStatus($phid) {
// Check for an individual invitee record first.
$invitees = $this->invitees;
$invitees = mpull($invitees, null, 'getInviteePHID');
$invitee = idx($invitees, $phid);
if ($invitee) {
return $invitee->getStatus();
}
// If we don't have one, try to find an invited status for the user's
// projects.
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getStatus() == $status_invited) {
return $status_invited;
}
}
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
/* -( Markup Interface )--------------------------------------------------- */

View file

@ -0,0 +1,53 @@
<?php
final class PhabricatorCalendarInviteeDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Invitees');
}
public function getPlaceholderText() {
return pht('Type a user or project name, or function...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorCalendarApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorCalendarInviteeUserDatasource(),
new PhabricatorCalendarInviteeViewerFunctionDatasource(),
new DifferentialExactUserFunctionDatasource(),
new PhabricatorProjectDatasource(),
);
}
public static function expandInvitees(
PhabricatorUser $viewer,
array $values) {
$phids = array();
foreach ($values as $value) {
if (phid_get_type($value) == PhabricatorPeopleUserPHIDType::TYPECONST) {
$phids[] = $value;
}
}
if (!$phids) {
return $values;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs($phids)
->execute();
foreach ($projects as $project) {
$values[] = $project->getPHID();
}
return $values;
}
}

View file

@ -0,0 +1,30 @@
<?php
final class PhabricatorCalendarInviteeUserDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse Users');
}
public function getPlaceholderText() {
return pht('Type a user name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorCalendarApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorPeopleDatasource(),
);
}
protected function evaluateValues(array $values) {
return PhabricatorCalendarInviteeDatasource::expandInvitees(
$this->getViewer(),
$values);
}
}

View file

@ -0,0 +1,77 @@
<?php
final class PhabricatorCalendarInviteeViewerFunctionDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Viewer');
}
public function getPlaceholderText() {
return pht('Type viewer()...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getDatasourceFunctions() {
return array(
'viewer' => array(
'name' => pht('Current Viewer'),
'summary' => pht('Use the current viewing user.'),
'description' => pht(
'Show invites the current viewer is invited to. This function '.
'includes events the user is invited to because a project they '.
'are a member of is invited.'),
),
);
}
public function loadResults() {
if ($this->getViewer()->getPHID()) {
$results = array($this->renderViewerFunctionToken());
} else {
$results = array();
}
return $this->filterResultsAgainstTokens($results);
}
protected function canEvaluateFunction($function) {
if (!$this->getViewer()->getPHID()) {
return false;
}
return parent::canEvaluateFunction($function);
}
protected function evaluateFunction($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = $this->getViewer()->getPHID();
}
return PhabricatorCalendarInviteeDatasource::expandInvitees(
$this->getViewer(),
$results);
}
public function renderFunctionTokens($function, array $argv_list) {
$tokens = array();
foreach ($argv_list as $argv) {
$tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$this->renderViewerFunctionToken());
}
return $tokens;
}
private function renderViewerFunctionToken() {
return $this->newFunctionResult()
->setName(pht('Current Viewer'))
->setPHID('viewer()')
->setIcon('fa-user')
->setUnique(true);
}
}

View file

@ -15,6 +15,7 @@ final class AphrontCalendarEventView extends AphrontView {
private $iconColor;
private $canEdit;
private $isCancelled;
private $datetimeSummary;
public function setIconColor($icon_color) {
$this->iconColor = $icon_color;
@ -135,6 +136,15 @@ final class AphrontCalendarEventView extends AphrontView {
return false;
}
public function setDatetimeSummary($datetime_summary) {
$this->datetimeSummary = $datetime_summary;
return $this;
}
public function getDatetimeSummary() {
return $this->datetimeSummary;
}
public function render() {
throw new Exception(pht('Events are only rendered indirectly.'));
}

View file

@ -45,9 +45,10 @@ final class PhabricatorConfigApplication extends PhabricatorApplication {
'group/(?P<key>[^/]+)/' => 'PhabricatorConfigGroupController',
'version/' => 'PhabricatorConfigVersionController',
'database/'.
'(?:(?P<ref>[^/]+)/'.
'(?:(?P<database>[^/]+)/'.
'(?:(?P<table>[^/]+)/'.
'(?:(?:col/(?P<column>[^/]+)|key/(?P<key>[^/]+))/)?)?)?'
'(?:(?:col/(?P<column>[^/]+)|key/(?P<key>[^/]+))/)?)?)?)?'
=> 'PhabricatorConfigDatabaseStatusController',
'dbissue/' => 'PhabricatorConfigDatabaseIssueController',
'(?P<verb>ignore|unignore)/(?P<key>[^/]+)/'

View file

@ -12,89 +12,6 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
}
protected function executeChecks() {
$master = PhabricatorDatabaseRef::getMasterDatabaseRef();
if (!$master) {
// If we're implicitly in read-only mode during disaster recovery,
// don't bother with these setup checks.
return;
}
$conn_raw = $master->newManagementConnection();
try {
queryfx($conn_raw, 'SELECT 1');
$database_exception = null;
} catch (AphrontInvalidCredentialsQueryException $ex) {
$database_exception = $ex;
} catch (AphrontConnectionQueryException $ex) {
$database_exception = $ex;
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception);
$this->addIssue($issue);
return;
}
$engines = queryfx_all($conn_raw, 'SHOW ENGINES');
$engines = ipull($engines, 'Support', 'Engine');
$innodb = idx($engines, 'InnoDB');
if ($innodb != 'YES' && $innodb != 'DEFAULT') {
$message = pht(
"The 'InnoDB' engine is not available in MySQL. Enable InnoDB in ".
"your MySQL configuration.".
"\n\n".
"(If you aleady created tables, MySQL incorrectly used some other ".
"engine to create them. You need to convert them or drop and ".
"reinitialize them.)");
$this->newIssue('mysql.innodb')
->setName(pht('MySQL InnoDB Engine Not Available'))
->setMessage($message)
->setIsFatal(true);
return;
}
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$databases = queryfx_all($conn_raw, 'SHOW DATABASES');
$databases = ipull($databases, 'Database', 'Database');
if (empty($databases[$namespace.'_meta_data'])) {
$message = pht(
"Run the storage upgrade script to setup Phabricator's database ".
"schema.");
$this->newIssue('storage.upgrade')
->setName(pht('Setup MySQL Schema'))
->setMessage($message)
->setIsFatal(true)
->addCommand(hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
} else {
$conn_meta = $master->newApplicationConnection(
$namespace.'_meta_data');
$applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status');
$applied = ipull($applied, 'patch', 'patch');
$all = PhabricatorSQLPatchList::buildAllPatches();
$diff = array_diff_key($all, $applied);
if ($diff) {
$this->newIssue('storage.patch')
->setName(pht('Upgrade MySQL Schema'))
->setMessage(
pht(
"Run the storage upgrade script to upgrade Phabricator's ".
"database schema. Missing patches:<br />%s<br />",
phutil_implode_html(phutil_tag('br'), array_keys($diff))))
->addCommand(
hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
}
}
$host = PhabricatorEnv::getEnvConfig('mysql.host');
$matches = null;
if (preg_match('/^([^:]+):(\d+)$/', $host, $matches)) {
@ -126,5 +43,97 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
$port));
}
$masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();
if (!$masters) {
// If we're implicitly in read-only mode during disaster recovery,
// don't bother with these setup checks.
return;
}
foreach ($masters as $master) {
if ($this->checkMasterDatabase($master)) {
break;
}
}
}
private function checkMasterDatabase(PhabricatorDatabaseRef $master) {
$conn_raw = $master->newManagementConnection();
try {
queryfx($conn_raw, 'SELECT 1');
$database_exception = null;
} catch (AphrontInvalidCredentialsQueryException $ex) {
$database_exception = $ex;
} catch (AphrontConnectionQueryException $ex) {
$database_exception = $ex;
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception);
$this->addIssue($issue);
return true;
}
$engines = queryfx_all($conn_raw, 'SHOW ENGINES');
$engines = ipull($engines, 'Support', 'Engine');
$innodb = idx($engines, 'InnoDB');
if ($innodb != 'YES' && $innodb != 'DEFAULT') {
$message = pht(
"The 'InnoDB' engine is not available in MySQL. Enable InnoDB in ".
"your MySQL configuration.".
"\n\n".
"(If you aleady created tables, MySQL incorrectly used some other ".
"engine to create them. You need to convert them or drop and ".
"reinitialize them.)");
$this->newIssue('mysql.innodb')
->setName(pht('MySQL InnoDB Engine Not Available'))
->setMessage($message)
->setIsFatal(true);
return true;
}
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$databases = queryfx_all($conn_raw, 'SHOW DATABASES');
$databases = ipull($databases, 'Database', 'Database');
if (empty($databases[$namespace.'_meta_data'])) {
$message = pht(
"Run the storage upgrade script to setup Phabricator's database ".
"schema.");
$this->newIssue('storage.upgrade')
->setName(pht('Setup MySQL Schema'))
->setMessage($message)
->setIsFatal(true)
->addCommand(hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
return true;
}
$conn_meta = $master->newApplicationConnection(
$namespace.'_meta_data');
$applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status');
$applied = ipull($applied, 'patch', 'patch');
$all = PhabricatorSQLPatchList::buildAllPatches();
$diff = array_diff_key($all, $applied);
if ($diff) {
$this->newIssue('storage.patch')
->setName(pht('Upgrade MySQL Schema'))
->setMessage(
pht(
"Run the storage upgrade script to upgrade Phabricator's ".
"database schema. Missing patches:<br />%s<br />",
phutil_implode_html(phutil_tag('br'), array_keys($diff))))
->addCommand(
hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
return true;
}
}
}

View file

@ -3,22 +3,6 @@
abstract class PhabricatorConfigDatabaseController
extends PhabricatorConfigController {
protected function buildSchemaQuery() {
$ref = PhabricatorDatabaseRef::getMasterDatabaseRef();
$api = id(new PhabricatorStorageManagementAPI())
->setUser($ref->getUser())
->setHost($ref->getHost())
->setPort($ref->getPort())
->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace())
->setPassword($ref->getPass());
$query = id(new PhabricatorConfigSchemaQuery())
->setAPI($api);
return $query;
}
protected function renderIcon($status) {
switch ($status) {
case PhabricatorConfigStorageSchema::STATUS_OKAY:

View file

@ -6,11 +6,11 @@ final class PhabricatorConfigDatabaseIssueController
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$query = $this->buildSchemaQuery();
$query = new PhabricatorConfigSchemaQuery();
$actual = $query->loadActualSchema();
$expect = $query->loadExpectedSchema();
$comp = $query->buildComparisonSchema($expect, $actual);
$actual = $query->loadActualSchemata();
$expect = $query->loadExpectedSchemata();
$comp_servers = $query->buildComparisonSchemata($expect, $actual);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Database Issues'));
@ -18,65 +18,70 @@ final class PhabricatorConfigDatabaseIssueController
// Collect all open issues.
$issues = array();
foreach ($comp->getDatabases() as $database_name => $database) {
foreach ($database->getLocalIssues() as $issue) {
$issues[] = array(
$database_name,
null,
null,
null,
$issue,
);
}
foreach ($database->getTables() as $table_name => $table) {
foreach ($table->getLocalIssues() as $issue) {
foreach ($comp_servers as $ref_name => $comp) {
foreach ($comp->getDatabases() as $database_name => $database) {
foreach ($database->getLocalIssues() as $issue) {
$issues[] = array(
$ref_name,
$database_name,
$table_name,
null,
null,
null,
$issue,
);
}
foreach ($table->getColumns() as $column_name => $column) {
foreach ($column->getLocalIssues() as $issue) {
foreach ($database->getTables() as $table_name => $table) {
foreach ($table->getLocalIssues() as $issue) {
$issues[] = array(
$ref_name,
$database_name,
$table_name,
'column',
$column_name,
null,
null,
$issue,
);
}
}
foreach ($table->getKeys() as $key_name => $key) {
foreach ($key->getLocalIssues() as $issue) {
$issues[] = array(
$database_name,
$table_name,
'key',
$key_name,
$issue,
);
foreach ($table->getColumns() as $column_name => $column) {
foreach ($column->getLocalIssues() as $issue) {
$issues[] = array(
$ref_name,
$database_name,
$table_name,
'column',
$column_name,
$issue,
);
}
}
foreach ($table->getKeys() as $key_name => $key) {
foreach ($key->getLocalIssues() as $issue) {
$issues[] = array(
$ref_name,
$database_name,
$table_name,
'key',
$key_name,
$issue,
);
}
}
}
}
}
// Sort all open issues so that the most severe issues appear first.
$order = array();
$counts = array();
foreach ($issues as $key => $issue) {
$const = $issue[4];
$const = $issue[5];
$status = PhabricatorConfigStorageSchema::getIssueStatus($const);
$severity = PhabricatorConfigStorageSchema::getStatusSeverity($status);
$order[$key] = sprintf(
'~%d~%s%s%s',
9 - $severity,
$issue[0],
$issue[1],
$issue[3]);
$issue[2],
$issue[4]);
if (empty($counts[$status])) {
$counts[$status] = 0;
@ -91,22 +96,25 @@ final class PhabricatorConfigDatabaseIssueController
// Render the issues.
$rows = array();
foreach ($issues as $issue) {
$const = $issue[4];
$const = $issue[5];
$uri = $this->getApplicationURI('/database/'.$issue[0].'/'.$issue[1].'/');
$database_link = phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('/database/'.$issue[0].'/'),
'href' => $uri,
),
$issue[0]);
$issue[1]);
$rows[] = array(
$this->renderIcon(
PhabricatorConfigStorageSchema::getIssueStatus($const)),
$issue[0],
$database_link,
$issue[1],
$issue[2],
$issue[3],
$issue[4],
PhabricatorConfigStorageSchema::getIssueDescription($const),
);
}
@ -117,6 +125,7 @@ final class PhabricatorConfigDatabaseIssueController
->setHeaders(
array(
null,
pht('Server'),
pht('Database'),
pht('Table'),
pht('Type'),
@ -130,6 +139,7 @@ final class PhabricatorConfigDatabaseIssueController
null,
null,
null,
null,
'wide',
));

View file

@ -7,6 +7,7 @@ final class PhabricatorConfigDatabaseStatusController
private $table;
private $column;
private $key;
private $ref;
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
@ -14,48 +15,59 @@ final class PhabricatorConfigDatabaseStatusController
$this->table = $request->getURIData('table');
$this->column = $request->getURIData('column');
$this->key = $request->getURIData('key');
$this->ref = $request->getURIData('ref');
$query = $this->buildSchemaQuery();
$query = new PhabricatorConfigSchemaQuery();
$actual = $query->loadActualSchema();
$expect = $query->loadExpectedSchema();
$comp = $query->buildComparisonSchema($expect, $actual);
$actual = $query->loadActualSchemata();
$expect = $query->loadExpectedSchemata();
$comp = $query->buildComparisonSchemata($expect, $actual);
if ($this->column) {
return $this->renderColumn(
$comp,
$expect,
$actual,
$this->database,
$this->table,
$this->column);
} else if ($this->key) {
return $this->renderKey(
$comp,
$expect,
$actual,
$this->database,
$this->table,
$this->key);
} else if ($this->table) {
return $this->renderTable(
$comp,
$expect,
$actual,
$this->database,
$this->table);
} else if ($this->database) {
return $this->renderDatabase(
$comp,
$expect,
$actual,
$this->database);
} else {
return $this->renderServer(
$comp,
$expect,
$actual);
if ($this->ref !== null) {
$server_actual = idx($actual, $this->ref);
if (!$server_actual) {
return new Aphront404Response();
}
$server_comparison = $comp[$this->ref];
$server_expect = $expect[$this->ref];
if ($this->column) {
return $this->renderColumn(
$server_comparison,
$server_expect,
$server_actual,
$this->database,
$this->table,
$this->column);
} else if ($this->key) {
return $this->renderKey(
$server_comparison,
$server_expect,
$server_actual,
$this->database,
$this->table,
$this->key);
} else if ($this->table) {
return $this->renderTable(
$server_comparison,
$server_expect,
$server_actual,
$this->database,
$this->table);
} else if ($this->database) {
return $this->renderDatabase(
$server_comparison,
$server_expect,
$server_actual,
$this->database);
}
}
return $this->renderServers(
$comp,
$expect,
$actual);
}
private function buildResponse($title, $body) {
@ -66,34 +78,57 @@ final class PhabricatorConfigDatabaseStatusController
$title = pht('Database Status');
}
$ref = $this->ref;
$database = $this->database;
$table = $this->table;
$column = $this->column;
$key = $this->key;
$links = array();
$links[] = array(
pht('Database Status'),
'database/',
);
if ($database) {
$links[] = array(
$database,
"database/{$ref}/{$database}/",
);
}
if ($table) {
$links[] = array(
$table,
"database/{$ref}/{$database}/{$table}/",
);
}
if ($column) {
$links[] = array(
$column,
"database/{$ref}/{$database}/{$table}/col/{$column}/",
);
}
if ($key) {
$links[] = array(
$key,
"database/{$ref}/{$database}/{$table}/key/{$key}/",
);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
if ($this->database) {
$crumbs->addTextCrumb(
pht('Database Status'),
$this->getApplicationURI('database/'));
if ($this->table) {
$crumbs->addTextCrumb(
$this->database,
$this->getApplicationURI('database/'.$this->database.'/'));
if ($this->column || $this->key) {
$crumbs->addTextCrumb(
$this->table,
$this->getApplicationURI(
'database/'.$this->database.'/'.$this->table.'/'));
if ($this->column) {
$crumbs->addTextCrumb($this->column);
} else {
$crumbs->addTextCrumb($this->key);
}
} else {
$crumbs->addTextCrumb($this->table);
}
$last_key = last_key($links);
foreach ($links as $link_key => $link) {
list($name, $href) = $link;
if ($link_key == $last_key) {
$crumbs->addTextCrumb($name);
} else {
$crumbs->addTextCrumb($this->database);
$crumbs->addTextCrumb($name, $this->getApplicationURI($href));
}
} else {
$crumbs->addTextCrumb(pht('Database Status'));
}
$doc_link = PhabricatorEnv::getDoclink('Managing Storage Adjustments');
@ -121,52 +156,64 @@ final class PhabricatorConfigDatabaseStatusController
}
private function renderServer(
PhabricatorConfigServerSchema $comp,
PhabricatorConfigServerSchema $expect,
PhabricatorConfigServerSchema $actual) {
private function renderServers(
array $comp_servers,
array $expect_servers,
array $actual_servers) {
$charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET;
$collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION;
$rows = array();
foreach ($comp->getDatabases() as $database_name => $database) {
$actual_database = $actual->getDatabase($database_name);
if ($actual_database) {
$charset = $actual_database->getCharacterSet();
$collation = $actual_database->getCollation();
} else {
$charset = null;
$collation = null;
}
foreach ($comp_servers as $ref_key => $comp) {
$actual = $actual_servers[$ref_key];
$expect = $expect_servers[$ref_key];
foreach ($comp->getDatabases() as $database_name => $database) {
$actual_database = $actual->getDatabase($database_name);
if ($actual_database) {
$charset = $actual_database->getCharacterSet();
$collation = $actual_database->getCollation();
} else {
$charset = null;
$collation = null;
}
$status = $database->getStatus();
$issues = $database->getIssues();
$status = $database->getStatus();
$issues = $database->getIssues();
$rows[] = array(
$this->renderIcon($status),
phutil_tag(
'a',
$uri = $this->getURI(
array(
'href' => $this->getApplicationURI(
'/database/'.$database_name.'/'),
),
$database_name),
$this->renderAttr($charset, $database->hasIssue($charset_issue)),
$this->renderAttr($collation, $database->hasIssue($collation_issue)),
);
'ref' => $ref_key,
'database' => $database_name,
));
$rows[] = array(
$this->renderIcon($status),
$ref_key,
phutil_tag(
'a',
array(
'href' => $uri,
),
$database_name),
$this->renderAttr($charset, $database->hasIssue($charset_issue)),
$this->renderAttr($collation, $database->hasIssue($collation_issue)),
);
}
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Server'),
pht('Database'),
pht('Charset'),
pht('Collation'),
))
->setColumnClasses(
array(
null,
null,
'wide pri',
null,
@ -200,13 +247,17 @@ final class PhabricatorConfigDatabaseStatusController
foreach ($database->getTables() as $table_name => $table) {
$status = $table->getStatus();
$uri = $this->getURI(
array(
'table' => $table_name,
));
$rows[] = array(
$this->renderIcon($status),
phutil_tag(
'a',
array(
'href' => $this->getApplicationURI(
'/database/'.$database_name.'/'.$table_name.'/'),
'href' => $uri,
),
$table_name),
$this->renderAttr(
@ -251,6 +302,10 @@ final class PhabricatorConfigDatabaseStatusController
$properties = $this->buildProperties(
array(
array(
pht('Server'),
$this->ref,
),
array(
pht('Character Set'),
$actual_charset,
@ -325,17 +380,17 @@ final class PhabricatorConfigDatabaseStatusController
$data_type = $expect_column->getDataType();
}
$uri = $this->getURI(
array(
'column' => $column_name,
));
$rows[] = array(
$this->renderIcon($status),
phutil_tag(
'a',
array(
'href' => $this->getApplicationURI(
'database/'.
$database_name.'/'.
$table_name.'/'.
'col/'.
$column_name.'/'),
'href' => $uri,
),
$column_name),
$data_type,
@ -407,17 +462,17 @@ final class PhabricatorConfigDatabaseStatusController
$key->hasIssue($longkey_issue));
}
$uri = $this->getURI(
array(
'key' => $key_name,
));
$key_rows[] = array(
$this->renderIcon($status),
phutil_tag(
'a',
array(
'href' => $this->getApplicationURI(
'database/'.
$database_name.'/'.
$table_name.'/'.
'key/'.
$key_name.'/'),
'href' => $uri,
),
$key_name),
$this->renderAttr(
@ -464,6 +519,10 @@ final class PhabricatorConfigDatabaseStatusController
$properties = $this->buildProperties(
array(
array(
pht('Server'),
$this->ref,
),
array(
pht('Collation'),
$actual_collation,
@ -561,6 +620,10 @@ final class PhabricatorConfigDatabaseStatusController
$properties = $this->buildProperties(
array(
array(
pht('Server'),
$this->ref,
),
array(
pht('Data Type'),
$data_type,
@ -678,6 +741,10 @@ final class PhabricatorConfigDatabaseStatusController
$properties = $this->buildProperties(
array(
array(
pht('Server'),
$this->ref,
),
array(
pht('Unique'),
$this->renderBoolean($actual_unique),
@ -745,4 +812,40 @@ final class PhabricatorConfigDatabaseStatusController
return phutil_tag_div('config-page-property', $view);
}
private function getURI(array $properties) {
$defaults = array(
'ref' => $this->ref,
'database' => $this->database,
'table' => $this->table,
'column' => $this->column,
'key' => $this->key,
);
$properties = $properties + $defaults;
$properties = array_select_keys($properties, array_keys($defaults));
$parts = array();
foreach ($properties as $key => $property) {
if (!strlen($property)) {
continue;
}
if ($key == 'column') {
$parts[] = 'col';
} else if ($key == 'key') {
$parts[] = 'key';
}
$parts[] = $property;
}
if ($parts) {
$parts = implode('/', $parts).'/';
} else {
$parts = null;
}
return $this->getApplicationURI('/database/'.$parts);
}
}

View file

@ -2,36 +2,70 @@
final class PhabricatorConfigSchemaQuery extends Phobject {
private $api;
private $refs;
private $apis;
public function setAPI(PhabricatorStorageManagementAPI $api) {
$this->api = $api;
public function setRefs(array $refs) {
$this->refs = $refs;
return $this;
}
protected function getAPI() {
if (!$this->api) {
throw new PhutilInvalidStateException('setAPI');
public function getRefs() {
if (!$this->refs) {
return PhabricatorDatabaseRef::getMasterDatabaseRefs();
}
return $this->api;
return $this->refs;
}
protected function getConn() {
return $this->getAPI()->getConn(null);
public function setAPIs(array $apis) {
$map = array();
foreach ($apis as $api) {
$map[$api->getRef()->getRefKey()] = $api;
}
$this->apis = $map;
return $this;
}
private function getDatabaseNames() {
$api = $this->getAPI();
private function getDatabaseNames(PhabricatorDatabaseRef $ref) {
$api = $this->getAPI($ref);
$patches = PhabricatorSQLPatchList::buildAllPatches();
return $api->getDatabaseList(
$patches,
$only_living = true);
}
public function loadActualSchema() {
$databases = $this->getDatabaseNames();
private function getAPI(PhabricatorDatabaseRef $ref) {
$key = $ref->getRefKey();
if (isset($this->apis[$key])) {
return $this->apis[$key];
}
return id(new PhabricatorStorageManagementAPI())
->setUser($ref->getUser())
->setHost($ref->getHost())
->setPort($ref->getPort())
->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace())
->setPassword($ref->getPass());
}
public function loadActualSchemata() {
$refs = $this->getRefs();
$schemata = array();
foreach ($refs as $ref) {
$schema = $this->loadActualSchemaForServer($ref);
$schemata[$schema->getRef()->getRefKey()] = $schema;
}
return $schemata;
}
private function loadActualSchemaForServer(PhabricatorDatabaseRef $ref) {
$databases = $this->getDatabaseNames($ref);
$conn = $ref->newManagementConnection();
$conn = $this->getConn();
$tables = queryfx_all(
$conn,
'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION
@ -92,7 +126,8 @@ final class PhabricatorConfigSchemaQuery extends Phobject {
// primary, unique, and foreign keys, so we can't use them here. We pull
// indexes later on using SHOW INDEXES.
$server_schema = new PhabricatorConfigServerSchema();
$server_schema = id(new PhabricatorConfigServerSchema())
->setRef($ref);
$tables = igroup($tables, 'TABLE_SCHEMA');
foreach ($tables as $database_name => $database_tables) {
@ -177,15 +212,29 @@ final class PhabricatorConfigSchemaQuery extends Phobject {
return $server_schema;
}
public function loadExpectedSchema() {
$databases = $this->getDatabaseNames();
$info = $this->getAPI()->getCharsetInfo();
public function loadExpectedSchemata() {
$refs = $this->getRefs();
$schemata = array();
foreach ($refs as $ref) {
$schema = $this->loadExpectedSchemaForServer($ref);
$schemata[$schema->getRef()->getRefKey()] = $schema;
}
return $schemata;
}
public function loadExpectedSchemaForServer(PhabricatorDatabaseRef $ref) {
$databases = $this->getDatabaseNames($ref);
$info = $this->getAPI($ref)->getCharsetInfo();
$specs = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorConfigSchemaSpec')
->execute();
$server_schema = new PhabricatorConfigServerSchema();
$server_schema = id(new PhabricatorConfigServerSchema())
->setRef($ref);
foreach ($specs as $spec) {
$spec
->setUTF8Charset(
@ -201,7 +250,21 @@ final class PhabricatorConfigSchemaQuery extends Phobject {
return $server_schema;
}
public function buildComparisonSchema(
public function buildComparisonSchemata(
array $expect_servers,
array $actual_servers) {
$schemata = array();
foreach ($actual_servers as $key => $actual_server) {
$schemata[$key] = $this->buildComparisonSchemaForServer(
$expect_servers[$key],
$actual_server);
}
return $schemata;
}
private function buildComparisonSchemaForServer(
PhabricatorConfigServerSchema $expect,
PhabricatorConfigServerSchema $actual) {

View file

@ -3,8 +3,18 @@
final class PhabricatorConfigServerSchema
extends PhabricatorConfigStorageSchema {
private $ref;
private $databases = array();
public function setRef(PhabricatorDatabaseRef $ref) {
$this->ref = $ref;
return $this;
}
public function getRef() {
return $this->ref;
}
public function addDatabase(PhabricatorConfigDatabaseSchema $database) {
$key = $database->getName();
if (isset($this->databases[$key])) {

View file

@ -29,18 +29,13 @@ final class DifferentialParseCommitMessageConduitAPIMethod
$corpus = $request->getValue('corpus');
$is_partial = $request->getValue('partial');
$revision = new DifferentialRevision();
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
new DifferentialRevision(),
DifferentialCustomField::ROLE_COMMITMESSAGE);
$field_list->setViewer($viewer);
$field_map = mpull($field_list->getFields(), null, 'getFieldKeyForConduit');
$this->errors = array();
$label_map = $this->buildLabelMap($field_list);
$corpus_map = $this->parseCommitMessage($corpus, $label_map);
$corpus_map = $this->parseCommitMessage($corpus);
$values = array();
foreach ($corpus_map as $field_key => $text_value) {
@ -94,44 +89,12 @@ final class DifferentialParseCommitMessageConduitAPIMethod
);
}
private function buildLabelMap(PhabricatorCustomFieldList $field_list) {
$label_map = array();
foreach ($field_list->getFields() as $key => $field) {
$labels = $field->getCommitMessageLabels();
$key = $field->getFieldKeyForConduit();
foreach ($labels as $label) {
$normal_label = DifferentialCommitMessageParser::normalizeFieldLabel(
$label);
if (!empty($label_map[$normal_label])) {
throw new Exception(
pht(
'Field label "%s" is parsed by two custom fields: "%s" and '.
'"%s". Each label must be parsed by only one field.',
$label,
$key,
$label_map[$normal_label]));
}
$label_map[$normal_label] = $key;
}
}
return $label_map;
}
private function parseCommitMessage($corpus, array $label_map) {
$key_title = id(new DifferentialTitleField())->getFieldKeyForConduit();
$key_summary = id(new DifferentialSummaryField())->getFieldKeyForConduit();
$parser = id(new DifferentialCommitMessageParser())
->setLabelMap($label_map)
->setTitleKey($key_title)
->setSummaryKey($key_summary);
private function parseCommitMessage($corpus) {
$viewer = $this->getViewer();
$parser = DifferentialCommitMessageParser::newStandardParser($viewer);
$result = $parser->parseCorpus($corpus);
$this->errors = array();
foreach ($parser->getErrors() as $error) {
$this->errors[] = $error;
}

View file

@ -10,6 +10,7 @@ abstract class DifferentialCoreCustomField
private $value;
private $fieldError;
private $fieldParser;
abstract protected function readValueFromRevision(
DifferentialRevision $revision);
@ -60,6 +61,32 @@ abstract class DifferentialCoreCustomField
$error->setIsMissingFieldError(true);
$errors[] = $error;
$this->setFieldError(pht('Required'));
continue;
}
}
if (is_string($value)) {
$parser = $this->getFieldParser();
$result = $parser->parseCorpus($value);
unset($result['__title__']);
unset($result['__summary__']);
if ($result) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'The value you have entered in "%s" can not be parsed '.
'unambiguously when rendered in a commit message. Edit the '.
'message so that keywords like "Summary:" and "Test Plan:" do '.
'not appear at the beginning of lines. Parsed keys: %s.',
$this->getFieldName(),
implode(', ', array_keys($result))),
$xaction);
$errors[] = $error;
$this->setFieldError(pht('Invalid'));
continue;
}
}
}
@ -67,6 +94,22 @@ abstract class DifferentialCoreCustomField
return $errors;
}
private function getFieldParser() {
if (!$this->fieldParser) {
$viewer = $this->getViewer();
$parser = DifferentialCommitMessageParser::newStandardParser($viewer);
// Set custom title and summary keys so we can detect the presence of
// "Summary:" in, e.g., a test plan.
$parser->setTitleKey('__title__');
$parser->setSummaryKey('__summary__');
$this->fieldParser = $parser;
}
return $this->fieldParser;
}
public function canDisableField() {
return false;
}

View file

@ -27,6 +27,45 @@ final class DifferentialCommitMessageParser extends Phobject {
private $errors;
public static function newStandardParser(PhabricatorUser $viewer) {
$key_title = id(new DifferentialTitleField())->getFieldKeyForConduit();
$key_summary = id(new DifferentialSummaryField())->getFieldKeyForConduit();
$field_list = PhabricatorCustomField::getObjectFields(
new DifferentialRevision(),
DifferentialCustomField::ROLE_COMMITMESSAGE);
$field_list->setViewer($viewer);
$label_map = array();
foreach ($field_list->getFields() as $field) {
$labels = $field->getCommitMessageLabels();
$key = $field->getFieldKeyForConduit();
foreach ($labels as $label) {
$normal_label = self::normalizeFieldLabel(
$label);
if (!empty($label_map[$normal_label])) {
throw new Exception(
pht(
'Field label "%s" is parsed by two custom fields: "%s" and '.
'"%s". Each label must be parsed by only one field.',
$label,
$key,
$label_map[$normal_label]));
}
$label_map[$normal_label] = $key;
}
}
return id(new self())
->setLabelMap($label_map)
->setTitleKey($key_title)
->setSummaryKey($key_summary);
}
/* -( Configuring the Parser )--------------------------------------------- */

View file

@ -12,7 +12,7 @@ final class DifferentialExactUserFunctionDatasource
}
public function getDatasourceApplicationClass() {
return 'PhabricatorDifferentialApplication';
return 'PhabricatorPeopleApplication';
}
public function getComponentDatasources() {

View file

@ -46,6 +46,53 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
}
public function getRoutes() {
$repository_routes = array(
'/' => array(
'' => 'DiffusionRepositoryController',
'repository/(?P<dblob>.*)' => 'DiffusionRepositoryController',
'change/(?P<dblob>.*)' => 'DiffusionChangeController',
'history/(?P<dblob>.*)' => 'DiffusionHistoryController',
'browse/(?P<dblob>.*)' => 'DiffusionBrowseController',
'lastmodified/(?P<dblob>.*)' => 'DiffusionLastModifiedController',
'diff/' => 'DiffusionDiffController',
'tags/(?P<dblob>.*)' => 'DiffusionTagListController',
'branches/(?P<dblob>.*)' => 'DiffusionBranchTableController',
'refs/(?P<dblob>.*)' => 'DiffusionRefTableController',
'lint/(?P<dblob>.*)' => 'DiffusionLintController',
'commit/(?P<commit>[a-z0-9]+)/branches/'
=> 'DiffusionCommitBranchesController',
'commit/(?P<commit>[a-z0-9]+)/tags/'
=> 'DiffusionCommitTagsController',
'commit/(?P<commit>[a-z0-9]+)/edit/'
=> 'DiffusionCommitEditController',
'manage/(?:(?P<panel>[^/]+)/)?'
=> 'DiffusionRepositoryManagePanelsController',
'uri/' => array(
'view/(?P<id>[0-9]\d*)/' => 'DiffusionRepositoryURIViewController',
'disable/(?P<id>[0-9]\d*)/'
=> 'DiffusionRepositoryURIDisableController',
$this->getEditRoutePattern('edit/')
=> 'DiffusionRepositoryURIEditController',
'credential/(?P<id>[0-9]\d*)/(?P<action>edit|remove)/'
=> 'DiffusionRepositoryURICredentialController',
),
'edit/' => array(
'activate/' => 'DiffusionRepositoryEditActivateController',
'dangerous/' => 'DiffusionRepositoryEditDangerousController',
'delete/' => 'DiffusionRepositoryEditDeleteController',
'update/' => 'DiffusionRepositoryEditUpdateController',
'testautomation/' => 'DiffusionRepositoryTestAutomationController',
),
'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
),
// NOTE: This must come after the rules above; it just gives us a
// catch-all for serving repositories over HTTP. We must accept requests
// without the trailing "/" because SVN commands don't necessarily
// include it.
'(?:/.*)?' => 'DiffusionRepositoryDefaultController',
);
return array(
'/(?:'.
'r(?P<repositoryCallsign>[A-Z]+)'.
@ -54,6 +101,9 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
')(?P<commit>[a-f0-9]+)'
=> 'DiffusionCommitController',
'/source/(?P<repositoryShortName>[^/.]+)(?P<dotgit>\.git)?'
=> $repository_routes,
'/diffusion/' => array(
$this->getQueryRoutePattern()
=> 'DiffusionRepositoryListController',
@ -63,57 +113,8 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
'(?:query/(?P<queryKey>[^/]+)/)?' => 'DiffusionPushLogListController',
'view/(?P<id>\d+)/' => 'DiffusionPushEventViewController',
),
'(?:'.
'(?P<repositoryCallsign>[A-Z]+)'.
'|'.
'(?P<repositoryID>[1-9]\d*)'.
')/' => array(
'' => 'DiffusionRepositoryController',
'repository/(?P<dblob>.*)' => 'DiffusionRepositoryController',
'change/(?P<dblob>.*)' => 'DiffusionChangeController',
'history/(?P<dblob>.*)' => 'DiffusionHistoryController',
'browse/(?P<dblob>.*)' => 'DiffusionBrowseController',
'lastmodified/(?P<dblob>.*)' => 'DiffusionLastModifiedController',
'diff/' => 'DiffusionDiffController',
'tags/(?P<dblob>.*)' => 'DiffusionTagListController',
'branches/(?P<dblob>.*)' => 'DiffusionBranchTableController',
'refs/(?P<dblob>.*)' => 'DiffusionRefTableController',
'lint/(?P<dblob>.*)' => 'DiffusionLintController',
'commit/(?P<commit>[a-z0-9]+)/branches/'
=> 'DiffusionCommitBranchesController',
'commit/(?P<commit>[a-z0-9]+)/tags/'
=> 'DiffusionCommitTagsController',
'commit/(?P<commit>[a-z0-9]+)/edit/'
=> 'DiffusionCommitEditController',
'manage/(?:(?P<panel>[^/]+)/)?'
=> 'DiffusionRepositoryManagePanelsController',
'uri/' => array(
'view/(?P<id>[0-9]\d*)/' => 'DiffusionRepositoryURIViewController',
'disable/(?P<id>[0-9]\d*)/'
=> 'DiffusionRepositoryURIDisableController',
$this->getEditRoutePattern('edit/')
=> 'DiffusionRepositoryURIEditController',
'credential/(?P<id>[0-9]\d*)/(?P<action>edit|remove)/'
=> 'DiffusionRepositoryURICredentialController',
),
'edit/' => array(
'activate/' => 'DiffusionRepositoryEditActivateController',
'dangerous/' => 'DiffusionRepositoryEditDangerousController',
'delete/' => 'DiffusionRepositoryEditDeleteController',
'update/' => 'DiffusionRepositoryEditUpdateController',
'testautomation/' => 'DiffusionRepositoryTestAutomationController',
),
'pathtree/(?P<dblob>.*)' => 'DiffusionPathTreeController',
),
// NOTE: This must come after the rule above; it just gives us a
// catch-all for serving repositories over HTTP. We must accept
// requests without the trailing "/" because SVN commands don't
// necessarily include it.
'(?:(?P<repositoryCallsign>[A-Z]+)|(?P<repositoryID>[1-9]\d*))'.
'(?:/.*)?'
=> 'DiffusionRepositoryDefaultController',
'(?P<repositoryCallsign>[A-Z]+)' => $repository_routes,
'(?P<repositoryID>[1-9]\d*)' => $repository_routes,
'inline/' => array(
'edit/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController',

View file

@ -90,6 +90,11 @@ abstract class DiffusionController extends PhabricatorController {
protected function getRepositoryIdentifierFromRequest(
AphrontRequest $request) {
$short_name = $request->getURIData('repositoryShortName');
if (strlen($short_name)) {
return $short_name;
}
$identifier = $request->getURIData('repositoryCallsign');
if (strlen($identifier)) {
return $identifier;

View file

@ -16,6 +16,7 @@ final class DiffusionLastModifiedController extends DiffusionController {
$drequest = $this->getDiffusionRequest();
$paths = $request->getStr('paths');
try {
$paths = phutil_json_decode($paths);
} catch (PhutilJSONParserException $ex) {

View file

@ -88,6 +88,13 @@ final class DiffusionServeController extends DiffusionController {
}
}
// If the request was for a path like "/source/libphutil.git" but the
// repository is not a Git repository, reject the request.
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
if ($request->getURIData('dotgit') && ($vcs !== $type_git)) {
return null;
}
return $vcs;
}
@ -607,7 +614,9 @@ final class DiffusionServeController extends DiffusionController {
$request = $this->getRequest();
$request_path = $request->getRequestURI()->getPath();
$info = PhabricatorRepository::parseRepositoryServicePath($request_path);
$info = PhabricatorRepository::parseRepositoryServicePath(
$request_path,
$repository->getVersionControlSystem());
$base_path = $info['path'];
// For Git repositories, strip an optional directory component if it

View file

@ -15,7 +15,9 @@ final class DiffusionGitLFSAuthenticateWorkflow
}
protected function identifyRepository() {
return $this->loadRepositoryWithPath($this->getLFSPathArgument());
return $this->loadRepositoryWithPath(
$this->getLFSPathArgument(),
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
private function getLFSPathArgument() {

View file

@ -168,8 +168,7 @@ final class DiffusionRepositoryBasicsManagementPanel
$short_name = $repository->getRepositorySlug();
if ($short_name === null) {
$short_name = $repository->getCloneName();
$short_name = phutil_tag('em', array(), $short_name);
$short_name = phutil_tag('em', array(), pht('No Short Name'));
}
$view->addProperty(pht('Short Name'), $short_name);

View file

@ -17,7 +17,9 @@ abstract class DiffusionGitSSHWorkflow
protected function identifyRepository() {
$args = $this->getArgs();
$path = head($args->getArg('dir'));
return $this->loadRepositoryWithPath($path);
return $this->loadRepositoryWithPath(
$path,
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
protected function waitForGitClient() {

View file

@ -27,7 +27,9 @@ final class DiffusionMercurialServeSSHWorkflow
protected function identifyRepository() {
$args = $this->getArgs();
$path = $args->getArg('repository');
return $this->loadRepositoryWithPath($path);
return $this->loadRepositoryWithPath(
$path,
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
}
protected function executeRepositoryOperations() {

View file

@ -161,18 +161,19 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
}
}
protected function loadRepositoryWithPath($path) {
protected function loadRepositoryWithPath($path, $vcs) {
$viewer = $this->getUser();
$info = PhabricatorRepository::parseRepositoryServicePath($path);
$info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs);
if ($info === null) {
throw new Exception(
pht(
'Unrecognized repository path "%s". Expected a path like "%s" '.
'or "%s".',
'Unrecognized repository path "%s". Expected a path like "%s", '.
'"%s", or "%s".',
$path,
'/diffusion/X/',
'/diffusion/123/'));
'/diffusion/123/',
'/source/thaumaturgy.git'));
}
$identifier = $info['identifier'];

View file

@ -117,7 +117,9 @@ final class DiffusionSubversionServeSSHWorkflow
$uri = $struct[2]['value'];
$path = $this->getPathFromSubversionURI($uri);
return $this->loadRepositoryWithPath($path);
return $this->loadRepositoryWithPath(
$path,
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
}
}

View file

@ -76,6 +76,7 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
'dropupload/' => 'PhabricatorFileDropUploadController',
'compose/' => 'PhabricatorFileComposeController',
'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController',
'thread/(?P<phid>[^/]+)/' => 'PhabricatorFileLightboxController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController',
'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
@ -129,4 +130,10 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
);
}
public function getQuicksandURIPatternBlacklist() {
return array(
'/file/data/.*',
);
}
}

View file

@ -0,0 +1,43 @@
<?php
final class PhabricatorFileLightboxController
extends PhabricatorFileController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
$transactions = id(new PhabricatorFileTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT));
$timeline = $this->buildTransactionTimeline($file, $transactions);
if ($timeline->isTimelineEmpty()) {
$timeline = phutil_tag(
'div',
array(
'class' => 'phui-comment-panel-empty',
),
pht('No comments.'));
}
require_celerity_resource('phui-comment-panel-css');
$content = phutil_tag(
'div',
array(
'class' => 'phui-comment-panel',
),
$timeline);
return id(new AphrontAjaxResponse())
->setContent($content);
}
}

View file

@ -98,7 +98,7 @@ final class PhabricatorEmbedFileRemarkupRule
PhabricatorObjectHandle $handle,
array $options) {
require_celerity_resource('lightbox-attachment-css');
require_celerity_resource('phui-lightbox-css');
$attrs = array();
$image_class = 'phabricator-remarkup-embed-image';
@ -176,6 +176,7 @@ final class PhabricatorEmbedFileRemarkupRule
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'viewable' => true,
'monogram' => $file->getMonogram(),
),
),
$img);
@ -279,7 +280,8 @@ final class PhabricatorEmbedFileRemarkupRule
->setFileName($this->assertFlatText($options['name']))
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable((bool)$options['viewable']);
->setFileViewable((bool)$options['viewable'])
->setFileMonogram($file->getMonogram());
}
private function parseDimension($string) {

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorGuidanceContext
extends Phobject {}

View file

@ -0,0 +1,96 @@
<?php
final class PhabricatorGuidanceEngine
extends Phobject {
private $viewer;
private $guidanceContext;
public function setGuidanceContext(
PhabricatorGuidanceContext $guidance_context) {
$this->guidanceContext = $guidance_context;
return $this;
}
public function getGuidanceContext() {
return $this->guidanceContext;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function newInfoView() {
$extensions = PhabricatorGuidanceEngineExtension::getAllExtensions();
$context = $this->getGuidanceContext();
$keep = array();
foreach ($extensions as $key => $extension) {
if (!$extension->canGenerateGuidance($context)) {
continue;
}
$keep[$key] = id(clone $extension);
}
$guidance_map = array();
foreach ($keep as $extension) {
$guidance_list = $extension->generateGuidance($context);
foreach ($guidance_list as $guidance) {
$key = $guidance->getKey();
if (isset($guidance_map[$key])) {
throw new Exception(
pht(
'Two guidance extensions generated guidance with the same '.
'key ("%s"). Each piece of guidance must have a unique key.',
$key));
}
$guidance_map[$key] = $guidance;
}
}
foreach ($keep as $extension) {
$guidance_map = $extension->didGenerateGuidance($context, $guidance_map);
}
if (!$guidance_map) {
return null;
}
$guidance_map = msortv($guidance_map, 'getSortVector');
$severity = PhabricatorGuidanceMessage::SEVERITY_NOTICE;
$strength = null;
foreach ($guidance_map as $guidance) {
if ($strength !== null) {
if ($guidance->getSeverityStrength() <= $strength) {
continue;
}
}
$strength = $guidance->getSeverityStrength();
$severity = $guidance->getSeverity();
}
$severity_map = array(
PhabricatorGuidanceMessage::SEVERITY_NOTICE
=> PHUIInfoView::SEVERITY_NOTICE,
PhabricatorGuidanceMessage::SEVERITY_WARNING
=> PHUIInfoView::SEVERITY_WARNING,
);
$messages = mpull($guidance_map, 'getMessage', 'getKey');
return id(new PHUIInfoView())
->setViewer($this->getViewer())
->setSeverity(idx($severity_map, $severity, $severity))
->setErrors($messages);
}
}

View file

@ -0,0 +1,39 @@
<?php
abstract class PhabricatorGuidanceEngineExtension
extends Phobject {
final public function getExtensionKey() {
return $this->getPhobjectClassConstant('GUIDANCEKEY', 64);
}
abstract public function canGenerateGuidance(
PhabricatorGuidanceContext $context);
abstract public function generateGuidance(
PhabricatorGuidanceContext $context);
public function didGenerateGuidance(
PhabricatorGuidanceContext $context,
array $guidance) {
return $guidance;
}
final protected function newGuidance($key) {
return id(new PhabricatorGuidanceMessage())
->setKey($key);
}
final protected function newWarning($key) {
return $this->newGuidance($key)
->setSeverity(PhabricatorGuidanceMessage::SEVERITY_WARNING);
}
final public static function getAllExtensions() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getExtensionKey')
->execute();
}
}

View file

@ -0,0 +1,65 @@
<?php
final class PhabricatorGuidanceMessage
extends Phobject {
private $key;
private $message;
private $severity = self::SEVERITY_NOTICE;
private $priority = 1000;
const SEVERITY_NOTICE = 'notice';
const SEVERITY_WARNING = 'warning';
public function setSeverity($severity) {
$this->severity = $severity;
return $this;
}
public function getSeverity() {
return $this->severity;
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setMessage($message) {
$this->message = $message;
return $this;
}
public function getMessage() {
return $this->message;
}
public function getSortVector() {
return id(new PhutilSortVector())
->addInt($this->getPriority());
}
public function setPriority($priority) {
$this->priority = $priority;
return $this;
}
public function getPriority() {
return $this->priority;
}
public function getSeverityStrength() {
$map = array(
self::SEVERITY_NOTICE => 1,
self::SEVERITY_WARNING => 2,
);
return idx($map, $this->getSeverity(), 0);
}
}

View file

@ -16,6 +16,10 @@ abstract class HarbormasterArtifact extends Phobject {
abstract public function getArtifactParameterDescriptions();
abstract public function willCreateArtifact(PhabricatorUser $actor);
public function readArtifactHTTPParameter($key, $value) {
return $value;
}
public function validateArtifactData(array $artifact_data) {
$artifact_spec = $this->getArtifactParameterSpecification();
PhutilTypeSpec::checkMap($artifact_data, $artifact_spec);

View file

@ -27,6 +27,16 @@ final class HarbormasterURIArtifact extends HarbormasterArtifact {
);
}
public function readArtifactHTTPParameter($key, $value) {
// TODO: This is hacky and artifact parameters should be replaced more
// broadly, likely with EditFields. See T11887.
switch ($key) {
case 'ui.external':
return (bool)$value;
}
return $value;
}
public function getArtifactParameterDescriptions() {
return array(
'uri' => pht('The URI to store.'),

View file

@ -115,11 +115,28 @@ final class HarbormasterCreateArtifactConduitAPIMethod
$build_target_phid));
}
$artifact_type = $request->getValue('artifactType');
// Cast "artifactData" parameters to acceptable types if this request
// is submitting raw HTTP parameters. This is not ideal. See T11887 for
// discussion.
$artifact_data = $request->getValue('artifactData');
if (!$request->getIsStrictlyTyped()) {
$impl = HarbormasterArtifact::getArtifactType($artifact_type);
if ($impl) {
foreach ($artifact_data as $key => $value) {
$artifact_data[$key] = $impl->readArtifactHTTPParameter(
$key,
$value);
}
}
}
$artifact = $build_target->createArtifact(
$viewer,
$request->getValue('artifactKey'),
$request->getValue('artifactType'),
$request->getValue('artifactData'));
$artifact_type,
$artifact_data);
return array(
'data' => $this->returnArtifactList(array($artifact)),

View file

@ -510,15 +510,23 @@ final class ManiphestTaskDetailController extends ManiphestController {
}
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$out_handles = $viewer->loadHandles($out_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$out_handles = $this->getCompleteHandles($out_handles);
if (!count($in_handles) && !count($out_handles)) {
return null;
}
$view = new PHUIPropertyListView();
if ($in_phids) {
$in_handles = $viewer->loadHandles($in_phids);
if (count($in_handles)) {
$view->addProperty(pht('Mentioned In'), $in_handles->renderList());
}
if ($out_phids) {
$out_handles = $viewer->loadHandles($out_phids);
if (count($out_handles)) {
$view->addProperty(pht('Mentioned Here'), $out_handles->renderList());
}
@ -528,4 +536,18 @@ final class ManiphestTaskDetailController extends ManiphestController {
->appendChild($view);
}
private function getCompleteHandles(PhabricatorHandleList $handles) {
$phids = array();
foreach ($handles as $phid => $handle) {
if (!$handle->isComplete()) {
continue;
}
$phids[] = $phid;
}
return $handles->newSublist($phids);
}
}

View file

@ -82,7 +82,7 @@ final class PassphraseQueryConduitAPIMethod
switch ($credential->getCredentialType()) {
case PassphraseSSHPrivateKeyFileCredentialType::CREDENTIAL_TYPE:
if ($secret) {
if ($secret !== null) {
$material['file'] = $secret;
}
if ($public_key) {
@ -91,7 +91,7 @@ final class PassphraseQueryConduitAPIMethod
break;
case PassphraseSSHGeneratedKeyCredentialType::CREDENTIAL_TYPE:
case PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE:
if ($secret) {
if ($secret !== null) {
$material['privateKey'] = $secret;
}
if ($public_key) {
@ -99,10 +99,15 @@ final class PassphraseQueryConduitAPIMethod
}
break;
case PassphrasePasswordCredentialType::CREDENTIAL_TYPE:
if ($secret) {
if ($secret !== null) {
$material['password'] = $secret;
}
break;
case PassphraseTokenCredentialType::CREDENTIAL_TYPE:
if ($secret !== null) {
$material['token'] = $secret;
}
break;
}
if (!$allow_api) {

View file

@ -101,9 +101,20 @@ final class PhabricatorPeopleCreateController
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$guidance_context = new PhabricatorPeopleCreateGuidanceContext();
$guidance = id(new PhabricatorGuidanceEngine())
->setViewer($admin)
->setGuidanceContext($guidance_context)
->newInfoView();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
->setFooter(
array(
$guidance,
$box,
));
return $this->newPage()
->setTitle($title)

View file

@ -194,11 +194,12 @@ final class PhabricatorPeopleProfileViewController
->withDateRange($range_start, $range_end)
->withInvitedPHIDs(array($user->getPHID()))
->withIsCancelled(false)
->needRSVPs(array($viewer->getPHID()))
->execute();
$event_views = array();
foreach ($events as $event) {
$viewer_is_invited = $event->getIsUserInvited($viewer->getPHID());
$viewer_is_invited = $event->isRSVPInvited($viewer->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
@ -216,6 +217,7 @@ final class PhabricatorPeopleProfileViewController
->setIcon($event->getIcon())
->setViewerIsInvited($viewer_is_invited)
->setName($event->getName())
->setDatetimeSummary($event->renderEventDate($viewer, true))
->setURI($event->getURI());
$event_views[] = $event_view;

View file

@ -0,0 +1,4 @@
<?php
final class PhabricatorPeopleCreateGuidanceContext
extends PhabricatorGuidanceContext {}

View file

@ -53,6 +53,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication {
'preview/' => 'PhabricatorMarkupPreviewController',
'move/(?P<id>\d+)/' => 'PhamePostMoveController',
'archive/(?P<id>\d+)/' => 'PhamePostArchiveController',
'header/(?P<id>[1-9]\d*)/' => 'PhamePostHeaderPictureController',
),
'blog/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhameBlogListController',

View file

@ -90,6 +90,7 @@ abstract class PhameLiveController extends PhameController {
if (strlen($post_id)) {
$post_query = id(new PhamePostQuery())
->setViewer($viewer)
->needHeaderImage(true)
->withIDs(array($post_id));
if ($blog) {

View file

@ -0,0 +1,136 @@
<?php
final class PhamePostHeaderPictureController
extends PhamePostController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$post = id(new PhamePostQuery())
->setViewer($viewer)
->withIDs(array($id))
->needHeaderImage(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$post_uri = '/phame/post/view/'.$id;
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_file = true;
$errors = array();
$delete_header = ($request->getInt('delete') == 1);
if ($request->isFormPost()) {
if ($request->getFileExists('header')) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['header'],
array(
'authorPHID' => $viewer->getPHID(),
'canCDN' => true,
));
} else if (!$delete_header) {
$e_file = pht('Required');
$errors[] = pht(
'You must choose a file when uploading a new post header.');
}
if (!$errors && !$delete_header) {
if (!$file->isTransformableImage()) {
$e_file = pht('Not Supported');
$errors[] = pht(
'This server only supports these image formats: %s.',
implode(', ', $supported_formats));
}
}
if (!$errors) {
if ($delete_header) {
$new_value = null;
} else {
$file->attachToObject($post->getPHID());
$new_value = $file->getPHID();
}
$xactions = array();
$xactions[] = id(new PhamePostTransaction())
->setTransactionType(PhamePostTransaction::TYPE_HEADERIMAGE)
->setNewValue($new_value);
$editor = id(new PhamePostEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($post, $xactions);
return id(new AphrontRedirectResponse())->setURI($post_uri);
}
}
$title = pht('Edit Post Header');
$upload_form = id(new AphrontFormView())
->setUser($viewer)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormFileControl())
->setName('header')
->setLabel(pht('Upload Header'))
->setError($e_file)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))))
->appendChild(
id(new AphrontFormCheckboxControl())
->setName('delete')
->setLabel(pht('Delete Header'))
->addCheckbox(
'delete',
1,
null,
null))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($post_uri)
->setValue(pht('Upload Header')));
$upload_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upload New Header'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($upload_form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
$post->getTitle(),
$this->getApplicationURI('post/view/'.$id));
$crumbs->addTextCrumb(pht('Post Header'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Post Header'))
->setHeaderIcon('fa-camera');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$upload_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
}

View file

@ -19,9 +19,11 @@ final class PhamePostViewController
$is_external = $this->getIsExternal();
$header = id(new PHUIHeaderView())
->setHeader($post->getTitle())
->addClass('phame-header-bar')
->setUser($viewer);
$hero = $this->buildPhamePostHeader($post);
if (!$is_external) {
$actions = $this->renderActions($post);
$header->setPolicyObject($post);
@ -167,6 +169,7 @@ final class PhamePostViewController
->setCrumbs($crumbs)
->appendChild(
array(
$hero,
$document,
$about,
$properties,
@ -204,6 +207,13 @@ final class PhamePostViewController
->setName(pht('Edit Post'))
->setDisabled(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-camera-retro')
->setHref($this->getApplicationURI('post/header/'.$id.'/'))
->setName(pht('Edit Header Image'))
->setDisabled(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-arrows')
@ -307,4 +317,33 @@ final class PhamePostViewController
return array(head($prev), head($next));
}
private function buildPhamePostHeader(
PhamePost $post) {
$image = null;
if ($post->getHeaderImagePHID()) {
$image = phutil_tag(
'div',
array(
'class' => 'phame-header-hero',
),
phutil_tag(
'img',
array(
'src' => $post->getHeaderImageURI(),
'class' => 'phame-header-image',
)));
}
$title = phutil_tag_div('phame-header-title', $post->getTitle());
$subtitle = null;
if ($post->getSubtitle()) {
$subtitle = phutil_tag_div('phame-header-subtitle', $post->getSubtitle());
}
return phutil_tag_div(
'phame-mega-header', array($image, $title, $subtitle));
}
}

View file

@ -99,6 +99,14 @@ final class PhamePostEditEngine
->setConduitTypeDescription(pht('New post title.'))
->setTransactionType(PhamePostTransaction::TYPE_TITLE)
->setValue($object->getTitle()),
id(new PhabricatorTextEditField())
->setKey('subtitle')
->setLabel(pht('Subtitle'))
->setDescription(pht('Post subtitle.'))
->setConduitDescription(pht('Change the post subtitle.'))
->setConduitTypeDescription(pht('New post subtitle.'))
->setTransactionType(PhamePostTransaction::TYPE_SUBTITLE)
->setValue($object->getSubtitle()),
id(new PhabricatorSelectEditField())
->setKey('visibility')
->setLabel(pht('Visibility'))

View file

@ -16,8 +16,10 @@ final class PhamePostEditor
$types[] = PhamePostTransaction::TYPE_BLOG;
$types[] = PhamePostTransaction::TYPE_TITLE;
$types[] = PhamePostTransaction::TYPE_SUBTITLE;
$types[] = PhamePostTransaction::TYPE_BODY;
$types[] = PhamePostTransaction::TYPE_VISIBILITY;
$types[] = PhamePostTransaction::TYPE_HEADERIMAGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
@ -32,10 +34,14 @@ final class PhamePostEditor
return $object->getBlogPHID();
case PhamePostTransaction::TYPE_TITLE:
return $object->getTitle();
case PhamePostTransaction::TYPE_SUBTITLE:
return $object->getSubtitle();
case PhamePostTransaction::TYPE_BODY:
return $object->getBody();
case PhamePostTransaction::TYPE_VISIBILITY:
return $object->getVisibility();
case PhamePostTransaction::TYPE_HEADERIMAGE:
return $object->getHeaderImagePHID();
}
}
@ -45,8 +51,10 @@ final class PhamePostEditor
switch ($xaction->getTransactionType()) {
case PhamePostTransaction::TYPE_TITLE:
case PhamePostTransaction::TYPE_SUBTITLE:
case PhamePostTransaction::TYPE_BODY:
case PhamePostTransaction::TYPE_VISIBILITY:
case PhamePostTransaction::TYPE_HEADERIMAGE:
case PhamePostTransaction::TYPE_BLOG:
return $xaction->getNewValue();
}
@ -59,10 +67,14 @@ final class PhamePostEditor
switch ($xaction->getTransactionType()) {
case PhamePostTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case PhamePostTransaction::TYPE_SUBTITLE:
return $object->setSubtitle($xaction->getNewValue());
case PhamePostTransaction::TYPE_BODY:
return $object->setBody($xaction->getNewValue());
case PhamePostTransaction::TYPE_BLOG:
return $object->setBlogPHID($xaction->getNewValue());
case PhamePostTransaction::TYPE_HEADERIMAGE:
return $object->setHeaderImagePHID($xaction->getNewValue());
case PhamePostTransaction::TYPE_VISIBILITY:
if ($xaction->getNewValue() == PhameConstants::VISIBILITY_DRAFT) {
$object->setDatePublished(0);
@ -84,8 +96,10 @@ final class PhamePostEditor
switch ($xaction->getTransactionType()) {
case PhamePostTransaction::TYPE_TITLE:
case PhamePostTransaction::TYPE_SUBTITLE:
case PhamePostTransaction::TYPE_BODY:
case PhamePostTransaction::TYPE_VISIBILITY:
case PhamePostTransaction::TYPE_HEADERIMAGE:
case PhamePostTransaction::TYPE_BLOG:
return;
}

View file

@ -9,6 +9,8 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $publishedAfter;
private $phids;
private $needHeaderImage;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
@ -39,6 +41,11 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this;
}
public function needHeaderImage($need) {
$this->needHeaderImage = $need;
return $this;
}
public function newResultObject() {
return new PhamePost();
}
@ -71,6 +78,28 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
$post->attachBlog($blog);
}
if ($this->needHeaderImage) {
$file_phids = mpull($posts, 'getHeaderImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($posts as $post) {
$file = idx($files, $post->getHeaderImagePHID());
if ($file) {
$post->attachHeaderImageFile($file);
}
}
}
return $posts;
}

View file

@ -18,6 +18,7 @@ final class PhamePost extends PhameDAO
protected $bloggerPHID;
protected $title;
protected $subtitle;
protected $phameTitle;
protected $body;
protected $visibility;
@ -25,8 +26,10 @@ final class PhamePost extends PhameDAO
protected $datePublished;
protected $blogPHID;
protected $mailKey;
protected $headerImagePHID;
private $blog = self::ATTACHABLE;
private $headerImageFile = self::ATTACHABLE;
public static function initializePost(
PhabricatorUser $blogger,
@ -122,9 +125,11 @@ final class PhamePost extends PhameDAO
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'subtitle' => 'text64',
'phameTitle' => 'sort64?',
'visibility' => 'uint32',
'mailKey' => 'bytes20',
'headerImagePHID' => 'phid?',
// T6203/NULLABILITY
// These seem like they should always be non-null?
@ -170,6 +175,19 @@ final class PhamePost extends PhameDAO
return PhabricatorSlug::normalizeProjectSlug($this->getTitle(), true);
}
public function getHeaderImageURI() {
return $this->getHeaderImageFile()->getBestURI();
}
public function attachHeaderImageFile(PhabricatorFile $file) {
$this->headerImageFile = $file;
return $this;
}
public function getHeaderImageFile() {
return $this->assertAttached($this->headerImageFile);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */

View file

@ -4,8 +4,10 @@ final class PhamePostTransaction
extends PhabricatorApplicationTransaction {
const TYPE_TITLE = 'phame.post.title';
const TYPE_SUBTITLE = 'phame.post.subtitle';
const TYPE_BODY = 'phame.post.body';
const TYPE_VISIBILITY = 'phame.post.visibility';
const TYPE_HEADERIMAGE = 'phame.post.headerimage';
const TYPE_BLOG = 'phame.post.blog';
const MAILTAG_CONTENT = 'phame-post-content';
@ -70,6 +72,9 @@ final class PhamePostTransaction
case PhabricatorTransactions::TYPE_CREATE:
return 'fa-plus';
break;
case self::TYPE_HEADERIMAGE:
return 'fa-camera-retro';
break;
case self::TYPE_VISIBILITY:
if ($new == PhameConstants::VISIBILITY_PUBLISHED) {
return 'fa-globe';
@ -94,6 +99,7 @@ final class PhamePostTransaction
$tags[] = self::MAILTAG_SUBSCRIBERS;
break;
case self::TYPE_TITLE:
case self::TYPE_SUBTITLE:
case self::TYPE_BODY:
$tags[] = self::MAILTAG_CONTENT;
break;
@ -136,11 +142,29 @@ final class PhamePostTransaction
$new);
}
break;
case self::TYPE_SUBTITLE:
if ($old === null) {
return pht(
'%s set the post\'s subtitle to "%s".',
$this->renderHandleLink($author_phid),
$new);
} else {
return pht(
'%s updated the post\'s subtitle to "%s".',
$this->renderHandleLink($author_phid),
$new);
}
break;
case self::TYPE_BODY:
return pht(
'%s updated the blog post.',
$this->renderHandleLink($author_phid));
break;
case self::TYPE_HEADERIMAGE:
return pht(
'%s updated the header image.',
$this->renderHandleLink($author_phid));
break;
case self::TYPE_VISIBILITY:
if ($new == PhameConstants::VISIBILITY_DRAFT) {
return pht(
@ -195,12 +219,24 @@ final class PhamePostTransaction
$this->renderHandleLink($object_phid));
}
break;
case self::TYPE_SUBTITLE:
return pht(
'%s updated the subtitle for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
break;
case self::TYPE_BODY:
return pht(
'%s updated the blog post %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
break;
case self::TYPE_HEADERIMAGE:
return pht(
'%s updated the header image for post %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
break;
case self::TYPE_VISIBILITY:
if ($new == PhameConstants::VISIBILITY_DRAFT) {
return pht(

View file

@ -74,6 +74,24 @@ final class PhabricatorHandleList
}
/**
* Create a new list with a subset of the PHIDs in this list.
*/
public function newSublist(array $phids) {
foreach ($phids as $phid) {
if (!isset($this[$phid])) {
throw new Exception(
pht(
'Trying to create a new sublist of an existsing handle list, '.
'but PHID "%s" does not appear in the parent list.',
$phid));
}
}
return $this->handlePool->newHandleList($phids);
}
/* -( Rendering )---------------------------------------------------------- */

View file

@ -205,11 +205,7 @@ final class PhabricatorPhurlURLEditor
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getPHID()) {
$phids[] = $object->getPHID();
}
$phids[] = $this->getActingAsPHID();
$phids = array_unique($phids);
return $phids;
}

View file

@ -416,7 +416,6 @@ final class PhabricatorProjectBoardViewController
->appendChild($board)
->addClass('project-board-wrapper');
$nav = $this->getProfileMenu();
$divider = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_DIVIDER);
$fullscreen = $this->buildFullscreenMenu();
@ -439,7 +438,6 @@ final class PhabricatorProjectBoardViewController
))
->setPageObjectPHIDs(array($project->getPHID()))
->setShowFooter(false)
->setNavigation($nav)
->setCrumbs($crumbs)
->addQuicksandConfig(
array(

View file

@ -119,7 +119,8 @@ abstract class PhabricatorProjectController extends PhabricatorController {
foreach ($ancestors as $ancestor) {
$crumbs->addTextCrumb(
$ancestor->getName(),
$ancestor->getURI());
$ancestor->getProfileURI()
);
}
}

View file

@ -367,6 +367,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO
return "/project/view/{$id}/";
}
public function getProfileURI() {
$id = $this->getID();
return "/project/profile/{$id}/";
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));

View file

@ -103,15 +103,10 @@ final class PhabricatorProjectDatasource
$all_strings = array();
$all_strings[] = $proj->getDisplayName();
// Add an extra space after the name so that the original project
// sorts ahead of milestones. This is kind of a hack but ehh?
$all_strings[] = null;
foreach ($proj->getSlugs() as $project_slug) {
$all_strings[] = $project_slug->getSlug();
}
$all_strings = implode(' ', $all_strings);
$all_strings = implode("\n", $all_strings);
$proj_result = id(new PhabricatorTypeaheadResult())
->setName($all_strings)
@ -135,7 +130,7 @@ final class PhabricatorProjectDatasource
$description = idx($descriptions, $phid);
if (strlen($description)) {
$summary = PhabricatorMarkupEngine::summarize($description);
$summary = PhabricatorMarkupEngine::summarizeSentence($description);
$proj_result->addAttribute($summary);
}
}

View file

@ -563,6 +563,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
public function getURI() {
$short_name = $this->getRepositorySlug();
if (strlen($short_name)) {
return "/source/{$short_name}/";
}
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/diffusion/{$callsign}/";
@ -573,7 +578,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
public function getPathURI($path) {
return $this->getURI().$path;
return $this->getURI().ltrim($path, '/');
}
public function getCommitURI($identifier) {
@ -586,14 +591,22 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return "/R{$id}:{$identifier}";
}
public static function parseRepositoryServicePath($request_path) {
public static function parseRepositoryServicePath($request_path, $vcs) {
// NOTE: In Mercurial over SSH, the path will begin without a leading "/",
// so we're matching it optionally.
if ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
$maybe_git = '(?:\\.git)?';
} else {
$maybe_git = null;
}
$patterns = array(
'(^'.
'(?P<base>/?diffusion/(?P<identifier>[A-Z]+|[0-9]\d*))'.
'(?P<path>(?:/.*)?)'.
'(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/.]+))'.
$maybe_git.
'(?P<path>(?:/|.*)?)'.
'\z)',
);
@ -624,28 +637,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
public function getCanonicalPath($request_path) {
$standard_pattern =
'(^'.
'(?P<prefix>/diffusion/)'.
'(?P<prefix>/(?:diffusion|source)/)'.
'(?P<identifier>[^/]+)'.
'(?P<suffix>(?:/.*)?)'.
'\z)';
$matches = null;
if (preg_match($standard_pattern, $request_path, $matches)) {
$prefix = $matches['prefix'];
$callsign = $this->getCallsign();
if ($callsign) {
$identifier = $callsign;
} else {
$identifier = $this->getID();
}
$suffix = $matches['suffix'];
if (!strlen($suffix)) {
$suffix = '/';
}
return $prefix.$identifier.$suffix;
return $this->getPathURI($suffix);
}
$commit_pattern =
@ -724,18 +724,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $this->getCommitURI($commit);
}
$identifier = $this->getID();
$callsign = $this->getCallsign();
if ($callsign !== null) {
$identifier = $callsign;
}
if (strlen($identifier)) {
$identifier = phutil_escape_uri_path_component($identifier);
}
if (strlen($path)) {
$path = ltrim($path, '/');
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
@ -766,13 +754,13 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
case 'lint':
case 'pathtree':
case 'refs':
$uri = "/diffusion/{$identifier}/{$action}/{$path}{$commit}{$line}";
$uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
break;
case 'branch':
if (strlen($path)) {
$uri = "/diffusion/{$identifier}/repository/{$path}";
$uri = $this->getPathURI("/repository/{$path}");
} else {
$uri = "/diffusion/{$identifier}/";
$uri = $this->getPathURI('/');
}
break;
case 'external':
@ -2108,9 +2096,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$has_callsign = ($this->getCallsign() !== null);
$has_shortname = ($this->getRepositorySlug() !== null);
// TODO: For now, never enable these because they don't work yet.
$has_shortname = false;
$identifier_map = array(
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname,

View file

@ -125,8 +125,8 @@ final class PhabricatorRepositoryURI
$other_uris = $repository->getURIs();
$identifier_value = array(
self::BUILTIN_IDENTIFIER_CALLSIGN => 3,
self::BUILTIN_IDENTIFIER_SHORTNAME => 2,
self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
self::BUILTIN_IDENTIFIER_ID => 1,
);

View file

@ -233,6 +233,10 @@ class PhabricatorApplicationTransactionView extends AphrontView {
return $view;
}
public function isTimelineEmpty() {
return !count($this->buildEvents(true));
}
protected function getOrBuildEngine() {
if (!$this->engine) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;

View file

@ -311,6 +311,11 @@ final class PhabricatorTypeaheadModularDatasourceController
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
// Make "\n" delimiters more visible.
foreach ($content as $key => $row) {
$content[$key][0] = str_replace("\n", '<\n>', $row[0]);
}
$table = new AphrontTableView($content);
$table->setHeaders(
array(

View file

@ -204,8 +204,8 @@ For many linters, you can do this by providing a `severity` map:
}
```
Here, the lint message "E221" (which is "multiple spaces before operator") is
disabled, so it won't be shown. The message "E401" (which is "multiple imports
Here, the lint message `E221` (which is "multiple spaces before operator") is
disabled, so it won't be shown. The message `E401` (which is "multiple imports
on one line") is set to "warning" severity.
If you want to remap a large number of messages, you can use `severity.rules`

View file

@ -37,6 +37,22 @@ revision or task.
You can click through to a user's profile to see more details about their
availability.
Status Icons
============
On the month and day views, Calendar shows an icon next to each event to
indicate status. The icons are:
- {icon user-plus, color=green} **Invited, Individual**: You're personally
invited to the event.
- {icon users, color=green} **Invited, Group**: A project you are a member
of is invited to the event.
- {icon check-circle, color=green} **Attending**: You're attending the event.
- {icon times-circle, color=grey} **Declined**: You've declined the event.
- {icon times, color=red} **Cancelled**: The event has been cancelled.
If you don't have any special relationship to the event and the event does not
have any special status, an event-specific icon is shown instead.
Importing Events
================

View file

@ -222,23 +222,92 @@ other Phabricator SSH services.
NOTE: The Phabricator `sshd` service **MUST** be 6.2 or newer, because
Phabricator relies on the `AuthorizedKeysCommand` option.
**Choose a Port**: These instructions will configure the alternate `sshd` on
Before continuing, you must choose a strategy for which port each copy of
`sshd` will run on. The next section lays out various approaches.
SSHD Port Assignment
====================
The normal `sshd` that lets you administrate the host and the special `sshd`
which serves repositories can't run on the same port. In particular, only one
of them can run on port `22`, which will make it a bit inconvenient to access
the other one.
These instructions will walk you through configuring the alternate `sshd` on
port `2222`. This is easy to configure, but if you run the service on this port
users will clone and push to URIs like `ssh://git@host.com:2222/`, which is
a little ugly.
users will clone and push to URIs like `ssh://git@host.com:2222/`, which is a
little ugly.
The easiest way to fix this is to put a load balancer in front of the host and
have it forward TCP traffic on port `22` to port `2222`. Then users can clone
from `ssh://git@host.com/` without an explicit port number and you don't need
to do anything else.
There are several different approaches you can use to mitigate or eliminate
this problem.
Alternatively, you can move the administrative `sshd` to a new port, then run
Phabricator `sshd` on port 22. This is complicated and risky. See "Moving the
sshd Port" below for help.
**Run on Port 2222**: You can do nothing, and just run the repository `sshd` on
port `2222` and accept the explicit port in the URIs. This is the simplest
approach, and you can always start here and clean things up later if you grow
tired of dealing with the port number.
Finally, you can just run on port `2222` and accept the explicit port in the
URIs. This is the simplest approach, and you can start here and clean things
up later.
**Use a Load Balancer**: You can configure a load balancer in front of the host
and have it forward TCP traffic on port `22` to port `2222`. Then users can
clone from `ssh://git@host.com/` without an explicit port number and you don't
need to do anything else.
This may be very easy to set up, particularly if you are hosted in AWS, and
is often the simplest and cleanest approach.
**Swap Ports**: You can move the administrative `sshd` to a new port, then run
Phabricator `sshd` on port 22. This is somewhat complicated and can be a bit
risky if you make a mistake. See "Moving the sshd Port" below for help.
**Change Client Config**: You can run on a nonstandard port, but configure SSH
on the client side so that `ssh` automatically defaults to the correct port
when connecting to the host. To do this, add a section like this to your
`~/.ssh/config`:
```
Host phabricator.corporation.com
Port 2222
```
(If you want, you can also add a default `User`.)
Command line tools like `ssh`, `git` and `hg` will now default to port
`2222` when connecting to this host.
A downside to this approach is that your users will each need to set up their
`~/.ssh/config` files individually.
This file also allows you to define short names for hosts using the `Host` and
`HostName` options. If you choose to do this, be aware that Phabricator uses
remote/clone URIs to figure out which repository it is operating in, but can
not resolve host aliases defined in your `ssh` config. If you create host
aliases they may break some features related to repository identification.
If you use this approach, you will also need to specify a port explicitly when
connecting to administrate the host. Any unit tests or other build automation
will also need to be configured or use explicit port numbers.
**Port Multiplexing**: If you have hardware access, you can power down the host
and find the network I/O pins on the motherboard (for onboard networking) or
network card.
Carefully strip and solder a short piece of copper wire between the pins for
the external interface `22` and internal `2222`, so the external interface can
receive traffic for both services.
(Make sure not to desolder the existing connection between external `22` and
internal `22` or you won't be able to connect normally to administrate the
host.)
The obvious downside to this approach is that it requires physical access to
the machine, so it won't work if you're hosted on a cloud provider.
SSHD Setup
==========
Now that you've decided how you'll handle port assignment, you're ready to
continue `sshd` setup.
If you plan to connect to a port other than `22`, you should set this port
as `diffusion.ssh-port` in your Phabricator config:

View file

@ -85,14 +85,6 @@ final class PhabricatorClusterDatabasesConfigOptionType
$map[$key] = true;
}
if (count($masters) > 1) {
throw new Exception(
pht(
'Database cluster configuration is invalid: it describes multiple '.
'masters. No more than one host may be a master. Hosts currently '.
'configured as masters: %s.',
implode(', ', $masters)));
}
}
}

View file

@ -157,6 +157,17 @@ final class PhabricatorDatabaseRef
return $this->isIndividual;
}
public function getRefKey() {
$host = $this->getHost();
$port = $this->getPort();
if (strlen($port)) {
return "{$host}:{$port}";
}
return $host;
}
public static function getConnectionStatusMap() {
return array(
self::STATUS_OKAY => array(
@ -212,7 +223,7 @@ final class PhabricatorDatabaseRef
);
}
public static function getLiveRefs() {
public static function getClusterRefs() {
$cache = PhabricatorCaches::getRequestCache();
$refs = $cache->getKey(self::KEY_REFS);
@ -446,24 +457,46 @@ final class PhabricatorDatabaseRef
return $this->healthRecord;
}
public static function getMasterDatabaseRef() {
$refs = self::getLiveRefs();
public static function getActiveDatabaseRefs() {
$refs = array();
if (!$refs) {
return self::getLiveIndividualRef();
foreach (self::getMasterDatabaseRefs() as $ref) {
$refs[] = $ref;
}
$master = null;
foreach (self::getReplicaDatabaseRefs() as $ref) {
$refs[] = $ref;
}
return $refs;
}
public static function getMasterDatabaseRefs() {
$refs = self::getClusterRefs();
if (!$refs) {
return array(self::getLiveIndividualRef());
}
$masters = array();
foreach ($refs as $ref) {
if ($ref->getDisabled()) {
continue;
}
if ($ref->getIsMaster()) {
return $ref;
$masters[] = $ref;
}
}
return null;
return $masters;
}
public static function getMasterDatabaseRefForDatabase($database) {
$masters = self::getMasterDatabaseRefs();
// TODO: Actually implement this.
return head($masters);
}
public static function newIndividualRef() {
@ -480,18 +513,14 @@ final class PhabricatorDatabaseRef
->setIsMaster(true);
}
public static function getReplicaDatabaseRef() {
$refs = self::getLiveRefs();
public static function getReplicaDatabaseRefs() {
$refs = self::getClusterRefs();
if (!$refs) {
return null;
return array();
}
// TODO: We may have multiple replicas to choose from, and could make
// more of an effort to pick the "best" one here instead of always
// picking the first one. Once we've picked one, we should try to use
// the same replica for the rest of the request, though.
$replicas = array();
foreach ($refs as $ref) {
if ($ref->getDisabled()) {
continue;
@ -499,10 +528,24 @@ final class PhabricatorDatabaseRef
if ($ref->getIsMaster()) {
continue;
}
return $ref;
$replicas[] = $ref;
}
return null;
return $replicas;
}
public static function getReplicaDatabaseRefForDatabase($database) {
$replicas = self::getReplicaDatabaseRefs();
// TODO: Actually implement this.
// TODO: We may have multiple replicas to choose from, and could make
// more of an effort to pick the "best" one here instead of always
// picking the first one. Once we've picked one, we should try to use
// the same replica for the rest of the request, though.
return head($replicas);
}
private function newConnection(array $options) {

View file

@ -223,13 +223,24 @@ final class PhabricatorEnv extends Phobject {
$stack->pushSource($site_source);
}
$master = PhabricatorDatabaseRef::getMasterDatabaseRef();
if (!$master) {
$masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();
if (!$masters) {
self::setReadOnly(true, self::READONLY_MASTERLESS);
} else if ($master->isSevered()) {
$master->checkHealth();
if ($master->isSevered()) {
self::setReadOnly(true, self::READONLY_SEVERED);
} else {
// If any master is severed, we drop to readonly mode. In theory we
// could try to continue if we're only missing some applications, but
// this is very complex and we're unlikely to get it right.
foreach ($masters as $master) {
// Give severed masters one last chance to get healthy.
if ($master->isSevered()) {
$master->checkHealth();
}
if ($master->isSevered()) {
self::setReadOnly(true, self::READONLY_SEVERED);
break;
}
}
}

View file

@ -1580,6 +1580,14 @@ final class PhabricatorUSEnglishTranslation
'Restart %s build?',
'Restart %s builds?',
),
'%s is starting in %s minute(s), at %s.' => array(
array(
'%s is starting in one minute, at %3$s.',
'%s is starting in %s minutes, at %s.',
),
),
);
}

View file

@ -607,6 +607,28 @@ final class PhabricatorMarkupEngine extends Phobject {
return array_values($files);
}
public static function summarizeSentence($corpus) {
$corpus = trim($corpus);
$blocks = preg_split('/\n+/', $corpus, 2);
$block = head($blocks);
$sentences = preg_split(
'/\b([.?!]+)\B/u',
$block,
2,
PREG_SPLIT_DELIM_CAPTURE);
if (count($sentences) > 1) {
$result = $sentences[0].$sentences[1];
} else {
$result = head($sentences);
}
return id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($result);
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.

View file

@ -0,0 +1,43 @@
<?php
final class PhabricatorMarkupEngineTestCase
extends PhabricatorTestCase {
public function testRemarkupSentenceSummmaries() {
$this->assertSentenceSummary(
'The quick brown fox. Jumped over the lazy dog.',
'The quick brown fox.');
$this->assertSentenceSummary(
'Go to www.help.com for details. Good day.',
'Go to www.help.com for details.');
$this->assertSentenceSummary(
'Coxy lummox gives squid who asks for job pen.',
'Coxy lummox gives squid who asks for job pen.');
$this->assertSentenceSummary(
'DEPRECATED',
'DEPRECATED');
$this->assertSentenceSummary(
'Never use this! It is deadly poison.',
'Never use this!');
$this->assertSentenceSummary(
"a short poem\nmeow meow meow\nmeow meow meow\n\n- cat",
'a short poem');
$this->assertSentenceSummary(
'WOW!! GREAT PROJECT!',
'WOW!!');
}
private function assertSentenceSummary($corpus, $summary) {
$this->assertEqual(
$summary,
PhabricatorMarkupEngine::summarizeSentence($corpus),
pht('Summary of: %s', $corpus));
}
}

View file

@ -114,7 +114,8 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
}
private function newClusterConnection($database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRef();
$master = PhabricatorDatabaseRef::getMasterDatabaseRefForDatabase(
$database);
if ($master && !$master->isSevered()) {
$connection = $master->newApplicationConnection($database);
@ -130,7 +131,8 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
}
}
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRef();
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForDatabase(
$database);
if ($replica) {
$connection = $replica->newApplicationConnection($database);
$connection->setReadOnly(true);

View file

@ -2,6 +2,7 @@
final class PhabricatorStorageManagementAPI extends Phobject {
private $ref;
private $host;
private $user;
private $port;
@ -74,6 +75,15 @@ final class PhabricatorStorageManagementAPI extends Phobject {
return $this->port;
}
public function setRef(PhabricatorDatabaseRef $ref) {
$this->ref = $ref;
return $this;
}
public function getRef() {
return $this->ref;
}
public function getDatabaseName($fragment) {
return $this->namespace.'_'.$fragment;
}

View file

@ -27,12 +27,19 @@ final class PhabricatorStorageManagementAdjustWorkflow
public function didExecute(PhutilArgumentParser $args) {
$unsafe = $args->getArg('unsafe');
$this->requireAllPatchesApplied();
return $this->adjustSchemata($unsafe);
foreach ($this->getMasterAPIs() as $api) {
$this->requireAllPatchesApplied($api);
$err = $this->adjustSchemata($api, $unsafe);
if ($err) {
return $err;
}
}
return 0;
}
private function requireAllPatchesApplied() {
$api = $this->getAPI();
private function requireAllPatchesApplied(
PhabricatorStorageManagementAPI $api) {
$applied = $api->getAppliedPatches();
if ($applied === null) {

View file

@ -15,7 +15,8 @@ final class PhabricatorStorageManagementDatabasesWorkflow
}
public function didExecute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$api = $this->getAnyAPI();
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, true);

View file

@ -23,6 +23,8 @@ final class PhabricatorStorageManagementDestroyWorkflow
public function didExecute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$api = $this->getSingleAPI();
if (!$this->isDryRun() && !$this->isForce()) {
$console->writeOut(
phutil_console_wrap(
@ -42,7 +44,6 @@ final class PhabricatorStorageManagementDestroyWorkflow
}
}
$api = $this->getAPI();
$patches = $this->getPatches();
if ($args->getArg('unittest-fixtures')) {

View file

@ -44,7 +44,7 @@ final class PhabricatorStorageManagementDumpWorkflow
}
public function didExecute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$api = $this->getSingleAPI();
$patches = $this->getPatches();
$console = PhutilConsole::getConsole();

View file

@ -15,13 +15,14 @@ final class PhabricatorStorageManagementProbeWorkflow
}
public function didExecute(PhutilArgumentParser $args) {
$api = $this->getSingleAPI();
$console = PhutilConsole::getConsole();
$console->writeErr(
"%s\n",
pht('Analyzing table sizes (this may take a moment)...'));
$api = $this->getAPI();
$patches = $this->getPatches();
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, true);
$conn_r = $api->getConn(null);

View file

@ -36,7 +36,14 @@ final class PhabricatorStorageManagementQuickstartWorkflow
$bin = dirname(phutil_get_library_root('phabricator')).'/bin/storage';
if (!$this->getAPI()->isCharacterSetAvailable('utf8mb4')) {
// We don't care which database we're using to generate a quickstart file,
// since all of the schemata should be identical.
$api = $this->getAnyAPI();
$ref = $api->getRef();
$ref_key = $ref->getRefKey();
if (!$api->isCharacterSetAvailable('utf8mb4')) {
throw new PhutilArgumentUsageException(
pht(
'You can only generate a new quickstart file if MySQL supports '.
@ -47,35 +54,39 @@ final class PhabricatorStorageManagementQuickstartWorkflow
}
$err = phutil_passthru(
'%s upgrade --force --no-quickstart --namespace %s',
'%s upgrade --force --no-quickstart --namespace %s --ref %s',
$bin,
$namespace);
$namespace,
$ref_key);
if ($err) {
return $err;
}
$err = phutil_passthru(
'%s adjust --force --namespace %s',
'%s adjust --force --namespace %s --ref %s',
$bin,
$namespace);
$namespace,
$ref_key);
if ($err) {
return $err;
}
$tmp = new TempFile();
$err = phutil_passthru(
'%s dump --namespace %s > %s',
'%s dump --namespace %s --ref %s > %s',
$bin,
$namespace,
$ref_key,
$tmp);
if ($err) {
return $err;
}
$err = phutil_passthru(
'%s destroy --force --namespace %s',
'%s destroy --force --namespace %s --ref %s',
$bin,
$namespace);
$namespace,
$ref_key);
if ($err) {
return $err;
}

View file

@ -62,7 +62,7 @@ final class PhabricatorStorageManagementRenamespaceWorkflow
if (!strlen($input) && !$is_live) {
throw new PhutilArgumentUsageException(
pht(
'Specify the dumpfile to read with "--in", or use "--live" to '.
'Specify the dumpfile to read with "--input", or use "--live" to '.
'generate one automatically.'));
}
@ -108,11 +108,15 @@ final class PhabricatorStorageManagementRenamespaceWorkflow
}
if ($is_live) {
$api = $this->getSingleAPI();
$ref_key = $api->getRef()->getRefKey();
$root = dirname(phutil_get_library_root('phabricator'));
$future = new ExecFuture(
'%R dump',
$root.'/bin/storage');
'%R dump --ref %s',
$root.'/bin/storage',
$ref_key);
$lines = new LinesOfALargeExecFuture($future);
} else {

View file

@ -15,7 +15,7 @@ final class PhabricatorStorageManagementShellWorkflow
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$api = $this->getSingleAPI();
list($host, $port) = $this->getBareHostAndPort($api->getHost());
$flag_port = $port

View file

@ -15,50 +15,54 @@ final class PhabricatorStorageManagementStatusWorkflow
}
public function didExecute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$patches = $this->getPatches();
foreach ($this->getAPIs() as $api) {
$patches = $this->getPatches();
$applied = $api->getAppliedPatches();
$applied = $api->getAppliedPatches();
if ($applied === null) {
echo phutil_console_format(
"**%s**: %s\n",
pht('Database Not Initialized'),
pht('Run **%s** to initialize.', './bin/storage upgrade'));
if ($applied === null) {
echo phutil_console_format(
"**%s**: %s\n",
pht('Database Not Initialized'),
pht('Run **%s** to initialize.', './bin/storage upgrade'));
return 1;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
->addColumn('id', array('title' => pht('ID')))
->addColumn('status', array('title' => pht('Status')))
->addColumn('duration', array('title' => pht('Duration')))
->addColumn('type', array('title' => pht('Type')))
->addColumn('name', array('title' => pht('Name')));
$durations = $api->getPatchDurations();
foreach ($patches as $patch) {
$duration = idx($durations, $patch->getFullKey());
if ($duration === null) {
$duration = '-';
} else {
$duration = pht('%s us', new PhutilNumber($duration));
return 1;
}
$table->addRow(array(
'id' => $patch->getFullKey(),
'status' => in_array($patch->getFullKey(), $applied)
? pht('Applied')
: pht('Not Applied'),
'duration' => $duration,
'type' => $patch->getType(),
'name' => $patch->getName(),
));
}
$ref = $api->getRef();
$table->draw();
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
->addColumn('id', array('title' => pht('ID')))
->addColumn('host', array('title' => pht('Host')))
->addColumn('status', array('title' => pht('Status')))
->addColumn('duration', array('title' => pht('Duration')))
->addColumn('type', array('title' => pht('Type')))
->addColumn('name', array('title' => pht('Name')));
$durations = $api->getPatchDurations();
foreach ($patches as $patch) {
$duration = idx($durations, $patch->getFullKey());
if ($duration === null) {
$duration = '-';
} else {
$duration = pht('%s us', new PhutilNumber($duration));
}
$table->addRow(array(
'id' => $patch->getFullKey(),
'host' => $ref->getRefKey(),
'status' => in_array($patch->getFullKey(), $applied)
? pht('Applied')
: pht('Not Applied'),
'duration' => $duration,
'type' => $patch->getType(),
'name' => $patch->getName(),
));
}
$table->draw();
}
return 0;
}

View file

@ -73,16 +73,24 @@ final class PhabricatorStorageManagementUpgradeWorkflow
$init_only = $args->getArg('init-only');
$no_adjust = $args->getArg('no-adjust');
$this->upgradeSchemata($apply_only, $no_quickstart, $init_only);
$apis = $this->getMasterAPIs();
if ($no_adjust || $init_only || $apply_only) {
$console->writeOut(
"%s\n",
pht('Declining to apply storage adjustments.'));
return 0;
} else {
return $this->adjustSchemata(false);
foreach ($apis as $api) {
$this->upgradeSchemata($api, $apply_only, $no_quickstart, $init_only);
if ($no_adjust || $init_only || $apply_only) {
$console->writeOut(
"%s\n",
pht('Declining to apply storage adjustments.'));
} else {
$err = $this->adjustSchemata($api, false);
if ($err) {
return $err;
}
}
}
return 0;
}
}

View file

@ -3,20 +3,56 @@
abstract class PhabricatorStorageManagementWorkflow
extends PhabricatorManagementWorkflow {
private $api;
private $apis = array();
private $dryRun;
private $force;
private $patches;
private $didInitialize;
final public function getAPI() {
return $this->api;
final public function setAPIs(array $apis) {
$this->apis = $apis;
return $this;
}
final public function setAPI(PhabricatorStorageManagementAPI $api) {
$this->api = $api;
return $this;
final public function getAnyAPI() {
return head($this->getAPIs());
}
final public function getMasterAPIs() {
$apis = $this->getAPIs();
$results = array();
foreach ($apis as $api) {
if ($api->getRef()->getIsMaster()) {
$results[] = $api;
}
}
if (!$results) {
throw new PhutilArgumentUsageException(
pht(
'This command only operates on database masters, but the selected '.
'database hosts do not include any masters.'));
}
return $results;
}
final public function getSingleAPI() {
$apis = $this->getAPIs();
if (count($apis) == 1) {
return head($apis);
}
throw new PhutilArgumentUsageException(
pht(
'Phabricator is configured in cluster mode, with multiple database '.
'hosts. Use "--host" to specify which host you want to operate on.'));
}
final public function getAPIs() {
return $this->apis;
}
final protected function isDryRun() {
@ -73,22 +109,34 @@ abstract class PhabricatorStorageManagementWorkflow
public function didExecute(PhutilArgumentParser $args) {}
private function loadSchemata() {
$query = id(new PhabricatorConfigSchemaQuery())
->setAPI($this->getAPI());
private function loadSchemata(PhabricatorStorageManagementAPI $api) {
$query = id(new PhabricatorConfigSchemaQuery());
$actual = $query->loadActualSchema();
$expect = $query->loadExpectedSchema();
$comp = $query->buildComparisonSchema($expect, $actual);
$ref = $api->getRef();
$ref_key = $ref->getRefKey();
return array($comp, $expect, $actual);
$query->setAPIs(array($api));
$query->setRefs(array($ref));
$actual = $query->loadActualSchemata();
$expect = $query->loadExpectedSchemata();
$comp = $query->buildComparisonSchemata($expect, $actual);
return array(
$comp[$ref_key],
$expect[$ref_key],
$actual[$ref_key],
);
}
final protected function adjustSchemata($unsafe) {
$lock = $this->lock();
final protected function adjustSchemata(
PhabricatorStorageManagementAPI $api,
$unsafe) {
$lock = $this->lock($api);
try {
$err = $this->doAdjustSchemata($unsafe);
$err = $this->doAdjustSchemata($api, $unsafe);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
@ -99,15 +147,19 @@ abstract class PhabricatorStorageManagementWorkflow
return $err;
}
final private function doAdjustSchemata($unsafe) {
final private function doAdjustSchemata(
PhabricatorStorageManagementAPI $api,
$unsafe) {
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht('Verifying database schemata...'));
pht(
'Verifying database schemata on "%s"...',
$api->getRef()->getRefKey()));
list($adjustments, $errors) = $this->findAdjustments();
$api = $this->getAPI();
list($adjustments, $errors) = $this->findAdjustments($api);
if (!$adjustments) {
$console->writeOut(
@ -415,8 +467,9 @@ abstract class PhabricatorStorageManagementWorkflow
return $this->printErrors($errors, $err);
}
private function findAdjustments() {
list($comp, $expect, $actual) = $this->loadSchemata();
private function findAdjustments(
PhabricatorStorageManagementAPI $api) {
list($comp, $expect, $actual) = $this->loadSchemata($api);
$issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET;
$issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION;
@ -766,14 +819,15 @@ abstract class PhabricatorStorageManagementWorkflow
}
final protected function upgradeSchemata(
PhabricatorStorageManagementAPI $api,
$apply_only = null,
$no_quickstart = false,
$init_only = false) {
$lock = $this->lock();
$lock = $this->lock($api);
try {
$this->doUpgradeSchemata($apply_only, $no_quickstart, $init_only);
$this->doUpgradeSchemata($api, $apply_only, $no_quickstart, $init_only);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
@ -783,13 +837,12 @@ abstract class PhabricatorStorageManagementWorkflow
}
final private function doUpgradeSchemata(
PhabricatorStorageManagementAPI $api,
$apply_only,
$no_quickstart,
$init_only) {
$api = $this->getAPI();
$applied = $this->getApi()->getAppliedPatches();
$applied = $api->getAppliedPatches();
if ($applied === null) {
if ($this->dryRun) {
echo pht(
@ -923,11 +976,13 @@ abstract class PhabricatorStorageManagementWorkflow
if (count($this->patches)) {
throw new Exception(
pht(
'Some patches could not be applied: %s',
'Some patches could not be applied to "%s": %s',
$api->getRef()->getRefKey(),
implode(', ', array_keys($this->patches))));
} else if (!$this->dryRun && !$apply_only) {
echo pht(
"Storage is up to date. Use '%s' for details.",
'Storage is up to date on "%s". Use "%s" for details.',
$api->getRef()->getRefKey(),
'storage status')."\n";
}
break;
@ -955,9 +1010,9 @@ abstract class PhabricatorStorageManagementWorkflow
*
* @return PhabricatorGlobalLock
*/
final protected function lock() {
final protected function lock(PhabricatorStorageManagementAPI $api) {
return PhabricatorGlobalLock::newLock(__CLASS__)
->useSpecificConnection($this->getApi()->getConn(null))
->useSpecificConnection($api->getConn(null))
->lock();
}

Some files were not shown because too many files have changed in this diff Show more