diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 646f4b6d93..b2fee217de 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -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', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 8e7d339902..affa180dc1 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -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', diff --git a/resources/sql/autopatches/20161115.phamepost.01.subtitle.sql b/resources/sql/autopatches/20161115.phamepost.01.subtitle.sql new file mode 100644 index 0000000000..c3047bba82 --- /dev/null +++ b/resources/sql/autopatches/20161115.phamepost.01.subtitle.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_post + ADD subtitle VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20161115.phamepost.02.header.sql b/resources/sql/autopatches/20161115.phamepost.02.header.sql new file mode 100644 index 0000000000..fd62d57058 --- /dev/null +++ b/resources/sql/autopatches/20161115.phamepost.02.header.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_post + ADD headerImagePHID VARBINARY(64); diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php index 2ba1e9920c..db21fc23f5 100755 --- a/scripts/sql/manage_storage.php +++ b/scripts/sql/manage_storage.php @@ -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); } diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3a06c89c2b..fe026df777 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index df168dfe14..ae5808ed78 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -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); - } - } diff --git a/src/applications/auth/data/PhabricatorAuthSessionInfo.php b/src/applications/auth/data/PhabricatorAuthSessionInfo.php new file mode 100644 index 0000000000..76f4dfd10f --- /dev/null +++ b/src/applications/auth/data/PhabricatorAuthSessionInfo.php @@ -0,0 +1,36 @@ +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; + } + +} diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index e7d5c94146..4e66d3c9f4 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -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); + } } } diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngineExtension.php b/src/applications/auth/engine/PhabricatorAuthSessionEngineExtension.php index 267f5be7b4..aa4b75db26 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngineExtension.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngineExtension.php @@ -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; } diff --git a/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php b/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php new file mode 100644 index 0000000000..1302846ec3 --- /dev/null +++ b/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php @@ -0,0 +1,4 @@ +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); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index b5b1c0cc48..91d3a9e336 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -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(); } diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php b/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php index a1264693bd..0bb409c0ba 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php @@ -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 diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php index 5b31afbe1a..a9eab9376a 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php @@ -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) diff --git a/src/applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php b/src/applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php index 4e7b928901..93c92b828d 100644 --- a/src/applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php +++ b/src/applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php @@ -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; } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 0c762ffa54..bf1d9b43e2 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -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; } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 37c4cc94f0..79d4321545 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -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); diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 6d76ad6816..75d573c79e 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -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 )--------------------------------------------------- */ diff --git a/src/applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php b/src/applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php new file mode 100644 index 0000000000..987f9cbf09 --- /dev/null +++ b/src/applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php @@ -0,0 +1,53 @@ +setViewer($viewer) + ->withMemberPHIDs($phids) + ->execute(); + foreach ($projects as $project) { + $values[] = $project->getPHID(); + } + + return $values; + } + +} diff --git a/src/applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php b/src/applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php new file mode 100644 index 0000000000..c799602d8a --- /dev/null +++ b/src/applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php @@ -0,0 +1,30 @@ +getViewer(), + $values); + } + +} diff --git a/src/applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php b/src/applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php new file mode 100644 index 0000000000..6f9a8292f4 --- /dev/null +++ b/src/applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php @@ -0,0 +1,77 @@ + 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); + } + +} diff --git a/src/applications/calendar/view/AphrontCalendarEventView.php b/src/applications/calendar/view/AphrontCalendarEventView.php index 0dc9231e6f..d25196bdd1 100644 --- a/src/applications/calendar/view/AphrontCalendarEventView.php +++ b/src/applications/calendar/view/AphrontCalendarEventView.php @@ -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.')); } diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php index dda50a2b73..6b2704b0b4 100644 --- a/src/applications/config/application/PhabricatorConfigApplication.php +++ b/src/applications/config/application/PhabricatorConfigApplication.php @@ -45,9 +45,10 @@ final class PhabricatorConfigApplication extends PhabricatorApplication { 'group/(?P[^/]+)/' => 'PhabricatorConfigGroupController', 'version/' => 'PhabricatorConfigVersionController', 'database/'. + '(?:(?P[^/]+)/'. '(?:(?P[^/]+)/'. '(?:(?P[^/]+)/'. - '(?:(?:col/(?P[^/]+)|key/(?P[^/]+))/)?)?)?' + '(?:(?:col/(?P[^/]+)|key/(?P[^/]+))/)?)?)?)?' => 'PhabricatorConfigDatabaseStatusController', 'dbissue/' => 'PhabricatorConfigDatabaseIssueController', '(?Pignore|unignore)/(?P[^/]+)/' diff --git a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php index fd318a5fa1..a0c83d6a73 100644 --- a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php +++ b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php @@ -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('phabricator/ $ ./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:
%s
", - phutil_implode_html(phutil_tag('br'), array_keys($diff)))) - ->addCommand( - hsprintf('phabricator/ $ ./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('phabricator/ $ ./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:
%s
", + phutil_implode_html(phutil_tag('br'), array_keys($diff)))) + ->addCommand( + hsprintf('phabricator/ $ ./bin/storage upgrade')); + return true; + } } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseController.php b/src/applications/config/controller/PhabricatorConfigDatabaseController.php index c9f2aa6a3c..53af9a6b92 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseController.php @@ -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: diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index f1a91d4d5b..7674d28f51 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -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', )); diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php index bdeb254437..d49a546387 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php @@ -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); + } + } diff --git a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php index ca9b44cc07..2b5fc79f62 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaQuery.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaQuery.php @@ -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) { diff --git a/src/applications/config/schema/PhabricatorConfigServerSchema.php b/src/applications/config/schema/PhabricatorConfigServerSchema.php index b8b21fe919..f067534b23 100644 --- a/src/applications/config/schema/PhabricatorConfigServerSchema.php +++ b/src/applications/config/schema/PhabricatorConfigServerSchema.php @@ -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])) { diff --git a/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php index 107d22d2a9..dc29ff07a9 100644 --- a/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php @@ -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; } diff --git a/src/applications/differential/customfield/DifferentialCoreCustomField.php b/src/applications/differential/customfield/DifferentialCoreCustomField.php index 87d18553ad..7b6d02276d 100644 --- a/src/applications/differential/customfield/DifferentialCoreCustomField.php +++ b/src/applications/differential/customfield/DifferentialCoreCustomField.php @@ -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; } diff --git a/src/applications/differential/parser/DifferentialCommitMessageParser.php b/src/applications/differential/parser/DifferentialCommitMessageParser.php index fda7ae05b6..ff76b30e51 100644 --- a/src/applications/differential/parser/DifferentialCommitMessageParser.php +++ b/src/applications/differential/parser/DifferentialCommitMessageParser.php @@ -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 )--------------------------------------------- */ diff --git a/src/applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php b/src/applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php index 8db39e164d..8ad8274ea3 100644 --- a/src/applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php +++ b/src/applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php @@ -12,7 +12,7 @@ final class DifferentialExactUserFunctionDatasource } public function getDatasourceApplicationClass() { - return 'PhabricatorDifferentialApplication'; + return 'PhabricatorPeopleApplication'; } public function getComponentDatasources() { diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index 7eff7d7bc8..c320c5cc26 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -46,6 +46,53 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { } public function getRoutes() { + $repository_routes = array( + '/' => array( + '' => 'DiffusionRepositoryController', + 'repository/(?P.*)' => 'DiffusionRepositoryController', + 'change/(?P.*)' => 'DiffusionChangeController', + 'history/(?P.*)' => 'DiffusionHistoryController', + 'browse/(?P.*)' => 'DiffusionBrowseController', + 'lastmodified/(?P.*)' => 'DiffusionLastModifiedController', + 'diff/' => 'DiffusionDiffController', + 'tags/(?P.*)' => 'DiffusionTagListController', + 'branches/(?P.*)' => 'DiffusionBranchTableController', + 'refs/(?P.*)' => 'DiffusionRefTableController', + 'lint/(?P.*)' => 'DiffusionLintController', + 'commit/(?P[a-z0-9]+)/branches/' + => 'DiffusionCommitBranchesController', + 'commit/(?P[a-z0-9]+)/tags/' + => 'DiffusionCommitTagsController', + 'commit/(?P[a-z0-9]+)/edit/' + => 'DiffusionCommitEditController', + 'manage/(?:(?P[^/]+)/)?' + => 'DiffusionRepositoryManagePanelsController', + 'uri/' => array( + 'view/(?P[0-9]\d*)/' => 'DiffusionRepositoryURIViewController', + 'disable/(?P[0-9]\d*)/' + => 'DiffusionRepositoryURIDisableController', + $this->getEditRoutePattern('edit/') + => 'DiffusionRepositoryURIEditController', + 'credential/(?P[0-9]\d*)/(?Pedit|remove)/' + => 'DiffusionRepositoryURICredentialController', + ), + 'edit/' => array( + 'activate/' => 'DiffusionRepositoryEditActivateController', + 'dangerous/' => 'DiffusionRepositoryEditDangerousController', + 'delete/' => 'DiffusionRepositoryEditDeleteController', + 'update/' => 'DiffusionRepositoryEditUpdateController', + 'testautomation/' => 'DiffusionRepositoryTestAutomationController', + ), + 'pathtree/(?P.*)' => '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[A-Z]+)'. @@ -54,6 +101,9 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { ')(?P[a-f0-9]+)' => 'DiffusionCommitController', + '/source/(?P[^/.]+)(?P\.git)?' + => $repository_routes, + '/diffusion/' => array( $this->getQueryRoutePattern() => 'DiffusionRepositoryListController', @@ -63,57 +113,8 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'DiffusionPushLogListController', 'view/(?P\d+)/' => 'DiffusionPushEventViewController', ), - '(?:'. - '(?P[A-Z]+)'. - '|'. - '(?P[1-9]\d*)'. - ')/' => array( - '' => 'DiffusionRepositoryController', - - 'repository/(?P.*)' => 'DiffusionRepositoryController', - 'change/(?P.*)' => 'DiffusionChangeController', - 'history/(?P.*)' => 'DiffusionHistoryController', - 'browse/(?P.*)' => 'DiffusionBrowseController', - 'lastmodified/(?P.*)' => 'DiffusionLastModifiedController', - 'diff/' => 'DiffusionDiffController', - 'tags/(?P.*)' => 'DiffusionTagListController', - 'branches/(?P.*)' => 'DiffusionBranchTableController', - 'refs/(?P.*)' => 'DiffusionRefTableController', - 'lint/(?P.*)' => 'DiffusionLintController', - 'commit/(?P[a-z0-9]+)/branches/' - => 'DiffusionCommitBranchesController', - 'commit/(?P[a-z0-9]+)/tags/' - => 'DiffusionCommitTagsController', - 'commit/(?P[a-z0-9]+)/edit/' - => 'DiffusionCommitEditController', - 'manage/(?:(?P[^/]+)/)?' - => 'DiffusionRepositoryManagePanelsController', - 'uri/' => array( - 'view/(?P[0-9]\d*)/' => 'DiffusionRepositoryURIViewController', - 'disable/(?P[0-9]\d*)/' - => 'DiffusionRepositoryURIDisableController', - $this->getEditRoutePattern('edit/') - => 'DiffusionRepositoryURIEditController', - 'credential/(?P[0-9]\d*)/(?Pedit|remove)/' - => 'DiffusionRepositoryURICredentialController', - ), - 'edit/' => array( - 'activate/' => 'DiffusionRepositoryEditActivateController', - 'dangerous/' => 'DiffusionRepositoryEditDangerousController', - 'delete/' => 'DiffusionRepositoryEditDeleteController', - 'update/' => 'DiffusionRepositoryEditUpdateController', - 'testautomation/' => 'DiffusionRepositoryTestAutomationController', - ), - 'pathtree/(?P.*)' => '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[A-Z]+)|(?P[1-9]\d*))'. - '(?:/.*)?' - => 'DiffusionRepositoryDefaultController', + '(?P[A-Z]+)' => $repository_routes, + '(?P[1-9]\d*)' => $repository_routes, 'inline/' => array( 'edit/(?P[^/]+)/' => 'DiffusionInlineCommentController', diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index a77b000db9..16fde7ac26 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -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; diff --git a/src/applications/diffusion/controller/DiffusionLastModifiedController.php b/src/applications/diffusion/controller/DiffusionLastModifiedController.php index 8a4debfcea..faf9457c15 100644 --- a/src/applications/diffusion/controller/DiffusionLastModifiedController.php +++ b/src/applications/diffusion/controller/DiffusionLastModifiedController.php @@ -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) { diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 69b8c64b17..65feb79ea0 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -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 diff --git a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php index 362a4887ec..aca7367f8c 100644 --- a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php +++ b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php @@ -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() { diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php index 6a2f70d693..45c2ca142a 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -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); diff --git a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php index 9843ca8401..52a8478fdc 100644 --- a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php @@ -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() { diff --git a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php index 3f66110abc..15dd9d7c6b 100644 --- a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php @@ -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() { diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 20a6a251a7..0115858832 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -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']; diff --git a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php index 57a44d4707..3a8b516443 100644 --- a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php @@ -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); } } diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index 4b1cb253bb..5733439fcf 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -76,6 +76,7 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { 'dropupload/' => 'PhabricatorFileDropUploadController', 'compose/' => 'PhabricatorFileComposeController', 'comment/(?P[1-9]\d*)/' => 'PhabricatorFileCommentController', + 'thread/(?P[^/]+)/' => 'PhabricatorFileLightboxController', 'delete/(?P[1-9]\d*)/' => 'PhabricatorFileDeleteController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorFileEditController', 'info/(?P[^/]+)/' => 'PhabricatorFileInfoController', @@ -129,4 +130,10 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { ); } + public function getQuicksandURIPatternBlacklist() { + return array( + '/file/data/.*', + ); + } + } diff --git a/src/applications/files/controller/PhabricatorFileLightboxController.php b/src/applications/files/controller/PhabricatorFileLightboxController.php new file mode 100644 index 0000000000..ca5576c6bd --- /dev/null +++ b/src/applications/files/controller/PhabricatorFileLightboxController.php @@ -0,0 +1,43 @@ +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); + } + +} diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php index 15b8771978..0fb2243d89 100644 --- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php @@ -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) { diff --git a/src/applications/guides/guidance/PhabricatorGuidanceContext.php b/src/applications/guides/guidance/PhabricatorGuidanceContext.php new file mode 100644 index 0000000000..75559fd14f --- /dev/null +++ b/src/applications/guides/guidance/PhabricatorGuidanceContext.php @@ -0,0 +1,4 @@ +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); + } + +} diff --git a/src/applications/guides/guidance/PhabricatorGuidanceEngineExtension.php b/src/applications/guides/guidance/PhabricatorGuidanceEngineExtension.php new file mode 100644 index 0000000000..b552b10b05 --- /dev/null +++ b/src/applications/guides/guidance/PhabricatorGuidanceEngineExtension.php @@ -0,0 +1,39 @@ +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(); + } + +} diff --git a/src/applications/guides/guidance/PhabricatorGuidanceMessage.php b/src/applications/guides/guidance/PhabricatorGuidanceMessage.php new file mode 100644 index 0000000000..d2720c1602 --- /dev/null +++ b/src/applications/guides/guidance/PhabricatorGuidanceMessage.php @@ -0,0 +1,65 @@ +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); + } + + +} diff --git a/src/applications/harbormaster/artifact/HarbormasterArtifact.php b/src/applications/harbormaster/artifact/HarbormasterArtifact.php index 8d3d8dd169..172469fccb 100644 --- a/src/applications/harbormaster/artifact/HarbormasterArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterArtifact.php @@ -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); diff --git a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php index 311492de20..345621f0f5 100644 --- a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php @@ -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.'), diff --git a/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php index 56702bf99e..1a04be934f 100644 --- a/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php +++ b/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php @@ -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)), diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 5d1999c2af..f1157bcb9f 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -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); + } + + } diff --git a/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php b/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php index 387e01cb2e..a9375c1b67 100644 --- a/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php +++ b/src/applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php @@ -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) { diff --git a/src/applications/people/controller/PhabricatorPeopleCreateController.php b/src/applications/people/controller/PhabricatorPeopleCreateController.php index 82a27e2f9e..04b2f4a8a4 100644 --- a/src/applications/people/controller/PhabricatorPeopleCreateController.php +++ b/src/applications/people/controller/PhabricatorPeopleCreateController.php @@ -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) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index f04bc1c5fa..79134de017 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -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; diff --git a/src/applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php b/src/applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php new file mode 100644 index 0000000000..9740f5b4df --- /dev/null +++ b/src/applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php @@ -0,0 +1,4 @@ + 'PhabricatorMarkupPreviewController', 'move/(?P\d+)/' => 'PhamePostMoveController', 'archive/(?P\d+)/' => 'PhamePostArchiveController', + 'header/(?P[1-9]\d*)/' => 'PhamePostHeaderPictureController', ), 'blog/' => array( '(?:query/(?P[^/]+)/)?' => 'PhameBlogListController', diff --git a/src/applications/phame/controller/PhameLiveController.php b/src/applications/phame/controller/PhameLiveController.php index 2d8b2ee45f..b5b1984816 100644 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -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) { diff --git a/src/applications/phame/controller/post/PhamePostHeaderPictureController.php b/src/applications/phame/controller/post/PhamePostHeaderPictureController.php new file mode 100644 index 0000000000..2e60c9ba71 --- /dev/null +++ b/src/applications/phame/controller/post/PhamePostHeaderPictureController.php @@ -0,0 +1,136 @@ +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, + )); + + } +} diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index a056502349..a73876a197 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -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)); + + } + } diff --git a/src/applications/phame/editor/PhamePostEditEngine.php b/src/applications/phame/editor/PhamePostEditEngine.php index 382ef3f8ff..e80c1f6d0d 100644 --- a/src/applications/phame/editor/PhamePostEditEngine.php +++ b/src/applications/phame/editor/PhamePostEditEngine.php @@ -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')) diff --git a/src/applications/phame/editor/PhamePostEditor.php b/src/applications/phame/editor/PhamePostEditor.php index 363f39fb46..929613fe80 100644 --- a/src/applications/phame/editor/PhamePostEditor.php +++ b/src/applications/phame/editor/PhamePostEditor.php @@ -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; } diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php index 1a29eda869..357418cfb3 100644 --- a/src/applications/phame/query/PhamePostQuery.php +++ b/src/applications/phame/query/PhamePostQuery.php @@ -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; } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 351bcb5dc0..fb959e61f7 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -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 )-------------------------- */ diff --git a/src/applications/phame/storage/PhamePostTransaction.php b/src/applications/phame/storage/PhamePostTransaction.php index a1efb584b0..6e54aeda6d 100644 --- a/src/applications/phame/storage/PhamePostTransaction.php +++ b/src/applications/phame/storage/PhamePostTransaction.php @@ -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( diff --git a/src/applications/phid/handle/pool/PhabricatorHandleList.php b/src/applications/phid/handle/pool/PhabricatorHandleList.php index 7a8fed529f..d9a279f8ba 100644 --- a/src/applications/phid/handle/pool/PhabricatorHandleList.php +++ b/src/applications/phid/handle/pool/PhabricatorHandleList.php @@ -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 )---------------------------------------------------------- */ diff --git a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php index 9fe55bcbeb..2b6290851b 100644 --- a/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php +++ b/src/applications/phurl/editor/PhabricatorPhurlURLEditor.php @@ -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; } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 107d3dc0d0..93ff739a90 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -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( diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 57d531c951..8da16f923b 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -119,7 +119,8 @@ abstract class PhabricatorProjectController extends PhabricatorController { foreach ($ancestors as $ancestor) { $crumbs->addTextCrumb( $ancestor->getName(), - $ancestor->getURI()); + $ancestor->getProfileURI() + ); } } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 2172156e38..6e4892b32a 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -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)); diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 03a6e39c33..90294f29fb 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -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); } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index d38eda1860..8c32109f37 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -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/?diffusion/(?P[A-Z]+|[0-9]\d*))'. - '(?P(?:/.*)?)'. + '(?P/?(?:diffusion|source)/(?P[^/.]+))'. + $maybe_git. + '(?P(?:/|.*)?)'. '\z)', ); @@ -624,28 +637,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO public function getCanonicalPath($request_path) { $standard_pattern = '(^'. - '(?P/diffusion/)'. + '(?P/(?:diffusion|source)/)'. '(?P[^/]+)'. '(?P(?:/.*)?)'. '\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, diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php index 2f2815da1c..1cb3182c16 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryURI.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -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, ); diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 6e27f8540b..5eaa5288de 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -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; diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index 5c641b7c1f..2f55c26384 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -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( diff --git a/src/docs/user/userguide/arcanist_lint.diviner b/src/docs/user/userguide/arcanist_lint.diviner index 54af96f432..d7fcce14e8 100644 --- a/src/docs/user/userguide/arcanist_lint.diviner +++ b/src/docs/user/userguide/arcanist_lint.diviner @@ -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` diff --git a/src/docs/user/userguide/calendar.diviner b/src/docs/user/userguide/calendar.diviner index dd0a31fe5d..5c09a753b7 100644 --- a/src/docs/user/userguide/calendar.diviner +++ b/src/docs/user/userguide/calendar.diviner @@ -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 ================ diff --git a/src/docs/user/userguide/diffusion_hosting.diviner b/src/docs/user/userguide/diffusion_hosting.diviner index f576e4fe7a..1a9e7d5bdb 100644 --- a/src/docs/user/userguide/diffusion_hosting.diviner +++ b/src/docs/user/userguide/diffusion_hosting.diviner @@ -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: diff --git a/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php b/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php index 85df807f3e..003ddb6beb 100644 --- a/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php +++ b/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php @@ -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))); - } } } diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index c81de56823..95d1cc95f4 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -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) { diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index aae75ac43d..bb8f719348 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -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; + } } } diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index d89b5c8f71..291097a6c0 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -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.', + ), + ), + ); } diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 565df6cb44..6f13525c76 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -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. diff --git a/src/infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php b/src/infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php new file mode 100644 index 0000000000..fa22876452 --- /dev/null +++ b/src/infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php @@ -0,0 +1,43 @@ +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)); + } + +} diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index d3a287259a..8bcd00c4f2 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -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); diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php index 87cb079c7e..15b46300f1 100644 --- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php +++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php @@ -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; } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php index 465f8728cc..cd1939d92d 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php @@ -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) { diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php index e29dd60e33..ef6735ec8f 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php @@ -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); diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php index 9bf5906148..b9cf82b2c3 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php @@ -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')) { diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index 6a481d4702..7195e735c6 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -44,7 +44,7 @@ final class PhabricatorStorageManagementDumpWorkflow } public function didExecute(PhutilArgumentParser $args) { - $api = $this->getAPI(); + $api = $this->getSingleAPI(); $patches = $this->getPatches(); $console = PhutilConsole::getConsole(); diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php index c45146e6d5..a75afe69e8 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php @@ -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); diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php index 2d67d97e48..e181063ac4 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php @@ -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; } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php index 51d3dd1482..0540bb3ebc 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php @@ -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 { diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php index 84e2ed5489..0bf185a086 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php @@ -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 diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php index f9e91427d4..88174d22ad 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php @@ -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; } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php index 70dc90fdf7..ce019f6ccb 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php @@ -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; } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index 7bfb4d873d..2404918b56 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -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(); } diff --git a/src/view/form/PHUIInfoView.php b/src/view/form/PHUIInfoView.php index 92481bc614..ca01ff294b 100644 --- a/src/view/form/PHUIInfoView.php +++ b/src/view/form/PHUIInfoView.php @@ -1,6 +1,6 @@ title = $title; @@ -26,6 +27,11 @@ final class PHUIInfoView extends AphrontView { return $this; } + private function getSeverity() { + $severity = $this->severity ? $this->severity : self::SEVERITY_ERROR; + return $severity; + } + public function setErrors(array $errors) { $this->errors = $errors; return $this; @@ -46,12 +52,66 @@ final class PHUIInfoView extends AphrontView { return $this; } + public function setIcon(PHUIIconView $icon) { + $this->icon = $icon; + return $this; + } + + private function getIcon() { + if ($this->icon) { + return $this->icon; + } + + switch ($this->getSeverity()) { + case self::SEVERITY_ERROR: + $icon = 'fa-exclamation-circle'; + break; + case self::SEVERITY_WARNING: + $icon = 'fa-exclamation-triangle'; + break; + case self::SEVERITY_NOTICE: + $icon = 'fa-info-circle'; + break; + case self::SEVERITY_NODATA: + return null; + break; + case self::SEVERITY_SUCCESS: + $icon = 'fa-check-circle'; + break; + } + + $icon = id(new PHUIIconView()) + ->setIcon($icon) + ->addClass('phui-info-icon'); + return $icon; + } + public function addButton(PHUIButtonView $button) { + $button->setColor(PHUIButtonView::GREY); $this->buttons[] = $button; return $this; } - public function render() { + protected function getTagAttributes() { + $classes = array(); + $classes[] = 'phui-info-view'; + $classes[] = 'phui-info-severity-'.$this->getSeverity(); + $classes[] = 'grouped'; + if ($this->flush) { + $classes[] = 'phui-info-view-flush'; + } + if ($this->getIcon()) { + $classes[] = 'phui-info-has-icon'; + } + + return array( + 'id' => $this->id, + 'class' => implode(' ', $classes), + 'style' => $this->isHidden ? 'display: none;' : null, + ); + } + + protected function getTagContent() { require_celerity_resource('phui-info-view-css'); $errors = $this->errors; @@ -70,7 +130,7 @@ final class PHUIInfoView extends AphrontView { ), $list); } else if (count($errors) == 1) { - $list = $this->errors[0]; + $list = head($this->errors); } else { $list = null; } @@ -87,17 +147,6 @@ final class PHUIInfoView extends AphrontView { $title = null; } - $this->severity = nonempty($this->severity, self::SEVERITY_ERROR); - - $classes = array(); - $classes[] = 'phui-info-view'; - $classes[] = 'phui-info-severity-'.$this->severity; - $classes[] = 'grouped'; - if ($this->flush) { - $classes[] = 'phui-info-view-flush'; - } - $classes = implode(' ', $classes); - $children = $this->renderChildren(); if ($list) { $children[] = $list; @@ -123,17 +172,21 @@ final class PHUIInfoView extends AphrontView { $this->buttons); } - return phutil_tag( - 'div', - array( - 'id' => $this->id, - 'class' => $classes, - 'style' => $this->isHidden ? 'display: none;' : null, - ), - array( - $buttons, - $title, - $body, - )); + $icon = null; + if ($this->getIcon()) { + $icon = phutil_tag( + 'div', + array( + 'class' => 'phui-info-view-icon', + ), + $this->getIcon()); + } + + return array( + $icon, + $buttons, + $title, + $body, + ); } } diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index a2fb7fc7a8..86bb416e90 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -29,7 +29,7 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { // We need to have this if previews render images, since Ajax can not // currently ship JS or CSS. - require_celerity_resource('lightbox-attachment-css'); + require_celerity_resource('phui-lightbox-css'); if (!$this->getDisabled()) { Javelin::initBehavior( diff --git a/src/view/layout/PhabricatorFileLinkView.php b/src/view/layout/PhabricatorFileLinkView.php index 8551b4c567..ab993b9b6d 100644 --- a/src/view/layout/PhabricatorFileLinkView.php +++ b/src/view/layout/PhabricatorFileLinkView.php @@ -7,12 +7,14 @@ final class PhabricatorFileLinkView extends AphrontView { private $fileViewURI; private $fileViewable; private $filePHID; + private $fileMonogram; private $customClass; public function setCustomClass($custom_class) { $this->customClass = $custom_class; return $this; } + public function getCustomClass() { return $this->customClass; } @@ -21,14 +23,25 @@ final class PhabricatorFileLinkView extends AphrontView { $this->filePHID = $file_phid; return $this; } + private function getFilePHID() { return $this->filePHID; } + public function setFileMonogram($monogram) { + $this->fileMonogram = $monogram; + return $this; + } + + private function getFileMonogram() { + return $this->fileMonogram; + } + public function setFileViewable($file_viewable) { $this->fileViewable = $file_viewable; return $this; } + private function getFileViewable() { return $this->fileViewable; } @@ -37,6 +50,7 @@ final class PhabricatorFileLinkView extends AphrontView { $this->fileViewURI = $file_view_uri; return $this; } + private function getFileViewURI() { return $this->fileViewURI; } @@ -45,6 +59,7 @@ final class PhabricatorFileLinkView extends AphrontView { $this->fileDownloadURI = $file_download_uri; return $this; } + private function getFileDownloadURI() { return $this->fileDownloadURI; } @@ -53,6 +68,7 @@ final class PhabricatorFileLinkView extends AphrontView { $this->fileName = $file_name; return $this; } + private function getFileName() { return $this->fileName; } @@ -64,12 +80,13 @@ final class PhabricatorFileLinkView extends AphrontView { 'uri' => $this->getFileViewURI(), 'dUri' => $this->getFileDownloadURI(), 'name' => $this->getFileName(), + 'monogram' => $this->getFileMonogram(), ); } public function render() { require_celerity_resource('phabricator-remarkup-css'); - require_celerity_resource('lightbox-attachment-css'); + require_celerity_resource('phui-lightbox-css'); $sigil = null; $meta = null; diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 8fa73ac9f3..93d02b8b73 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -271,6 +271,9 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView $default_img_uri = celerity_get_resource_uri( 'rsrc/image/icon/fatcow/document_black.png'); + $icon = id(new PHUIIconView()) + ->setIcon('fa-download'); + $lightbox_id = celerity_generate_unique_node_id(); $download_form = phabricator_form( $user, array( @@ -281,12 +284,18 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView ), phutil_tag( 'button', - array(), - pht('Download'))); + array( + 'class' => 'button grey has-icon', + ), + array( + $icon, + pht('Download'), + ))); Javelin::initBehavior( 'lightbox-attachments', array( + 'lightbox_id' => $lightbox_id, 'defaultImageUri' => $default_img_uri, 'downloadForm' => $download_form, )); diff --git a/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php b/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php index 09f8c0934b..c246378e1b 100644 --- a/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php +++ b/src/view/phui/__tests__/PHUIInvisibleCharacterTestCase.php @@ -9,7 +9,7 @@ final class PHUIInvisibleCharacterTestCase extends PhabricatorTestCase { } public function testEmptyPlainText() { - $view = (new PHUIInvisibleCharacterView('')) + $view = id(new PHUIInvisibleCharacterView('')) ->setPlainText(true); $res = $view->render(); $this->assertEqual($res, ''); @@ -17,7 +17,7 @@ final class PHUIInvisibleCharacterTestCase extends PhabricatorTestCase { public function testWithNamedChars() { $test_input = "\x00\n\t "; - $view = (new PHUIInvisibleCharacterView($test_input)) + $view = id(new PHUIInvisibleCharacterView($test_input)) ->setPlainText(true); $res = $view->render(); $this->assertEqual($res, ''); @@ -25,7 +25,7 @@ final class PHUIInvisibleCharacterTestCase extends PhabricatorTestCase { public function testWithHexChars() { $test_input = "abc\x01"; - $view = (new PHUIInvisibleCharacterView($test_input)) + $view = id(new PHUIInvisibleCharacterView($test_input)) ->setPlainText(true); $res = $view->render(); $this->assertEqual($res, 'abc<0x01>'); @@ -33,7 +33,7 @@ final class PHUIInvisibleCharacterTestCase extends PhabricatorTestCase { public function testWithNamedAsHex() { $test_input = "\x00\x0a\x09\x20"; - $view = (new PHUIInvisibleCharacterView($test_input)) + $view = id(new PHUIInvisibleCharacterView($test_input)) ->setPlainText(true); $res = $view->render(); $this->assertEqual($res, ''); diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php index be167fc023..bf4726796c 100644 --- a/src/view/phui/calendar/PHUICalendarListView.php +++ b/src/view/phui/calendar/PHUICalendarListView.php @@ -54,6 +54,8 @@ final class PHUICalendarListView extends AphrontTagView { return ''; } + Javelin::initBehavior('phabricator-tooltips'); + $singletons = array(); $allday = false; foreach ($this->events as $event) { @@ -97,7 +99,7 @@ final class PHUICalendarListView extends AphrontTagView { $event_classes[] = 'event-cancelled'; } - $tip = $this->getEventTooltip($event); + $tip = $event->getDateTimeSummary(); if ($this->getView() == 'day') { $tip_align = 'E'; } else if ($this->getView() == 'month') { @@ -183,52 +185,6 @@ final class PHUICalendarListView extends AphrontTagView { $event->getName()); } - private function getEventTooltip(AphrontCalendarEventView $event) { - $viewer = $this->getViewer(); - $time_key = PhabricatorTimeFormatSetting::SETTINGKEY; - $time_pref = $viewer->getUserSetting($time_key); - - Javelin::initBehavior('phabricator-tooltips'); - - $start = id(AphrontFormDateControlValue::newFromEpoch( - $viewer, - $event->getEpochStart())); - - $end = id(AphrontFormDateControlValue::newFromEpoch( - $viewer, - $event->getEpochEnd())); - - $end_date = $end->getDateTime(); - $end_date = $end_date->modify('-1 second'); - - $start_date = $start->getDateTime()->format('m d Y'); - $end_date = $end_date->format('m d Y'); - - if ($event->getIsAllDay()) { - if ($start_date == $end_date) { - $tip = pht('All day'); - } else { - $tip = pht( - 'All day, %s - %s', - $start->getValueAsFormat('M j, Y'), - $end->getValueAsFormat('M j, Y')); - } - } else { - if ($start->getValueDate() == $end->getValueDate()) { - $tip = pht( - '%s - %s', - $start->getValueAsFormat($time_pref), - $end->getValueAsFormat($time_pref)); - } else { - $tip = pht( - '%s - %s', - $start->getValueAsFormat('M j, Y, '.$time_pref), - $end->getValueAsFormat('M j, Y, '.$time_pref)); - } - } - return $tip; - } - public function getIsViewerInvitedOnList() { foreach ($this->events as $event) { if ($event->getViewerIsInvited()) { diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 7e3f911af9..9cf56394df 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -108,6 +108,20 @@ opacity: 0.95; } +.jx-white-mask { + background: #fff; + opacity: 1; +} + +.jx-date-mask { + background: #292f33; + opacity: 0.5; +} + +.device-desktop .jx-date-mask { + display: none; +} + .aphront-exception-dialog { width: 95%; } diff --git a/webroot/rsrc/css/aphront/lightbox-attachment.css b/webroot/rsrc/css/aphront/lightbox-attachment.css deleted file mode 100644 index a11512ce4e..0000000000 --- a/webroot/rsrc/css/aphront/lightbox-attachment.css +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @provides lightbox-attachment-css - */ - - -.lightbox-attached { - overflow: hidden; -} - -.lightbox-attachment { - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - overflow-y: auto; -} - -.lightbox-attachment img { - margin: 3% auto 0; - max-height: 90%; - max-width: 90%; -} - -.lightbox-attachment .loading { - position: absolute; - top: -9999px; -} - -.lightbox-attachment .attachment-name { - width: 100%; - color: #F2F2F2; - line-height: 30px; - text-align: center; -} - -.lightbox-attachment .lightbox-status { - background: #010101; - color: #F2F2F2; - line-height: 30px; - position: fixed; - bottom: 0px; - width: 100%; -} - -.lightbox-attachment .lightbox-status .lightbox-status-txt { - padding: 0px 0px 0px 20px; -} - -.lightbox-attachment .lightbox-status .lightbox-download { - padding: 0px 20px 0px 0px; - float: right; -} - -.lightbox-attachment .lightbox-status .lightbox-download -.lightbox-download-form { - display: inline; -} -.lightbox-attachment .lightbox-status .lightbox-download -.lightbox-download-form button { - border: 0; - background: #010101; -} -.lightbox-attachment .lightbox-status .lightbox-download -.lightbox-download-form button:hover { - background: #333; -} - -.lightbox-attachment .lightbox-close { - top: 22px; - right: 20px; - position: fixed; - display: block; - height: 26px; - width: 26px; - background: url('/rsrc/image/icon/lightbox/close-2.png'); -} -.lightbox-attachment .lightbox-close:hover { - background: url('/rsrc/image/icon/lightbox/close-hover-2.png'); -} - -.lightbox-attachment .lightbox-left { - top: 46%; - left: 20px; - position: fixed; - display: block; - height: 38px; - width: 21px; - background: url('/rsrc/image/icon/lightbox/left-arrow-2.png'); -} -.lightbox-attachment .lightbox-left:hover { - background: url('/rsrc/image/icon/lightbox/left-arrow-hover-2.png'); -} - -.lightbox-attachment .lightbox-right { - top: 46%; - right: 20px; - position: fixed; - display: block; - height: 38px; - width: 21px; - background: url('/rsrc/image/icon/lightbox/right-arrow-2.png'); -} -.lightbox-attachment .lightbox-right:hover { - background: url('/rsrc/image/icon/lightbox/right-arrow-hover-2.png'); -} diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index 75f1326af7..6a9a5b5717 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -309,6 +309,15 @@ padding-top: 8px; } -.phame-comment-view .aphront-form-control.aphront-form-control-select { +.phame-comment-view .phui-comment-form-view .phui-comment-action-bar { display: none; } + +.phame-comment-view .phui-comment-form-view .phui-comment-has-actions + .phui-comment-textarea-control { + padding-top: 16px; +} + +.phame-comment-view .phui-document-view-pro-box .phui-object-box { + margin-bottom: 16px; +} diff --git a/webroot/rsrc/css/phui/phui-comment-panel.css b/webroot/rsrc/css/phui/phui-comment-panel.css new file mode 100644 index 0000000000..7eacd59fd2 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-comment-panel.css @@ -0,0 +1,60 @@ +/** + * @provides phui-comment-panel-css + * @requires phui-timeline-view-css + */ + +.phui-comment-panel .phui-timeline-view { + background: none; +} + +.phui-comment-panel .phui-comment-panel-empty { + margin: 16px; + padding: 12px; + border: 1px solid {$thinblueborder}; + background: {$lightbluebackground}; + text-align: center; + color: {$lightbluetext}; +} + +.phui-comment-panel .phui-timeline-view .phui-timeline-event-view { + margin: 0; +} + +.phui-comment-panel .phui-timeline-view .phui-timeline-image { + display: none; +} + +.phui-comment-panel .phui-timeline-view .phui-timeline-wedge { + display: none; +} + +.phui-comment-panel .phui-timeline-major-event .phui-timeline-group { + border: none; +} + +.phui-comment-panel .phui-timeline-major-event .phui-timeline-title { + background: none; + border: none; + padding: 0; + visibility: hidden; +} + +.phui-comment-panel .phui-timeline-major-event .phui-timeline-title a { + visibility: visible; +} + +.phui-comment-panel .phui-timeline-icon-fill, +.phui-comment-panel .phui-timeline-menu { + display: none; +} + +.phui-comment-panel .phui-timeline-major-event .phui-timeline-content { + border: none; +} + +.phui-comment-panel .phui-timeline-major-event .phui-timeline-content + .phui-timeline-core-content { + border: none; + padding: 4px 0; + background: transparent; +} diff --git a/webroot/rsrc/css/phui/phui-document-pro.css b/webroot/rsrc/css/phui/phui-document-pro.css index 4d0dfbdefd..6cf0df1c68 100644 --- a/webroot/rsrc/css/phui/phui-document-pro.css +++ b/webroot/rsrc/css/phui/phui-document-pro.css @@ -167,7 +167,7 @@ a.button.phui-document-toc { } .phui-document-view-pro .phui-info-view { - margin: 16px 0 0 0; + margin: 16px 0; } .phui-document-view-pro .phabricator-remarkup-embed-image-wide { @@ -198,10 +198,6 @@ a.button.phui-document-toc { border-top: 1px solid rgba({$alphablue}, 0.20); } -.phui-document-view-pro-box .phui-timeline-image { - border-radius: 25px; -} - .phui-document-view-pro-box .phui-timeline-wedge { display: none; } diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index 11852a87f6..37cf8dfeed 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -296,9 +296,9 @@ table.aphront-form-control-checkbox-layout th { padding-bottom: 12px; } -.aphront-textarea-drag-and-drop { - background: {$lightgreen}; - border-color: {$green}; +body .phui-form-view .remarkup-assist-textarea.aphront-textarea-drag-and-drop { + background: {$sh-greenbackground}; + border: 1px solid {$sh-greenborder}; } .aphront-form-crop .crop-box { @@ -360,7 +360,13 @@ table.aphront-form-control-checkbox-layout th { width: 240px; } +.device .fancy-datepicker { + width: 100%; +} + .fancy-datepicker-core { + width: 240px; + margin: 0 auto; padding: 1px; font-size: {$smallerfontsize}; text-align: center; diff --git a/webroot/rsrc/css/phui/phui-info-view.css b/webroot/rsrc/css/phui/phui-info-view.css index d3b12dd4fe..1642822b77 100644 --- a/webroot/rsrc/css/phui/phui-info-view.css +++ b/webroot/rsrc/css/phui/phui-info-view.css @@ -5,6 +5,7 @@ .phui-info-view { border-style: solid; border-width: 1px; + background: #fff; margin: 16px; padding: 12px; border-radius: 3px; @@ -22,10 +23,19 @@ padding: 0; } +.phui-info-view-icon { + width: 24px; + float: left; +} + .phui-info-view-body { line-height: 1.6em; } +.phui-info-view.phui-info-has-icon .phui-info-view-body { + margin-left: 24px; +} + .phui-info-view-body tt { padding: 0 2px; background-color: rgba({$alphagrey},.1); @@ -48,68 +58,58 @@ h1.phui-info-view-head { font-weight: bold; font-size: {$biggerfontsize}; - line-height: 1.2em; + line-height: 1.3em; } .phui-info-view-list { - margin: 0 0 0 16px; - list-style: disc; - line-height: 1.5em; + margin: 0; + list-style: none; + line-height: 1.6em; } -.phui-info-view .phui-info-view-actions .button:hover { - background: #fff; - box-shadow: none; +.phui-info-view .phui-info-icon { + padding-top: 1px; + font-size: 16px; } -.phui-info-severity-error, -.phui-info-severity-error a.button { - border-color: {$sh-redborder}; - background: {$sh-redbackground}; +.phui-info-severity-error { + border-color: {$red}; + border-left-width: 6px; } -.phui-info-severity-error a.button { - color: {$sh-redtext}; +.phui-info-severity-error .phui-info-icon { + color: {$red}; } -.phui-info-severity-warning, -.phui-info-severity-warning a.button { - border-color: {$sh-yellowborder}; - background: {$sh-yellowbackground}; +.phui-info-severity-warning { + border-color: {$yellow}; + border-left-width: 6px; } -.phui-info-severity-warning a.button { - color: {$sh-yellowtext}; +.phui-info-severity-warning .phui-info-icon { + color: {$yellow}; } -.phui-info-severity-notice, -.phui-info-severity-notice a.button { - border-color: {$sh-blueborder}; - background: {$sh-bluebackground}; +.phui-info-severity-notice { + border-color: {$blue}; + border-left-width: 6px; } -.phui-info-severity-notice a.button { - color: {$sh-bluetext}; +.phui-info-severity-notice .phui-info-icon { + color: {$blue}; } -.phui-info-severity-nodata, -.phui-info-severity-nodata a.button { +.phui-info-severity-nodata { border-color: {$lightgreyborder}; - background: #fff; } -.phui-info-severity-nodata a.button { - color: {$greytext}; +.phui-info-severity-success { + border-color: {$green}; + border-left-width: 6px; } -.phui-info-severity-success, -.phui-info-severity-success a.button { - border-color: {$sh-greenborder}; - background: {$sh-greenbackground}; -} - -.phui-info-severity-success a.button { - color: {$sh-greentext}; +.phui-info-severity-success .phui-info-icon { + color: {$green}; } .aphront-dialog-body .phui-info-view { @@ -123,3 +123,7 @@ h1.phui-info-view-head { .phui-crumbs-view.phui-crumbs-border + .phui-info-view { margin-top: 16px; } + +div.phui-object-box .phui-header-shell + .phui-info-view { + margin: 16px 0 8px; +} diff --git a/webroot/rsrc/css/phui/phui-lightbox.css b/webroot/rsrc/css/phui/phui-lightbox.css new file mode 100644 index 0000000000..e70cb4e9b6 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-lightbox.css @@ -0,0 +1,155 @@ +/** + * @provides phui-lightbox-css + */ + + +.lightbox-attached { + overflow: hidden; +} + +.lightbox-attachment { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow-y: auto; +} + +.lightbox-attachment .lightbox-image-frame { + position: absolute; + top: 44px; + right: 0; + bottom: 0; + left: 0; +} + +.lightbox-attachment.comment-panel-open .lightbox-image-frame { + right: 320px; +} + +.lightbox-attachment .lightbox-image-frame img { + max-width: calc(100% - 40px); + max-height: calc(100% - 24px); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; +} + +.lightbox-comment-frame { + position: absolute; + top: -19999px; + bottom: -19999px; + opacity: 0; + transition: all 0.2s; +} + +.comment-panel-open .lightbox-comment-frame { + position: fixed; + top: 44px; + bottom: 0; + right: 0; + width: 320px; + overflow-y: auto; + background: #fff; + opacity: 1; +} + +.jx-mask + .lightbox-attachment { + background: {$lightgreybackground}; +} + +.lightbox-attachment .attachment-name { + width: 100%; + line-height: 30px; + text-align: center; +} + +.lightbox-attachment .lightbox-status { + background: #fff; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 44px; + padding: 0 20px; + line-height: 44px; + border-bottom: 1px solid {$thinblueborder}; + color: {$greytext}; +} + +.lightbox-attachment .lightbox-status .lightbox-download { + float: right; +} + +.lightbox-attachment .lightbox-status a { + color: #000; + margin-right: 4px; + font-size: {$biggerfontsize}; +} + +.lightbox-download button.has-icon { + padding-left: 28px; +} + +.lightbox-attachment .lightbox-status .lightbox-download + .lightbox-download-form { + display: inline; +} + +.lightbox-attachment .lightbox-comment { + float: right; + margin: 9px 0 0 8px; + padding-left: 28px; +} + +.lightbox-attachment.comment-panel-open .lightbox-comment, +.lightbox-attachment.comment-panel-open .lightbox-comment .phui-icon-view { + color: {$sky}; +} + +.lightbox-attachment .lightbox-close { + float: right; + margin: 9px 0 0 8px; +} + +.lightbox-attachment .lightbox-left { + top: 46%; + left: 12px; + position: fixed; + display: block; + height: 40px; + width: 40px; +} + +.lightbox-attachment .lightbox-left .phui-icon-view { + font-size: 40px; +} + +.lightbox-attachment .lightbox-left:hover .phui-icon-view { + color: {$sky}; +} + +.lightbox-attachment .lightbox-right .phui-icon-view { + top: 46%; + right: 12px; + position: fixed; + display: block; + height: 38px; + width: 21px; +} + +.lightbox-attachment.comment-panel-open .lightbox-right .phui-icon-view { + right: 322px; +} + +.lightbox-attachment .lightbox-right .phui-icon-view { + font-size: 40px; +} + +.lightbox-attachment .lightbox-right:hover .phui-icon-view { + color: {$sky}; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard-color.css b/webroot/rsrc/css/phui/workboards/phui-workboard-color.css index d983622a99..c8b514e23a 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard-color.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard-color.css @@ -2,10 +2,6 @@ * @provides phui-workboard-color-css */ -.phui-workboard-color .phabricator-nav-content .phui-workboard-view-shadow { - background-color: transparent; -} - .phui-workboard-color .phui-crumbs-view { background-color: rgba({$alphagrey},.15); border: none; @@ -30,18 +26,6 @@ background-color: rgba({$alphawhite},.6); } -body.phui-workboard-color .phui-profile-menu .phabricator-side-menu { - background-color: rgba({$alphagrey},.3); -} - -body.phui-workboard-color .phabricator-side-menu .phui-profile-menu-footer-1 { - background-color: transparent; -} - -.phui-workboard-color .phui-profile-menu .phabricator-side-menu { - box-shadow: none; -} - .phui-workboard-color-preview { width: 50px; height: 50px; diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index df51e7623d..a14e5e7ae6 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -19,7 +19,6 @@ left: 0; right: 0; padding: 16px; - background-color: #fff; } .phui-workboard-view-shadow::-webkit-scrollbar { @@ -32,15 +31,6 @@ background: {$lightbluetext}; } -.device-desktop .project-board-wrapper .phui-workboard-view-shadow { - left: {$menu.profile.width}; -} - -.device-desktop .phui-profile-menu-collapsed .project-board-wrapper - .phui-workboard-view-shadow { - left: {$menu.profile.width.collapsed}; -} - !print .project-board-wrapper .phui-workboard-view-shadow { position: static; } @@ -75,21 +65,15 @@ display: none; } -.device-desktop .phui-workboard-fullscreen .phui-profile-menu - .phui-workboard-view-shadow { - top: 35px; - left: 0; +.device-desktop .phui-workboard-fullscreen .phui-workboard-view-shadow { + top: 35px; + left: 0; } .device-desktop .phui-workboard-fullscreen .phui-workpanel-body-content { max-height: calc(100vh - 120px); } -.device-desktop .phui-workboard-fullscreen .phui-profile-menu - .phabricator-nav-local { - display: none; -} - .device .phui-workboard-expand-icon { display: none; } diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js index 8433e798d6..45e5b0b322 100644 --- a/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js +++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js @@ -18,6 +18,10 @@ JX.install('TypeaheadNormalizer', { // NOTE: We specifically normalize "(" and ")" into spaces so that // we can match tokenizer functions like "members(project)". + // NOTE: We specifically do NOT normalize "\n" because it is used as + // a delimiter between components of typeahead result names, like the + // name of a project and its tags. + return ('' + str) .toLocaleLowerCase() .replace(/[\.,\/#!$%\^&\*;:{}=_`~]/g, '') diff --git a/webroot/rsrc/image/icon/lightbox/close-2.png b/webroot/rsrc/image/icon/lightbox/close-2.png deleted file mode 100644 index de2a3b8608..0000000000 Binary files a/webroot/rsrc/image/icon/lightbox/close-2.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/lightbox/close-hover-2.png b/webroot/rsrc/image/icon/lightbox/close-hover-2.png deleted file mode 100644 index f4ce42479c..0000000000 Binary files a/webroot/rsrc/image/icon/lightbox/close-hover-2.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/lightbox/left-arrow-2.png b/webroot/rsrc/image/icon/lightbox/left-arrow-2.png deleted file mode 100644 index 6499c826b0..0000000000 Binary files a/webroot/rsrc/image/icon/lightbox/left-arrow-2.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/lightbox/left-arrow-hover-2.png b/webroot/rsrc/image/icon/lightbox/left-arrow-hover-2.png deleted file mode 100644 index 73bb7927a6..0000000000 Binary files a/webroot/rsrc/image/icon/lightbox/left-arrow-hover-2.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/lightbox/right-arrow-2.png b/webroot/rsrc/image/icon/lightbox/right-arrow-2.png deleted file mode 100644 index bfe7d21027..0000000000 Binary files a/webroot/rsrc/image/icon/lightbox/right-arrow-2.png and /dev/null differ diff --git a/webroot/rsrc/image/icon/lightbox/right-arrow-hover-2.png b/webroot/rsrc/image/icon/lightbox/right-arrow-hover-2.png deleted file mode 100644 index 1bd868f92d..0000000000 Binary files a/webroot/rsrc/image/icon/lightbox/right-arrow-hover-2.png and /dev/null differ diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 99dd6e8fba..a545ed7272 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -297,8 +297,13 @@ JX.install('DraggableList', { if (group[ii].getHasInfiniteHeight()) { // The math doesn't work out quite right if we actually use - // Math.Infinity, so approximate infinity as the document height. - infinity = infinity || JX.Vector.getDocument().y; + // Math.Infinity, so approximate infinity as the larger of the + // document height or viewport height. + if (!infinity) { + infinity = Math.max( + JX.Vector.getViewport().y, + JX.Vector.getDocument().y); + } rp.y = 0; rd.y = infinity; diff --git a/webroot/rsrc/js/core/behavior-fancy-datepicker.js b/webroot/rsrc/js/core/behavior-fancy-datepicker.js index d3cdb1a8d3..15558dbfe1 100644 --- a/webroot/rsrc/js/core/behavior-fancy-datepicker.js +++ b/webroot/rsrc/js/core/behavior-fancy-datepicker.js @@ -14,6 +14,7 @@ JX.behavior('fancy-datepicker', function(config, statics) { statics.initialized = true; var picker; + var anchor_node; var root; var value_y; @@ -79,19 +80,23 @@ JX.behavior('fancy-datepicker', function(config, statics) { picker = JX.$N( 'div', - {className: 'fancy-datepicker', sigil: 'phabricator-datepicker'}, - JX.$N('div', {className: 'fancy-datepicker-core'})); + { + className: 'fancy-datepicker', + sigil: 'phabricator-datepicker' + }, + JX.$N( + 'div', + { + className: 'fancy-datepicker-core', + sigil: 'fancy-datepicker-core' + })); document.body.appendChild(picker); - var button = e.getNode('calendar-button'); - var p = JX.$V(button); - var d = JX.Vector.getDim(picker); - - picker.style.left = (p.x - d.x - 2) + 'px'; - picker.style.top = (p.y) + 'px'; - + anchor_node = e.getNode('calendar-button'); JX.DOM.alterClass(root, 'picker-open', true); + JX.Mask.show('jx-date-mask'); + read_date(); render(); }; @@ -101,6 +106,8 @@ JX.behavior('fancy-datepicker', function(config, statics) { return; } + JX.Mask.hide('jx-date-mask'); + JX.DOM.remove(picker); picker = null; JX.DOM.alterClass(root, 'picker-open', false); @@ -198,6 +205,23 @@ JX.behavior('fancy-datepicker', function(config, statics) { }; var render = function() { + if (!picker) { + return; + } + + var button = anchor_node; + var p = JX.$V(button); + var d = JX.Vector.getDim(picker); + var b = JX.Vector.getDim(button); + + if (JX.Device.isDesktop()) { + picker.style.top = (p.y) + 'px'; + picker.style.left = (p.x - d.x - 2) + 'px'; + } else { + picker.style.top = (p.y + b.y) + 'px'; + picker.style.left = ''; + } + JX.DOM.setContent( picker.firstChild, [ @@ -408,10 +432,12 @@ JX.behavior('fancy-datepicker', function(config, statics) { }); JX.Stratcom.listen('click', null, function(e){ - if (e.getNode('phabricator-datepicker')) { + if (e.getNode('phabricator-datepicker-core')) { return; } onclose(); }); + JX.Stratcom.listen('resize', null, render); + }); diff --git a/webroot/rsrc/js/core/behavior-lightbox-attachments.js b/webroot/rsrc/js/core/behavior-lightbox-attachments.js index a1c29687db..e3d863561c 100644 --- a/webroot/rsrc/js/core/behavior-lightbox-attachments.js +++ b/webroot/rsrc/js/core/behavior-lightbox-attachments.js @@ -5,6 +5,7 @@ * javelin-dom * javelin-mask * javelin-util + * phuix-icon-view * phabricator-busy */ @@ -13,17 +14,41 @@ JX.behavior('lightbox-attachments', function (config) { var lightbox = null; var prev = null; var next = null; + var shown = false; var downloadForm = JX.$H(config.downloadForm).getFragment().firstChild; + var lightbox_id = config.lightbox_id; + + function _toggleComment(e) { + e.kill(); + shown = !shown; + JX.DOM.alterClass(JX.$(lightbox_id), 'comment-panel-open', shown); + } + + function markCommentsLoading(loading) { + var frame = JX.$('lightbox-comment-frame'); + JX.DOM.alterClass(frame, 'loading', loading); + } + + function onLoadCommentsResponse(r) { + var frame = JX.$('lightbox-comment-frame'); + JX.DOM.setContent(frame, JX.$H(r)); + markCommentsLoading(false); + } + + function loadComments(phid) { + markCommentsLoading(true); + var uri = '/file/thread/' + phid + '/'; + new JX.Workflow(uri) + .setHandler(onLoadCommentsResponse) + .start(); + } function loadLightBox(e) { if (!e.isNormalClick()) { return; } - if (JX.Stratcom.pass()) { - return; - } - e.prevent(); + e.kill(); var links = JX.DOM.scry(document, 'a', 'lightboxable'); var phids = {}; @@ -63,10 +88,13 @@ JX.behavior('lightbox-attachments', function (config) { } else { img_uri = config.defaultImageUri; extra_status = ' Image may not be representative of actual attachment.'; - name_element = JX.$N('div', - { className : 'attachment-name' }, - target_data.name - ); + name_element = + JX.$N('div', + { + className : 'attachment-name' + }, + target_data.name + ); } var alt_name = ''; @@ -74,38 +102,105 @@ JX.behavior('lightbox-attachments', function (config) { alt_name = target_data.name; } - var img = JX.$N('img', - { - className : 'loading', - alt : alt_name - } - ); + var img = + JX.$N('img', + { + className : 'loading', + alt : alt_name + } + ); - lightbox = JX.$N('div', - { - className : 'lightbox-attachment', - sigil: 'lightbox-attachment' - }, - img - ); + var imgFrame = + JX.$N('div', + { + className : 'lightbox-image-frame', + }, + img + ); + + var commentFrame = + JX.$N('div', + { + className : 'lightbox-comment-frame', + id : 'lightbox-comment-frame' + } + ); + + var commentClass = (shown) ? 'comment-panel-open' : ''; + lightbox = + JX.$N('div', + { + className : 'lightbox-attachment ' + commentClass, + sigil : 'lightbox-attachment', + id : lightbox_id + }, + [imgFrame, commentFrame] + ); + + var monogram = JX.$N('strong', {}, target_data.monogram); + var m_url = JX.$N('a', { href : '/' + target_data.monogram }, monogram); + var statusSpan = + JX.$N('span', + { + className: 'lightbox-status-txt' + }, + [ + m_url, + ' Image ' + current + ' of ' + total + '.' + extra_status + ] + ); + + var downloadSpan = + JX.$N('span', + { + className : 'lightbox-download' + } + ); + + var commentIcon = new JX.PHUIXIconView() + .setIcon('fa-comment-o') + .getNode(); + var commentButton = + JX.$N('a', + { + className : 'lightbox-comment button grey has-icon', + href : '#', + sigil : 'lightbox-comment' + }, + [commentIcon, 'Comment'] + ); + var closeButton = + JX.$N('a', + { + className : 'lightbox-close button grey', + href : '#' + }, + 'Close'); + var statusHTML = + JX.$N('div', + { + className : 'lightbox-status' + }, + [statusSpan, closeButton, commentButton, downloadSpan] + ); + JX.DOM.appendContent(lightbox, statusHTML); JX.DOM.appendContent(lightbox, name_element); + JX.DOM.listen(closeButton, 'click', null, closeLightBox); - var closeIcon = JX.$N('a', - { - className : 'lightbox-close', - href : '#' - } - ); - JX.DOM.listen(closeIcon, 'click', null, closeLightBox); - JX.DOM.appendContent(lightbox, closeIcon); var leftIcon = ''; if (next) { - leftIcon = JX.$N('a', - { - className : 'lightbox-right', - href : '#' - } - ); + var r_icon = new JX.PHUIXIconView() + .setIcon('fa-angle-right') + .setColor('lightgreytext') + .getNode(); + leftIcon = + JX.$N('a', + { + className : 'lightbox-right', + href : '#' + }, + r_icon + ); JX.DOM.listen(leftIcon, 'click', null, @@ -115,12 +210,18 @@ JX.behavior('lightbox-attachments', function (config) { JX.DOM.appendContent(lightbox, leftIcon); var rightIcon = ''; if (prev) { - rightIcon = JX.$N('a', - { - className : 'lightbox-left', - href : '#' - } - ); + var l_icon = new JX.PHUIXIconView() + .setIcon('fa-angle-left') + .setColor('lightgreytext') + .getNode(); + rightIcon = + JX.$N('a', + { + className : 'lightbox-left', + href : '#' + }, + l_icon + ); JX.DOM.listen(rightIcon, 'click', null, @@ -129,24 +230,6 @@ JX.behavior('lightbox-attachments', function (config) { } JX.DOM.appendContent(lightbox, rightIcon); - var statusSpan = JX.$N('span', - { - className: 'lightbox-status-txt' - }, - 'Image '+current+' of '+total+'.'+extra_status - ); - - var downloadSpan = JX.$N('span', - { - className : 'lightbox-download' - }); - var statusHTML = JX.$N('div', - { - className : 'lightbox-status' - }, - [statusSpan, downloadSpan] - ); - JX.DOM.appendContent(lightbox, statusHTML); JX.DOM.alterClass(document.body, 'lightbox-attached', true); JX.Mask.show('jx-dark-mask'); @@ -162,6 +245,7 @@ JX.behavior('lightbox-attachments', function (config) { }; img.src = img_uri; + loadComments(target_data.phid); } // TODO - make this work with KeyboardShortcut, which means @@ -221,7 +305,7 @@ JX.behavior('lightbox-attachments', function (config) { JX.Stratcom.listen( 'click', - ['lightboxable', 'tag:a'], + ['lightboxable'], loadLightBox); JX.Stratcom.listen( @@ -246,4 +330,9 @@ JX.behavior('lightbox-attachments', function (config) { e.kill(); }); + JX.Stratcom.listen( + 'click', + 'lightbox-comment', + _toggleComment); + });