diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f5a5a57904..dabb6f1d5b 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,8 +9,8 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'c218ed53', - 'core.pkg.js' => '8581cd02', + 'core.pkg.css' => '3fd3b7b8', + 'core.pkg.js' => 'b9b4a943', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', 'diffusion.pkg.css' => 'a2d17c7d', @@ -112,7 +112,7 @@ return array( 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '62fa3ace', - 'rsrc/css/core/remarkup.css' => '97dc3523', + 'rsrc/css/core/remarkup.css' => 'b375546d', 'rsrc/css/core/syntax.css' => 'cae95e89', 'rsrc/css/core/z-index.css' => '9d8f7c4b', 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', @@ -120,7 +120,7 @@ return array( 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360', 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97', - 'rsrc/css/layout/phabricator-source-code-view.css' => '926ced2d', + 'rsrc/css/layout/phabricator-source-code-view.css' => '31ee3c83', 'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', 'rsrc/css/phui/button/phui-button.css' => '1863cc6e', @@ -168,7 +168,7 @@ return array( 'rsrc/css/phui/phui-object-box.css' => '9cff003c', 'rsrc/css/phui/phui-pager.css' => 'edcbc226', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-property-list-view.css' => '2dc7993f', + 'rsrc/css/phui/phui-property-list-view.css' => 'de4754d8', 'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863', 'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892', 'rsrc/css/phui/phui-spacing.css' => '042804d6', @@ -253,7 +253,7 @@ return array( 'rsrc/externals/javelin/lib/URI.js' => 'c989ade3', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', 'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6', - 'rsrc/externals/javelin/lib/Workflow.js' => '0eb1db0c', + 'rsrc/externals/javelin/lib/Workflow.js' => '6a726c55', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68', @@ -392,6 +392,7 @@ return array( 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', + 'rsrc/js/application/files/behavior-document-engine.js' => 'd3f8623c', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', @@ -472,7 +473,7 @@ return array( 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a', - 'rsrc/js/core/behavior-line-linker.js' => 'a9b946f8', + 'rsrc/js/core/behavior-line-linker.js' => '13e39479', 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', @@ -507,7 +508,7 @@ return array( 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', - 'rsrc/js/phuix/PHUIXActionView.js' => '442efd08', + 'rsrc/js/phuix/PHUIXActionView.js' => 'ed18356a', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '7fa5c915', 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', @@ -606,6 +607,7 @@ return array( 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', + 'javelin-behavior-document-engine' => 'd3f8623c', 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', @@ -636,7 +638,7 @@ return array( 'javelin-behavior-phabricator-gesture-example' => '558829c2', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', - 'javelin-behavior-phabricator-line-linker' => 'a9b946f8', + 'javelin-behavior-phabricator-line-linker' => '13e39479', 'javelin-behavior-phabricator-nav' => '836f966d', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '77c1f0b0', @@ -737,7 +739,7 @@ return array( 'javelin-workboard-card' => 'c587b80f', 'javelin-workboard-column' => '758b4758', 'javelin-workboard-controller' => '26167537', - 'javelin-workflow' => '0eb1db0c', + 'javelin-workflow' => '6a726c55', 'maniphest-report-css' => '9b9580b7', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', @@ -778,11 +780,11 @@ return array( 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '77b0ae28', - 'phabricator-remarkup-css' => '97dc3523', + 'phabricator-remarkup-css' => 'b375546d', 'phabricator-search-results-css' => '505dd8cf', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-slowvote-css' => 'a94b7230', - 'phabricator-source-code-view-css' => '926ced2d', + 'phabricator-source-code-view-css' => '31ee3c83', 'phabricator-standard-page-view' => '34ee718b', 'phabricator-textareautils' => '320810c8', 'phabricator-title' => '485aaa6c', @@ -848,7 +850,7 @@ return array( 'phui-oi-simple-ui-css' => 'a8beebea', 'phui-pager-css' => 'edcbc226', 'phui-pinboard-view-css' => '2495140e', - 'phui-property-list-view-css' => '2dc7993f', + 'phui-property-list-view-css' => 'de4754d8', 'phui-remarkup-preview-css' => '54a34863', 'phui-segment-bar-view-css' => 'b1d1b892', 'phui-spacing-css' => '042804d6', @@ -862,7 +864,7 @@ return array( 'phui-workcard-view-css' => 'cca5fa92', 'phui-workpanel-view-css' => 'a3a63478', 'phuix-action-list-view' => 'b5c256b8', - 'phuix-action-view' => '442efd08', + 'phuix-action-view' => 'ed18356a', 'phuix-autocomplete' => '7fa5c915', 'phuix-button-view' => '8a91e1ac', 'phuix-dropdown-menu' => '04b2ae03', @@ -958,21 +960,16 @@ return array( 'javelin-dom', 'javelin-router', ), - '0eb1db0c' => array( - 'javelin-stratcom', - 'javelin-request', - 'javelin-dom', - 'javelin-vector', - 'javelin-install', - 'javelin-util', - 'javelin-mask', - 'javelin-uri', - 'javelin-routable', - ), '0f764c35' => array( 'javelin-install', 'javelin-util', ), + '13e39479' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-history', + ), '15d5ff71' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', @@ -1182,11 +1179,6 @@ return array( 'javelin-workflow', 'javelin-workboard-controller', ), - '442efd08' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - ), '44959b73' => array( 'javelin-util', 'javelin-uri', @@ -1434,6 +1426,17 @@ return array( '69adf288' => array( 'javelin-install', ), + '6a726c55' => array( + 'javelin-stratcom', + 'javelin-request', + 'javelin-dom', + 'javelin-vector', + 'javelin-install', + 'javelin-util', + 'javelin-mask', + 'javelin-uri', + 'javelin-routable', + ), '6b31879a' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1749,12 +1752,6 @@ return array( 'javelin-uri', 'phabricator-keyboard-shortcut', ), - 'a9b946f8' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-history', - ), 'a9f88de2' => array( 'javelin-behavior', 'javelin-dom', @@ -2005,6 +2002,11 @@ return array( 'd254d646' => array( 'javelin-util', ), + 'd3f8623c' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), 'd4505101' => array( 'javelin-stratcom', 'javelin-install', @@ -2123,6 +2125,11 @@ return array( 'javelin-stratcom', 'javelin-vector', ), + 'ed18356a' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + ), 'edf8a145' => array( 'javelin-behavior', 'javelin-uri', diff --git a/resources/sql/autopatches/20180322.lock.01.identifier.sql b/resources/sql/autopatches/20180322.lock.01.identifier.sql new file mode 100644 index 0000000000..b115a691fa --- /dev/null +++ b/resources/sql/autopatches/20180322.lock.01.identifier.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + ADD requestIdentifier VARBINARY(12); + +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + ADD UNIQUE KEY `key_request` (requestIdentifier); diff --git a/resources/sql/autopatches/20180322.lock.02.wait.sql b/resources/sql/autopatches/20180322.lock.02.wait.sql new file mode 100644 index 0000000000..cba7cc64d0 --- /dev/null +++ b/resources/sql/autopatches/20180322.lock.02.wait.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + ADD writeWait BIGINT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + ADD readWait BIGINT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + ADD hostWait BIGINT UNSIGNED; diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php index 51abcb6c89..07d6d7cfa2 100755 --- a/scripts/repository/commit_hook.php +++ b/scripts/repository/commit_hook.php @@ -187,6 +187,11 @@ if (strlen($remote_protocol)) { $engine->setRemoteProtocol($remote_protocol); } +$request_identifier = getenv(DiffusionCommitHookEngine::ENV_REQUEST); +if (strlen($request_identifier)) { + $engine->setRequestIdentifier($request_identifier); +} + try { $err = $engine->execute(); } catch (DiffusionCommitHookRejectException $ex) { diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index 2ff1bdc198..0f2275cda8 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -8,6 +8,12 @@ require_once $root.'/scripts/__init_script__.php'; $ssh_log = PhabricatorSSHLog::getLog(); +$request_identifier = Filesystem::readRandomCharacters(12); +$ssh_log->setData( + array( + 'Q' => $request_identifier, + )); + $args = new PhutilArgumentParser($argv); $args->setTagline(pht('execute SSH requests')); $args->setSynopsis(<<setSSHUser($user); $workflow->setOriginalArguments($original_argv); $workflow->setIsClusterRequest($is_cluster_request); + $workflow->setRequestIdentifier($request_identifier); $sock_stdin = fopen('php://stdin', 'r'); if (!$sock_stdin) { diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4caa7f563d..b1afef3600 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2066,6 +2066,7 @@ phutil_register_library_map(array( 'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php', + 'PhabricatorAudioDocumentEngine' => 'applications/files/document/PhabricatorAudioDocumentEngine.php', 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', 'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php', 'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php', @@ -2808,6 +2809,8 @@ phutil_register_library_map(array( 'PhabricatorDividerEditField' => 'applications/transactions/editfield/PhabricatorDividerEditField.php', 'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php', 'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php', + 'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php', + 'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php', 'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', @@ -2998,6 +3001,7 @@ phutil_register_library_map(array( 'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php', 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php', + 'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', 'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php', @@ -3011,7 +3015,6 @@ phutil_register_library_map(array( 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', 'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php', 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', - 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php', 'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', @@ -3047,6 +3050,7 @@ phutil_register_library_map(array( 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', 'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php', 'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php', + 'PhabricatorFileViewController' => 'applications/files/controller/PhabricatorFileViewController.php', 'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php', 'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php', 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', @@ -3137,6 +3141,7 @@ phutil_register_library_map(array( 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', 'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php', 'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php', + 'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php', 'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php', 'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php', 'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php', @@ -3155,6 +3160,7 @@ phutil_register_library_map(array( 'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php', 'PhabricatorIconSetEditField' => 'applications/transactions/editfield/PhabricatorIconSetEditField.php', 'PhabricatorIconSetIcon' => 'applications/files/iconset/PhabricatorIconSetIcon.php', + 'PhabricatorImageDocumentEngine' => 'applications/files/document/PhabricatorImageDocumentEngine.php', 'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php', 'PhabricatorImageRemarkupRule' => 'applications/files/markup/PhabricatorImageRemarkupRule.php', 'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', @@ -3181,9 +3187,11 @@ phutil_register_library_map(array( 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php', + 'PhabricatorJSONDocumentEngine' => 'applications/files/document/PhabricatorJSONDocumentEngine.php', 'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', + 'PhabricatorJupyterDocumentEngine' => 'applications/files/document/PhabricatorJupyterDocumentEngine.php', 'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php', 'PhabricatorKeyValueSerializingCacheProxy' => 'applications/cache/PhabricatorKeyValueSerializingCacheProxy.php', 'PhabricatorKeyboardRemarkupRule' => 'infrastructure/markup/rule/PhabricatorKeyboardRemarkupRule.php', @@ -3513,6 +3521,7 @@ phutil_register_library_map(array( 'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php', 'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php', 'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php', + 'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php', 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', @@ -3961,6 +3970,7 @@ phutil_register_library_map(array( 'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php', 'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php', 'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php', + 'PhabricatorRemarkupDocumentEngine' => 'applications/files/document/PhabricatorRemarkupDocumentEngine.php', 'PhabricatorRemarkupEditField' => 'applications/transactions/editfield/PhabricatorRemarkupEditField.php', 'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php', 'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php', @@ -4213,6 +4223,7 @@ phutil_register_library_map(array( 'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php', 'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php', 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php', + 'PhabricatorSourceDocumentEngine' => 'applications/files/document/PhabricatorSourceDocumentEngine.php', 'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php', 'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php', 'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php', @@ -4354,6 +4365,7 @@ phutil_register_library_map(array( 'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php', 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', 'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php', + 'PhabricatorTextDocumentEngine' => 'applications/files/document/PhabricatorTextDocumentEngine.php', 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', 'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php', 'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php', @@ -4481,7 +4493,9 @@ phutil_register_library_map(array( 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', + 'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php', 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', + 'PhabricatorVoidDocumentEngine' => 'applications/files/document/PhabricatorVoidDocumentEngine.php', 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', 'PhabricatorWebContentSource' => 'infrastructure/contentsource/PhabricatorWebContentSource.php', 'PhabricatorWebServerSetupCheck' => 'applications/config/check/PhabricatorWebServerSetupCheck.php', @@ -7497,6 +7511,7 @@ phutil_register_library_map(array( 'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorAudioDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorAuditActionConstants' => 'Phobject', 'PhabricatorAuditApplication' => 'PhabricatorApplication', 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', @@ -8362,6 +8377,8 @@ phutil_register_library_map(array( 'PhabricatorDividerEditField' => 'PhabricatorEditField', 'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDivinerApplication' => 'PhabricatorApplication', + 'PhabricatorDocumentEngine' => 'Phobject', + 'PhabricatorDocumentRef' => 'Phobject', 'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', @@ -8581,6 +8598,7 @@ phutil_register_library_map(array( 'PhabricatorFileDataController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType', + 'PhabricatorFileDocumentController' => 'PhabricatorFileController', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', 'PhabricatorFileEditEngine' => 'PhabricatorEditEngine', @@ -8604,7 +8622,6 @@ phutil_register_library_map(array( ), 'PhabricatorFileImageProxyController' => 'PhabricatorFileController', 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', - 'PhabricatorFileInfoController' => 'PhabricatorFileController', 'PhabricatorFileIntegrityException' => 'Exception', 'PhabricatorFileLightboxController' => 'PhabricatorFileController', 'PhabricatorFileLinkView' => 'AphrontTagView', @@ -8640,6 +8657,7 @@ phutil_register_library_map(array( 'PhabricatorFileUploadException' => 'Exception', 'PhabricatorFileUploadSource' => 'Phobject', 'PhabricatorFileUploadSourceByteLimitException' => 'Exception', + 'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFilesApplication' => 'PhabricatorApplication', 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', @@ -8738,6 +8756,7 @@ phutil_register_library_map(array( 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 'PhabricatorHeraldApplication' => 'PhabricatorApplication', 'PhabricatorHeraldContentSource' => 'PhabricatorContentSource', + 'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorHomeApplication' => 'PhabricatorApplication', 'PhabricatorHomeConstants' => 'PhabricatorHomeController', @@ -8756,6 +8775,7 @@ phutil_register_library_map(array( 'PhabricatorIconSet' => 'Phobject', 'PhabricatorIconSetEditField' => 'PhabricatorEditField', 'PhabricatorIconSetIcon' => 'Phobject', + 'PhabricatorImageDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorImageTransformer' => 'Phobject', @@ -8781,9 +8801,11 @@ phutil_register_library_map(array( 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType', + 'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine', 'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat', 'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorJupyterDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache', 'PhabricatorKeyValueSerializingCacheProxy' => 'PhutilKeyValueCacheProxy', 'PhabricatorKeyboardRemarkupRule' => 'PhutilRemarkupRule', @@ -9154,6 +9176,7 @@ phutil_register_library_map(array( 'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField', + 'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', @@ -9696,6 +9719,7 @@ phutil_register_library_map(array( 'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule', 'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule', + 'PhabricatorRemarkupDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorRemarkupEditField' => 'PhabricatorEditField', 'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample', @@ -10021,6 +10045,7 @@ phutil_register_library_map(array( 'PhabricatorSlug' => 'Phobject', 'PhabricatorSlugTestCase' => 'PhabricatorTestCase', 'PhabricatorSourceCodeView' => 'AphrontView', + 'PhabricatorSourceDocumentEngine' => 'PhabricatorTextDocumentEngine', 'PhabricatorSpaceEditField' => 'PhabricatorEditField', 'PhabricatorSpacesApplication' => 'PhabricatorApplication', 'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController', @@ -10168,6 +10193,7 @@ phutil_register_library_map(array( 'PhabricatorTestWorker' => 'PhabricatorWorker', 'PhabricatorTextAreaEditField' => 'PhabricatorEditField', 'PhabricatorTextConfigType' => 'PhabricatorConfigType', + 'PhabricatorTextDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorTextEditField' => 'PhabricatorEditField', 'PhabricatorTextExportFormat' => 'PhabricatorExportFormat', 'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType', @@ -10330,7 +10356,9 @@ phutil_register_library_map(array( 'PhabricatorVCSResponse' => 'AphrontResponse', 'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', + 'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorVoidDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorWebContentSource' => 'PhabricatorContentSource', 'PhabricatorWebServerSetupCheck' => 'PhabricatorSetupCheck', diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 2ee222d61c..fe1e80318f 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -28,6 +28,7 @@ abstract class AphrontResponse extends Phobject { 'connect-src' => array(), 'frame-src' => array(), 'form-action' => array(), + 'object-src' => array(), ); } @@ -163,8 +164,10 @@ abstract class AphrontResponse extends Phobject { $csp[] = "frame-ancestors 'none'"; } - // Block relics of the old world: Flash, Java applets, and so on. - $csp[] = "object-src 'none'"; + // Block relics of the old world: Flash, Java applets, and so on. Note + // that Chrome prevents the user from viewing PDF documents if they are + // served with a policy which excludes the domain they are served from. + $csp[] = $this->newContentSecurityPolicy('object-src', "'none'"); // Don't allow forms to submit offsite. diff --git a/src/applications/config/option/PhabricatorAccessLogConfigOptions.php b/src/applications/config/option/PhabricatorAccessLogConfigOptions.php index 9ae60825ea..2b9c4bbc75 100644 --- a/src/applications/config/option/PhabricatorAccessLogConfigOptions.php +++ b/src/applications/config/option/PhabricatorAccessLogConfigOptions.php @@ -47,6 +47,10 @@ final class PhabricatorAccessLogConfigOptions 's' => pht('The system user.'), 'S' => pht('The system sudo user.'), 'k' => pht('ID of the SSH key used to authenticate the request.'), + + // TODO: This is a reasonable thing to support in the HTTP access + // log, too. + 'Q' => pht('A random, unique string which identifies the request.'), ); $http_desc = pht( diff --git a/src/applications/console/plugin/DarkConsoleServicesPlugin.php b/src/applications/console/plugin/DarkConsoleServicesPlugin.php index a14ed4b541..4a26665e0a 100644 --- a/src/applications/console/plugin/DarkConsoleServicesPlugin.php +++ b/src/applications/console/plugin/DarkConsoleServicesPlugin.php @@ -279,7 +279,7 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { $analysis, ); - if ($row['trace']) { + if (isset($row['trace'])) { $rows[] = array( null, null, diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php index a1e66379f7..07809dca59 100644 --- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php @@ -80,7 +80,7 @@ abstract class DifferentialRevisionActionTransaction DifferentialRevision $revision) { return array( array(), - null, + array(), ); } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index a0769a51f0..cc4526dbdc 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -12,6 +12,7 @@ final class DiffusionCommitHookEngine extends Phobject { const ENV_REPOSITORY = 'PHABRICATOR_REPOSITORY'; const ENV_USER = 'PHABRICATOR_USER'; + const ENV_REQUEST = 'PHABRICATOR_REQUEST'; const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS'; const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL'; @@ -25,6 +26,7 @@ final class DiffusionCommitHookEngine extends Phobject { private $subversionRepository; private $remoteAddress; private $remoteProtocol; + private $requestIdentifier; private $transactionKey; private $mercurialHook; private $mercurialCommits = array(); @@ -58,6 +60,15 @@ final class DiffusionCommitHookEngine extends Phobject { return $this->remoteAddress; } + public function setRequestIdentifier($request_identifier) { + $this->requestIdentifier = $request_identifier; + return $this; + } + + public function getRequestIdentifier() { + return $this->requestIdentifier; + } + public function setSubversionTransactionInfo($transaction, $repository) { $this->subversionTransaction = $transaction; $this->subversionRepository = $repository; @@ -620,6 +631,7 @@ final class DiffusionCommitHookEngine extends Phobject { $env = array( self::ENV_REPOSITORY => $this->getRepository()->getPHID(), self::ENV_USER => $this->getViewer()->getUsername(), + self::ENV_REQUEST => $this->getRequestIdentifier(), self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), ); @@ -1081,16 +1093,24 @@ final class DiffusionCommitHookEngine extends Phobject { ->setDevicePHID($device_phid) ->setRepositoryPHID($this->getRepository()->getPHID()) ->attachRepository($this->getRepository()) - ->setEpoch(time()); + ->setEpoch(PhabricatorTime::getNow()); } private function newPushEvent() { $viewer = $this->getViewer(); - return PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) + + $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) ->setRepositoryPHID($this->getRepository()->getPHID()) ->setRemoteAddress($this->getRemoteAddress()) ->setRemoteProtocol($this->getRemoteProtocol()) - ->setEpoch(time()); + ->setEpoch(PhabricatorTime::getNow()); + + $identifier = $this->getRequestIdentifier(); + if (strlen($identifier)) { + $event->setRequestIdentifier($identifier); + } + + return $event; } private function rejectEnormousChanges(array $content_updates) { diff --git a/src/applications/diffusion/protocol/DiffusionCommandEngine.php b/src/applications/diffusion/protocol/DiffusionCommandEngine.php index 53a086db33..d7da62b11d 100644 --- a/src/applications/diffusion/protocol/DiffusionCommandEngine.php +++ b/src/applications/diffusion/protocol/DiffusionCommandEngine.php @@ -135,6 +135,11 @@ abstract class DiffusionCommandEngine extends Phobject { $future->setEnv($env); + // See T13108. By default, don't let any cluster command run indefinitely + // to try to avoid cases where `git fetch` hangs for some reason and we're + // left sitting with a held lock forever. + $future->setTimeout(phutil_units('15 minutes in seconds')); + return $future; } diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php index 35bb4d4acf..d5ba74a30e 100644 --- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php +++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php @@ -151,8 +151,8 @@ final class DiffusionRepositoryClusterEngine extends Phobject { $this->logLine( pht( - 'Waiting up to %s second(s) for a cluster read lock on "%s"...', - new PhutilNumber($lock_wait), + 'Acquiring read lock for repository "%s" on device "%s"...', + $repository->getDisplayName(), $device->getName())); try { @@ -308,18 +308,37 @@ final class DiffusionRepositoryClusterEngine extends Phobject { $write_lock->useSpecificConnection($locked_connection); - $lock_wait = phutil_units('2 minutes in seconds'); - $this->logLine( pht( - 'Waiting up to %s second(s) for a cluster write lock...', - new PhutilNumber($lock_wait))); + 'Acquiring write lock for repository "%s"...', + $repository->getDisplayName())); + $lock_wait = phutil_units('2 minutes in seconds'); try { - $start = PhabricatorTime::getNow(); - $write_lock->lock($lock_wait); - $waited = (PhabricatorTime::getNow() - $start); + $write_wait_start = microtime(true); + $start = PhabricatorTime::getNow(); + $step_wait = 1; + + while (true) { + try { + $write_lock->lock((int)floor($step_wait)); + $write_wait_end = microtime(true); + break; + } catch (PhutilLockException $ex) { + $waited = (PhabricatorTime::getNow() - $start); + if ($waited > $lock_wait) { + throw $ex; + } + $this->logActiveWriter($viewer, $repository); + } + + // Wait a little longer before the next message we print. + $step_wait = $step_wait + 0.5; + $step_wait = min($step_wait, 3); + } + + $waited = (PhabricatorTime::getNow() - $start); if ($waited) { $this->logLine( pht( @@ -354,12 +373,14 @@ final class DiffusionRepositoryClusterEngine extends Phobject { 'documentation for instructions.')); } + $read_wait_start = microtime(true); try { $max_version = $this->synchronizeWorkingCopyBeforeRead(); } catch (Exception $ex) { $write_lock->unlock(); throw $ex; } + $read_wait_end = microtime(true); $pid = getmypid(); $hash = Filesystem::readRandomCharacters(12); @@ -378,6 +399,15 @@ final class DiffusionRepositoryClusterEngine extends Phobject { $this->clusterWriteVersion = $max_version; $this->clusterWriteLock = $write_lock; + + $write_wait = ($write_wait_end - $write_wait_start); + $read_wait = ($read_wait_end - $read_wait_start); + + $log = $this->logger; + if ($log) { + $log->writeClusterEngineLogProperty('readWait', $read_wait); + $log->writeClusterEngineLogProperty('writeWait', $write_wait); + } } @@ -763,4 +793,32 @@ final class DiffusionRepositoryClusterEngine extends Phobject { } } + private function logActiveWriter( + PhabricatorUser $viewer, + PhabricatorRepository $repository) { + + $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter( + $repository->getPHID()); + if (!$writer) { + $this->logLine(pht('Waiting on another user to finish writing...')); + return; + } + + $user_phid = $writer->getWriteProperty('userPHID'); + $device_phid = $writer->getWriteProperty('devicePHID'); + $epoch = $writer->getWriteProperty('epoch'); + + $phids = array($user_phid, $device_phid); + $handles = $viewer->loadHandles($phids); + + $duration = (PhabricatorTime::getNow() - $epoch) + 1; + + $this->logLine( + pht( + 'Waiting for %s to finish writing (on device "%s" for %ss)...', + $handles[$user_phid]->getName(), + $handles[$device_phid]->getName(), + new PhutilNumber($duration))); + } + } diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php index 9b1fe9a506..3e43d72779 100644 --- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php +++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php @@ -3,5 +3,6 @@ interface DiffusionRepositoryClusterEngineLogInterface { public function writeClusterEngineLogMessage($message); + public function writeClusterEngineLogProperty($key, $value); } diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php index 91c8238718..400340abeb 100644 --- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php @@ -14,6 +14,8 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { } protected function executeRepositoryOperations() { + $host_wait_start = microtime(true); + $repository = $this->getRepository(); $viewer = $this->getSSHUser(); $device = AlmanacKeys::getLiveDevice(); @@ -71,6 +73,14 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, PhabricatorRepositoryStatusMessage::CODE_OKAY); $this->waitForGitClient(); + + $host_wait_end = microtime(true); + + $this->updatePushLogWithTimingInformation( + $this->getClusterEngineLogProperty('writeWait'), + $this->getClusterEngineLogProperty('readWait'), + ($host_wait_end - $host_wait_start)); + } return $err; @@ -89,4 +99,37 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { ->execute(); } + private function updatePushLogWithTimingInformation( + $write_wait, + $read_wait, + $host_wait) { + + if ($write_wait !== null) { + $write_wait = (int)(1000000 * $write_wait); + } + + if ($read_wait !== null) { + $read_wait = (int)(1000000 * $read_wait); + } + + if ($host_wait !== null) { + $host_wait = (int)(1000000 * $host_wait); + } + + $identifier = $this->getRequestIdentifier(); + + $event = new PhabricatorRepositoryPushEvent(); + $conn = $event->establishConnection('w'); + + queryfx( + $conn, + 'UPDATE %T SET writeWait = %nd, readWait = %nd, hostWait = %nd + WHERE requestIdentifier = %s', + $event->getTableName(), + $write_wait, + $read_wait, + $host_wait, + $identifier); + } + } diff --git a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php index 6de16e723d..6bc56d767d 100644 --- a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php @@ -4,6 +4,8 @@ abstract class DiffusionGitSSHWorkflow extends DiffusionSSHWorkflow implements DiffusionRepositoryClusterEngineLogInterface { + private $engineLogProperties = array(); + protected function writeError($message) { // Git assumes we'll add our own newlines. return parent::writeError($message."\n"); @@ -14,6 +16,14 @@ abstract class DiffusionGitSSHWorkflow $this->getErrorChannel()->update(); } + public function writeClusterEngineLogProperty($key, $value) { + $this->engineLogProperties[$key] = $value; + } + + protected function getClusterEngineLogProperty($key, $default = null) { + return idx($this->engineLogProperties, $key, $default); + } + protected function identifyRepository() { $args = $this->getArgs(); $path = head($args->getArg('dir')); diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index e40d8e1f51..baf1749252 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -30,6 +30,11 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh', ); + $identifier = $this->getRequestIdentifier(); + if ($identifier !== null) { + $env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier; + } + $remote_address = $this->getSSHRemoteAddress(); if ($remote_address !== null) { $env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address; diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php index 8d018c61b3..47dde8c98a 100644 --- a/src/applications/feed/PhabricatorFeedStoryPublisher.php +++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php @@ -12,6 +12,7 @@ final class PhabricatorFeedStoryPublisher extends Phobject { private $mailRecipientPHIDs = array(); private $notifyAuthor; private $mailTags = array(); + private $unexpandablePHIDs = array(); public function setMailTags(array $mail_tags) { $this->mailTags = $mail_tags; @@ -46,6 +47,15 @@ final class PhabricatorFeedStoryPublisher extends Phobject { return $this; } + public function setUnexpandablePHIDs(array $unexpandable_phids) { + $this->unexpandablePHIDs = $unexpandable_phids; + return $this; + } + + public function getUnexpandablePHIDs() { + return $this->unexpandablePHIDs; + } + public function setStoryType($story_type) { $this->storyType = $story_type; return $this; @@ -254,10 +264,36 @@ final class PhabricatorFeedStoryPublisher extends Phobject { } private function expandRecipients(array $phids) { - return id(new PhabricatorMetaMTAMemberQuery()) + $expanded_phids = id(new PhabricatorMetaMTAMemberQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($phids) ->executeExpansion(); + + // Filter out unexpandable PHIDs from the results. The typical case for + // this is that resigned reviewers should not be notified just because + // they are a member of some project or package reviewer. + + $original_map = array_fuse($phids); + $unexpandable_map = array_fuse($this->unexpandablePHIDs); + + foreach ($expanded_phids as $key => $phid) { + // We can keep this expanded PHID if it was present originally. + if (isset($original_map[$phid])) { + continue; + } + + // We can also keep it if it isn't marked as unexpandable. + if (!isset($unexpandable_map[$phid])) { + continue; + } + + // If it's unexpandable and we produced it by expanding recipients, + // throw it away. + unset($expanded_phids[$key]); + } + $expanded_phids = array_values($expanded_phids); + + return $expanded_phids; } /** diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index 2d3a1b35c6..5bf1ebccc4 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -69,9 +69,15 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { public function getRoutes() { return array( - '/F(?P[1-9]\d*)' => 'PhabricatorFileInfoController', + '/F(?P[1-9]\d*)(?:\$(?P\d+(?:-\d+)?))?' + => 'PhabricatorFileViewController', '/file/' => array( '(query/(?P[^/]+)/)?' => 'PhabricatorFileListController', + 'view/(?P[^/]+)/'. + '(?:(?P[^/]+)/)?'. + '(?:\$(?P\d+(?:-\d+)?))?' + => 'PhabricatorFileViewController', + 'info/(?P[^/]+)/' => 'PhabricatorFileViewController', 'upload/' => 'PhabricatorFileUploadController', 'dropupload/' => 'PhabricatorFileDropUploadController', 'compose/' => 'PhabricatorFileComposeController', @@ -80,7 +86,6 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { 'delete/(?P[1-9]\d*)/' => 'PhabricatorFileDeleteController', $this->getEditRoutePattern('edit/') => 'PhabricatorFileEditController', - 'info/(?P[^/]+)/' => 'PhabricatorFileInfoController', 'imageproxy/' => 'PhabricatorFileImageProxyController', 'transforms/(?P[1-9]\d*)/' => 'PhabricatorFileTransformListController', @@ -89,6 +94,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { 'iconset/(?P[^/]+)/' => array( 'select/' => 'PhabricatorFileIconSetSelectController', ), + 'document/(?P[^/]+)/(?P[^/]+)/' + => 'PhabricatorFileDocumentController', ) + $this->getResourceSubroutes(), ); } diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php index 1493c54f59..751a06ffdb 100644 --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -45,6 +45,8 @@ final class PhabricatorFilesConfigOptions 'video/ogg' => 'video/ogg', 'video/webm' => 'video/webm', 'video/quicktime' => 'video/quicktime', + + 'application/pdf' => 'application/pdf', ); $image_default = array( diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index 91dfb4fa4f..bd3d933283 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -73,11 +73,14 @@ final class PhabricatorFileDataController extends PhabricatorFileController { list($begin, $end) = $response->parseHTTPRange($range); } - $is_viewable = $file->isViewableInBrowser(); + if (!$file->isViewableInBrowser()) { + $is_download = true; + } + $request_type = $request->getHTTPHeader('X-Phabricator-Request-Type'); $is_lfs = ($request_type == 'git-lfs'); - if ($is_viewable && !$is_download) { + if (!$is_download) { $response->setMimeType($file->getViewableMimeType()); } else { $is_post = $request->isHTTPPost(); @@ -109,6 +112,19 @@ final class PhabricatorFileDataController extends PhabricatorFileController { $response->setContentLength($file->getByteSize()); $response->setContentIterator($iterator); + // In Chrome, we must permit this domain in "object-src" CSP when serving a + // PDF or the browser will refuse to render it. + if (!$is_download && $file->isPDF()) { + $request_uri = id(clone $request->getAbsoluteRequestURI()) + ->setPath(null) + ->setFragment(null) + ->setQueryParams(array()); + + $response->addContentSecurityPolicyURI( + 'object-src', + (string)$request_uri); + } + return $response; } diff --git a/src/applications/files/controller/PhabricatorFileDocumentController.php b/src/applications/files/controller/PhabricatorFileDocumentController.php new file mode 100644 index 0000000000..b74d98f48e --- /dev/null +++ b/src/applications/files/controller/PhabricatorFileDocumentController.php @@ -0,0 +1,113 @@ +getViewer(); + + $file_phid = $request->getURIData('phid'); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + return $this->newErrorResponse( + pht( + 'This file ("%s") does not exist or could not be loaded.', + $file_phid)); + } + $this->file = $file; + + $ref = id(new PhabricatorDocumentRef()) + ->setFile($file); + $this->ref = $ref; + + $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); + $engine_key = $request->getURIData('engineKey'); + if (!isset($engines[$engine_key])) { + return $this->newErrorResponse( + pht( + 'The engine ("%s") is unknown, or unable to render this document.', + $engine_key)); + } + $engine = $engines[$engine_key]; + $this->engine = $engine; + + try { + $content = $engine->newDocument($ref); + } catch (Exception $ex) { + return $this->newErrorResponse($ex->getMessage()); + } + + return $this->newContentResponse($content); + } + + private function newErrorResponse($message) { + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-error', + ), + array( + id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle red'), + ' ', + $message, + )); + + return $this->newContentResponse($container); + } + + + private function newContentResponse($content) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + $file = $this->file; + $engine = $this->engine; + $ref = $this->ref; + + if ($request->isAjax()) { + return id(new AphrontAjaxResponse()) + ->setContent( + array( + 'markup' => hsprintf('%s', $content), + )); + } + + $crumbs = $this->buildApplicationCrumbs(); + if ($file) { + $crumbs->addTextCrumb($file->getMonogram(), $file->getInfoURI()); + } + + $label = $engine->getViewAsLabel($ref); + if ($label) { + $crumbs->addTextCrumb($label); + } + + $crumbs->setBorder(true); + + $content_frame = id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($content); + + $page_frame = id(new PHUITwoColumnView()) + ->setFooter($content_frame); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle( + array( + $ref->getName(), + pht('Standalone'), + )) + ->appendChild($page_frame); + } + +} diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileViewController.php similarity index 76% rename from src/applications/files/controller/PhabricatorFileInfoController.php rename to src/applications/files/controller/PhabricatorFileViewController.php index 976324d0b2..5251fdd8f1 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileViewController.php @@ -1,6 +1,6 @@ setURI($file->getInfoURI()); } + $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withIDs(array($id)) @@ -62,31 +63,34 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $timeline = $this->buildTransactionView($file); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( - 'F'.$file->getID(), - $this->getApplicationURI("/info/{$phid}/")); + $file->getMonogram(), + $file->getInfoURI()); $crumbs->setBorder(true); $object_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('File')) + ->setHeaderText(pht('File Metadata')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); $this->buildPropertyViews($object_box, $file); $title = $file->getName(); + $file_content = $this->newFileContent($file); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) - ->setMainColumn(array( - $object_box, - $timeline, - )); + ->setMainColumn( + array( + $object_box, + $file_content, + $timeline, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($file->getPHID())) ->appendChild($view); - } private function buildTransactionView(PhabricatorFile $file) { @@ -325,61 +329,6 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $viewer->renderHandleList($phids)); } - if ($file->isViewableImage()) { - $image = phutil_tag( - 'img', - array( - 'src' => $file->getViewURI(), - 'class' => 'phui-property-list-image', - )); - - $linked_image = phutil_tag( - 'a', - array( - 'href' => $file->getViewURI(), - ), - $image); - - $media = id(new PHUIPropertyListView()) - ->addImageContent($linked_image); - - $box->addPropertyList($media); - } else if ($file->isVideo()) { - $video = phutil_tag( - 'video', - array( - 'controls' => 'controls', - 'class' => 'phui-property-list-video', - ), - phutil_tag( - 'source', - array( - 'src' => $file->getViewURI(), - 'type' => $file->getMimeType(), - ))); - $media = id(new PHUIPropertyListView()) - ->addImageContent($video); - - $box->addPropertyList($media); - } else if ($file->isAudio()) { - $audio = phutil_tag( - 'audio', - array( - 'controls' => 'controls', - 'class' => 'phui-property-list-audio', - ), - phutil_tag( - 'source', - array( - 'src' => $file->getViewURI(), - 'type' => $file->getMimeType(), - ))); - $media = id(new PHUIPropertyListView()) - ->addImageContent($audio); - - $box->addPropertyList($media); - } - $engine = $this->loadStorageEngine($file); if ($engine) { if ($engine->isChunkEngine()) { @@ -453,5 +402,99 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { return $engine; } + private function newFileContent(PhabricatorFile $file) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + $ref = id(new PhabricatorDocumentRef()) + ->setFile($file); + + $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); + + $engine_key = $request->getURIData('engineKey'); + if (!isset($engines[$engine_key])) { + $engine_key = head_key($engines); + } + $engine = $engines[$engine_key]; + + $lines = $request->getURILineRange('lines', 1000); + if ($lines) { + $engine->setHighlightedLines(range($lines[0], $lines[1])); + } + + $views = array(); + foreach ($engines as $candidate_key => $candidate_engine) { + $label = $candidate_engine->getViewAsLabel($ref); + if ($label === null) { + continue; + } + + $view_uri = '/file/view/'.$file->getID().'/'.$candidate_key.'/'; + + $view_icon = $candidate_engine->getViewAsIconIcon($ref); + $view_color = $candidate_engine->getViewAsIconColor($ref); + $loading = $candidate_engine->newLoadingContent($ref); + + $views[] = array( + 'viewKey' => $candidate_engine->getDocumentEngineKey(), + 'icon' => $view_icon, + 'color' => $view_color, + 'name' => $label, + 'engineURI' => $candidate_engine->getRenderURI($ref), + 'viewURI' => $view_uri, + 'loadingMarkup' => hsprintf('%s', $loading), + ); + } + + $viewport_id = celerity_generate_unique_node_id(); + $control_id = celerity_generate_unique_node_id(); + $icon = $engine->newDocumentIcon($ref); + + if ($engine->shouldRenderAsync($ref)) { + $content = $engine->newLoadingContent($ref); + $config = array( + 'renderControlID' => $control_id, + ); + } else { + $content = $engine->newDocument($ref); + $config = array(); + } + + Javelin::initBehavior('document-engine', $config); + + $viewport = phutil_tag( + 'div', + array( + 'id' => $viewport_id, + ), + $content); + + $meta = array( + 'viewportID' => $viewport_id, + 'viewKey' => $engine->getDocumentEngineKey(), + 'views' => $views, + 'standaloneURI' => $engine->getRenderURI($ref), + ); + + $view_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View Options')) + ->setIcon('fa-file-image-o') + ->setColor(PHUIButtonView::GREY) + ->setID($control_id) + ->setMetadata($meta) + ->setDropdown(true) + ->addSigil('document-engine-view-dropdown'); + + $header = id(new PHUIHeaderView()) + ->setHeaderIcon($icon) + ->setHeader($ref->getName()) + ->addActionLink($view_button); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header) + ->appendChild($viewport); + } } diff --git a/src/applications/files/document/PhabricatorAudioDocumentEngine.php b/src/applications/files/document/PhabricatorAudioDocumentEngine.php new file mode 100644 index 0000000000..859f167627 --- /dev/null +++ b/src/applications/files/document/PhabricatorAudioDocumentEngine.php @@ -0,0 +1,69 @@ +getFile(); + if ($file) { + return $file->isAudio(); + } + + $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); + $viewable_types = array_keys($viewable_types); + + $audio_types = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); + $audio_types = array_keys($audio_types); + + return + $ref->hasAnyMimeType($viewable_types) && + $ref->hasAnyMimeType($audio_types); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + throw new PhutilMethodNotImplementedException(); + } + + $mime_type = $ref->getMimeType(); + + $audio = phutil_tag( + 'audio', + array( + 'controls' => 'controls', + ), + phutil_tag( + 'source', + array( + 'src' => $source_uri, + 'type' => $mime_type, + ))); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-audio', + ), + $audio); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php new file mode 100644 index 0000000000..a225d55ea9 --- /dev/null +++ b/src/applications/files/document/PhabricatorDocumentEngine.php @@ -0,0 +1,214 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setHighlightedLines(array $highlighted_lines) { + $this->highlightedLines = $highlighted_lines; + return $this; + } + + final public function getHighlightedLines() { + return $this->highlightedLines; + } + + final public function canRenderDocument(PhabricatorDocumentRef $ref) { + return $this->canRenderDocumentType($ref); + } + + public function shouldRenderAsync(PhabricatorDocumentRef $ref) { + return false; + } + + abstract protected function canRenderDocumentType( + PhabricatorDocumentRef $ref); + + final public function newDocument(PhabricatorDocumentRef $ref) { + $can_complete = $this->canRenderCompleteDocument($ref); + $can_partial = $this->canRenderPartialDocument($ref); + + if (!$can_complete && !$can_partial) { + return $this->newMessage( + pht( + 'This document is too large to be rendered inline. (The document '. + 'is %s bytes, the limit for this engine is %s bytes.)', + new PhutilNumber($ref->getByteLength()), + new PhutilNumber($this->getByteLengthLimit()))); + } + + return $this->newDocumentContent($ref); + } + + final public function newDocumentIcon(PhabricatorDocumentRef $ref) { + return id(new PHUIIconView()) + ->setIcon($this->getDocumentIconIcon($ref)); + } + + abstract protected function newDocumentContent( + PhabricatorDocumentRef $ref); + + protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { + return 'fa-file-o'; + } + + protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) { + return pht('Loading...'); + } + + final public function getDocumentEngineKey() { + return $this->getPhobjectClassConstant('ENGINEKEY'); + } + + final public static function getAllEngines() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getDocumentEngineKey') + ->execute(); + } + + final public function newSortVector(PhabricatorDocumentRef $ref) { + $content_score = $this->getContentScore($ref); + + // Prefer engines which can render the entire file over engines which + // can only render a header, and engines which can render a header over + // engines which can't render anything. + if ($this->canRenderCompleteDocument($ref)) { + $limit_score = 0; + } else if ($this->canRenderPartialDocument($ref)) { + $limit_score = 1; + } else { + $limit_score = 2; + } + + return id(new PhutilSortVector()) + ->addInt($limit_score) + ->addInt(-$content_score); + } + + protected function getContentScore(PhabricatorDocumentRef $ref) { + return 2000; + } + + abstract public function getViewAsLabel(PhabricatorDocumentRef $ref); + + public function getViewAsIconIcon(PhabricatorDocumentRef $ref) { + $can_complete = $this->canRenderCompleteDocument($ref); + $can_partial = $this->canRenderPartialDocument($ref); + + if (!$can_complete && !$can_partial) { + return 'fa-times'; + } + + return $this->getDocumentIconIcon($ref); + } + + public function getViewAsIconColor(PhabricatorDocumentRef $ref) { + $can_complete = $this->canRenderCompleteDocument($ref); + + if (!$can_complete) { + return 'grey'; + } + + return null; + } + + public function getRenderURI(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if (!$file) { + throw new PhutilMethodNotImplementedException(); + } + + $engine_key = $this->getDocumentEngineKey(); + $file_phid = $file->getPHID(); + + return "/file/document/{$engine_key}/{$file_phid}/"; + } + + final public static function getEnginesForRef( + PhabricatorUser $viewer, + PhabricatorDocumentRef $ref) { + $engines = self::getAllEngines(); + + foreach ($engines as $key => $engine) { + $engine = id(clone $engine) + ->setViewer($viewer); + + if (!$engine->canRenderDocument($ref)) { + unset($engines[$key]); + continue; + } + + $engines[$key] = $engine; + } + + if (!$engines) { + throw new Exception(pht('No content engine can render this document.')); + } + + $vectors = array(); + foreach ($engines as $key => $usable_engine) { + $vectors[$key] = $usable_engine->newSortVector($ref); + } + $vectors = msortv($vectors, 'getSelf'); + + return array_select_keys($engines, array_keys($vectors)); + } + + protected function getByteLengthLimit() { + return (1024 * 1024 * 8); + } + + protected function canRenderCompleteDocument(PhabricatorDocumentRef $ref) { + $limit = $this->getByteLengthLimit(); + if ($limit) { + $length = $ref->getByteLength(); + if ($length > $limit) { + return false; + } + } + + return true; + } + + protected function canRenderPartialDocument(PhabricatorDocumentRef $ref) { + return false; + } + + protected function newMessage($message) { + return phutil_tag( + 'div', + array( + 'class' => 'document-engine-error', + ), + $message); + } + + final public function newLoadingContent(PhabricatorDocumentRef $ref) { + $spinner = id(new PHUIIconView()) + ->setIcon('fa-gear') + ->addClass('ph-spin'); + + return phutil_tag( + 'div', + array( + 'class' => 'document-engine-loading', + ), + array( + $spinner, + $this->getDocumentRenderingText($ref), + )); + } + +} diff --git a/src/applications/files/document/PhabricatorDocumentRef.php b/src/applications/files/document/PhabricatorDocumentRef.php new file mode 100644 index 0000000000..cca0c102e2 --- /dev/null +++ b/src/applications/files/document/PhabricatorDocumentRef.php @@ -0,0 +1,134 @@ +file = $file; + return $this; + } + + public function getFile() { + return $this->file; + } + + public function setMimeType($mime_type) { + $this->mimeType = $mime_type; + return $this; + } + + public function getMimeType() { + if ($this->mimeType !== null) { + return $this->mimeType; + } + + if ($this->file) { + return $this->file->getMimeType(); + } + + return null; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + if ($this->name !== null) { + return $this->name; + } + + if ($this->file) { + return $this->file->getName(); + } + + return null; + } + + public function setByteLength($length) { + $this->byteLength = $length; + return $this; + } + + public function getByteLength() { + if ($this->byteLength !== null) { + return $this->byteLength; + } + + if ($this->file) { + return (int)$this->file->getByteSize(); + } + + return null; + } + + public function loadData($begin = null, $end = null) { + if ($this->file) { + $iterator = $this->file->getFileDataIterator($begin, $end); + + $result = ''; + foreach ($iterator as $chunk) { + $result .= $chunk; + } + return $result; + } + + throw new PhutilMethodNotImplementedException(); + } + + public function hasAnyMimeType(array $candidate_types) { + $mime_full = $this->getMimeType(); + $mime_parts = explode(';', $mime_full); + + $mime_type = head($mime_parts); + $mime_type = $this->normalizeMimeType($mime_type); + + foreach ($candidate_types as $candidate_type) { + if ($this->normalizeMimeType($candidate_type) === $mime_type) { + return true; + } + } + + return false; + } + + private function normalizeMimeType($mime_type) { + $mime_type = trim($mime_type); + $mime_type = phutil_utf8_strtolower($mime_type); + return $mime_type; + } + + public function isProbablyText() { + $snippet = $this->getSnippet(); + return (strpos($snippet, "\0") === false); + } + + public function isProbablyJSON() { + if (!$this->isProbablyText()) { + return false; + } + + $snippet = $this->getSnippet(); + if (!preg_match('/^\s*[{[]/', $snippet)) { + return false; + } + + return phutil_is_utf8($snippet); + } + + public function getSnippet() { + if ($this->snippet === null) { + $this->snippet = $this->loadData(null, (1024 * 1024 * 1)); + } + + return $this->snippet; + } + +} diff --git a/src/applications/files/document/PhabricatorHexdumpDocumentEngine.php b/src/applications/files/document/PhabricatorHexdumpDocumentEngine.php new file mode 100644 index 0000000000..4217c24d62 --- /dev/null +++ b/src/applications/files/document/PhabricatorHexdumpDocumentEngine.php @@ -0,0 +1,114 @@ +getByteLengthLimit(); + $length = $ref->getByteLength(); + + $is_partial = false; + if ($limit) { + if ($length > $limit) { + $is_partial = true; + $length = $limit; + } + } + + $content = $ref->loadData(null, $length); + + $output = array(); + $offset = 0; + + $lines = str_split($content, 16); + foreach ($lines as $line) { + $output[] = sprintf( + '%08x %- 23s %- 23s %- 16s', + $offset, + $this->renderHex(substr($line, 0, 8)), + $this->renderHex(substr($line, 8)), + $this->renderBytes($line)); + + $offset += 16; + } + + $output = implode("\n", $output); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-hexdump PhabricatorMonospaced', + ), + $output); + + $message = null; + if ($is_partial) { + $message = $this->newMessage( + pht( + 'This document is too large to be completely rendered inline. The '. + 'first %s bytes are shown.', + new PhutilNumber($limit))); + } + + return array( + $message, + $container, + ); + } + + private function renderHex($bytes) { + $length = strlen($bytes); + + $output = array(); + for ($ii = 0; $ii < $length; $ii++) { + $output[] = sprintf('%02x', ord($bytes[$ii])); + } + + return implode(' ', $output); + } + + private function renderBytes($bytes) { + $length = strlen($bytes); + + $output = array(); + for ($ii = 0; $ii < $length; $ii++) { + $chr = $bytes[$ii]; + $ord = ord($chr); + + if ($ord < 0x20 || $ord >= 0x7F) { + $chr = '.'; + } + + $output[] = $chr; + } + + return implode('', $output); + } + +} diff --git a/src/applications/files/document/PhabricatorImageDocumentEngine.php b/src/applications/files/document/PhabricatorImageDocumentEngine.php new file mode 100644 index 0000000000..d0b2099dd0 --- /dev/null +++ b/src/applications/files/document/PhabricatorImageDocumentEngine.php @@ -0,0 +1,71 @@ +getFile(); + if ($file) { + return $file->isViewableImage(); + } + + $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); + $viewable_types = array_keys($viewable_types); + + $image_types = PhabricatorEnv::getEnvConfig('files.image-mime-types'); + $image_types = array_keys($image_types); + + return + $ref->hasAnyMimeType($viewable_types) && + $ref->hasAnyMimeType($image_types); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + // We could use a "data:" URI here. It's not yet clear if or when we'll + // have a ref but no backing file. + throw new PhutilMethodNotImplementedException(); + } + + $image = phutil_tag( + 'img', + array( + 'src' => $source_uri, + )); + + $linked_image = phutil_tag( + 'a', + array( + 'href' => $source_uri, + 'rel' => 'noreferrer', + ), + $image); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-image', + ), + $linked_image); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorJSONDocumentEngine.php b/src/applications/files/document/PhabricatorJSONDocumentEngine.php new file mode 100644 index 0000000000..331a7e6820 --- /dev/null +++ b/src/applications/files/document/PhabricatorJSONDocumentEngine.php @@ -0,0 +1,59 @@ +getName())) { + return 2000; + } + + if ($ref->isProbablyJSON()) { + return 1750; + } + + return 500; + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $raw_data = $this->loadTextData($ref); + + try { + $data = phutil_json_decode($raw_data); + + if (preg_match('/^\s*\[/', $raw_data)) { + $content = id(new PhutilJSON())->encodeAsList($data); + } else { + $content = id(new PhutilJSON())->encodeFormatted($data); + } + + $message = null; + $content = PhabricatorSyntaxHighlighter::highlightWithLanguage( + 'json', + $content); + } catch (PhutilJSONParserException $ex) { + $message = $this->newMessage( + pht( + 'This document is not valid JSON: %s', + $ex->getMessage())); + + $content = $raw_data; + } + + return array( + $message, + $this->newTextDocumentContent($content), + ); + } + +} diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php new file mode 100644 index 0000000000..f960f5c8c0 --- /dev/null +++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php @@ -0,0 +1,313 @@ +getName(); + + if (preg_match('/\\.ipynb\z/i', $name)) { + return 2000; + } + + return 500; + } + + protected function canRenderDocumentType(PhabricatorDocumentRef $ref) { + return $ref->isProbablyJSON(); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $viewer = $this->getViewer(); + $content = $ref->loadData(); + + try { + $data = phutil_json_decode($content); + } catch (PhutilJSONParserException $ex) { + return $this->newMessage( + pht( + 'This is not a valid JSON document and can not be rendered as '. + 'a Jupyter notebook: %s.', + $ex->getMessage())); + } + + if (!is_array($data)) { + return $this->newMessage( + pht( + 'This document does not encode a valid JSON object and can not '. + 'be rendered as a Jupyter notebook.')); + } + + + $nbformat = idx($data, 'nbformat'); + if (!strlen($nbformat)) { + return $this->newMessage( + pht( + 'This document is missing an "nbformat" field. Jupyter notebooks '. + 'must have this field.')); + } + + if ($nbformat !== 4) { + return $this->newMessage( + pht( + 'This Jupyter notebook uses an unsupported version of the file '. + 'format (found version %s, expected version 4).', + $nbformat)); + } + + $cells = idx($data, 'cells'); + if (!is_array($cells)) { + return $this->newMessage( + pht( + 'This Jupyter notebook does not specify a list of "cells".')); + } + + if (!$cells) { + return $this->newMessage( + pht( + 'This Jupyter notebook does not specify any notebook cells.')); + } + + $rows = array(); + foreach ($cells as $cell) { + $rows[] = $this->renderJupyterCell($viewer, $cell); + } + + $notebook_table = phutil_tag( + 'table', + array( + 'class' => 'jupyter-notebook', + ), + $rows); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-jupyter', + ), + $notebook_table); + + return $container; + } + + private function renderJupyterCell( + PhabricatorUser $viewer, + array $cell) { + + list($label, $content) = $this->renderJupyterCellContent($viewer, $cell); + + $label_cell = phutil_tag( + 'th', + array(), + $label); + + $content_cell = phutil_tag( + 'td', + array(), + $content); + + return phutil_tag( + 'tr', + array(), + array( + $label_cell, + $content_cell, + )); + } + + private function renderJupyterCellContent( + PhabricatorUser $viewer, + array $cell) { + + $cell_type = idx($cell, 'cell_type'); + switch ($cell_type) { + case 'markdown': + return $this->newMarkdownCell($cell); + case 'code': + return $this->newCodeCell($cell); + } + + return $this->newRawCell(id(new PhutilJSON())->encodeFormatted($cell)); + } + + private function newRawCell($content) { + return array( + null, + phutil_tag( + 'div', + array( + 'class' => 'jupyter-cell-raw PhabricatorMonospaced', + ), + $content), + ); + } + + private function newMarkdownCell(array $cell) { + $content = idx($cell, 'source'); + if (!is_array($content)) { + $content = array(); + } + + $content = implode('', $content); + $content = phutil_escape_html_newlines($content); + + return array( + null, + phutil_tag( + 'div', + array( + 'class' => 'jupyter-cell-markdown', + ), + $content), + ); + } + + private function newCodeCell(array $cell) { + $execution_count = idx($cell, 'execution_count'); + if ($execution_count) { + $label = 'In ['.$execution_count.']:'; + } else { + $label = null; + } + + $content = idx($cell, 'source'); + if (!is_array($content)) { + $content = array(); + } + + $content = implode('', $content); + + $content = PhabricatorSyntaxHighlighter::highlightWithLanguage( + 'python', + $content); + + $outputs = array(); + $output_list = idx($cell, 'outputs'); + if (is_array($output_list)) { + foreach ($output_list as $output) { + $outputs[] = $this->newOutput($output); + } + } + + return array( + $label, + array( + phutil_tag( + 'div', + array( + 'class' => 'jupyter-cell-code PhabricatorMonospaced remarkup-code', + ), + array( + $content, + )), + $outputs, + ), + ); + } + + private function newOutput(array $output) { + if (!is_array($output)) { + return pht(''); + } + + $classes = array( + 'jupyter-output', + 'PhabricatorMonospaced', + ); + + $output_name = idx($output, 'name'); + switch ($output_name) { + case 'stderr': + $classes[] = 'jupyter-output-stderr'; + break; + } + + $output_type = idx($output, 'output_type'); + switch ($output_type) { + case 'execute_result': + case 'display_data': + $data = idx($output, 'data'); + + $image_formats = array( + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + ); + + foreach ($image_formats as $image_format) { + if (!isset($data[$image_format])) { + continue; + } + + $raw_data = $data[$image_format]; + if (!is_array($raw_data)) { + continue; + } + + $raw_data = implode('', $raw_data); + + $content = phutil_tag( + 'img', + array( + 'src' => 'data:'.$image_format.';base64,'.$raw_data, + )); + + break 2; + } + + if (isset($data['text/html'])) { + $content = $data['text/html']; + $classes[] = 'jupyter-output-html'; + break; + } + + if (isset($data['application/javascript'])) { + $content = $data['application/javascript']; + $classes[] = 'jupyter-output-html'; + break; + } + + if (isset($data['text/plain'])) { + $content = $data['text/plain']; + break; + } + + break; + case 'stream': + default: + $content = idx($output, 'text'); + if (!is_array($content)) { + $content = array(); + } + $content = implode('', $content); + break; + } + + return phutil_tag( + 'div', + array( + 'class' => implode(' ', $classes), + ), + $content); + } + +} diff --git a/src/applications/files/document/PhabricatorPDFDocumentEngine.php b/src/applications/files/document/PhabricatorPDFDocumentEngine.php new file mode 100644 index 0000000000..1e85bd4ae5 --- /dev/null +++ b/src/applications/files/document/PhabricatorPDFDocumentEngine.php @@ -0,0 +1,57 @@ +hasAnyMimeType( + array( + 'application/pdf', + )); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $viewer = $this->getViewer(); + + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + throw new PhutilMethodNotImplementedException(); + } + + $name = $ref->getName(); + $length = $ref->getByteLength(); + + $link = id(new PhabricatorFileLinkView()) + ->setViewer($viewer) + ->setFileName($name) + ->setFileViewURI($source_uri) + ->setFileViewable(true) + ->setFileSize(phutil_format_bytes($length)); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-pdf', + ), + $link); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorRemarkupDocumentEngine.php b/src/applications/files/document/PhabricatorRemarkupDocumentEngine.php new file mode 100644 index 0000000000..296b78196f --- /dev/null +++ b/src/applications/files/document/PhabricatorRemarkupDocumentEngine.php @@ -0,0 +1,47 @@ +getName(); + if (preg_match('/\\.remarkup\z/i', $name)) { + return 2000; + } + + return 500; + } + + protected function canRenderDocumentType(PhabricatorDocumentRef $ref) { + return $ref->isProbablyText(); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $viewer = $this->getViewer(); + + $content = $ref->loadData(); + $content = phutil_utf8ize($content); + + $remarkup = new PHUIRemarkupView($viewer, $content); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-remarkup', + ), + $remarkup); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorSourceDocumentEngine.php b/src/applications/files/document/PhabricatorSourceDocumentEngine.php new file mode 100644 index 0000000000..1c3e54575a --- /dev/null +++ b/src/applications/files/document/PhabricatorSourceDocumentEngine.php @@ -0,0 +1,30 @@ +loadTextData($ref); + + $content = PhabricatorSyntaxHighlighter::highlightWithFilename( + $ref->getName(), + $content); + + return $this->newTextDocumentContent($content); + } + +} diff --git a/src/applications/files/document/PhabricatorTextDocumentEngine.php b/src/applications/files/document/PhabricatorTextDocumentEngine.php new file mode 100644 index 0000000000..4fb8d052e1 --- /dev/null +++ b/src/applications/files/document/PhabricatorTextDocumentEngine.php @@ -0,0 +1,33 @@ +isProbablyText(); + } + + protected function newTextDocumentContent($content) { + $lines = phutil_split_lines($content); + + $view = id(new PhabricatorSourceCodeView()) + ->setHighlights($this->getHighlightedLines()) + ->setLines($lines); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-text', + ), + $view); + + return $container; + } + + protected function loadTextData(PhabricatorDocumentRef $ref) { + $content = $ref->loadData(); + $content = phutil_utf8ize($content); + return $content; + } + +} diff --git a/src/applications/files/document/PhabricatorVideoDocumentEngine.php b/src/applications/files/document/PhabricatorVideoDocumentEngine.php new file mode 100644 index 0000000000..4e03c62ebc --- /dev/null +++ b/src/applications/files/document/PhabricatorVideoDocumentEngine.php @@ -0,0 +1,75 @@ +getFile(); + if ($file) { + return $file->isVideo(); + } + + $viewable_types = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); + $viewable_types = array_keys($viewable_types); + + $video_types = PhabricatorEnv::getEnvConfig('files.video-mime-types'); + $video_types = array_keys($video_types); + + return + $ref->hasAnyMimeType($viewable_types) && + $ref->hasAnyMimeType($video_types); + } + + protected function newDocumentContent(PhabricatorDocumentRef $ref) { + $file = $ref->getFile(); + if ($file) { + $source_uri = $file->getViewURI(); + } else { + throw new PhutilMethodNotImplementedException(); + } + + $mime_type = $ref->getMimeType(); + + $video = phutil_tag( + 'video', + array( + 'controls' => 'controls', + ), + phutil_tag( + 'source', + array( + 'src' => $source_uri, + 'type' => $mime_type, + ))); + + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-video', + ), + $video); + + return $container; + } + +} diff --git a/src/applications/files/document/PhabricatorVoidDocumentEngine.php b/src/applications/files/document/PhabricatorVoidDocumentEngine.php new file mode 100644 index 0000000000..c5395632eb --- /dev/null +++ b/src/applications/files/document/PhabricatorVoidDocumentEngine.php @@ -0,0 +1,42 @@ + 'document-engine-message', + ), + $message); + + return $container; + } + +} diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 60b4ca0e74..a8d16b7651 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -930,6 +930,19 @@ final class PhabricatorFile extends PhabricatorFileDAO return idx($mime_map, $mime_type); } + public function isPDF() { + if (!$this->isViewableInBrowser()) { + return false; + } + + $mime_map = array( + 'application/pdf' => 'application/pdf', + ); + + $mime_type = $this->getMimeType(); + return idx($mime_map, $mime_type); + } + public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php index 8ad76987f9..7b626fff32 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php @@ -98,54 +98,66 @@ final class PhabricatorRepositoryPushLogSearchEngine $viewer = $this->requireViewer(); $fields = array( - $fields[] = id(new PhabricatorIDExportField()) + id(new PhabricatorIDExportField()) ->setKey('pushID') ->setLabel(pht('Push ID')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) + ->setKey('unique') + ->setLabel(pht('Unique')), + id(new PhabricatorStringExportField()) ->setKey('protocol') ->setLabel(pht('Protocol')), - $fields[] = id(new PhabricatorPHIDExportField()) + id(new PhabricatorPHIDExportField()) ->setKey('repositoryPHID') ->setLabel(pht('Repository PHID')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('repository') ->setLabel(pht('Repository')), - $fields[] = id(new PhabricatorPHIDExportField()) + id(new PhabricatorPHIDExportField()) ->setKey('pusherPHID') ->setLabel(pht('Pusher PHID')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('pusher') ->setLabel(pht('Pusher')), - $fields[] = id(new PhabricatorPHIDExportField()) + id(new PhabricatorPHIDExportField()) ->setKey('devicePHID') ->setLabel(pht('Device PHID')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('device') ->setLabel(pht('Device')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('type') ->setLabel(pht('Ref Type')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('name') ->setLabel(pht('Ref Name')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('old') ->setLabel(pht('Ref Old')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('new') ->setLabel(pht('Ref New')), - $fields[] = id(new PhabricatorIntExportField()) + id(new PhabricatorIntExportField()) ->setKey('flags') ->setLabel(pht('Flags')), - $fields[] = id(new PhabricatorStringListExportField()) + id(new PhabricatorStringListExportField()) ->setKey('flagNames') ->setLabel(pht('Flag Names')), - $fields[] = id(new PhabricatorIntExportField()) + id(new PhabricatorIntExportField()) ->setKey('result') ->setLabel(pht('Result')), - $fields[] = id(new PhabricatorStringExportField()) + id(new PhabricatorStringExportField()) ->setKey('resultName') ->setLabel(pht('Result Name')), + id(new PhabricatorIntExportField()) + ->setKey('writeWait') + ->setLabel(pht('Write Wait (us)')), + id(new PhabricatorIntExportField()) + ->setKey('readWait') + ->setLabel(pht('Read Wait (us)')), + id(new PhabricatorIntExportField()) + ->setKey('hostWait') + ->setLabel(pht('Host Wait (us)')), ); if ($viewer->getIsAdmin()) { @@ -209,6 +221,7 @@ final class PhabricatorRepositoryPushLogSearchEngine $map = array( 'pushID' => $event->getID(), + 'unique' => $event->getRequestIdentifier(), 'protocol' => $event->getRemoteProtocol(), 'repositoryPHID' => $repository_phid, 'repository' => $repository_name, @@ -224,6 +237,9 @@ final class PhabricatorRepositoryPushLogSearchEngine 'flagNames' => $flag_names, 'result' => $result, 'resultName' => $result_name, + 'writeWait' => $event->getWriteWait(), + 'readWait' => $event->getReadWait(), + 'hostWait' => $event->getHostWait(), ); if ($viewer->getIsAdmin()) { diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php index 2bc751ffca..451f8acda5 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php @@ -11,10 +11,14 @@ final class PhabricatorRepositoryPushEvent protected $repositoryPHID; protected $epoch; protected $pusherPHID; + protected $requestIdentifier; protected $remoteAddress; protected $remoteProtocol; protected $rejectCode; protected $rejectDetails; + protected $writeWait; + protected $readWait; + protected $hostWait; private $repository = self::ATTACHABLE; private $logs = self::ATTACHABLE; @@ -29,15 +33,23 @@ final class PhabricatorRepositoryPushEvent self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( + 'requestIdentifier' => 'bytes12?', 'remoteAddress' => 'ipaddress?', 'remoteProtocol' => 'text32?', 'rejectCode' => 'uint32', 'rejectDetails' => 'text64?', + 'writeWait' => 'uint64?', + 'readWait' => 'uint64?', + 'hostWait' => 'uint64?', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), + 'key_request' => array( + 'columns' => array('requestIdentifier'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php b/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php index 750cc91c47..da5d54b57d 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php +++ b/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php @@ -28,6 +28,17 @@ final class PhabricatorRepositoryWorkingCopyVersion ) + parent::getConfiguration(); } + public function getWriteProperty($key, $default = null) { + // The "writeProperties" don't currently get automatically serialized or + // deserialized. Perhaps they should. + try { + $properties = phutil_json_decode($this->writeProperties); + return idx($properties, $key, $default); + } catch (Exception $ex) { + return null; + } + } + public static function loadVersions($repository_phid) { $version = new self(); $conn_w = $version->establishConnection('w'); @@ -43,6 +54,27 @@ final class PhabricatorRepositoryWorkingCopyVersion return $version->loadAllFromArray($rows); } + public static function loadWriter($repository_phid) { + $version = new self(); + $conn_w = $version->establishConnection('w'); + $table = $version->getTableName(); + + // We're forcing this read to go to the master. + $row = queryfx_one( + $conn_w, + 'SELECT * FROM %T WHERE repositoryPHID = %s AND isWriting = 1 + LIMIT 1', + $table, + $repository_phid); + + if (!$row) { + return null; + } + + return $version->loadFromArray($row); + } + + public static function getReadLock($repository_phid, $device_phid) { $repository_hash = PhabricatorHash::digestForIndex($repository_phid); $device_hash = PhabricatorHash::digestForIndex($device_phid); diff --git a/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php b/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php index a7d9570c57..fa03bcafdd 100644 --- a/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php +++ b/src/applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php @@ -104,7 +104,7 @@ final class PhabricatorFerretFulltextStorageEngine // Reorder the results so that the highest-ranking results come first, // no matter which object types they belong to. - $metadata = msort($metadata, 'getRelevanceSortVector'); + $metadata = msortv($metadata, 'getRelevanceSortVector'); $list = array_select_keys($list, array_keys($metadata)) + $list; $result_slice = array_slice($list, $offset, $limit, true); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 26be0f87af..8184eafb50 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3199,6 +3199,11 @@ abstract class PhabricatorApplicationTransactionEditor $story_type = $this->getFeedStoryType(); $story_data = $this->getFeedStoryData($object, $xactions); + $unexpandable_phids = $this->mailUnexpandablePHIDs; + if (!is_array($unexpandable_phids)) { + $unexpandable_phids = array(); + } + id(new PhabricatorFeedStoryPublisher()) ->setStoryType($story_type) ->setStoryData($story_data) @@ -3207,6 +3212,7 @@ abstract class PhabricatorApplicationTransactionEditor ->setRelatedPHIDs($related_phids) ->setPrimaryObjectPHID($object->getPHID()) ->setSubscribedPHIDs($subscribed_phids) + ->setUnexpandablePHIDs($unexpandable_phids) ->setMailRecipientPHIDs($mailed_phids) ->setMailTags($this->getMailTags($object, $xactions)) ->publish(); diff --git a/src/docs/user/userguide/remarkup.diviner b/src/docs/user/userguide/remarkup.diviner index 28a1a31fc4..d052a9a248 100644 --- a/src/docs/user/userguide/remarkup.diviner +++ b/src/docs/user/userguide/remarkup.diviner @@ -530,7 +530,7 @@ This renders: {icon camera color=blue} For a list of available icons and colors, check the UIExamples application. (The icons are sourced from -[[ http://fortawesome.github.io/Font-Awesome/ | FontAwesome ]], so you can also +[[ https://fontawesome.com/v4.7.0/icons/ | FontAwesome ]], so you can also browse the collection there.) You can add `spin` to make the icon spin: diff --git a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php index 6b6222304c..f7739f1332 100644 --- a/src/infrastructure/ssh/PhabricatorSSHWorkflow.php +++ b/src/infrastructure/ssh/PhabricatorSSHWorkflow.php @@ -13,6 +13,7 @@ abstract class PhabricatorSSHWorkflow private $errorChannel; private $isClusterRequest; private $originalArguments; + private $requestIdentifier; public function isExecutable() { return false; @@ -89,6 +90,15 @@ abstract class PhabricatorSSHWorkflow return $this->originalArguments; } + public function setRequestIdentifier($request_identifier) { + $this->requestIdentifier = $request_identifier; + return $this; + } + + public function getRequestIdentifier() { + return $this->requestIdentifier; + } + public function getSSHRemoteAddress() { $ssh_client = getenv('SSH_CLIENT'); if (!strlen($ssh_client)) { diff --git a/src/view/layout/PhabricatorFileLinkView.php b/src/view/layout/PhabricatorFileLinkView.php index 82715c0294..49320c4b8c 100644 --- a/src/view/layout/PhabricatorFileLinkView.php +++ b/src/view/layout/PhabricatorFileLinkView.php @@ -101,26 +101,39 @@ final class PhabricatorFileLinkView extends AphrontTagView { } protected function getTagName() { - return 'div'; + if ($this->getFileDownloadURI()) { + return 'div'; + } else { + return 'a'; + } } protected function getTagAttributes() { - $mustcapture = true; - $sigil = 'lightboxable'; - $meta = $this->getMeta(); - $class = 'phabricator-remarkup-embed-layout-link'; if ($this->getCustomClass()) { $class = $this->getCustomClass(); } - return array( - 'href' => $this->getFileViewURI(), - 'class' => $class, - 'sigil' => $sigil, - 'meta' => $meta, - 'mustcapture' => $mustcapture, + $attributes = array( + 'href' => $this->getFileViewURI(), + 'target' => '_blank', + 'rel' => 'noreferrer', + 'class' => $class, ); + + if ($this->getFilePHID()) { + $mustcapture = true; + $sigil = 'lightboxable'; + $meta = $this->getMeta(); + + $attributes += array( + 'sigil' => $sigil, + 'meta' => $meta, + 'mustcapture' => $mustcapture, + ); + } + + return $attributes; } protected function getTagContent() { @@ -131,16 +144,21 @@ final class PhabricatorFileLinkView extends AphrontTagView { ->setIcon($this->getFileIcon()) ->addClass('phabricator-remarkup-embed-layout-icon'); - $dl_icon = id(new PHUIIconView()) - ->setIcon('fa-download'); + $download_link = null; - $download_link = phutil_tag( - 'a', - array( - 'class' => 'phabricator-remarkup-embed-layout-download', - 'href' => $this->getFileDownloadURI(), - ), - pht('Download')); + $download_uri = $this->getFileDownloadURI(); + if ($download_uri) { + $dl_icon = id(new PHUIIconView()) + ->setIcon('fa-download'); + + $download_link = phutil_tag( + 'a', + array( + 'class' => 'phabricator-remarkup-embed-layout-download', + 'href' => $download_uri, + ), + pht('Download')); + } $info = phutil_tag( 'span', diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php index f5f6c22b51..ce26fa3df7 100644 --- a/src/view/layout/PhabricatorSourceCodeView.php +++ b/src/view/layout/PhabricatorSourceCodeView.php @@ -85,7 +85,11 @@ final class PhabricatorSourceCodeView extends AphrontView { } if ($this->canClickHighlight) { - $line_href = $base_uri.'$'.$line_number; + if ($base_uri) { + $line_href = $base_uri.'$'.$line_number; + } else { + $line_href = null; + } $tag_number = phutil_tag( 'a', diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 53ec096265..4ac4b2de54 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -307,9 +307,14 @@ final class PHUIHeaderView extends AphrontTagView { $icon = null; if ($this->headerIcon) { - $icon = id(new PHUIIconView()) - ->setIcon($this->headerIcon) - ->addClass('phui-header-icon'); + if ($this->headerIcon instanceof PHUIIconView) { + $icon = id(clone $this->headerIcon) + ->addClass('phui-header-icon'); + } else { + $icon = id(new PHUIIconView()) + ->setIcon($this->headerIcon) + ->addClass('phui-header-icon'); + } } $header_content = $this->header; diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index a51c1ce1d9..949deadbe4 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -405,8 +405,9 @@ video.phabricator-media { color: {$blacktext}; min-width: 256px; position: relative; - /*height: 22px;*/ line-height: 20px; + overflow: hidden; + min-height: 38px; } .phabricator-remarkup-embed-layout-icon { @@ -426,6 +427,9 @@ video.phabricator-media { .phabricator-remarkup-embed-layout-link:hover { border-color: {$violet}; cursor: pointer; +} + +.device-desktop .phabricator-remarkup-embed-layout-link:hover { text-decoration: none; } diff --git a/webroot/rsrc/css/layout/phabricator-source-code-view.css b/webroot/rsrc/css/layout/phabricator-source-code-view.css index 446366cdf1..1836d321a5 100644 --- a/webroot/rsrc/css/layout/phabricator-source-code-view.css +++ b/webroot/rsrc/css/layout/phabricator-source-code-view.css @@ -14,14 +14,6 @@ margin-left: 8px; } -.phabricator-source-code-view tr:first-child * { - padding-top: 8px; -} - -.phabricator-source-code-view tr:last-child * { - padding-bottom: 8px; -} - .phabricator-source-code { white-space: pre-wrap; padding: 2px 8px 1px; @@ -45,12 +37,16 @@ white-space: nowrap; } -th.phabricator-source-line a { - color: {$darkbluetext}; +th.phabricator-source-line a, +th.phabricator-source-line span { display: block; padding: 2px 6px 1px 12px; } +th.phabricator-source-line a { + color: {$darkbluetext}; +} + th.phabricator-source-line a:hover { background: {$paste.border}; text-decoration: none; @@ -60,10 +56,6 @@ th.phabricator-source-line a:hover { background: {$paste.highlight}; } -.phabricator-source-highlight th.phabricator-source-line { - background: {$paste.border}; -} - .phabricator-source-code-summary { padding-bottom: 8px; } diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css index 16a78e219c..15632c902f 100644 --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -149,24 +149,6 @@ div.phui-property-list-stacked .phui-property-list-properties } -.phui-property-list-image { - margin: auto; - max-width: 95%; -} - -.phui-property-list-audio { - display: block; - margin: 16px auto; - width: 50%; - min-width: 240px; -} - -.phui-property-list-video { - display: block; - margin: 0 auto; - max-width: 95%; -} - /* When tags appear in property lists, give them a little more vertical spacing. */ .phui-property-list-value .phui-tag-view { @@ -220,3 +202,126 @@ div.phui-property-list-stacked .phui-property-list-properties border-right: 1px solid {$lightblueborder}; border-bottom: 1px solid {$blueborder}; } + + +.document-engine-image img { + margin: 20px auto; + background: url('/rsrc/image/checker_light.png'); +} + +.device-desktop .document-engine-image img:hover { + background: url('/rsrc/image/checker_dark.png'); +} + +.document-engine-video video { + margin: 20px auto; + display: block; + max-width: 95%; +} + +.document-engine-audio audio { + display: block; + margin: 16px auto; + width: 50%; + min-width: 240px; +} + +.document-engine-message { + margin: 20px auto; + text-align: center; + color: {$greytext}; +} + +.document-engine-error { + margin: 20px; + padding: 12px; + text-align: center; + color: {$redtext}; + background: {$sh-redbackground}; +} + +.document-engine-hexdump { + margin: 20px; + white-space: pre; +} + +.document-engine-remarkup { + margin: 20px; +} + +.document-engine-pdf { + margin: 20px; + text-align: center; +} + +.document-engine-pdf .phabricator-remarkup-embed-layout-link { + text-align: left; +} + +.document-engine-text .phabricator-source-code-container { + border: none; +} + +.document-engine-jupyter { + overflow: hidden; + margin: 20px; +} + +.document-engine-in-flight { + opacity: 0.25; +} + +.document-engine-loading { + margin: 20px; + text-align: center; + color: {$lightgreytext}; +} + +.document-engine-loading .phui-icon-view { + display: block; + font-size: 48px; + color: {$lightgreyborder}; + padding: 8px; +} + +.jupyter-cell-raw { + white-space: pre-wrap; + background: {$lightgreybackground}; + color: {$greytext}; + padding: 8px; +} + +.jupyter-cell-code { + white-space: pre-wrap; + background: {$lightgreybackground}; + padding: 8px; + border: 1px solid {$lightgreyborder}; + border-radius: 2px; +} + +.jupyter-notebook > tbody > tr > th, +.jupyter-notebook > tbody > tr > td { + padding: 8px; +} + +.jupyter-notebook > tbody > tr > th { + white-space: nowrap; + text-align: right; + min-width: 48px; + font-weight: bold; +} + +.jupyter-output { + margin: 4px 0; + padding: 8px; + white-space: pre-wrap; + word-break: break-all; +} + +.jupyter-output-stderr { + background: {$sh-redbackground}; +} + +.jupyter-output-html { + background: {$sh-indigobackground}; +} diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js index 995f204c5d..d767a58780 100644 --- a/webroot/rsrc/externals/javelin/lib/Workflow.js +++ b/webroot/rsrc/externals/javelin/lib/Workflow.js @@ -59,12 +59,15 @@ JX.install('Workflow', { workflow.setDataWithListOfPairs(pairs); workflow.setMethod(form.getAttribute('method')); - workflow.listen('finally', function() { - // Re-enable form elements - for (var ii = 0; ii < inputs.length; ii++) { - inputs[ii] && (inputs[ii].disabled = false); + + var onfinally = JX.bind(workflow, function() { + if (!this._keepControlsDisabled) { + for (var ii = 0; ii < inputs.length; ii++) { + inputs[ii] && (inputs[ii].disabled = false); + } } }); + workflow.listen('finally', onfinally); return workflow; }, @@ -148,6 +151,11 @@ JX.install('Workflow', { // NOTE: Don't remove the current dialog yet because additional // handlers may still want to access the nodes. + // Disable whatever button the user clicked to prevent duplicate + // submission mistakes when you accidentally click a button multiple + // times. See T11145. + button.disabled = true; + active .setURI(form.getAttribute('action') || active.getURI()) .setDataWithListOfPairs(data) @@ -242,6 +250,7 @@ JX.install('Workflow', { _form: null, _paused: 0, _nextCallback: null, + _keepControlsDisabled: false, getSourceForm: function() { return this._form; @@ -283,6 +292,9 @@ JX.install('Workflow', { this._pop(); } + // If we're redirecting, don't re-enable for controls. + this._keepControlsDisabled = true; + JX.$U(r.redirect).go(); } else if (r && r.dialog) { this._push(); diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js new file mode 100644 index 0000000000..4cb7d17723 --- /dev/null +++ b/webroot/rsrc/js/application/files/behavior-document-engine.js @@ -0,0 +1,143 @@ +/** + * @provides javelin-behavior-document-engine + * @requires javelin-behavior + * javelin-dom + * javelin-stratcom + */ + +JX.behavior('document-engine', function(config, statics) { + + + + function onmenu(e) { + var node = e.getNode('document-engine-view-dropdown'); + var data = JX.Stratcom.getData(node); + + if (data.menu) { + return; + } + + e.prevent(); + + var menu = new JX.PHUIXDropdownMenu(node); + var list = new JX.PHUIXActionListView(); + + var view; + var engines = []; + for (var ii = 0; ii < data.views.length; ii++) { + var spec = data.views[ii]; + + view = new JX.PHUIXActionView() + .setName(spec.name) + .setIcon(spec.icon) + .setIconColor(spec.color) + .setHref(spec.engineURI); + + view.setHandler(JX.bind(null, function(spec, e) { + if (!e.isNormalClick()) { + return; + } + + e.prevent(); + menu.close(); + + onview(data, spec, false); + }, spec)); + + list.addItem(view); + + engines.push({ + spec: spec, + view: view + }); + } + + menu.setContent(list.getNode()); + + menu.listen('open', function() { + for (var ii = 0; ii < engines.length; ii++) { + var engine = engines[ii]; + + // Highlight the current rendering engine. + var is_selected = (engine.spec.viewKey == data.viewKey); + engine.view.setSelected(is_selected); + } + }); + + data.menu = menu; + menu.open(); + } + + function onview(data, spec, immediate) { + data.sequence = (data.sequence || 0) + 1; + var handler = JX.bind(null, onrender, data, data.sequence); + + data.viewKey = spec.viewKey; + JX.History.replace(spec.viewURI); + + new JX.Request(spec.engineURI, handler) + .send(); + + if (data.loadingView) { + // If we're already showing "Loading...", immediately change it to + // show the new document type. + onloading(data, spec); + } else if (!immediate) { + // Otherwise, grey out the document and show "Loading..." after a + // short delay. This prevents the content from flickering when rendering + // is fast. + var viewport = JX.$(data.viewportID); + JX.DOM.alterClass(viewport, 'document-engine-in-flight', true); + + var load = JX.bind(null, onloading, data, spec); + data.loadTimer = setTimeout(load, 333); + } + } + + function onloading(data, spec) { + data.loadingView = true; + + var viewport = JX.$(data.viewportID); + JX.DOM.alterClass(viewport, 'document-engine-in-flight', false); + JX.DOM.setContent(viewport, JX.$H(spec.loadingMarkup)); + } + + function onrender(data, sequence, r) { + // If this isn't the most recent request we sent, throw it away. This can + // happen if the user makes multiple selections from the menu while we are + // still rendering the first view. + if (sequence != data.sequence) { + return; + } + + if (data.loadTimer) { + clearTimeout(data.loadTimer); + data.loadTimer = null; + } + + var viewport = JX.$(data.viewportID); + + JX.DOM.alterClass(viewport, 'document-engine-in-flight', false); + data.loadingView = false; + + JX.DOM.setContent(viewport, JX.$H(r.markup)); + } + + if (!statics.initialized) { + JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu); + statics.initialized = true; + } + + if (config.renderControlID) { + var control = JX.$(config.renderControlID); + var data = JX.Stratcom.getData(control); + + for (var ii = 0; ii < data.views.length; ii++) { + if (data.views[ii].viewKey == data.viewKey) { + onview(data, data.views[ii], true); + break; + } + } + } + +}); diff --git a/webroot/rsrc/js/core/behavior-line-linker.js b/webroot/rsrc/js/core/behavior-line-linker.js index a0f99fb3bc..8a68cec842 100644 --- a/webroot/rsrc/js/core/behavior-line-linker.js +++ b/webroot/rsrc/js/core/behavior-line-linker.js @@ -145,6 +145,10 @@ JX.behavior('phabricator-line-linker', function() { var t = getRowNumber(target); var uri = JX.Stratcom.getData(root).uri; + if (!uri) { + uri = ('' + window.location).split('$')[0]; + } + origin = null; target = null; root = null; diff --git a/webroot/rsrc/js/phuix/PHUIXActionView.js b/webroot/rsrc/js/phuix/PHUIXActionView.js index 2db58dbdc4..7967fa7366 100644 --- a/webroot/rsrc/js/phuix/PHUIXActionView.js +++ b/webroot/rsrc/js/phuix/PHUIXActionView.js @@ -12,6 +12,7 @@ JX.install('PHUIXActionView', { _node: null, _name: null, _icon: 'none', + _iconColor: null, _disabled: false, _label: false, _handler: null, @@ -79,6 +80,12 @@ JX.install('PHUIXActionView', { return this; }, + setIconColor: function(color) { + this._iconColor = color; + this._buildIconNode(true); + return this; + }, + setHref: function(href) { this._href = href; this._buildNameNode(true); @@ -129,6 +136,10 @@ JX.install('PHUIXActionView', { icon_class = icon_class + ' grey'; } + if (this._iconColor) { + icon_class = icon_class + ' ' + this._iconColor; + } + JX.DOM.alterClass(node, icon_class, true); if (this._iconNode && this._iconNode.parentNode) {