1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-10 06:41:04 +01:00

(stable) Promote 2018 Week 9

This commit is contained in:
epriestley 2018-03-02 10:52:18 -08:00
commit ae7236c7a4
77 changed files with 3567 additions and 670 deletions

View file

@ -10,7 +10,7 @@ return array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => '2fa91e14',
'core.pkg.js' => 'dc13d4b7',
'core.pkg.js' => 'a3ceffdb',
'darkconsole.pkg.js' => '1f9a31bc',
'differential.pkg.css' => '113e692c',
'differential.pkg.js' => 'f6d809c0',
@ -78,7 +78,7 @@ return array(
'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948',
'rsrc/css/application/flag/flag.css' => 'bba8f811',
'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4',
'rsrc/css/application/harbormaster/harbormaster.css' => '730a4a3c',
'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
'rsrc/css/application/herald/herald.css' => 'cd8d0134',
'rsrc/css/application/maniphest/report.css' => '9b9580b7',
@ -122,7 +122,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' => 'aea41829',
'rsrc/css/layout/phabricator-source-code-view.css' => '926ced2d',
'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',
@ -211,12 +211,12 @@ return array(
'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2',
'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742',
'rsrc/externals/javelin/core/Event.js' => '2ee659ce',
'rsrc/externals/javelin/core/Stratcom.js' => '6ad39b6f',
'rsrc/externals/javelin/core/Stratcom.js' => '327f418a',
'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4',
'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85',
'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313',
'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d',
'rsrc/externals/javelin/core/init.js' => '3010e992',
'rsrc/externals/javelin/core/init.js' => '638a4e2b',
'rsrc/externals/javelin/core/init_node.js' => 'c234aded',
'rsrc/externals/javelin/core/install.js' => '05270951',
'rsrc/externals/javelin/core/util.js' => '93cc50d6',
@ -255,7 +255,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' => '1e911d0f',
'rsrc/externals/javelin/lib/Workflow.js' => '0eb1db0c',
'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68',
@ -416,6 +416,7 @@ return array(
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef',
'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',
'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e',
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
@ -493,8 +494,8 @@ return array(
'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64',
'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => '560f41da',
'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'e31fad01',
'rsrc/js/core/behavior-line-linker.js' => 'a9b946f8',
'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0',
'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
@ -578,7 +579,7 @@ return array(
'font-fontawesome' => 'e838e088',
'font-lato' => 'c7ccd872',
'global-drag-and-drop-css' => 'b556a948',
'harbormaster-css' => 'f491c9f4',
'harbormaster-css' => '730a4a3c',
'herald-css' => 'cd8d0134',
'herald-rule-editor' => 'dca75c0e',
'herald-test-css' => 'a52e323e',
@ -635,12 +636,13 @@ return array(
'javelin-behavior-event-all-day' => 'b41537c9',
'javelin-behavior-fancy-datepicker' => 'ecf4e799',
'javelin-behavior-global-drag-and-drop' => '960f6a39',
'javelin-behavior-harbormaster-log' => '191b4909',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => 'a464fe03',
'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8499b6ab',
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => '560f41da',
'javelin-behavior-lightbox-attachments' => 'e31fad01',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
@ -656,7 +658,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' => '1499a8cb',
'javelin-behavior-phabricator-line-linker' => 'a9b946f8',
'javelin-behavior-phabricator-nav' => '836f966d',
'javelin-behavior-phabricator-notification-example' => '8ce821c5',
'javelin-behavior-phabricator-object-selector' => '77c1f0b0',
@ -720,7 +722,7 @@ return array(
'javelin-install' => '05270951',
'javelin-json' => '69adf288',
'javelin-leader' => '7f243deb',
'javelin-magical-init' => '3010e992',
'javelin-magical-init' => '638a4e2b',
'javelin-mask' => '8a41885b',
'javelin-quicksand' => '6b8ef10b',
'javelin-reactor' => '2b8de964',
@ -733,7 +735,7 @@ return array(
'javelin-router' => '29274e2b',
'javelin-scrollbar' => '9065f639',
'javelin-sound' => '949c0fe5',
'javelin-stratcom' => '6ad39b6f',
'javelin-stratcom' => '327f418a',
'javelin-tokenizer' => '8d3bc1b2',
'javelin-typeahead' => '70baed2f',
'javelin-typeahead-composite-source' => '503e17fd',
@ -755,7 +757,7 @@ return array(
'javelin-workboard-card' => 'c587b80f',
'javelin-workboard-column' => '758b4758',
'javelin-workboard-controller' => '26167537',
'javelin-workflow' => '1e911d0f',
'javelin-workflow' => '0eb1db0c',
'maniphest-report-css' => '9b9580b7',
'maniphest-task-edit-css' => 'fda62a9b',
'maniphest-task-summary-css' => '11cc5344',
@ -800,7 +802,7 @@ return array(
'phabricator-search-results-css' => '505dd8cf',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
'phabricator-source-code-view-css' => 'aea41829',
'phabricator-source-code-view-css' => '926ced2d',
'phabricator-standard-page-view' => '34ee718b',
'phabricator-textareautils' => '320810c8',
'phabricator-title' => '485aaa6c',
@ -975,6 +977,17 @@ 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',
@ -985,12 +998,6 @@ return array(
'javelin-dom',
'javelin-typeahead-normalizer',
),
'1499a8cb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'15d5ff71' => array(
'aphront-typeahead-control-css',
'phui-tag-view-css',
@ -1015,6 +1022,9 @@ return array(
'185bbd53' => array(
'javelin-install',
),
'191b4909' => array(
'javelin-behavior',
),
'1ad0a787' => array(
'javelin-install',
'javelin-reactor',
@ -1033,17 +1043,6 @@ return array(
'javelin-request',
'javelin-uri',
),
'1e911d0f' => array(
'javelin-stratcom',
'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
'javelin-util',
'javelin-mask',
'javelin-uri',
'javelin-routable',
),
'1f6794f6' => array(
'javelin-behavior',
'javelin-stratcom',
@ -1129,6 +1128,12 @@ return array(
'javelin-dom',
'javelin-workflow',
),
'327f418a' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'358b8c04' => array(
'javelin-install',
'javelin-util',
@ -1351,15 +1356,6 @@ return array(
'javelin-vector',
'javelin-dom',
),
'560f41da' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'58dea2fa' => array(
'javelin-install',
'javelin-util',
@ -1444,12 +1440,6 @@ return array(
'69adf288' => array(
'javelin-install',
),
'6ad39b6f' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'6b8ef10b' => array(
'javelin-install',
),
@ -1756,6 +1746,12 @@ return array(
'javelin-uri',
'phabricator-keyboard-shortcut',
),
'a9b946f8' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'a9f88de2' => array(
'javelin-behavior',
'javelin-dom',
@ -2065,6 +2061,15 @@ return array(
'javelin-dom',
'phabricator-draggable-list',
),
'e31fad01' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'e379b58e' => array(
'javelin-behavior',
'javelin-stratcom',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
ADD filePHID VARBINARY(64);

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
ADD byteLength BIGINT UNSIGNED NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
ADD chunkFormat VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildlog
SET chunkFormat = 'text' WHERE chunkFormat = '';

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
ADD lineMap LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildlog
SET lineMap = '[]' WHERE lineMap = '';

View file

@ -0,0 +1,5 @@
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk
ADD headOffset BIGINT UNSIGNED NOT NULL;
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlogchunk
ADD tailOffset BIGINT UNSIGNED NOT NULL;

View file

@ -652,10 +652,12 @@ phutil_register_library_map(array(
'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php',
'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php',
'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php',
'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php',
'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php',
'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php',
'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php',
'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php',
'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php',
'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php',
@ -801,9 +803,11 @@ phutil_register_library_map(array(
'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php',
'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php',
'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php',
'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php',
'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php',
@ -1227,8 +1231,15 @@ phutil_register_library_map(array(
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php',
'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php',
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php',
'HarbormasterBuildLogSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php',
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
@ -1306,11 +1317,14 @@ phutil_register_library_map(array(
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
'HarbormasterLogWorker' => 'applications/harbormaster/worker/HarbormasterLogWorker.php',
'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php',
'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
'HarbormasterManagementRebuildLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php',
'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php',
'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php',
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
@ -3030,7 +3044,6 @@ phutil_register_library_map(array(
'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php',
'PhabricatorFilesComposeAvatarExample' => 'applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.php',
'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php',
'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
@ -5859,10 +5872,12 @@ phutil_register_library_map(array(
'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBranchesController' => 'DiffusionController',
'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitController' => 'DiffusionController',
'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField',
@ -6011,9 +6026,11 @@ phutil_register_library_map(array(
'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField',
@ -6505,11 +6522,20 @@ phutil_register_library_map(array(
'HarbormasterBuildLog' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'HarbormasterBuildLogChunk' => 'HarbormasterDAO',
'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator',
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildLogRenderController' => 'HarbormasterController',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildLogView' => 'AphrontView',
'HarbormasterBuildLogViewController' => 'HarbormasterController',
'HarbormasterBuildMessage' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
@ -6609,11 +6635,14 @@ phutil_register_library_map(array(
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView',
'HarbormasterLogWorker' => 'HarbormasterWorker',
'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRebuildLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
@ -8598,7 +8627,6 @@ phutil_register_library_map(array(
'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorFilesBuiltinFile' => 'Phobject',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesComposeAvatarExample' => 'PhabricatorUIExample',
'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',

View file

@ -49,6 +49,54 @@ final class AphrontRequest extends Phobject {
return idx($this->uriData, $key, $default);
}
/**
* Read line range parameter data from the request.
*
* Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the
* URI to allow users to link to particular lines.
*
* @param string URI data key to pull line range information from.
* @param int|null Maximum length of the range.
* @return null|pair<int, int> Null, or beginning and end of the range.
*/
public function getURILineRange($key, $limit) {
$range = $this->getURIData($key);
if (!strlen($range)) {
return null;
}
$range = explode('-', $range, 2);
foreach ($range as $key => $value) {
$value = (int)$value;
if (!$value) {
// If either value is "0", discard the range.
return null;
}
$range[$key] = $value;
}
// If the range is like "$10", treat it like "$10-10".
if (count($range) == 1) {
$range[] = head($range);
}
// If the range is "$7-5", treat it like "$5-7".
if ($range[1] < $range[0]) {
$range = array_reverse($range);
}
// If the user specified something like "$1-999999999" and we have a limit,
// clamp it to a more reasonable range.
if ($limit !== null) {
if ($range[1] - $range[0] > $limit) {
$range[1] = $range[0] + $limit;
}
}
return $range;
}
public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;

View file

@ -8,6 +8,7 @@ class AphrontRedirectResponse extends AphrontResponse {
private $uri;
private $stackWhenCreated;
private $isExternal;
private $closeDialogBeforeRedirect;
public function setIsExternal($external) {
$this->isExternal = $external;
@ -37,6 +38,15 @@ class AphrontRedirectResponse extends AphrontResponse {
return PhabricatorEnv::getEnvConfig('debug.stop-on-redirect');
}
public function setCloseDialogBeforeRedirect($close) {
$this->closeDialogBeforeRedirect = $close;
return $this;
}
public function getCloseDialogBeforeRedirect() {
return $this->closeDialogBeforeRedirect;
}
public function getHeaders() {
$headers = array();
if (!$this->shouldStopForDebugging()) {

View file

@ -7,9 +7,11 @@ abstract class AphrontResponse extends Phobject {
private $canCDN;
private $responseCode = 200;
private $lastModified = null;
private $contentSecurityPolicyURIs;
private $disableContentSecurityPolicy;
protected $frameable;
public function setRequest($request) {
$this->request = $request;
return $this;
@ -19,6 +21,33 @@ abstract class AphrontResponse extends Phobject {
return $this->request;
}
final public function addContentSecurityPolicyURI($kind, $uri) {
if ($this->contentSecurityPolicyURIs === null) {
$this->contentSecurityPolicyURIs = array(
'script-src' => array(),
'connect-src' => array(),
'frame-src' => array(),
'form-action' => array(),
);
}
if (!isset($this->contentSecurityPolicyURIs[$kind])) {
throw new Exception(
pht(
'Unknown Content-Security-Policy URI kind "%s".',
$kind));
}
$this->contentSecurityPolicyURIs[$kind][] = (string)$uri;
return $this;
}
final public function setDisableContentSecurityPolicy($disable) {
$this->disableContentSecurityPolicy = $disable;
return $this;
}
/* -( Content )------------------------------------------------------------ */
@ -59,9 +88,128 @@ abstract class AphrontResponse extends Phobject {
);
}
$csp = $this->newContentSecurityPolicyHeader();
if ($csp !== null) {
$headers[] = array('Content-Security-Policy', $csp);
}
$headers[] = array('Referrer-Policy', 'no-referrer');
return $headers;
}
private function newContentSecurityPolicyHeader() {
if ($this->disableContentSecurityPolicy) {
return null;
}
$csp = array();
$cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
if ($cdn) {
$default = $this->newContentSecurityPolicySource($cdn);
} else {
// If an alternate file domain is not configured and the user is viewing
// a Phame blog on a custom domain or some other custom site, we'll still
// serve resources from the main site. Include the main site explicitly.
$base_uri = PhabricatorEnv::getURI('/');
$base_uri = $this->newContentSecurityPolicySource($base_uri);
$default = "'self' {$base_uri}";
}
$csp[] = "default-src {$default}";
// We use "data:" URIs to inline small images into CSS. This policy allows
// "data:" URIs to be used anywhere, but there doesn't appear to be a way
// to say that "data:" URIs are okay in CSS files but not in the document.
$csp[] = "img-src {$default} data:";
// We use inline style="..." attributes in various places, many of which
// are legitimate. We also currently use a <style> tag to implement the
// "Monospaced Font Preference" setting.
$csp[] = "style-src {$default} 'unsafe-inline'";
// On a small number of pages, including the Stripe workflow and the
// ReCAPTCHA challenge, we embed external Javascript directly.
$csp[] = $this->newContentSecurityPolicy('script-src', $default);
// We need to specify that we can connect to ourself in order for AJAX
// requests to work.
$csp[] = $this->newContentSecurityPolicy('connect-src', "'self'");
// DarkConsole and PHPAST both use frames to render some content.
$csp[] = $this->newContentSecurityPolicy('frame-src', "'self'");
// This is a more modern flavor of of "X-Frame-Options" and prevents
// clickjacking attacks where the page is included in a tiny iframe and
// the user is convinced to click a element on the page, which really
// clicks a dangerous button hidden under a picture of a cat.
if ($this->frameable) {
$csp[] = "frame-ancestors 'self'";
} else {
$csp[] = "frame-ancestors 'none'";
}
// Block relics of the old world: Flash, Java applets, and so on.
$csp[] = "object-src 'none'";
// Don't allow forms to submit offsite.
// This can result in some trickiness with file downloads if applications
// try to start downloads by submitting a dialog. Redirect to the file's
// download URI instead of submitting a form to it.
$csp[] = $this->newContentSecurityPolicy('form-action', "'self'");
// Block use of "<base>" to change the origin of relative URIs on the page.
$csp[] = "base-uri 'none'";
$csp = implode('; ', $csp);
return $csp;
}
private function newContentSecurityPolicy($type, $defaults) {
if ($defaults === null) {
$sources = array();
} else {
$sources = (array)$defaults;
}
$uris = $this->contentSecurityPolicyURIs;
if (isset($uris[$type])) {
foreach ($uris[$type] as $uri) {
$sources[] = $this->newContentSecurityPolicySource($uri);
}
}
$sources = array_unique($sources);
return $type.' '.implode(' ', $sources);
}
private function newContentSecurityPolicySource($uri) {
// Some CSP URIs are ultimately user controlled (like notification server
// URIs and CDN URIs) so attempt to stop an attacker from injecting an
// unsafe source (like 'unsafe-eval') into the CSP header.
$uri = id(new PhutilURI($uri))
->setPath(null)
->setFragment(null)
->setQueryParams(array());
$uri = (string)$uri;
if (preg_match('/[ ;\']/', $uri)) {
throw new Exception(
pht(
'Attempting to emit a response with an unsafe source ("%s") in the '.
'Content-Security-Policy header.',
$uri));
}
return $uri;
}
public function setCacheDurationInSeconds($duration) {
$this->cacheable = $duration;
return $this;

View file

@ -24,10 +24,12 @@ final class PhabricatorAuthSSHKeyGenerateController
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
$key_name = $default_name.'.key';
$file = PhabricatorFile::newFromFileData(
$private_key,
array(
'name' => $default_name.'.key',
'name' => $key_name,
'ttl.relative' => phutil_units('10 minutes in seconds'),
'viewPolicy' => $viewer->getPHID(),
));
@ -62,25 +64,33 @@ final class PhabricatorAuthSSHKeyGenerateController
->setContentSourceFromRequest($request)
->applyTransactions($key, $xactions);
// NOTE: We're disabling workflow on submit so the download works. We're
// disabling workflow on cancel so the page reloads, showing the new
// key.
$download_link = phutil_tag(
'a',
array(
'href' => $file->getDownloadURI(),
),
array(
id(new PHUIIconView())->setIcon('fa-download'),
' ',
pht('Download Private Key (%s)', $key_name),
));
$download_link = phutil_tag('strong', array(), $download_link);
// NOTE: We're disabling workflow on cancel so the page reloads, showing
// the new key.
return $this->newDialog()
->setTitle(pht('Download Private Key'))
->setDisableWorkflowOnCancel(true)
->setDisableWorkflowOnSubmit(true)
->setSubmitURI($file->getDownloadURI())
->appendParagraph(
pht(
'A keypair has been generated, and the public key has been '.
'added as a recognized key. Use the button below to download '.
'the private key.'))
'added as a recognized key.'))
->appendParagraph($download_link)
->appendParagraph(
pht(
'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.'))
->addSubmitButton(pht('Download Private Key'))
->setDisableWorkflowOnCancel(true)
->addCancelButton($cancel_uri, pht('Done'));
}

View file

@ -447,6 +447,13 @@ abstract class PhabricatorAuthProvider extends Phobject {
));
}
$static_response = CelerityAPI::getStaticResourceResponse();
$static_response->addContentSecurityPolicyURI('form-action', (string)$uri);
foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {
$static_response->addContentSecurityPolicyURI('form-action', $csp_uri);
}
return phabricator_form(
$viewer,
array(
@ -505,4 +512,8 @@ abstract class PhabricatorAuthProvider extends Phobject {
throw new PhutilMethodNotImplementedException();
}
protected function getContentSecurityPolicyFormActions() {
return array();
}
}

View file

@ -208,6 +208,9 @@ abstract class PhabricatorOAuth1AuthProvider
parent::willRenderLinkedAccount($viewer, $item, $account);
}
protected function getContentSecurityPolicyFormActions() {
return $this->getAdapter()->getContentSecurityPolicyFormActions();
}
/* -( Temporary Secrets )-------------------------------------------------- */

View file

@ -298,6 +298,7 @@ abstract class PhabricatorController extends AphrontController {
->setContent(
array(
'redirect' => $response->getURI(),
'close' => $response->getCloseDialogBeforeRedirect(),
));
}
}

View file

@ -17,6 +17,7 @@ final class CelerityStaticResourceResponse extends Phobject {
private $behaviors = array();
private $hasRendered = array();
private $postprocessorKey;
private $contentSecurityPolicyURIs = array();
public function __construct() {
if (isset($_REQUEST['__metablock__'])) {
@ -37,6 +38,15 @@ final class CelerityStaticResourceResponse extends Phobject {
return $this->metadataBlock.'_'.$id;
}
public function addContentSecurityPolicyURI($kind, $uri) {
$this->contentSecurityPolicyURIs[$kind][] = $uri;
return $this;
}
public function getContentSecurityPolicyURIMap() {
return $this->contentSecurityPolicyURIs;
}
public function getMetadataBlock() {
return $this->metadataBlock;
}
@ -196,23 +206,16 @@ final class CelerityStaticResourceResponse extends Phobject {
$type));
}
public function renderHTMLFooter() {
public function renderHTMLFooter($is_frameable) {
$this->metadataLocked = true;
$data = array();
if ($this->metadata) {
$json_metadata = AphrontResponse::encodeJSONForHTTPResponse(
$this->metadata);
$this->metadata = array();
} else {
$json_metadata = '{}';
}
// Even if there is no metadata on the page, Javelin uses the mergeData()
// call to start dispatching the event queue.
$data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '.
$json_metadata.');';
$merge_data = array(
'block' => $this->metadataBlock,
'data' => $this->metadata,
);
$this->metadata = array();
$onload = array();
$behavior_lists = array();
if ($this->behaviors) {
$behaviors = $this->behaviors;
$this->behaviors = array();
@ -241,24 +244,52 @@ final class CelerityStaticResourceResponse extends Phobject {
if (!$group) {
continue;
}
$group_json = AphrontResponse::encodeJSONForHTTPResponse(
$group);
$onload[] = 'JX.initBehaviors('.$group_json.')';
$behavior_lists[] = $group;
}
}
if ($onload) {
foreach ($onload as $func) {
$data[] = 'JX.onload(function(){'.$func.'});';
}
$initializers = array();
// Even if there is no metadata on the page, Javelin uses the mergeData()
// call to start dispatching the event queue, so we always want to include
// this initializer.
$initializers[] = array(
'kind' => 'merge',
'data' => $merge_data,
);
foreach ($behavior_lists as $behavior_list) {
$initializers[] = array(
'kind' => 'behaviors',
'data' => $behavior_list,
);
}
if ($data) {
$data = implode("\n", $data);
return self::renderInlineScript($data);
} else {
return '';
if ($is_frameable) {
$initializers[] = array(
'data' => 'frameable',
'kind' => (bool)$is_frameable,
);
}
$tags = array();
foreach ($initializers as $initializer) {
$data = $initializer['data'];
if (is_array($data)) {
$json_data = AphrontResponse::encodeJSONForHTTPResponse($data);
} else {
$json_data = json_encode($data);
}
$tags[] = phutil_tag(
'data',
array(
'data-javelin-init-kind' => $initializer['kind'],
'data-javelin-init-data' => $json_data,
));
}
return $tags;
}
public static function renderInlineScript($data) {

View file

@ -106,6 +106,11 @@ abstract class CelerityResourceController extends PhabricatorController {
$response = id(new AphrontFileResponse())
->setMimeType($type_map[$type]);
// The "Content-Security-Policy" header has no effect on the actual
// resources, only on the main request. Disable it on the resource
// responses to limit confusion.
$response->setDisableContentSecurityPolicy(true);
$range = AphrontRequest::getHTTPHeader('Range');
if (strlen($range)) {

View file

@ -1046,7 +1046,7 @@ final class DiffusionServeController extends DiffusionController {
// <https://github.com/github/git-lfs/issues/1088>
$no_authorization = 'Basic '.base64_encode('none');
$get_uri = $file->getCDNURI();
$get_uri = $file->getCDNURI('data');
$actions['download'] = array(
'href' => $get_uri,
'header' => array(

View file

@ -0,0 +1,38 @@
<?php
final class DiffusionCommitAuthorProjectsHeraldField
extends DiffusionCommitHeraldField {
const FIELDCONST = 'diffusion.commit.author.projects';
public function getHeraldFieldName() {
return pht("Author's projects");
}
public function getHeraldFieldValue($object) {
$adapter = $this->getAdapter();
$phid = $object->getCommitData()->getCommitDetail('authorPHID');
if (!$phid) {
return array();
}
$viewer = $adapter->getViewer();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($phid))
->execute();
return mpull($projects, 'getPHID');
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}

View file

@ -10,6 +10,8 @@ final class DiffusionCommitBranchesHeraldField
}
public function getHeraldFieldValue($object) {
$viewer = $this->getAdapter()->getViewer();
$commit = $object;
$repository = $object->getRepository();
@ -19,7 +21,7 @@ final class DiffusionCommitBranchesHeraldField
);
$result = id(new ConduitCall('diffusion.branchquery', $params))
->setUser(PhabricatorUser::getOmnipotentUser())
->setUser($viewer)
->execute();
$refs = DiffusionRepositoryRef::loadAllFromDictionaries($result);

View file

@ -0,0 +1,38 @@
<?php
final class DiffusionCommitCommitterProjectsHeraldField
extends DiffusionCommitHeraldField {
const FIELDCONST = 'diffusion.commit.committer.projects';
public function getHeraldFieldName() {
return pht("Committer's projects");
}
public function getHeraldFieldValue($object) {
$adapter = $this->getAdapter();
$phid = $object->getCommitData()->getCommitDetail('committerPHID');
if (!$phid) {
return array();
}
$viewer = $adapter->getViewer();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($phid))
->execute();
return mpull($projects, 'getPHID');
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}

View file

@ -0,0 +1,38 @@
<?php
final class DiffusionPreCommitContentAuthorProjectsHeraldField
extends DiffusionPreCommitContentHeraldField {
const FIELDCONST = 'diffusion.pre.commit.author.projects';
public function getHeraldFieldName() {
return pht("Author's projects");
}
public function getHeraldFieldValue($object) {
$adapter = $this->getAdapter();
$phid = $adapter->getAuthorPHID();
if (!$phid) {
return array();
}
$viewer = $adapter->getViewer();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($phid))
->execute();
return mpull($projects, 'getPHID');
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}

View file

@ -0,0 +1,38 @@
<?php
final class DiffusionPreCommitContentCommitterProjectsHeraldField
extends DiffusionPreCommitContentHeraldField {
const FIELDCONST = 'diffusion.pre.commit.committer.projects';
public function getHeraldFieldName() {
return pht("Committer's projects");
}
public function getHeraldFieldValue($object) {
$adapter = $this->getAdapter();
$phid = $adapter->getCommitterPHID();
if (!$phid) {
return array();
}
$viewer = $adapter->getViewer();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($phid))
->execute();
return mpull($projects, 'getPHID');
}
protected function getHeraldFieldStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new PhabricatorProjectDatasource();
}
}

View file

@ -6,7 +6,7 @@ final class DiffusionPreCommitContentPusherProjectsHeraldField
const FIELDCONST = 'diffusion.pre.content.pusher.projects';
public function getHeraldFieldName() {
return pht('Pusher projects');
return pht("Pusher's projects");
}
public function getHeraldFieldValue($object) {

View file

@ -6,7 +6,7 @@ final class DiffusionPreCommitRefPusherProjectsHeraldField
const FIELDCONST = 'diffusion.pre.ref.pusher.projects';
public function getHeraldFieldName() {
return pht('Pusher projects');
return pht("Pusher's projects");
}
public function getHeraldFieldValue($object) {

View file

@ -16,7 +16,7 @@ final class DiffusionPreCommitRefRepositoryProjectsHeraldField
}
protected function getHeraldFieldStandardType() {
return HeraldField::STANDARD_PHID_LIST;
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {

View file

@ -333,8 +333,12 @@ final class PhabricatorImageTransformer extends Phobject {
return null;
}
// NOTE: Empirically, the highest compression level (9) seems to take
// up to twice as long as the default compression level (6) but produce
// only slightly smaller files (10% on avatars, 3% on screenshots).
ob_start();
$result = imagepng($image, null, 9);
$result = imagepng($image, null, 6);
$output = ob_get_clean();
if (!$result) {

View file

@ -86,7 +86,6 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
'PhabricatorFileTransformListController',
'uploaddialog/(?P<single>single/)?'
=> 'PhabricatorFileUploadDialogController',
'download/(?P<phid>[^/]+)/' => 'PhabricatorFileDialogController',
'iconset/(?P<key>[^/]+)/' => array(
'select/' => 'PhabricatorFileIconSetSelectController',
),
@ -102,7 +101,7 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
private function getResourceSubroutes() {
return array(
'data/'.
'(?P<kind>data|download)/'.
'(?:@(?P<instance>[^/]+)/)?'.
'(?P<key>[^/]+)/'.
'(?P<phid>[^/]+)/'.
@ -133,7 +132,7 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
public function getQuicksandURIPatternBlacklist() {
return array(
'/file/data/.*',
'/file/(data|download)/.*',
);
}

View file

@ -7,8 +7,79 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
private $color;
private $border;
private $maps = array();
const VERSION = 'v1';
public function updateUser(PhabricatorUser $user) {
$username = $user->getUsername();
$image_map = $this->getMap('image');
$initial = phutil_utf8_strtoupper(substr($username, 0, 1));
$pack = $this->pickMap('pack', $username);
$icon = "alphanumeric/{$pack}/{$initial}.png";
if (!isset($image_map[$icon])) {
$icon = "alphanumeric/{$pack}/_default.png";
}
$border = $this->pickMap('border', $username);
$color = $this->pickMap('color', $username);
$data = $this->composeImage($color, $icon, $border);
$name = $this->getImageDisplayName($color, $icon, $border);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $name,
'profile' => true,
'canCDN' => true,
));
$user
->setDefaultProfileImagePHID($file->getPHID())
->setDefaultProfileImageVersion(self::VERSION)
->saveWithoutIndex();
unset($unguarded);
return $file;
}
private function getMap($map_key) {
if (!isset($this->maps[$map_key])) {
switch ($map_key) {
case 'pack':
$map = $this->newPackMap();
break;
case 'image':
$map = $this->newImageMap();
break;
case 'color':
$map = $this->newColorMap();
break;
case 'border':
$map = $this->newBorderMap();
break;
default:
throw new Exception(pht('Unknown map "%s".', $map_key));
}
$this->maps[$map_key] = $map;
}
return $this->maps[$map_key];
}
private function pickMap($map_key, $username) {
$map = $this->getMap($map_key);
$seed = $username.'_'.$map_key;
$key = PhabricatorHash::digestToRange($seed, 0, count($map) - 1);
return $map[$key];
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
@ -46,15 +117,22 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
}
public function getBuiltinDisplayName() {
$icon = $this->getIcon();
$color = $this->getColor();
$border = implode(',', $this->getBorder());
return $this->getImageDisplayName(
$this->getIcon(),
$this->getColor(),
$this->getBorder());
}
private function getImageDisplayName($icon, $color, $border) {
$border = implode(',', $border);
return "{$icon}-{$color}-{$border}.png";
}
public function loadBuiltinFileData() {
return $this->composeImage(
$this->getColor(), $this->getIcon(), $this->getBorder());
$this->getColor(),
$this->getIcon(),
$this->getBorder());
}
private function composeImage($color, $image, $border) {
@ -68,7 +146,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
$color_const = hexdec(trim($color, '#'));
$true_border = self::rgba2gd($border);
$image_map = self::getImageMap();
$image_map = $this->getMap('image');
$data = Filesystem::readFile($image_map[$image]);
$img = imagecreatefromstring($data);
@ -111,10 +189,10 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
$b = $rgba[2];
$a = $rgba[3];
$a = (1 - $a) * 255;
return ($a << 24) | ($r << 16) | ($g << 8) | $b;
return ($a << 24) | ($r << 16) | ($g << 8) | $b;
}
public static function getImageMap() {
private function newImageMap() {
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/';
@ -131,64 +209,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
return $map;
}
public function getUniqueProfileImage($username) {
$pack_map = $this->getImagePackMap();
$image_map = $this->getImageMap();
$color_map = $this->getColorMap();
$border_map = $this->getBorderMap();
$file = phutil_utf8_strtoupper(substr($username, 0, 1));
$pack_count = count($pack_map);
$color_count = count($color_map);
$border_count = count($border_map);
$pack_seed = $username.'_pack';
$color_seed = $username.'_color';
$border_seed = $username.'_border';
$pack_key =
PhabricatorHash::digestToRange($pack_seed, 0, $pack_count - 1);
$color_key =
PhabricatorHash::digestToRange($color_seed, 0, $color_count - 1);
$border_key =
PhabricatorHash::digestToRange($border_seed, 0, $border_count - 1);
$pack = $pack_map[$pack_key];
$icon = 'alphanumeric/'.$pack.'/'.$file.'.png';
$color = $color_map[$color_key];
$border = $border_map[$border_key];
if (!isset($image_map[$icon])) {
$icon = 'alphanumeric/'.$pack.'/_default.png';
}
return array('color' => $color, 'icon' => $icon, 'border' => $border);
}
public function getUserProfileImageFile($username) {
$unique = $this->getUniqueProfileImage($username);
$composer = id(new self())
->setIcon($unique['icon'])
->setColor($unique['color'])
->setBorder($unique['border']);
$data = $composer->loadBuiltinFileData();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $composer->getBuiltinDisplayName(),
'profile' => true,
'canCDN' => true,
));
unset($unguarded);
return $file;
}
public static function getImagePackMap() {
private function newPackMap() {
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/';
@ -196,28 +217,24 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
->withType('d')
->withFollowSymlinks(false)
->find();
$map = array_values($map);
return array_values($map);
return $map;
}
public static function getBorderMap() {
$map = array(
private function newBorderMap() {
return array(
array(0, 0, 0, 0),
array(0, 0, 0, 0.3),
array(255, 255, 255, 0.4),
array(255, 255, 255, 0.7),
);
return $map;
}
public static function getColorMap() {
//
// Generated Colors
// http://tools.medialab.sciences-po.fr/iwanthue/
//
$map = array(
private function newColorMap() {
// Via: http://tools.medialab.sciences-po.fr/iwanthue/
return array(
'#335862',
'#2d5192',
'#3c5da0',
@ -447,7 +464,6 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
'#335862',
'#335862',
);
return $map;
}
}

View file

@ -26,6 +26,9 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$req_domain = $request->getHost();
$main_domain = id(new PhutilURI($base_uri))->getDomain();
$request_kind = $request->getURIData('kind');
$is_download = ($request_kind === 'download');
if (!strlen($alt) || $main_domain == $alt_domain) {
// No alternate domain.
$should_redirect = false;
@ -50,7 +53,7 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
if ($should_redirect) {
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($file->getCDNURI());
->setURI($file->getCDNURI($request_kind));
}
$response = new AphrontFileResponse();
@ -71,34 +74,30 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
}
$is_viewable = $file->isViewableInBrowser();
$force_download = $request->getExists('download');
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
$is_lfs = ($request_type == 'git-lfs');
if ($is_viewable && !$force_download) {
if ($is_viewable && !$is_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
$is_public = !$viewer->isLoggedIn();
$is_post = $request->isHTTPPost();
// NOTE: Require POST to download files from the primary domain if the
// request includes credentials. The "Download File" links we generate
// in the web UI are forms which use POST to satisfy this requirement.
// NOTE: Require POST to download files from the primary domain. If the
// request is not a POST request but arrives on the primary domain, we
// render a confirmation dialog. For discussion, see T13094.
// The intent is to make attacks based on tags like "<iframe />" and
// "<script />" (which can issue GET requests, but can not easily issue
// POST requests) more difficult to execute.
// The best defense against these attacks is to use an alternate file
// domain, which is why we strongly recommend doing so.
$is_safe = ($is_alternate_domain || $is_lfs || $is_post || $is_public);
$is_safe = ($is_alternate_domain || $is_lfs || $is_post);
if (!$is_safe) {
// This is marked as "external" because it is fully qualified.
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
return $this->newDialog()
->setSubmitURI($file->getDownloadURI())
->setTitle(pht('Download File'))
->appendParagraph(
pht(
'Download file %s (%s)?',
phutil_tag('strong', array(), $file->getName()),
phutil_format_bytes($file->getByteSize())))
->addCancelButton($file->getURI())
->addSubmitButton(pht('Download File'));
}
$response->setMimeType($file->getMimeType());

View file

@ -137,11 +137,10 @@ final class PhabricatorFileInfoController extends PhabricatorFileController {
$curtain->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
->setRenderAsForm($can_download)
->setDownload($can_download)
->setName(pht('Download File'))
->setIcon('fa-download')
->setHref($file->getViewURI())
->setHref($file->getDownloadURI())
->setDisabled(!$can_download)
->setWorkflow(!$can_download));
}

View file

@ -144,7 +144,7 @@ final class PhabricatorEmbedFileRemarkupRule
$existing_xform = $file->getTransform($preview_key);
if ($existing_xform) {
$xform_uri = $existing_xform->getCDNURI();
$xform_uri = $existing_xform->getCDNURI('data');
} else {
$xform_uri = $file->getURIForTransform($xform);
}

View file

@ -810,16 +810,24 @@ final class PhabricatorFile extends PhabricatorFileDAO
pht('You must save a file before you can generate a view URI.'));
}
return $this->getCDNURI();
return $this->getCDNURI('data');
}
public function getCDNURI() {
public function getCDNURI($request_kind) {
if (($request_kind !== 'data') &&
($request_kind !== 'download')) {
throw new Exception(
pht(
'Unknown file content request kind "%s".',
$request_kind));
}
$name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name);
$parts = array();
$parts[] = 'file';
$parts[] = 'data';
$parts[] = $request_kind;
// If this is an instanced install, add the instance identifier to the URI.
// Instanced configurations behind a CDN may not be able to control the
@ -861,9 +869,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
}
public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI()))
->setQueryParam('download', true);
return (string)$uri;
return $this->getCDNURI('download');
}
public function getURIForTransform(PhabricatorFileTransform $transform) {
@ -1469,6 +1475,16 @@ final class PhabricatorFile extends PhabricatorFileDAO
->setURI($uri);
}
public function newDownloadResponse() {
// We're cheating a little bit here and relying on the fact that
// getDownloadURI() always returns a fully qualified URI with a complete
// domain.
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setCloseDialogBeforeRedirect(true)
->setURI($this->getDownloadURI());
}
public function attachTransforms(array $map) {
$this->transforms = $map;
return $this;

View file

@ -0,0 +1,117 @@
<?php
final class HarbormasterBuildLogTestCase
extends PhabricatorTestCase {
public function testBuildLogLineMaps() {
$snowman = "\xE2\x98\x83";
$inputs = array(
'no_newlines.log' => array(
64,
array(
str_repeat('AAAAAAAA', 32),
),
array(
array(64, 0),
array(128, 0),
array(192, 0),
array(255, 0),
),
),
'no_newlines_updated.log' => array(
64,
array_fill(0, 32, 'AAAAAAAA'),
array(
array(64, 0),
array(128, 0),
array(192, 0),
),
),
'one_newline.log' => array(
64,
array(
str_repeat('AAAAAAAA', 16),
"\n",
str_repeat('AAAAAAAA', 16),
),
array(
array(64, 0),
array(127, 0),
array(191, 1),
array(255, 1),
),
),
'several_newlines.log' => array(
64,
array_fill(0, 12, "AAAAAAAAAAAAAAAAAA\n"),
array(
array(56, 2),
array(113, 5),
array(170, 8),
array(227, 11),
),
),
'mixed_newlines.log' => array(
64,
array(
str_repeat('A', 63)."\r",
str_repeat('A', 63)."\r\n",
str_repeat('A', 63)."\n",
str_repeat('A', 63),
),
array(
array(63, 0),
array(127, 1),
array(191, 2),
array(255, 3),
),
),
'more_mixed_newlines.log' => array(
64,
array(
str_repeat('A', 63)."\r",
str_repeat('A', 62)."\r\n",
str_repeat('A', 63)."\n",
str_repeat('A', 63),
),
array(
array(63, 0),
array(128, 2),
array(191, 2),
array(254, 3),
),
),
'emoji.log' => array(
64,
array(
str_repeat($snowman, 64),
),
array(
array(63, 0),
array(126, 0),
array(189, 0),
),
),
);
foreach ($inputs as $label => $input) {
list($distance, $parts, $expect) = $input;
$log = id(new HarbormasterBuildLog())
->setByteLength(0);
foreach ($parts as $part) {
$log->updateLineMap($part, $distance);
}
list($actual) = $log->getLineMap();
$this->assertEqual(
$expect,
$actual,
pht('Line Map for "%s"', $label));
}
}
}

View file

@ -96,6 +96,13 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
'circleci/' => 'HarbormasterCircleCIHookController',
'buildkite/' => 'HarbormasterBuildkiteHookController',
),
'log/' => array(
'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
=> 'HarbormasterBuildLogViewController',
'render/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
=> 'HarbormasterBuildLogRenderController',
'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController',
),
),
);
}

View file

@ -0,0 +1,18 @@
<?php
final class HarbormasterBuildLogSearchConduitAPIMethod
extends PhabricatorSearchEngineAPIMethod {
public function getAPIMethodName() {
return 'harbormaster.log.search';
}
public function newSearchEngine() {
return new HarbormasterBuildLogSearchEngine();
}
public function getMethodSummary() {
return pht('Find out information about build logs.');
}
}

View file

@ -0,0 +1,50 @@
<?php
final class HarbormasterBuildLogDownloadController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $request->getURIData('id');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$cancel_uri = $log->getURI();
$file_phid = $log->getFilePHID();
if (!$file_phid) {
return $this->newDialog()
->setTitle(pht('Log Not Finalized'))
->appendParagraph(
pht(
'Logs must be fully written and processed before they can be '.
'downloaded. This log is still being written or processed.'))
->addCancelButton($cancel_uri, pht('Wait Patiently'));
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
return $this->newDialog()
->setTitle(pht('Unable to Load File'))
->appendParagraph(
pht(
'Unable to load the file for this log. The file may have been '.
'destroyed.'))
->addCancelButton($cancel_uri);
}
return $file->newDownloadResponse();
}
}

View file

@ -0,0 +1,893 @@
<?php
final class HarbormasterBuildLogRenderController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$highlight_range = $request->getURILineRange('lines', 1000);
$log_size = $this->getTotalByteLength($log);
$head_lines = $request->getInt('head');
if ($head_lines === null) {
$head_lines = 8;
}
$head_lines = min($head_lines, 1024);
$head_lines = max($head_lines, 0);
$tail_lines = $request->getInt('tail');
if ($tail_lines === null) {
$tail_lines = 16;
}
$tail_lines = min($tail_lines, 1024);
$tail_lines = max($tail_lines, 0);
$head_offset = $request->getInt('headOffset');
if ($head_offset === null) {
$head_offset = 0;
}
$tail_offset = $request->getInt('tailOffset');
if ($tail_offset === null) {
$tail_offset = $log_size;
}
// Figure out which ranges we're actually going to read. We'll read either
// one range (either just at the head, or just at the tail) or two ranges
// (one at the head and one at the tail).
// This gets a little bit tricky because: the ranges may overlap; we just
// want to do one big read if there is only a little bit of text left
// between the ranges; we may not know where the tail range ends; and we
// can only read forward from line map markers, not from any arbitrary
// position in the file.
$bytes_per_line = 140;
$body_lines = 8;
$views = array();
if ($head_lines > 0) {
$views[] = array(
'offset' => $head_offset,
'lines' => $head_lines,
'direction' => 1,
'limit' => $tail_offset,
);
}
if ($highlight_range) {
$highlight_views = $this->getHighlightViews(
$log,
$highlight_range,
$log_size);
foreach ($highlight_views as $highlight_view) {
$views[] = $highlight_view;
}
}
if ($tail_lines > 0) {
$views[] = array(
'offset' => $tail_offset,
'lines' => $tail_lines,
'direction' => -1,
'limit' => $head_offset,
);
}
$reads = $views;
foreach ($reads as $key => $read) {
$offset = $read['offset'];
$lines = $read['lines'];
$read_length = 0;
$read_length += ($lines * $bytes_per_line);
$read_length += ($body_lines * $bytes_per_line);
$direction = $read['direction'];
if ($direction < 0) {
if ($offset > $read_length) {
$offset -= $read_length;
} else {
$read_length = $offset;
$offset = 0;
}
}
$position = $log->getReadPosition($offset);
list($position_offset, $position_line) = $position;
$read_length += ($offset - $position_offset);
$reads[$key]['fetchOffset'] = $position_offset;
$reads[$key]['fetchLength'] = $read_length;
$reads[$key]['fetchLine'] = $position_line;
}
$reads = $this->mergeOverlappingReads($reads);
foreach ($reads as $key => $read) {
$fetch_offset = $read['fetchOffset'];
$fetch_length = $read['fetchLength'];
if ($fetch_offset + $fetch_length > $log_size) {
$fetch_length = $log_size - $fetch_offset;
}
$data = $log->loadData($fetch_offset, $fetch_length);
$offset = $read['fetchOffset'];
$line = $read['fetchLine'];
$lines = $this->getLines($data);
$line_data = array();
foreach ($lines as $line_text) {
$length = strlen($line_text);
$line_data[] = array(
'offset' => $offset,
'length' => $length,
'line' => $line,
'data' => $line_text,
);
$line += 1;
$offset += $length;
}
$reads[$key]['data'] = $data;
$reads[$key]['lines'] = $line_data;
}
foreach ($views as $view_key => $view) {
$anchor_byte = $view['offset'];
if ($view['direction'] < 0) {
$anchor_byte = $anchor_byte - 1;
}
$data_key = null;
foreach ($reads as $read_key => $read) {
$s = $read['fetchOffset'];
$e = $s + $read['fetchLength'];
if (($s <= $anchor_byte) && ($e >= $anchor_byte)) {
$data_key = $read_key;
break;
}
}
if ($data_key === null) {
throw new Exception(
pht('Unable to find fetch!'));
}
$anchor_key = null;
foreach ($reads[$data_key]['lines'] as $line_key => $line) {
$s = $line['offset'];
$e = $s + $line['length'];
if (($s <= $anchor_byte) && ($e > $anchor_byte)) {
$anchor_key = $line_key;
break;
}
}
if ($anchor_key === null) {
throw new Exception(
pht(
'Unable to find lines.'));
}
if ($view['direction'] > 0) {
$slice_offset = $anchor_key;
} else {
$slice_offset = max(0, $anchor_key - ($view['lines'] - 1));
}
$slice_length = $view['lines'];
$views[$view_key] += array(
'sliceKey' => $data_key,
'sliceOffset' => $slice_offset,
'sliceLength' => $slice_length,
);
}
foreach ($views as $view_key => $view) {
$slice_key = $view['sliceKey'];
$lines = array_slice(
$reads[$slice_key]['lines'],
$view['sliceOffset'],
$view['sliceLength']);
$data_offset = null;
$data_length = null;
foreach ($lines as $line) {
if ($data_offset === null) {
$data_offset = $line['offset'];
}
$data_length += $line['length'];
}
// If the view cursor starts in the middle of a line, we're going to
// strip part of the line.
$direction = $view['direction'];
if ($direction > 0) {
$view_offset = $view['offset'];
$view_length = $data_length;
if ($data_offset < $view_offset) {
$trim = ($view_offset - $data_offset);
$view_length -= $trim;
}
$limit = $view['limit'];
if ($limit !== null) {
if ($limit < ($view_offset + $view_length)) {
$view_length = ($limit - $view_offset);
}
}
} else {
$view_offset = $data_offset;
$view_length = $data_length;
if ($data_offset + $data_length > $view['offset']) {
$view_length -= (($data_offset + $data_length) - $view['offset']);
}
$limit = $view['limit'];
if ($limit !== null) {
if ($limit > $view_offset) {
$view_length -= ($limit - $view_offset);
$view_offset = $limit;
}
}
}
$views[$view_key] += array(
'viewOffset' => $view_offset,
'viewLength' => $view_length,
);
}
$views = $this->mergeOverlappingViews($views);
foreach ($views as $view_key => $view) {
$slice_key = $view['sliceKey'];
$lines = array_slice(
$reads[$slice_key]['lines'],
$view['sliceOffset'],
$view['sliceLength']);
$view_offset = $view['viewOffset'];
foreach ($lines as $line_key => $line) {
$line_offset = $line['offset'];
if ($line_offset >= $view_offset) {
break;
}
$trim = ($view_offset - $line_offset);
if ($trim && ($trim >= strlen($line['data']))) {
unset($lines[$line_key]);
continue;
}
$line_data = substr($line['data'], $trim);
$lines[$line_key]['data'] = $line_data;
$lines[$line_key]['length'] = strlen($line_data);
$lines[$line_key]['offset'] += $trim;
break;
}
$view_end = $view['viewOffset'] + $view['viewLength'];
foreach ($lines as $line_key => $line) {
$line_end = $line['offset'] + $line['length'];
if ($line_end <= $view_end) {
continue;
}
$trim = ($line_end - $view_end);
if ($trim && ($trim >= strlen($line['data']))) {
unset($lines[$line_key]);
continue;
}
$line_data = substr($line['data'], -$trim);
$lines[$line_key]['data'] = $line_data;
$lines[$line_key]['length'] = strlen($line_data);
}
$views[$view_key]['viewData'] = $lines;
}
$spacer = null;
$render = array();
$head_view = head($views);
if ($head_view['viewOffset'] > $head_offset) {
$render[] = array(
'spacer' => true,
'head' => $head_offset,
'tail' => $head_view['viewOffset'],
);
}
foreach ($views as $view) {
if ($spacer) {
$spacer['tail'] = $view['viewOffset'];
$render[] = $spacer;
}
$render[] = $view;
$spacer = array(
'spacer' => true,
'head' => ($view['viewOffset'] + $view['viewLength']),
);
}
$tail_view = last($views);
if ($tail_view['viewOffset'] + $tail_view['viewLength'] < $tail_offset) {
$render[] = array(
'spacer' => true,
'head' => $tail_view['viewOffset'] + $tail_view['viewLength'],
'tail' => $tail_offset,
);
}
$uri = $log->getURI();
$rows = array();
foreach ($render as $range) {
if (isset($range['spacer'])) {
$rows[] = $this->renderExpandRow($range);
continue;
}
$lines = $range['viewData'];
foreach ($lines as $line) {
$display_line = ($line['line'] + 1);
$display_text = ($line['data']);
$row_attr = array();
if ($highlight_range) {
if (($display_line >= $highlight_range[0]) &&
($display_line <= $highlight_range[1])) {
$row_attr = array(
'class' => 'phabricator-source-highlight',
);
}
}
$display_line = phutil_tag(
'a',
array(
'href' => $uri.'$'.$display_line,
'data-n' => $display_line,
),
'');
$line_cell = phutil_tag('th', array(), $display_line);
$text_cell = phutil_tag('td', array(), $display_text);
$rows[] = phutil_tag(
'tr',
$row_attr,
array(
$line_cell,
$text_cell,
));
}
}
if ($log->getLive()) {
$last_view = last($views);
$last_line = last($last_view['viewData']);
if ($last_line) {
$last_offset = $last_line['offset'];
} else {
$last_offset = 0;
}
$last_tail = $last_view['viewOffset'] + $last_view['viewLength'];
$show_live = ($last_tail === $log_size);
if ($show_live) {
$rows[] = $this->renderLiveRow($last_offset);
}
}
$table = javelin_tag(
'table',
array(
'class' => 'harbormaster-log-table PhabricatorMonospaced',
'sigil' => 'phabricator-source',
'meta' => array(
'uri' => $log->getURI(),
),
),
$rows);
// When this is a normal AJAX request, return the rendered log fragment
// in an AJAX payload.
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'markup' => hsprintf('%s', $table),
));
}
// If the page is being accessed as a standalone page, present a
// readable version of the fragment for debugging.
require_celerity_resource('harbormaster-css');
$header = pht('Standalone Log Fragment');
$render_view = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeaderText($header)
->appendChild($table);
$page_view = id(new PHUITwoColumnView())
->setFooter($render_view);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI())
->addTextCrumb(pht('Fragment'))
->setBorder(true);
return $this->newPage()
->setTitle(
array(
pht('Build Log %d', $log->getID()),
pht('Standalone Fragment'),
))
->setCrumbs($crumbs)
->appendChild($page_view);
}
private function getTotalByteLength(HarbormasterBuildLog $log) {
$total_bytes = $log->getByteLength();
if ($total_bytes) {
return (int)$total_bytes;
}
// TODO: Remove this after enough time has passed for installs to run
// log rebuilds or decide they don't care about older logs.
// Older logs don't have this data denormalized onto the log record unless
// an administrator has run `bin/harbormaster rebuild-log --all` or
// similar. Try to figure it out by summing up the size of each chunk.
// Note that the log may also be legitimately empty and have actual size
// zero.
$chunk = new HarbormasterBuildLogChunk();
$conn = $chunk->establishConnection('r');
$row = queryfx_one(
$conn,
'SELECT SUM(size) total FROM %T WHERE logID = %d',
$chunk->getTableName(),
$log->getID());
return (int)$row['total'];
}
private function getLines($data) {
$parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE);
if (last($parts) === '') {
array_pop($parts);
}
$lines = array();
for ($ii = 0; $ii < count($parts); $ii += 2) {
$line = $parts[$ii];
if (isset($parts[$ii + 1])) {
$line .= $parts[$ii + 1];
}
$lines[] = $line;
}
return $lines;
}
private function mergeOverlappingReads(array $reads) {
// Find planned reads which will overlap and merge them into a single
// larger read.
$uk = array_keys($reads);
$vk = array_keys($reads);
foreach ($uk as $ukey) {
foreach ($vk as $vkey) {
// Don't merge a range into itself, even though they do technically
// overlap.
if ($ukey === $vkey) {
continue;
}
$uread = idx($reads, $ukey);
if ($uread === null) {
continue;
}
$vread = idx($reads, $vkey);
if ($vread === null) {
continue;
}
$us = $uread['fetchOffset'];
$ue = $us + $uread['fetchLength'];
$vs = $vread['fetchOffset'];
$ve = $vs + $vread['fetchLength'];
if (($vs > $ue) || ($ve < $us)) {
continue;
}
$min = min($us, $vs);
$max = max($ue, $ve);
$reads[$ukey]['fetchOffset'] = $min;
$reads[$ukey]['fetchLength'] = ($max - $min);
$reads[$ukey]['fetchLine'] = min(
$uread['fetchLine'],
$vread['fetchLine']);
unset($reads[$vkey]);
}
}
return $reads;
}
private function mergeOverlappingViews(array $views) {
$uk = array_keys($views);
$vk = array_keys($views);
$body_lines = 8;
$body_bytes = ($body_lines * 140);
foreach ($uk as $ukey) {
foreach ($vk as $vkey) {
if ($ukey === $vkey) {
continue;
}
$uview = idx($views, $ukey);
if ($uview === null) {
continue;
}
$vview = idx($views, $vkey);
if ($vview === null) {
continue;
}
// If these views don't use the same line data, don't try to
// merge them.
if ($uview['sliceKey'] != $vview['sliceKey']) {
continue;
}
// If these views are overlapping or separated by only a few bytes,
// merge them into a single view.
$us = $uview['viewOffset'];
$ue = $us + $uview['viewLength'];
$vs = $vview['viewOffset'];
$ve = $vs + $vview['viewLength'];
// Don't merge if one of the slices starts at a byte offset
// significantly after the other ends.
if (($vs > $ue + $body_bytes) || ($us > $ve + $body_bytes)) {
continue;
}
$uss = $uview['sliceOffset'];
$use = $uss + $uview['sliceLength'];
$vss = $vview['sliceOffset'];
$vse = $vss + $vview['sliceLength'];
// Don't merge if one of the slices starts at a line offset
// significantly after the other ends.
if ($uss > ($vse + $body_lines) || $vss > ($use + $body_lines)) {
continue;
}
// These views are overlapping or nearly overlapping, so we merge
// them. We merge views even if they aren't exactly adjacent since
// it's silly to render an "expand more" which only expands a couple
// of lines.
$offset = min($us, $vs);
$length = max($ue, $ve) - $offset;
$slice_offset = min($uss, $vss);
$slice_length = max($use, $vse) - $slice_offset;
$views[$ukey] = array(
'viewOffset' => $offset,
'viewLength' => $length,
'sliceOffset' => $slice_offset,
'sliceLength' => $slice_length,
) + $views[$ukey];
unset($views[$vkey]);
}
}
return $views;
}
private function renderExpandRow($range) {
$icon_up = id(new PHUIIconView())
->setIcon('fa-chevron-up');
$icon_down = id(new PHUIIconView())
->setIcon('fa-chevron-down');
$up_text = array(
pht('Show More Above'),
' ',
$icon_up,
);
$expand_up = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'meta' => array(
'headOffset' => $range['head'],
'tailOffset' => $range['tail'],
'head' => 128,
'tail' => 0,
),
),
$up_text);
$mid_text = pht(
'Show More (%s Bytes)',
new PhutilNumber($range['tail'] - $range['head']));
$expand_mid = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'meta' => array(
'headOffset' => $range['head'],
'tailOffset' => $range['tail'],
'head' => 128,
'tail' => 128,
),
),
$mid_text);
$down_text = array(
$icon_down,
' ',
pht('Show More Below'),
);
$expand_down = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'meta' => array(
'headOffset' => $range['head'],
'tailOffset' => $range['tail'],
'head' => 0,
'tail' => 128,
),
),
$down_text);
$expand_cells = array(
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-up',
),
$expand_up),
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-mid',
),
$expand_mid),
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-down',
),
$expand_down),
);
return $this->renderActionTable($expand_cells);
}
private function renderLiveRow($log_size) {
$icon_down = id(new PHUIIconView())
->setIcon('fa-angle-double-down');
$icon_pause = id(new PHUIIconView())
->setIcon('fa-pause');
$follow = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand harbormaster-log-live',
'class' => 'harbormaster-log-follow-start',
'meta' => array(
'headOffset' => $log_size,
'head' => 0,
'tail' => 1024,
'live' => true,
),
),
array(
$icon_down,
' ',
pht('Follow Log'),
));
$stop_following = javelin_tag(
'a',
array(
'sigil' => 'harbormaster-log-expand',
'class' => 'harbormaster-log-follow-stop',
'meta' => array(
'stop' => true,
),
),
array(
$icon_pause,
' ',
pht('Stop Following Log'),
));
$expand_cells = array(
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-follow',
),
array(
$follow,
$stop_following,
)),
);
return $this->renderActionTable($expand_cells);
}
private function renderActionTable(array $action_cells) {
$action_row = phutil_tag('tr', array(), $action_cells);
$action_table = phutil_tag(
'table',
array(
'class' => 'harbormaster-log-expand-table',
),
$action_row);
$format_cells = array(
phutil_tag('th', array()),
phutil_tag(
'td',
array(
'class' => 'harbormaster-log-expand-cell',
),
$action_table),
);
return phutil_tag('tr', array(), $format_cells);
}
private function getHighlightViews(
HarbormasterBuildLog $log,
array $range,
$log_size) {
// If we're highlighting a line range in the file, we first need to figure
// out the offsets for the lines we care about.
list($range_min, $range_max) = $range;
// Read the markers to find a range we can load which includes both lines.
$read_range = $log->getLineSpanningRange($range_min, $range_max);
list($min_pos, $max_pos, $min_line) = $read_range;
$length = ($max_pos - $min_pos);
// Reject to do the read if it requires us to examine a huge amount of
// data. For example, the user may request lines "$1-1000" of a file where
// each line has 100MB of text.
$limit = (1024 * 1024 * 16);
if ($length > $limit) {
return array();
}
$data = $log->loadData($min_pos, $length);
$offset = $min_pos;
$min_offset = null;
$max_offset = null;
$lines = $this->getLines($data);
$number = ($min_line + 1);
foreach ($lines as $line) {
if ($min_offset === null) {
if ($number === $range_min) {
$min_offset = $offset;
}
}
$offset += strlen($line);
if ($max_offset === null) {
if ($number === $range_max) {
$max_offset = $offset;
break;
}
}
$number += 1;
}
$context_lines = 8;
// Build views around the beginning and ends of the respective lines. We
// expect these views to overlap significantly in normal circumstances
// and be merged later.
$views = array();
if ($min_offset !== null) {
$views[] = array(
'offset' => $min_offset,
'lines' => $context_lines + ($range_max - $range_min) - 1,
'direction' => 1,
'limit' => null,
);
if ($min_offset > 0) {
$views[] = array(
'offset' => $min_offset,
'lines' => $context_lines,
'direction' => -1,
'limit' => null,
);
}
}
if ($max_offset !== null) {
$views[] = array(
'offset' => $max_offset,
'lines' => $context_lines + ($range_max - $range_min),
'direction' => -1,
'limit' => null,
);
if ($max_offset < $log_size) {
$views[] = array(
'offset' => $max_offset,
'lines' => $context_lines,
'direction' => 1,
'limit' => null,
);
}
}
return $views;
}
}

View file

@ -0,0 +1,51 @@
<?php
final class HarbormasterBuildLogViewController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$target = $log->getBuildTarget();
$build = $target->getBuild();
$page_title = pht('Build Log %d', $log->getID());
$log_view = id(new HarbormasterBuildLogView())
->setViewer($viewer)
->setBuildLog($log)
->setHighlightedLineRange($request->getURIData('lines'))
->setEnableHighlighter(true);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Build Logs'))
->addTextCrumb(
pht('Build %d', $build->getID()),
$build->getURI())
->addTextCrumb($page_title)
->setBorder(true);
$page_header = id(new PHUIHeaderView())
->setHeader($page_title);
$page_view = id(new PHUITwoColumnView())
->setHeader($page_header)
->setFooter($log_view);
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->appendChild($page_view);
}
}

View file

@ -363,12 +363,19 @@ final class HarbormasterBuildViewController
$log_view->setLines($lines);
$log_view->setStart($start);
$prototype_view = id(new PHUIButtonView())
->setTag('a')
->setHref($log->getURI())
->setIcon('fa-file-text-o')
->setText(pht('New View (Prototype)'));
$header = id(new PHUIHeaderView())
->setHeader(pht(
'Build Log %d (%s - %s)',
$log->getID(),
$log->getLogSource(),
$log->getLogType()))
->addActionLink($prototype_view)
->setSubheader($this->createLogHeader($build, $log))
->setUser($viewer);

View file

@ -0,0 +1,102 @@
<?php
final class HarbormasterManagementRebuildLogWorkflow
extends HarbormasterManagementWorkflow {
protected function didConstruct() {
$this
->setName('rebuild-log')
->setExamples(
pht(
"**rebuild-log** --id __id__ [__options__]\n".
"**rebuild-log** --all"))
->setSynopsis(
pht(
'Rebuild the file and summary for a log. This is primarily '.
'intended to make it easier to develop new log summarizers.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('Log to rebuild.'),
),
array(
'name' => 'all',
'help' => pht('Rebuild all logs.'),
),
array(
'name' => 'force',
'help' => pht(
'Force logs to rebuild even if they appear to be in good '.
'shape already.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$is_force = $args->getArg('force');
$log_id = $args->getArg('id');
$is_all = $args->getArg('all');
if (!$is_all && !$log_id) {
throw new PhutilArgumentUsageException(
pht(
'Choose a build log to rebuild with "--id", or rebuild all '.
'logs with "--all".'));
}
if ($is_all && $log_id) {
throw new PhutilArgumentUsageException(
pht(
'You can not specify both "--id" and "--all". Choose one or '.
'the other.'));
}
if ($log_id) {
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withIDs(array($log_id))
->executeOne();
if (!$log) {
throw new PhutilArgumentUsageException(
pht(
'Unable to load build log "%s".',
$log_id));
}
$logs = array($log);
} else {
$logs = new LiskMigrationIterator(new HarbormasterBuildLog());
}
PhabricatorWorker::setRunAllTasksInProcess(true);
foreach ($logs as $log) {
echo tsprintf(
"%s\n",
pht(
'Rebuilding log "%s"...',
pht('Build Log %d', $log->getID())));
try {
$log->scheduleRebuild($is_force);
} catch (Exception $ex) {
if ($is_all) {
phlog($ex);
} else {
throw $ex;
}
}
}
echo tsprintf(
"%s\n",
pht('Done.'));
return 0;
}
}

View file

@ -0,0 +1,111 @@
<?php
final class HarbormasterManagementWriteLogWorkflow
extends HarbormasterManagementWorkflow {
protected function didConstruct() {
$this
->setName('write-log')
->setExamples('**write-log** --target __id__ [__options__]')
->setSynopsis(
pht(
'Write a new Harbormaster build log. This is primarily intended '.
'to make development and testing easier.'))
->setArguments(
array(
array(
'name' => 'target',
'param' => 'id',
'help' => pht('Build Target ID to attach the log to.'),
),
array(
'name' => 'rate',
'param' => 'bytes',
'help' => pht(
'Limit the rate at which the log is written, to test '.
'live log streaming.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$target_id = $args->getArg('target');
if (!$target_id) {
throw new PhutilArgumentUsageException(
pht('Choose a build target to attach the log to with "--target".'));
}
$target = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withIDs(array($target_id))
->executeOne();
if (!$target) {
throw new PhutilArgumentUsageException(
pht(
'Unable to load build target "%s".',
$target_id));
}
$log = HarbormasterBuildLog::initializeNewBuildLog($target);
$log->openBuildLog();
echo tsprintf(
"%s\n\n __%s__\n\n",
pht('Opened a new build log:'),
PhabricatorEnv::getURI($log->getURI()));
echo tsprintf(
"%s\n",
pht('Reading log content from stdin...'));
$content = file_get_contents('php://stdin');
$rate = $args->getArg('rate');
if ($rate) {
if ($rate <= 0) {
throw new Exception(
pht(
'Write rate must be more than 0 bytes/sec.'));
}
echo tsprintf(
"%s\n",
pht('Writing log, slowly...'));
$offset = 0;
$total = strlen($content);
$pieces = str_split($content, $rate);
$bar = id(new PhutilConsoleProgressBar())
->setTotal($total);
foreach ($pieces as $piece) {
$log->append($piece);
$bar->update(strlen($piece));
sleep(1);
}
$bar->done();
} else {
$log->append($content);
}
echo tsprintf(
"%s\n",
pht('Write completed. Closing log...'));
PhabricatorWorker::setRunAllTasksInProcess(true);
$log->closeBuildLog();
echo tsprintf(
"%s\n",
pht('Done.'));
return 0;
}
}

View file

@ -31,6 +31,10 @@ final class HarbormasterBuildLogPHIDType extends PhabricatorPHIDType {
foreach ($handles as $phid => $handle) {
$build_log = $objects[$phid];
$handle
->setName(pht('Build Log %d', $build_log->getID()))
->setURI($build_log->getURI());
}
}

View file

@ -23,19 +23,12 @@ final class HarbormasterBuildLogQuery
return $this;
}
public function newResultObject() {
return new HarbormasterBuildLog();
}
protected function loadPage() {
$table = new HarbormasterBuildLog();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $page) {
@ -63,33 +56,31 @@ final class HarbormasterBuildLogQuery
return $page;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids) {
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->buildTargetPHIDs) {
if ($this->buildTargetPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'buildTargetPHID IN (%Ls)',
$this->buildTargetPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
return $where;
}
public function getQueryApplicationClass() {

View file

@ -0,0 +1,79 @@
<?php
final class HarbormasterBuildLogSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Harbormaster Build Logs');
}
public function getApplicationClassName() {
return 'PhabricatorHarbormasterApplication';
}
public function newQuery() {
return new HarbormasterBuildLogQuery();
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Build Targets'))
->setKey('buildTargetPHIDs')
->setAliases(
array(
'buildTargetPHID',
'buildTargets',
'buildTarget',
'targetPHIDs',
'targetPHID',
'targets',
'target',
))
->setDescription(
pht('Search for logs that belong to a particular build target.')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['buildTargetPHIDs']) {
$query->withBuildTargetPHIDs($map['buildTargetPHIDs']);
}
return $query;
}
protected function getURI($path) {
return '/harbormaster/log/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Builds'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $builds,
PhabricatorSavedQuery $query,
array $handles) {
// For now, this SearchEngine is only for driving the API.
throw new PhutilMethodNotImplementedException();
}
}

View file

@ -2,19 +2,27 @@
final class HarbormasterBuildLog
extends HarbormasterDAO
implements PhabricatorPolicyInterface {
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $buildTargetPHID;
protected $logSource;
protected $logType;
protected $duration;
protected $live;
protected $filePHID;
protected $byteLength;
protected $chunkFormat;
protected $lineMap = array();
private $buildTarget = self::ATTACHABLE;
private $rope;
private $isOpen;
private $lock;
const CHUNK_BYTE_LIMIT = 102400;
const CHUNK_BYTE_LIMIT = 1048576;
public function __construct() {
$this->rope = new PhutilRope();
@ -24,6 +32,12 @@ final class HarbormasterBuildLog
if ($this->isOpen) {
$this->closeBuildLog();
}
if ($this->lock) {
if ($this->lock->isLocked()) {
$this->lock->unlock();
}
}
}
public static function initializeNewBuildLog(
@ -32,43 +46,29 @@ final class HarbormasterBuildLog
return id(new HarbormasterBuildLog())
->setBuildTargetPHID($build_target->getPHID())
->setDuration(null)
->setLive(0);
}
public function openBuildLog() {
if ($this->isOpen) {
throw new Exception(pht('This build log is already open!'));
}
$this->isOpen = true;
return $this
->setLive(1)
->save();
->setByteLength(0)
->setChunkFormat(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
public function closeBuildLog() {
if (!$this->isOpen) {
throw new Exception(pht('This build log is not open!'));
}
if ($this->canCompressLog()) {
$this->compressLog();
}
$start = $this->getDateCreated();
$now = PhabricatorTime::getNow();
return $this
->setDuration($now - $start)
->setLive(0)
->save();
public function scheduleRebuild($force) {
PhabricatorWorker::scheduleTask(
'HarbormasterLogWorker',
array(
'logPHID' => $this->getPHID(),
'force' => $force,
),
array(
'objectPHID' => $this->getPHID(),
));
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'lineMap' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
// T6203/NULLABILITY
// It seems like these should be non-nullable? All logs should have a
@ -78,6 +78,9 @@ final class HarbormasterBuildLog
'duration' => 'uint32?',
'live' => 'bool',
'filePHID' => 'phid?',
'byteLength' => 'uint64',
'chunkFormat' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildtarget' => array(
@ -105,9 +108,366 @@ final class HarbormasterBuildLog
return pht('Build Log');
}
public function append($content) {
public function newChunkIterator() {
return id(new HarbormasterBuildLogChunkIterator($this))
->setPageSize(8);
}
public function newDataIterator() {
return $this->newChunkIterator()
->setAsString(true);
}
private function loadLastChunkInfo() {
$chunk_table = new HarbormasterBuildLogChunk();
$conn_w = $chunk_table->establishConnection('w');
return queryfx_one(
$conn_w,
'SELECT id, size, encoding FROM %T WHERE logID = %d
ORDER BY id DESC LIMIT 1',
$chunk_table->getTableName(),
$this->getID());
}
public function loadData($offset, $length) {
$end = ($offset + $length);
$chunks = id(new HarbormasterBuildLogChunk())->loadAllWhere(
'logID = %d AND headOffset < %d AND tailOffset >= %d
ORDER BY headOffset ASC',
$this->getID(),
$end,
$offset);
// Make sure that whatever we read out of the database is a single
// contiguous range which contains all of the requested bytes.
$ranges = array();
foreach ($chunks as $chunk) {
$ranges[] = array(
'head' => $chunk->getHeadOffset(),
'tail' => $chunk->getTailOffset(),
);
}
$ranges = isort($ranges, 'head');
$ranges = array_values($ranges);
$count = count($ranges);
for ($ii = 0; $ii < ($count - 1); $ii++) {
if ($ranges[$ii + 1]['head'] === $ranges[$ii]['tail']) {
$ranges[$ii + 1]['head'] = $ranges[$ii]['head'];
unset($ranges[$ii]);
}
}
if (count($ranges) !== 1) {
$display_ranges = array();
foreach ($ranges as $range) {
$display_ranges[] = pht(
'(%d - %d)',
$range['head'],
$range['tail']);
}
if (!$display_ranges) {
$display_ranges[] = pht('<null>');
}
throw new Exception(
pht(
'Attempt to load log bytes (%d - %d) failed: failed to '.
'load a single contiguous range. Actual ranges: %s.',
implode('; ', $display_ranges)));
}
$range = head($ranges);
if ($range['head'] > $offset || $range['tail'] < $end) {
throw new Exception(
pht(
'Attempt to load log bytes (%d - %d) failed: the loaded range '.
'(%d - %d) does not span the requested range.',
$offset,
$end,
$range['head'],
$range['tail']));
}
$parts = array();
foreach ($chunks as $chunk) {
$parts[] = $chunk->getChunkDisplayText();
}
$parts = implode('', $parts);
$chop_head = ($offset - $range['head']);
$chop_tail = ($range['tail'] - $end);
if ($chop_head) {
$parts = substr($parts, $chop_head);
}
if ($chop_tail) {
$parts = substr($parts, 0, -$chop_tail);
}
return $parts;
}
public function getLineSpanningRange($min_line, $max_line) {
$map = $this->getLineMap();
if (!$map) {
throw new Exception(pht('No line map.'));
}
$min_pos = 0;
$min_line = 0;
$max_pos = $this->getByteLength();
list($map) = $map;
foreach ($map as $marker) {
list($offset, $count) = $marker;
if ($count < $min_line) {
if ($offset > $min_pos) {
$min_pos = $offset;
$min_line = $count;
}
}
if ($count > $max_line) {
$max_pos = min($max_pos, $offset);
break;
}
}
return array($min_pos, $max_pos, $min_line);
}
public function getReadPosition($read_offset) {
$position = array(0, 0);
$map = $this->getLineMap();
if (!$map) {
throw new Exception(pht('No line map.'));
}
list($map) = $map;
foreach ($map as $marker) {
list($offset, $count) = $marker;
if ($offset > $read_offset) {
break;
}
$position = $marker;
}
return $position;
}
public function getLogText() {
// TODO: Remove this method since it won't scale for big logs.
$all_chunks = $this->newChunkIterator();
$full_text = array();
foreach ($all_chunks as $chunk) {
$full_text[] = $chunk->getChunkDisplayText();
}
return implode('', $full_text);
}
public function getURI() {
$id = $this->getID();
return "/harbormaster/log/view/{$id}/";
}
public function getRenderURI($lines) {
if (strlen($lines)) {
$lines = '$'.$lines;
}
$id = $this->getID();
return "/harbormaster/log/render/{$id}/{$lines}";
}
/* -( Chunks )------------------------------------------------------------- */
public function canCompressLog() {
return function_exists('gzdeflate');
}
public function compressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP);
}
public function decompressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
private function processLog($mode) {
if (!$this->getLock()->isLocked()) {
throw new Exception(
pht(
'You can not process build log chunks unless the log lock is '.
'held.'));
}
$chunks = $this->newChunkIterator();
// NOTE: Because we're going to insert new chunks, we need to stop the
// iterator once it hits the final chunk which currently exists. Otherwise,
// it may start consuming chunks we just wrote and run forever.
$last = $this->loadLastChunkInfo();
if ($last) {
$chunks->setRange(null, $last['id']);
}
$byte_limit = self::CHUNK_BYTE_LIMIT;
$rope = new PhutilRope();
$this->openTransaction();
$offset = 0;
foreach ($chunks as $chunk) {
$rope->append($chunk->getChunkDisplayText());
$chunk->delete();
while ($rope->getByteLength() > $byte_limit) {
$offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
}
while ($rope->getByteLength()) {
$offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
$this
->setChunkFormat($mode)
->save();
$this->saveTransaction();
}
private function writeEncodedChunk(
PhutilRope $rope,
$offset,
$length,
$mode) {
$data = $rope->getPrefixBytes($length);
$size = strlen($data);
switch ($mode) {
case HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT:
// Do nothing.
break;
case HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP:
$data = gzdeflate($data);
if ($data === false) {
throw new Exception(pht('Failed to gzdeflate() log data!'));
}
break;
default:
throw new Exception(pht('Unknown chunk encoding "%s"!', $mode));
}
$this->writeChunk($mode, $offset, $size, $data);
$rope->removeBytesFromHead($size);
return $size;
}
private function writeChunk($encoding, $offset, $raw_size, $data) {
$head_offset = $offset;
$tail_offset = $offset + $raw_size;
return id(new HarbormasterBuildLogChunk())
->setLogID($this->getID())
->setEncoding($encoding)
->setHeadOffset($head_offset)
->setTailOffset($tail_offset)
->setSize($raw_size)
->setChunk($data)
->save();
}
/* -( Writing )------------------------------------------------------------ */
public function getLock() {
if (!$this->lock) {
$phid = $this->getPHID();
$phid_key = PhabricatorHash::digestToLength($phid, 14);
$lock_key = "build.log({$phid_key})";
$lock = PhabricatorGlobalLock::newLock($lock_key);
$this->lock = $lock;
}
return $this->lock;
}
public function openBuildLog() {
if ($this->isOpen) {
throw new Exception(pht('This build log is already open!'));
}
$is_new = !$this->getID();
if ($is_new) {
$this->save();
}
$this->getLock()->lock();
$this->isOpen = true;
$this->reload();
if (!$this->getLive()) {
throw new PhutilInvalidStateException('openBuildLog');
$this->setLive(1)->save();
}
return $this;
}
public function closeBuildLog($forever = true) {
if (!$this->isOpen) {
throw new Exception(
pht(
'You must openBuildLog() before you can closeBuildLog().'));
}
$this->flush();
if ($forever) {
$start = $this->getDateCreated();
$now = PhabricatorTime::getNow();
$this
->setDuration($now - $start)
->setLive(0)
->save();
}
$this->getLock()->unlock();
$this->isOpen = false;
if ($forever) {
$this->scheduleRebuild(false);
}
return $this;
}
public function append($content) {
if (!$this->isOpen) {
throw new Exception(
pht(
'You must openBuildLog() before you can append() content to '.
'the log.'));
}
$content = (string)$content;
@ -155,126 +515,160 @@ final class HarbormasterBuildLog
$append_data = $rope->getPrefixBytes($data_limit);
$data_size = strlen($append_data);
if ($append_id) {
queryfx(
$conn_w,
'UPDATE %T SET chunk = CONCAT(chunk, %B), size = %d WHERE id = %d',
$chunk_table,
$append_data,
$prefix_size + $data_size,
$append_id);
} else {
$this->writeChunk($encoding_text, $data_size, $append_data);
}
$this->openTransaction();
if ($append_id) {
queryfx(
$conn_w,
'UPDATE %T SET
chunk = CONCAT(chunk, %B),
size = %d,
tailOffset = headOffset + %d
WHERE
id = %d',
$chunk_table,
$append_data,
$prefix_size + $data_size,
$prefix_size + $data_size,
$append_id);
} else {
$this->writeChunk(
$encoding_text,
$this->getByteLength(),
$data_size,
$append_data);
}
$this->updateLineMap($append_data);
$this->save();
$this->saveTransaction();
$rope->removeBytesFromHead($data_size);
}
}
public function newChunkIterator() {
return id(new HarbormasterBuildLogChunkIterator($this))
->setPageSize(32);
}
public function updateLineMap($append_data, $marker_distance = null) {
$this->byteLength += strlen($append_data);
private function loadLastChunkInfo() {
$chunk_table = new HarbormasterBuildLogChunk();
$conn_w = $chunk_table->establishConnection('w');
return queryfx_one(
$conn_w,
'SELECT id, size, encoding FROM %T WHERE logID = %d
ORDER BY id DESC LIMIT 1',
$chunk_table->getTableName(),
$this->getID());
}
public function getLogText() {
// TODO: Remove this method since it won't scale for big logs.
$all_chunks = $this->newChunkIterator();
$full_text = array();
foreach ($all_chunks as $chunk) {
$full_text[] = $chunk->getChunkDisplayText();
if (!$marker_distance) {
$marker_distance = (self::CHUNK_BYTE_LIMIT / 2);
}
return implode('', $full_text);
}
private function canCompressLog() {
return function_exists('gzdeflate');
}
public function compressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP);
}
public function decompressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
private function processLog($mode) {
$chunks = $this->newChunkIterator();
// NOTE: Because we're going to insert new chunks, we need to stop the
// iterator once it hits the final chunk which currently exists. Otherwise,
// it may start consuming chunks we just wrote and run forever.
$last = $this->loadLastChunkInfo();
if ($last) {
$chunks->setRange(null, $last['id']);
if (!$this->lineMap) {
$this->lineMap = array(
array(),
0,
0,
null,
);
}
$byte_limit = self::CHUNK_BYTE_LIMIT;
$rope = new PhutilRope();
list($map, $map_bytes, $line_count, $prefix) = $this->lineMap;
$this->openTransaction();
$buffer = $append_data;
foreach ($chunks as $chunk) {
$rope->append($chunk->getChunkDisplayText());
$chunk->delete();
if ($prefix) {
$prefix = base64_decode($prefix);
$buffer = $prefix.$buffer;
}
while ($rope->getByteLength() > $byte_limit) {
$this->writeEncodedChunk($rope, $byte_limit, $mode);
if ($map) {
list($last_marker, $last_count) = last($map);
} else {
$last_marker = 0;
$last_count = 0;
}
$max_utf8_width = 8;
$next_marker = $last_marker + $marker_distance;
$pos = 0;
$len = strlen($buffer);
while (true) {
// If we only have a few bytes left in the buffer, leave it as a prefix
// for next time.
if (($len - $pos) <= ($max_utf8_width * 2)) {
$prefix = substr($buffer, $pos);
break;
}
// The next slice we're going to look at is the smaller of:
//
// - the number of bytes we need to make it to the next marker; or
// - all the bytes we have left, minus one.
$slice_length = min(
($marker_distance - $map_bytes),
($len - $pos) - 1);
// We don't slice all the way to the end for two reasons.
// First, we want to avoid slicing immediately after a "\r" if we don't
// know what the next character is, because we want to make sure to
// count "\r\n" as a single newline, rather than counting the "\r" as
// a newline and then later counting the "\n" as another newline.
// Second, we don't want to slice in the middle of a UTF8 character if
// we can help it. We may not be able to avoid this, since the whole
// buffer may just be binary data, but in most cases we can backtrack
// a little bit and try to make it out of emoji or other legitimate
// multibyte UTF8 characters which appear in the log.
$min_width = max(1, $slice_length - $max_utf8_width);
while ($slice_length >= $min_width) {
$here = $buffer[$pos + ($slice_length - 1)];
$next = $buffer[$pos + ($slice_length - 1) + 1];
// If this is "\r" and the next character is "\n", extend the slice
// to include the "\n". Otherwise, we're fine to slice here since we
// know we're not in the middle of a UTF8 character.
if ($here === "\r") {
if ($next === "\n") {
$slice_length++;
}
break;
}
// If the next character is 0x7F or lower, or between 0xC2 and 0xF4,
// we're not slicing in the middle of a UTF8 character.
$ord = ord($next);
if ($ord <= 0x7F || ($ord >= 0xC2 && $ord <= 0xF4)) {
break;
}
$slice_length--;
}
$slice = substr($buffer, $pos, $slice_length);
$pos += $slice_length;
$map_bytes += $slice_length;
$line_count += count(preg_split("/\r\n|\r|\n/", $slice)) - 1;
if ($map_bytes >= ($marker_distance - $max_utf8_width)) {
$map[] = array(
$last_marker + $map_bytes,
$last_count + $line_count,
);
$last_count = $last_count + $line_count;
$line_count = 0;
$last_marker = $last_marker + $map_bytes;
$map_bytes = 0;
$next_marker = $last_marker + $marker_distance;
}
}
while ($rope->getByteLength()) {
$this->writeEncodedChunk($rope, $byte_limit, $mode);
}
$this->lineMap = array(
$map,
$map_bytes,
$line_count,
base64_encode($prefix),
);
$this->saveTransaction();
}
private function writeEncodedChunk(PhutilRope $rope, $length, $mode) {
$data = $rope->getPrefixBytes($length);
$size = strlen($data);
switch ($mode) {
case HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT:
// Do nothing.
break;
case HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP:
$data = gzdeflate($data);
if ($data === false) {
throw new Exception(pht('Failed to gzdeflate() log data!'));
}
break;
default:
throw new Exception(pht('Unknown chunk encoding "%s"!', $mode));
}
$this->writeChunk($mode, $size, $data);
$rope->removeBytesFromHead($size);
}
private function writeChunk($encoding, $raw_size, $data) {
return id(new HarbormasterBuildLogChunk())
->setLogID($this->getID())
->setEncoding($encoding)
->setSize($raw_size)
->setChunk($data)
->save();
return $this;
}
@ -299,8 +693,88 @@ final class HarbormasterBuildLog
public function describeAutomaticCapability($capability) {
return pht(
"Users must be able to see a build target to view it's build log.");
'Users must be able to see a build target to view its build log.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->destroyFile($engine);
$this->destroyChunks();
$this->delete();
}
public function destroyFile(PhabricatorDestructionEngine $engine = null) {
if (!$engine) {
$engine = new PhabricatorDestructionEngine();
}
$file_phid = $this->getFilePHID();
if ($file_phid) {
$viewer = $engine->getViewer();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if ($file) {
$engine->destroyObject($file);
}
}
$this->setFilePHID(null);
return $this;
}
public function destroyChunks() {
$chunk = new HarbormasterBuildLogChunk();
$conn = $chunk->establishConnection('w');
// Just delete the chunks directly so we don't have to pull the data over
// the wire for large logs.
queryfx(
$conn,
'DELETE FROM %T WHERE logID = %d',
$chunk->getTableName(),
$this->getID());
return $this;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildTargetPHID')
->setType('phid')
->setDescription(pht('Build target this log is attached to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('byteLength')
->setType('int')
->setDescription(pht('Length of the log in bytes.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('filePHID')
->setType('phid?')
->setDescription(pht('A file containing the log data.')),
);
}
public function getFieldValuesForConduit() {
return array(
'buildTargetPHID' => $this->getBuildTargetPHID(),
'byteLength' => (int)$this->getByteLength(),
'filePHID' => $this->getFilePHID(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}

View file

@ -5,6 +5,8 @@ final class HarbormasterBuildLogChunk
protected $logID;
protected $encoding;
protected $headOffset;
protected $tailOffset;
protected $size;
protected $chunk;
@ -20,6 +22,8 @@ final class HarbormasterBuildLogChunk
self::CONFIG_COLUMN_SCHEMA => array(
'logID' => 'id',
'encoding' => 'text32',
'headOffset' => 'uint64',
'tailOffset' => 'uint64',
// T6203/NULLABILITY
// Both the type and nullability of this column are crazily wrong.
@ -28,8 +32,8 @@ final class HarbormasterBuildLogChunk
'chunk' => 'bytes',
),
self::CONFIG_KEY_SCHEMA => array(
'key_log' => array(
'columns' => array('logID'),
'key_offset' => array(
'columns' => array('logID', 'headOffset', 'tailOffset'),
),
),
) + parent::getConfiguration();

View file

@ -5,6 +5,7 @@ final class HarbormasterBuildLogChunkIterator
private $log;
private $cursor;
private $asString;
private $min = 0;
private $max = PHP_INT_MAX;
@ -27,6 +28,11 @@ final class HarbormasterBuildLogChunkIterator
return $this;
}
public function setAsString($as_string) {
$this->asString = $as_string;
return $this;
}
protected function loadPage() {
if ($this->cursor > $this->max) {
return array();
@ -43,7 +49,11 @@ final class HarbormasterBuildLogChunkIterator
$this->cursor = last($results)->getID() + 1;
}
return $results;
if ($this->asString) {
return mpull($results, 'getChunkDisplayText');
} else {
return $results;
}
}
}

View file

@ -0,0 +1,101 @@
<?php
final class HarbormasterBuildLogView extends AphrontView {
private $log;
private $highlightedLineRange;
private $enableHighlighter;
public function setBuildLog(HarbormasterBuildLog $log) {
$this->log = $log;
return $this;
}
public function getBuildLog() {
return $this->log;
}
public function setHighlightedLineRange($range) {
$this->highlightedLineRange = $range;
return $this;
}
public function getHighlightedLineRange() {
return $this->highlightedLineRange;
}
public function setEnableHighlighter($enable) {
$this->enableHighlighter = $enable;
return $this;
}
public function render() {
$viewer = $this->getViewer();
$log = $this->getBuildLog();
$id = $log->getID();
$header = id(new PHUIHeaderView())
->setViewer($viewer)
->setHeader(pht('Build Log %d', $id));
$download_uri = "/harbormaster/log/download/{$id}/";
$can_download = (bool)$log->getFilePHID();
$download_button = id(new PHUIButtonView())
->setTag('a')
->setHref($download_uri)
->setIcon('fa-download')
->setDisabled(!$can_download)
->setWorkflow(!$can_download)
->setText(pht('Download Log'));
$header->addActionLink($download_button);
$box_view = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeader($header);
if ($this->enableHighlighter) {
Javelin::initBehavior('phabricator-line-linker');
}
$has_linemap = $log->getLineMap();
if ($has_linemap) {
$content_id = celerity_generate_unique_node_id();
$content_div = javelin_tag(
'div',
array(
'id' => $content_id,
'class' => 'harbormaster-log-view-loading',
),
pht('Loading...'));
require_celerity_resource('harbormaster-css');
Javelin::initBehavior(
'harbormaster-log',
array(
'contentNodeID' => $content_id,
'initialURI' => $log->getRenderURI($this->getHighlightedLineRange()),
'renderURI' => $log->getRenderURI(null),
));
$box_view->appendChild($content_div);
} else {
$box_view->setFormErrors(
array(
pht(
'This older log is missing required rendering data. To rebuild '.
'rendering data, run: %s',
phutil_tag(
'tt',
array(),
'$ bin/harbormaster rebuild-log --force --id '.$log->getID())),
));
}
return $box_view;
}
}

View file

@ -0,0 +1,105 @@
<?php
final class HarbormasterLogWorker extends HarbormasterWorker {
protected function doWork() {
$viewer = $this->getViewer();
$data = $this->getTaskData();
$log_phid = idx($data, 'logPHID');
$log = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withPHIDs(array($log_phid))
->executeOne();
if (!$log) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Invalid build log PHID "%s".',
$log_phid));
}
$lock = $log->getLock();
try {
$lock->lock();
} catch (PhutilLockException $ex) {
throw new PhabricatorWorkerYieldException(15);
}
$caught = null;
try {
$log->reload();
if ($log->getLive()) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Log "%s" is still live. Logs can not be finalized until '.
'they have closed.',
$log_phid));
}
$this->finalizeBuildLog($log);
} catch (Exception $ex) {
$caught = $ex;
}
$lock->unlock();
if ($caught) {
throw $caught;
}
}
private function finalizeBuildLog(HarbormasterBuildLog $log) {
$viewer = $this->getViewer();
$data = $this->getTaskData();
$is_force = idx($data, 'force');
if (!$log->getByteLength() || !$log->getLineMap() || $is_force) {
$iterator = $log->newDataIterator();
$log
->setByteLength(0)
->setLineMap(array());
foreach ($iterator as $block) {
$log->updateLineMap($block);
}
$log->save();
}
$format_text = HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT;
if (($log->getChunkFormat() === $format_text) || $is_force) {
if ($log->canCompressLog()) {
$log->compressLog();
}
}
if ($is_force) {
$log->destroyFile();
}
if (!$log->getFilePHID()) {
$iterator = $log->newDataIterator();
$source = id(new PhabricatorIteratorFileUploadSource())
->setName('harbormaster-log-'.$log->getID().'.log')
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
->setMIMEType('application/octet-stream')
->setIterator($iterator);
$file = $source->uploadFile();
$file->attachToObject($log->getPHID());
$log
->setFilePHID($file->getPHID())
->save();
}
}
}

View file

@ -2,30 +2,10 @@
final class PhabricatorPasteViewController extends PhabricatorPasteController {
private $highlightMap;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$raw_lines = idx($data, 'lines');
$map = array();
if ($raw_lines) {
$lines = explode('-', $raw_lines);
$first = idx($lines, 0, 0);
$last = idx($lines, 1);
if ($last) {
$min = min($first, $last);
$max = max($first, $last);
$map = array_fuse(range($min, $max));
} else {
$map[$first] = $first;
}
}
$this->highlightMap = $map;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
@ -40,11 +20,18 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController {
return new Aphront404Response();
}
$lines = $request->getURILineRange('lines', 1000);
if ($lines) {
$map = range($lines[0], $lines[1]);
} else {
$map = array();
}
$header = $this->buildHeaderView($paste);
$curtain = $this->buildCurtain($paste);
$subheader = $this->buildSubheaderView($paste);
$source_code = $this->buildSourceCodeView($paste, $this->highlightMap);
$source_code = $this->buildSourceCodeView($paste, $map);
require_celerity_resource('paste-css');

View file

@ -45,21 +45,10 @@ final class PhabricatorUserProfileImageCacheType
$generate_users[] = $user;
}
// Generate Files for anyone without a default
foreach ($generate_users as $generate_user) {
$generate_user_phid = $generate_user->getPHID();
$generate_username = $generate_user->getUsername();
$generate_version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION;
$generate_file = id(new PhabricatorFilesComposeAvatarBuiltinFile())
->getUserProfileImageFile($generate_username);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$generate_user->setDefaultProfileImagePHID($generate_file->getPHID());
$generate_user->setDefaultProfileImageVersion($generate_version);
$generate_user->save();
unset($unguarded);
$file_phids[$generate_user_phid] = $generate_file->getPHID();
$generator = new PhabricatorFilesComposeAvatarBuiltinFile();
foreach ($generate_users as $user) {
$file = $generator->updateUser($user);
$file_phids[$user->getPHID()] = $file->getPHID();
}
if ($file_phids) {

View file

@ -51,6 +51,7 @@ final class PhabricatorPeopleProfileImageWorkflow
}
$version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION;
$generator = new PhabricatorFilesComposeAvatarBuiltinFile();
foreach ($iterator as $user) {
$username = $user->getUsername();
@ -63,16 +64,13 @@ final class PhabricatorPeopleProfileImageWorkflow
}
if ($default_phid == null || $is_force || $generate) {
$file = id(new PhabricatorFilesComposeAvatarBuiltinFile())
->getUserProfileImageFile($username);
$user->setDefaultProfileImagePHID($file->getPHID());
$user->setDefaultProfileImageVersion($version);
$user->save();
$console->writeOut(
"%s\n",
pht(
'Generating profile image for "%s".',
$username));
$generator->updateUser($user);
} else {
$console->writeOut(
"%s\n",

View file

@ -267,6 +267,10 @@ final class PhabricatorUser
return !($this->getPHID() === null);
}
public function saveWithoutIndex() {
return parent::save();
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
@ -276,7 +280,7 @@ final class PhabricatorUser
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = parent::save();
$result = $this->saveWithoutIndex();
if ($this->profile) {
$this->profile->save();

View file

@ -275,12 +275,18 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
AphrontRequest $request,
array $errors) {
$src = 'https://js.stripe.com/v2/';
$ccform = id(new PhortuneCreditCardForm())
->setSecurityAssurance(
pht('Payments are processed securely by Stripe.'))
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.stripe.com/v2/');
->addScript($src);
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('script-src', $src)
->addContentSecurityPolicyURI('frame-src', $src);
Javelin::initBehavior(
'stripe-payment-form',

View file

@ -273,7 +273,6 @@ final class PhrictionTransactionEditor
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getContent()->getAuthorPHID(),
$this->getActingAsPHID(),
);
}

View file

@ -156,11 +156,14 @@ final class PhabricatorProjectTransactionEditor
PhabricatorPolicyCapability::CAN_EDIT);
}
} else {
// You need CAN_EDIT to change members other than yourself.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$this->getIsNewObject()) {
// You need CAN_EDIT to change members other than yourself.
// (PHI193) Just skip this check if we're creating a project.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
return;
}

View file

@ -5,7 +5,7 @@ final class HeraldProjectsField extends HeraldField {
const FIELDCONST = 'projects';
public function getHeraldFieldName() {
return pht('Projects');
return pht('Project tags');
}
public function getFieldGroupKey() {

View file

@ -512,15 +512,7 @@ final class PhabricatorApplicationSearchController
->setURI($job->getMonitorURI());
} else {
$file = $export_engine->exportFile();
return $this->newDialog()
->setTitle(pht('Download Results'))
->appendParagraph(
pht('Click the download button to download the exported data.'))
->addCancelButton($cancel_uri, pht('Done'))
->setSubmitURI($file->getDownloadURI())
->setDisableWorkflowOnSubmit(true)
->addSubmitButton(pht('Download Data'));
return $file->newDownloadResponse();
}
}
}
@ -535,12 +527,18 @@ final class PhabricatorApplicationSearchController
->setValue($format_key)
->setOptions($format_options));
if ($is_large_export) {
$submit_button = pht('Continue');
} else {
$submit_button = pht('Download Data');
}
return $this->newDialog()
->setTitle(pht('Export Results'))
->setErrors($errors)
->appendForm($export_form)
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Continue'));
->addSubmitButton($submit_button);
}
private function processEditRequest() {

View file

@ -1,95 +0,0 @@
<?php
final class PhabricatorFilesComposeAvatarExample extends PhabricatorUIExample {
public function getName() {
return pht('Avatars');
}
public function getDescription() {
return pht('Tests various color palettes and sizes.');
}
public function getCategory() {
return pht('Technical');
}
public function renderExample() {
$request = $this->getRequest();
$viewer = $request->getUser();
$colors = PhabricatorFilesComposeAvatarBuiltinFile::getColorMap();
$packs = PhabricatorFilesComposeAvatarBuiltinFile::getImagePackMap();
$builtins = PhabricatorFilesComposeAvatarBuiltinFile::getImageMap();
$borders = PhabricatorFilesComposeAvatarBuiltinFile::getBorderMap();
$images = array();
foreach ($builtins as $builtin => $raw_file) {
$file = PhabricatorFile::loadBuiltin($viewer, $builtin);
$images[] = $file->getBestURI();
}
$content = array();
shuffle($colors);
foreach ($colors as $color) {
shuffle($borders);
$color_const = hexdec(trim($color, '#'));
$border = head($borders);
$border_color = implode(', ', $border);
$styles = array();
$styles[] = 'background-color: '.$color.';';
$styles[] = 'display: inline-block;';
$styles[] = 'height: 42px;';
$styles[] = 'width: 42px;';
$styles[] = 'border-radius: 3px;';
$styles[] = 'border: 4px solid rgba('.$border_color.');';
shuffle($images);
$png = head($images);
$image = phutil_tag(
'img',
array(
'src' => $png,
'height' => 42,
'width' => 42,
));
$tag = phutil_tag(
'div',
array(
'style' => implode(' ', $styles),
),
$image);
$content[] = phutil_tag(
'div',
array(
'class' => 'mlr mlb',
'style' => 'float: left;',
),
$tag);
}
$count = new PhutilNumber(
count($colors) * count($builtins) * count($borders));
$infoview = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('This installation can generate %s unique '.
'avatars. You can add additional image packs in '.
'resources/builtins/alphanumeric/.', $count));
$info = phutil_tag_div('pmb', $infoview);
$view = phutil_tag_div('ml', $content);
return phutil_tag(
'div',
array(),
array(
$info,
$view,
));
}
}

View file

@ -36,7 +36,6 @@ final class PhabricatorExportEngineBulkJobType
->setName(pht('Temporary File Expired'));
} else {
$actions[] = id(new PhabricatorActionView())
->setRenderAsForm(true)
->setHref($file->getDownloadURI())
->setIcon('fa-download')
->setName(pht('Download Data Export'));

View file

@ -4,7 +4,7 @@ final class AphrontTableView extends AphrontView {
protected $data;
protected $headers;
protected $shortHeaders;
protected $shortHeaders = array();
protected $rowClasses = array();
protected $columnClasses = array();
protected $cellClasses = array();
@ -21,7 +21,7 @@ final class AphrontTableView extends AphrontView {
protected $sortParam;
protected $sortSelected;
protected $sortReverse;
protected $sortValues;
protected $sortValues = array();
private $deviceReadyTable;
public function __construct(array $data) {
@ -248,7 +248,7 @@ final class AphrontTableView extends AphrontView {
foreach ($col_classes as $key => $value) {
if (($sort_values[$key] !== null) &&
if (isset($sort_values[$key]) &&
($sort_values[$key] == $this->sortSelected)) {
$value = trim($value.' sorted-column');
}

View file

@ -42,16 +42,24 @@ final class AphrontFormRecaptchaControl extends AphrontFormControl {
$js = 'https://www.google.com/recaptcha/api.js';
$pubkey = PhabricatorEnv::getEnvConfig('recaptcha.public-key');
return array(
phutil_tag('div', array(
'class' => 'g-recaptcha',
'data-sitekey' => $pubkey,
)),
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('script-src', $js)
->addContentSecurityPolicyURI('script-src', 'https://www.gstatic.com/')
->addContentSecurityPolicyURI('frame-src', 'https://www.google.com/');
phutil_tag('script', array(
'type' => 'text/javascript',
'src' => $js,
)),
return array(
phutil_tag(
'div',
array(
'class' => 'g-recaptcha',
'data-sitekey' => $pubkey,
)),
phutil_tag(
'script',
array(
'type' => 'text/javascript',
'src' => $js,
)),
);
}
}

View file

@ -134,21 +134,13 @@ final class PhabricatorFileLinkView extends AphrontTagView {
$dl_icon = id(new PHUIIconView())
->setIcon('fa-download');
$download_form = phabricator_form(
$this->getViewer(),
$download_link = phutil_tag(
'a',
array(
'action' => $this->getFileDownloadURI(),
'method' => 'POST',
'class' => 'embed-download-form',
'sigil' => 'embed-download-form download',
'class' => 'phabricator-remarkup-embed-layout-download',
'href' => $this->getFileDownloadURI(),
),
phutil_tag(
'button',
array(
'class' => 'phabricator-remarkup-embed-layout-download',
'type' => 'submit',
),
pht('Download')));
pht('Download'));
$info = phutil_tag(
'span',
@ -177,7 +169,7 @@ final class PhabricatorFileLinkView extends AphrontTagView {
return array(
$icon,
$inner,
$download_form,
$download_link,
);
}
}

View file

@ -73,6 +73,7 @@ final class PhabricatorSourceCodeView extends AphrontView {
pht('...')));
}
$base_uri = (string)$this->uri;
foreach ($lines as $line) {
// NOTE: See phabricator-oncopy behavior.
@ -84,17 +85,16 @@ final class PhabricatorSourceCodeView extends AphrontView {
}
if ($this->canClickHighlight) {
$line_uri = $this->uri.'$'.$line_number;
$line_href = (string)new PhutilURI($line_uri);
$line_href = $base_uri.'$'.$line_number;
$tag_number = javelin_tag(
$tag_number = phutil_tag(
'a',
array(
'href' => $line_href,
),
$line_number);
} else {
$tag_number = javelin_tag(
$tag_number = phutil_tag(
'span',
array(),
$line_number);
@ -104,11 +104,10 @@ final class PhabricatorSourceCodeView extends AphrontView {
'tr',
$row_attributes,
array(
javelin_tag(
phutil_tag(
'th',
array(
'class' => 'phabricator-source-line',
'sigil' => 'phabricator-source-line',
),
$tag_number),
phutil_tag(
@ -134,6 +133,9 @@ final class PhabricatorSourceCodeView extends AphrontView {
array(
'class' => implode(' ', $classes),
'sigil' => 'phabricator-source',
'meta' => array(
'uri' => (string)$this->uri,
),
),
phutil_implode_html('', $rows)));
}

View file

@ -59,9 +59,15 @@ abstract class AphrontPageView extends AphrontView {
),
array($body, $tail));
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$data_fragment = phutil_safe_html(' data-developer-mode="1"');
} else {
$data_fragment = null;
}
$response = hsprintf(
'<!DOCTYPE html>'.
'<html>'.
'<html%s>'.
'<head>'.
'<meta charset="UTF-8" />'.
'<title>%s</title>'.
@ -69,6 +75,7 @@ abstract class AphrontPageView extends AphrontView {
'</head>'.
'%s'.
'</html>',
$data_fragment,
$title,
$head,
$body);

View file

@ -59,11 +59,6 @@ class PhabricatorBarePageView extends AphrontPageView {
}
protected function getHead() {
$framebust = null;
if (!$this->getFrameable()) {
$framebust = '(top == self) || top.location.replace(self.location.href);';
}
$viewport_tag = null;
if ($this->getDeviceReady()) {
$viewport_tag = phutil_tag(
@ -124,7 +119,7 @@ class PhabricatorBarePageView extends AphrontPageView {
'meta',
array(
'name' => 'referrer',
'content' => 'never',
'content' => 'no-referrer',
));
$response = CelerityAPI::getStaticResourceResponse();
@ -140,9 +135,8 @@ class PhabricatorBarePageView extends AphrontPageView {
}
}
$developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
return hsprintf(
'%s%s%s%s%s%s%s%s%s',
'%s%s%s%s%s%s%s%s',
$viewport_tag,
$mask_icon,
$icon_tag_76,
@ -150,8 +144,6 @@ class PhabricatorBarePageView extends AphrontPageView {
$icon_tag_152,
$favicon_tag,
$referrer_tag,
CelerityStaticResourceResponse::renderInlineScript(
$framebust.jsprintf('window.__DEV__=%d;', ($developer ? 1 : 0))),
$response->renderResourcesOfType('css'));
}

View file

@ -279,35 +279,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
}
}
$icon = id(new PHUIIconView())
->setIcon('fa-download')
->addClass('phui-icon-circle-icon');
$lightbox_id = celerity_generate_unique_node_id();
$download_form = phabricator_form(
$user,
array(
'action' => '#',
'method' => 'POST',
'class' => 'lightbox-download-form',
'sigil' => 'download lightbox-download-submit',
'id' => 'lightbox-download-form',
),
phutil_tag(
'a',
array(
'class' => 'lightbox-download phui-icon-circle hover-green',
'href' => '#',
),
array(
$icon,
)));
Javelin::initBehavior(
'lightbox-attachments',
array(
'lightbox_id' => $lightbox_id,
'downloadForm' => $download_form,
));
Javelin::initBehavior('lightbox-attachments');
}
Javelin::initBehavior('aphront-form-disable-on-submit');
@ -608,12 +580,15 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
Javelin::initBehavior(
'aphlict-listen',
array(
'websocketURI' => (string)$client_uri,
'websocketURI' => (string)$client_uri,
) + $this->buildAphlictListenConfigData());
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('connect-src', $client_uri);
}
}
$tail[] = $response->renderHTMLFooter();
$tail[] = $response->renderHTMLFooter($this->getFrameable());
return $tail;
}
@ -860,6 +835,19 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
$blacklist[] = $application->getQuicksandURIPatternBlacklist();
}
// See T4340. Currently, Phortune and Auth both require pulling in external
// Javascript (for Stripe card management and Recaptcha, respectively).
// This can put us in a position where the user loads a page with a
// restrictive Content-Security-Policy, then uses Quicksand to navigate to
// a page which needs to load external scripts. For now, just blacklist
// these entire applications since we aren't giving up anything
// significant by doing so.
$blacklist[] = array(
'/phortune/.*',
'/auth/.*',
);
return array_mergev($blacklist);
}
@ -903,9 +891,17 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
->setContent($content);
} else {
$content = $this->render();
$response = id(new AphrontWebpageResponse())
->setContent($content)
->setFrameable($this->getFrameable());
$static = CelerityAPI::getStaticResourceResponse();
foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) {
foreach ($uris as $uri) {
$response->addContentSecurityPolicyURI($kind, $uri);
}
}
}
return $response;

View file

@ -30,3 +30,140 @@
text-overflow: ellipsis;
color: {$lightgreytext};
}
.harbormaster-log-view-loading {
padding: 8px;
text-align: center;
color: {$lightgreytext};
}
.harbormaster-log-table > tbody > tr > th {
background-color: {$paste.highlight};
border-right: 1px solid {$paste.border};
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.harbormaster-log-table > tbody > tr > th a::before {
/* Render the line numbers into the document using a pseudo-element so that
the text is not copied. */
content: attr(data-n);
}
.harbormaster-log-table > tbody > tr > th a {
display: block;
color: {$darkbluetext};
text-align: right;
padding: 2px 6px 1px 12px;
}
.harbormaster-log-table > tbody > tr > th a:hover {
background: {$paste.border};
}
.harbormaster-log-table > tbody > tr > td {
white-space: pre-wrap;
padding: 2px 8px 1px;
width: 100%;
}
.harbormaster-log-table > tbody > tr > td.harbormaster-log-expand-cell {
padding: 4px 0;
}
.harbormaster-log-table tr.phabricator-source-highlight > th {
background: {$paste.border};
}
.harbormaster-log-table tr.phabricator-source-highlight > td {
background: {$paste.highlight};
}
.harbormaster-log-expand-table {
width: 100%;
background: {$paste.highlight};
border-top: 1px solid {$paste.border};
border-bottom: 1px solid {$paste.border};
}
.harbormaster-log-expand-table a {
display: block;
padding: 6px 16px;
color: {$darkbluetext};
}
.device-desktop .harbormaster-log-expand-table a:hover {
background: {$paste.border};
text-decoration: none;
}
.harbormaster-log-expand-table td {
vertical-align: middle;
font: 13px 'Segoe UI', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Lato',
'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.harbormaster-log-expand-up {
text-align: right;
width: 50%;
}
.harbormaster-log-expand-up .phui-icon-view {
margin: 0 0 0px 4px;
}
.harbormaster-log-follow {
text-align: center;
}
.harbormaster-log-follow .phui-icon-view {
margin: 0 4px;
}
.harbormaster-log-expand-mid {
text-align: center;
white-space: nowrap;
border-left: 1px solid {$paste.border};
border-right: 1px solid {$paste.border};
}
.harbormaster-log-expand-down {
text-align: left;
width: 50%;
}
.harbormaster-log-expand-down .phui-icon-view {
margin: 0 4px 0 0;
}
.harbormaster-log-following .harbormaster-log-table
.harbormaster-log-follow-start {
display: none;
}
.harbormaster-log-table .harbormaster-log-follow-stop {
display: none;
}
.harbormaster-log-following .harbormaster-log-table
.harbormaster-log-follow-stop {
display: block;
}
.harbormaster-log-appear > td {
animation: harbormaster-fade-in 1s linear;
}
@keyframes harbormaster-fade-in {
0% {
opacity: 0.5;
}
100% {
opacity: 1.0;
}
}

View file

@ -31,7 +31,6 @@
.phabricator-source-line {
background-color: {$paste.highlight};
text-align: right;
padding: 2px 6px 1px 12px;
border-right: 1px solid {$paste.border};
color: {$sh-yellowtext};
@ -48,17 +47,23 @@
th.phabricator-source-line a {
color: {$darkbluetext};
display: block;
padding: 2px 6px 1px 12px;
}
th.phabricator-source-line:hover {
th.phabricator-source-line a:hover {
background: {$paste.border};
cursor: pointer;
text-decoration: none;
}
.phabricator-source-highlight {
background: {$paste.highlight};
}
.phabricator-source-highlight th.phabricator-source-line {
background: {$paste.border};
}
.phabricator-source-code-summary {
padding-bottom: 8px;
}

View file

@ -517,6 +517,36 @@ JX.install('Stratcom', {
return len ? this._execContext[len - 1].event : null;
},
initialize: function(initializers) {
var frameable = false;
for (var ii = 0; ii < initializers.length; ii++) {
var kind = initializers[ii].kind;
var data = initializers[ii].data;
switch (kind) {
case 'behaviors':
JX.initBehaviors(data);
break;
case 'merge':
JX.Stratcom.mergeData(data.block, data.data);
JX.Stratcom.ready = true;
break;
case 'frameable':
frameable = !!data;
break;
}
}
// If the initializer tags did not explicitly allow framing, framebust.
// This protects us from clickjacking attacks on older versions of IE.
// The "X-Frame-Options" and "Content-Security-Policy" headers provide
// more modern variations of this protection.
if (!frameable) {
if (window.top != window.self) {
window.top.location.replace(window.self.location.href);
}
}
},
/**
* Merge metadata. You must call this (even if you have no metadata) to
@ -542,7 +572,6 @@ JX.install('Stratcom', {
} else {
this._data[block] = data;
if (block === 0) {
JX.Stratcom.ready = true;
JX.flushHoldingQueue('install-init', function(fn) {
fn();
});

View file

@ -46,25 +46,51 @@
makeHoldingQueue('behavior');
makeHoldingQueue('install-init');
window.__DEV__ = window.__DEV__ || 0;
var loaded = false;
var onload = [];
var master_event_queue = [];
var root = document.documentElement;
var has_add_event_listener = !!root.addEventListener;
window.__DEV__ = !!root.getAttribute('data-developer-mode');
JX.__rawEventQueue = function(what) {
master_event_queue.push(what);
// Evade static analysis - JX.Stratcom
var ii;
var Stratcom = JX['Stratcom'];
if (Stratcom && Stratcom.ready) {
// Empty the queue now so that exceptions don't cause us to repeatedly
// try to handle events.
if (!loaded && what.type == 'domready') {
var initializers = [];
var tags = JX.DOM.scry(document.body, 'data');
for (ii = 0; ii < tags.length; ii++) {
// Ignore tags which are not immediate children of the document
// body. If an attacker somehow injects arbitrary tags into the
// content of the document, that should not give them access to
// modify initialization behaviors.
if (tags[ii].parentNode !== document.body) {
continue;
}
var tag_kind = tags[ii].getAttribute('data-javelin-init-kind');
var tag_data = tags[ii].getAttribute('data-javelin-init-data');
tag_data = JX.JSON.parse(tag_data);
initializers.push({kind: tag_kind, data: tag_data});
}
Stratcom.initialize(initializers);
loaded = true;
}
if (loaded) {
// Empty the queue now so that exceptions don't cause us to repeatedly
// try to handle events.
var local_queue = master_event_queue;
master_event_queue = [];
for (var ii = 0; ii < local_queue.length; ++ii) {
for (ii = 0; ii < local_queue.length; ++ii) {
var evt = local_queue[ii];
// Sometimes IE gives us events which throw when ".type" is accessed;
@ -72,11 +98,10 @@
// figure out where these are coming from.
try { var test = evt.type; } catch (x) { continue; }
if (!loaded && evt.type == 'domready') {
if (evt.type == 'domready') {
// NOTE: Firefox interprets "document.body.id = null" as the string
// literal "null".
document.body && (document.body.id = '');
loaded = true;
for (var jj = 0; jj < onload.length; jj++) {
onload[jj]();
}

View file

@ -276,6 +276,13 @@ JX.install('Workflow', {
// It is permissible to send back a falsey redirect to force a page
// reload, so we need to take this branch if the key is present.
if (r && (typeof r.redirect != 'undefined')) {
// Before we redirect to file downloads, we close the dialog. These
// redirects aren't real navigation events so we end up stuck in the
// dialog otherwise.
if (r.close) {
this._pop();
}
JX.$U(r.redirect).go();
} else if (r && r.dialog) {
this._push();

View file

@ -0,0 +1,114 @@
/**
* @provides javelin-behavior-harbormaster-log
* @requires javelin-behavior
*/
JX.behavior('harbormaster-log', function(config) {
var contentNode = JX.$(config.contentNodeID);
var following = false;
var autoscroll = false;
JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) {
if (!e.isNormalClick()) {
return;
}
e.kill();
expand(e.getTarget(), true);
});
function expand(node, is_action) {
var row = JX.DOM.findAbove(node, 'tr');
row = JX.DOM.findAbove(row, 'tr');
var data = JX.Stratcom.getData(node);
if (data.stop) {
following = false;
autoscroll = false;
JX.DOM.alterClass(contentNode, 'harbormaster-log-following', false);
return;
}
var uri = new JX.URI(config.renderURI)
.addQueryParams(data);
if (data.live && is_action) {
following = true;
autoscroll = true;
JX.DOM.alterClass(contentNode, 'harbormaster-log-following', true);
}
var request = new JX.Request(uri, function(r) {
var result = JX.$H(r.markup).getNode();
var rows = [].slice.apply(result.firstChild.childNodes);
// If we're following the bottom of the log, the result always includes
// the last line from the previous render. Throw it away, then add the
// new data.
if (data.live && row.previousSibling) {
JX.DOM.remove(row.previousSibling);
}
JX.DOM.replace(row, rows);
if (data.live) {
// If this was a live follow, scroll the new data into view. This is
// probably intensely annoying in practice but seems cool for now.
if (autoscroll) {
var last_row = rows[rows.length - 1];
var tail_pos = JX.$V(last_row).y + JX.Vector.getDim(last_row).y;
var view_y = JX.Vector.getViewport().y;
JX.DOM.scrollToPosition(null, (tail_pos - view_y) + 32);
// This will fire a scroll event, but we want to keep autoscroll
// enabled until we see an explicit scroll event by the user.
setTimeout(function() { autoscroll = true; }, 0);
}
setTimeout(follow, 2000);
for (var ii = 1; ii < (rows.length - 1); ii++) {
JX.DOM.alterClass(rows[ii], 'harbormaster-log-appear', true);
}
}
});
request.send();
}
// If the user explicitly scrolls while following a log, keep live updating
// it but stop following it with the scrollbar.
JX.Stratcom.listen('scroll', null, function() {
autoscroll = false;
});
function follow() {
if (!following) {
return;
}
var live;
try {
live = JX.DOM.find(contentNode, 'a', 'harbormaster-log-live');
} catch (e) {
return;
}
expand(live);
}
function onresponse(r) {
JX.DOM.alterClass(contentNode, 'harbormaster-log-view-loading', false);
JX.DOM.setContent(contentNode, JX.$H(r.markup));
}
var uri = new JX.URI(config.initialURI);
new JX.Request(uri, onresponse)
.send();
});

View file

@ -9,19 +9,18 @@
* phabricator-busy
*/
JX.behavior('lightbox-attachments', function (config) {
JX.behavior('lightbox-attachments', function() {
var lightbox = null;
var lightbox = null;
var prev = null;
var next = null;
var shown = false;
var downloadForm = JX.$H(config.downloadForm).getFragment().firstChild;
var lightbox_id = config.lightbox_id;
function _toggleComment(e) {
e.kill();
shown = !shown;
JX.DOM.alterClass(JX.$(lightbox_id), 'comment-panel-open', shown);
JX.DOM.alterClass(lightbox, 'comment-panel-open', shown);
}
function markCommentsLoading(loading) {
@ -48,6 +47,12 @@ JX.behavior('lightbox-attachments', function (config) {
return;
}
// If you click the "Download" link inside an embedded file element,
// don't lightbox the file.
if (e.getNode('tag:a')) {
return;
}
e.kill();
var mainFrame = JX.$('main-page-frame');
@ -82,8 +87,7 @@ JX.behavior('lightbox-attachments', function (config) {
var img_uri = '';
var img = '';
var extra_status = '';
// for now, this conditional is always true
// revisit if / when we decide to add non-images to lightbox view
if (target_data.viewable) {
img_uri = target_data.uri;
var alt_name = '';
@ -114,7 +118,7 @@ JX.behavior('lightbox-attachments', function (config) {
{
className : 'lightbox-icon-frame',
sigil : 'lightbox-download-submit',
href : '#',
href : target_data.dUri,
},
[ imgIcon, nameElement ]
);
@ -138,12 +142,12 @@ JX.behavior('lightbox-attachments', function (config) {
);
var commentClass = (shown) ? 'comment-panel-open' : '';
lightbox =
JX.$N('div',
{
className : 'lightbox-attachment ' + commentClass,
sigil : 'lightbox-attachment',
id : lightbox_id
sigil : 'lightbox-attachment'
},
[imgFrame, commentFrame]
);
@ -161,12 +165,17 @@ JX.behavior('lightbox-attachments', function (config) {
]
);
var downloadSpan =
JX.$N('span',
{
className : 'lightbox-download'
}
);
var download_icon = new JX.PHUIXIconView()
.setIcon('fa-download phui-icon-circle-icon')
.getNode();
var download_button = JX.$N(
'a',
{
className: 'lightbox-download phui-icon-circle hover-sky',
href: target_data.dUri
},
download_icon);
var commentIcon = new JX.PHUIXIconView()
.setIcon('fa-comments phui-icon-circle-icon')
@ -180,6 +189,7 @@ JX.behavior('lightbox-attachments', function (config) {
},
commentIcon
);
var closeIcon = new JX.PHUIXIconView()
.setIcon('fa-times phui-icon-circle-icon')
.getNode();
@ -190,12 +200,13 @@ JX.behavior('lightbox-attachments', function (config) {
href : '#'
},
closeIcon);
var statusHTML =
JX.$N('div',
{
className : 'lightbox-status'
},
[statusSpan, closeButton, commentButton, downloadSpan]
[statusSpan, closeButton, commentButton, download_button]
);
JX.DOM.appendContent(lightbox, statusHTML);
JX.DOM.listen(closeButton, 'click', null, closeLightBox);
@ -246,9 +257,6 @@ JX.behavior('lightbox-attachments', function (config) {
JX.DOM.alterClass(document.body, 'lightbox-attached', true);
JX.Mask.show('jx-dark-mask');
downloadForm.action = target_data.dUri;
downloadSpan.appendChild(downloadForm);
document.body.appendChild(lightbox);
if (img_uri) {
@ -365,23 +373,12 @@ JX.behavior('lightbox-attachments', function (config) {
'lightbox-comment-form',
_sendMessage);
var _startDownload = function(e) {
e.kill();
var form = JX.$('lightbox-download-form');
form.submit();
};
var _startPageDownload = function(e) {
e.kill();
var form = e.getNode('tag:form');
form.submit();
};
JX.Stratcom.listen(
'click',
'lightbox-download-submit',
_startDownload);
JX.Stratcom.listen(
'click',
'embed-download-form',

View file

@ -10,6 +10,7 @@ JX.behavior('phabricator-line-linker', function() {
var origin = null;
var target = null;
var root = null;
var highlighted = null;
var editor_link = null;
try {
@ -19,48 +20,111 @@ JX.behavior('phabricator-line-linker', function() {
}
function getRowNumber(tr) {
var th = JX.DOM.find(tr, 'th', 'phabricator-source-line');
var th = tr.firstChild;
// If the "<th />" tag contains an "<a />" with "data-n" that we're using
// to prevent copy/paste of line numbers, use that.
if (th.firstChild) {
var line = th.firstChild.getAttribute('data-n');
if (line) {
return line;
}
}
return +(th.textContent || th.innerText);
}
JX.Stratcom.listen(
'mousedown',
'phabricator-source-line',
['click', 'mousedown'],
['phabricator-source', 'tag:tr', 'tag:th', 'tag:a'],
function(e) {
if (!e.isNormalMouseEvent()) {
return;
}
origin = e.getNode('tag:tr');
target = origin;
root = e.getNode('phabricator-source');
e.kill();
});
JX.Stratcom.listen(
'click',
'phabricator-source-line',
function(e) {
// Make sure the link we clicked is actually a line number in a source
// table, not some kind of link in some element embedded inside the
// table. The row's immediate ancestor table needs to be the table with
// the "phabricator-source" sigil.
var row = e.getNode('tag:tr');
var table = e.getNode('phabricator-source');
if (JX.DOM.findAbove(row, 'table') !== table) {
return;
}
var number = getRowNumber(row);
if (!number) {
return;
}
e.kill();
// If this is a click event, kill it. We handle mousedown and mouseup
// instead.
if (e.getType() === 'click') {
return;
}
origin = row;
target = origin;
root = table;
});
var highlight = function(e) {
if (!origin || e.getNode('phabricator-source') !== root) {
if (!origin) {
return;
}
if (e.getNode('phabricator-source') !== root) {
return;
}
target = e.getNode('tag:tr');
var highlighting = false;
var source = null;
var trs = JX.DOM.scry(root, 'tr');
for (var i = 0; i < trs.length; i++) {
if (!highlighting && (trs[i] === origin || trs[i] === target)) {
highlighting = true;
source = trs[i];
var min;
var max;
// NOTE: We're using position to figure out which order these rows are in,
// not row numbers. We do this because Harbormaster build logs may have
// multiple rows with the same row number.
if (JX.$V(origin).y <= JX.$V(target).y) {
min = origin;
max = target;
} else {
min = target;
max = origin;
}
// If we haven't changed highlighting, we don't have a list of highlighted
// nodes yet. Assume every row is highlighted.
var ii;
if (highlighted === null) {
highlighted = [];
var rows = JX.DOM.scry(root, 'tr');
for (ii = 0; ii < rows.length; ii++) {
highlighted.push(rows[ii]);
}
JX.DOM.alterClass(trs[i], 'phabricator-source-highlight', highlighting);
if (trs[i] === (source === origin ? target : origin)) {
highlighting = false;
}
// Unhighlight any existing highlighted rows.
for (ii = 0; ii < highlighted.length; ii++) {
JX.DOM.alterClass(highlighted[ii], 'phabricator-source-highlight', false);
}
highlighted = [];
// Highlight the newly selected rows.
var cursor = min;
while (true) {
JX.DOM.alterClass(cursor, 'phabricator-source-highlight', true);
highlighted.push(cursor);
if (cursor === max) {
break;
}
cursor = cursor.nextSibling;
}
};
@ -75,20 +139,26 @@ JX.behavior('phabricator-line-linker', function() {
}
highlight(e);
e.kill();
var o = getRowNumber(origin);
var t = getRowNumber(target);
var lines = (o == t ? o : Math.min(o, t) + '-' + Math.max(o, t));
var th = JX.DOM.find(origin, 'th', 'phabricator-source-line');
var uri = JX.DOM.find(th, 'a').href;
uri = uri.replace(/(.*\$)\d+/, '$1' + lines);
var uri = JX.Stratcom.getData(root).uri;
origin = null;
target = null;
e.kill();
root = null;
var lines = (o == t ? o : Math.min(o, t) + '-' + Math.max(o, t));
uri = uri + '$' + lines;
JX.History.replace(uri);
if (editor_link.href) {
var editdata = JX.Stratcom.getData(editor_link);
editor_link.href = editdata.link_template.replace('%25l', o);
if (editor_link) {
if (editor_link.href) {
var editdata = JX.Stratcom.getData(editor_link);
editor_link.href = editdata.link_template.replace('%25l', o);
}
}
});