1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-10 14:51:06 +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.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65', 'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => '2fa91e14', 'core.pkg.css' => '2fa91e14',
'core.pkg.js' => 'dc13d4b7', 'core.pkg.js' => 'a3ceffdb',
'darkconsole.pkg.js' => '1f9a31bc', 'darkconsole.pkg.js' => '1f9a31bc',
'differential.pkg.css' => '113e692c', 'differential.pkg.css' => '113e692c',
'differential.pkg.js' => 'f6d809c0', 'differential.pkg.js' => 'f6d809c0',
@ -78,7 +78,7 @@ return array(
'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948', 'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948',
'rsrc/css/application/flag/flag.css' => 'bba8f811', '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-test.css' => 'a52e323e',
'rsrc/css/application/herald/herald.css' => 'cd8d0134', 'rsrc/css/application/herald/herald.css' => 'cd8d0134',
'rsrc/css/application/maniphest/report.css' => '9b9580b7', 'rsrc/css/application/maniphest/report.css' => '9b9580b7',
@ -122,7 +122,7 @@ return array(
'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/font-lato.css' => 'c7ccd872',
'rsrc/css/font/phui-font-icon-base.css' => '870a7360', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360',
'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97', '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-bar.css' => 'f1ff5494',
'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68',
'rsrc/css/phui/button/phui-button.css' => '1863cc6e', '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.woff' => '13d39fe2',
'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742', 'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742',
'rsrc/externals/javelin/core/Event.js' => '2ee659ce', '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__/event-stop-and-kill.js' => '717554e4',
'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85',
'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313',
'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', '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/init_node.js' => 'c234aded',
'rsrc/externals/javelin/core/install.js' => '05270951', 'rsrc/externals/javelin/core/install.js' => '05270951',
'rsrc/externals/javelin/core/util.js' => '93cc50d6', 'rsrc/externals/javelin/core/util.js' => '93cc50d6',
@ -255,7 +255,7 @@ return array(
'rsrc/externals/javelin/lib/URI.js' => 'c989ade3', 'rsrc/externals/javelin/lib/URI.js' => 'c989ade3',
'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8',
'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6', '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__/Cookie.js' => '5ed109e8',
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68', '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/drydock/drydock-live-operation-status.js' => '901935ef',
'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab',
'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', '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/HeraldRuleEditor.js' => 'dca75c0e',
'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec',
'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', '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-hovercard.js' => 'bcaccd64',
'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => '560f41da', 'rsrc/js/core/behavior-lightbox-attachments.js' => 'e31fad01',
'rsrc/js/core/behavior-line-linker.js' => '1499a8cb', 'rsrc/js/core/behavior-line-linker.js' => 'a9b946f8',
'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0',
'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
@ -578,7 +579,7 @@ return array(
'font-fontawesome' => 'e838e088', 'font-fontawesome' => 'e838e088',
'font-lato' => 'c7ccd872', 'font-lato' => 'c7ccd872',
'global-drag-and-drop-css' => 'b556a948', 'global-drag-and-drop-css' => 'b556a948',
'harbormaster-css' => 'f491c9f4', 'harbormaster-css' => '730a4a3c',
'herald-css' => 'cd8d0134', 'herald-css' => 'cd8d0134',
'herald-rule-editor' => 'dca75c0e', 'herald-rule-editor' => 'dca75c0e',
'herald-test-css' => 'a52e323e', 'herald-test-css' => 'a52e323e',
@ -635,12 +636,13 @@ return array(
'javelin-behavior-event-all-day' => 'b41537c9', 'javelin-behavior-event-all-day' => 'b41537c9',
'javelin-behavior-fancy-datepicker' => 'ecf4e799', 'javelin-behavior-fancy-datepicker' => 'ecf4e799',
'javelin-behavior-global-drag-and-drop' => '960f6a39', 'javelin-behavior-global-drag-and-drop' => '960f6a39',
'javelin-behavior-harbormaster-log' => '191b4909',
'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-herald-rule-editor' => '7ebaeed3',
'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-high-security-warning' => 'a464fe03',
'javelin-behavior-history-install' => '7ee2b591', 'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8499b6ab', 'javelin-behavior-icon-composer' => '8499b6ab',
'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => '560f41da', 'javelin-behavior-lightbox-attachments' => 'e31fad01',
'javelin-behavior-line-chart' => 'e4232876', 'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
@ -656,7 +658,7 @@ return array(
'javelin-behavior-phabricator-gesture-example' => '558829c2', 'javelin-behavior-phabricator-gesture-example' => '558829c2',
'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0',
'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', '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-nav' => '836f966d',
'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-notification-example' => '8ce821c5',
'javelin-behavior-phabricator-object-selector' => '77c1f0b0', 'javelin-behavior-phabricator-object-selector' => '77c1f0b0',
@ -720,7 +722,7 @@ return array(
'javelin-install' => '05270951', 'javelin-install' => '05270951',
'javelin-json' => '69adf288', 'javelin-json' => '69adf288',
'javelin-leader' => '7f243deb', 'javelin-leader' => '7f243deb',
'javelin-magical-init' => '3010e992', 'javelin-magical-init' => '638a4e2b',
'javelin-mask' => '8a41885b', 'javelin-mask' => '8a41885b',
'javelin-quicksand' => '6b8ef10b', 'javelin-quicksand' => '6b8ef10b',
'javelin-reactor' => '2b8de964', 'javelin-reactor' => '2b8de964',
@ -733,7 +735,7 @@ return array(
'javelin-router' => '29274e2b', 'javelin-router' => '29274e2b',
'javelin-scrollbar' => '9065f639', 'javelin-scrollbar' => '9065f639',
'javelin-sound' => '949c0fe5', 'javelin-sound' => '949c0fe5',
'javelin-stratcom' => '6ad39b6f', 'javelin-stratcom' => '327f418a',
'javelin-tokenizer' => '8d3bc1b2', 'javelin-tokenizer' => '8d3bc1b2',
'javelin-typeahead' => '70baed2f', 'javelin-typeahead' => '70baed2f',
'javelin-typeahead-composite-source' => '503e17fd', 'javelin-typeahead-composite-source' => '503e17fd',
@ -755,7 +757,7 @@ return array(
'javelin-workboard-card' => 'c587b80f', 'javelin-workboard-card' => 'c587b80f',
'javelin-workboard-column' => '758b4758', 'javelin-workboard-column' => '758b4758',
'javelin-workboard-controller' => '26167537', 'javelin-workboard-controller' => '26167537',
'javelin-workflow' => '1e911d0f', 'javelin-workflow' => '0eb1db0c',
'maniphest-report-css' => '9b9580b7', 'maniphest-report-css' => '9b9580b7',
'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-edit-css' => 'fda62a9b',
'maniphest-task-summary-css' => '11cc5344', 'maniphest-task-summary-css' => '11cc5344',
@ -800,7 +802,7 @@ return array(
'phabricator-search-results-css' => '505dd8cf', 'phabricator-search-results-css' => '505dd8cf',
'phabricator-shaped-request' => '7cbe244b', 'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230', 'phabricator-slowvote-css' => 'a94b7230',
'phabricator-source-code-view-css' => 'aea41829', 'phabricator-source-code-view-css' => '926ced2d',
'phabricator-standard-page-view' => '34ee718b', 'phabricator-standard-page-view' => '34ee718b',
'phabricator-textareautils' => '320810c8', 'phabricator-textareautils' => '320810c8',
'phabricator-title' => '485aaa6c', 'phabricator-title' => '485aaa6c',
@ -975,6 +977,17 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-router', 'javelin-router',
), ),
'0eb1db0c' => array(
'javelin-stratcom',
'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
'javelin-util',
'javelin-mask',
'javelin-uri',
'javelin-routable',
),
'0f764c35' => array( '0f764c35' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -985,12 +998,6 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-typeahead-normalizer', 'javelin-typeahead-normalizer',
), ),
'1499a8cb' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'15d5ff71' => array( '15d5ff71' => array(
'aphront-typeahead-control-css', 'aphront-typeahead-control-css',
'phui-tag-view-css', 'phui-tag-view-css',
@ -1015,6 +1022,9 @@ return array(
'185bbd53' => array( '185bbd53' => array(
'javelin-install', 'javelin-install',
), ),
'191b4909' => array(
'javelin-behavior',
),
'1ad0a787' => array( '1ad0a787' => array(
'javelin-install', 'javelin-install',
'javelin-reactor', 'javelin-reactor',
@ -1033,17 +1043,6 @@ return array(
'javelin-request', 'javelin-request',
'javelin-uri', 'javelin-uri',
), ),
'1e911d0f' => array(
'javelin-stratcom',
'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
'javelin-util',
'javelin-mask',
'javelin-uri',
'javelin-routable',
),
'1f6794f6' => array( '1f6794f6' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',
@ -1129,6 +1128,12 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-workflow', 'javelin-workflow',
), ),
'327f418a' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'358b8c04' => array( '358b8c04' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1351,15 +1356,6 @@ return array(
'javelin-vector', 'javelin-vector',
'javelin-dom', 'javelin-dom',
), ),
'560f41da' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'58dea2fa' => array( '58dea2fa' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1444,12 +1440,6 @@ return array(
'69adf288' => array( '69adf288' => array(
'javelin-install', 'javelin-install',
), ),
'6ad39b6f' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'6b8ef10b' => array( '6b8ef10b' => array(
'javelin-install', 'javelin-install',
), ),
@ -1756,6 +1746,12 @@ return array(
'javelin-uri', 'javelin-uri',
'phabricator-keyboard-shortcut', 'phabricator-keyboard-shortcut',
), ),
'a9b946f8' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
'a9f88de2' => array( 'a9f88de2' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-dom', 'javelin-dom',
@ -2065,6 +2061,15 @@ return array(
'javelin-dom', 'javelin-dom',
'phabricator-draggable-list', 'phabricator-draggable-list',
), ),
'e31fad01' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'e379b58e' => array( 'e379b58e' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', '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', 'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php',
'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php', 'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php',
'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php', 'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php',
'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php',
'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php', 'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php',
'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php', 'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php', 'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php',
'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php', 'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php',
'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php',
'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php', 'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php',
'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php', 'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php', 'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php',
@ -801,9 +803,11 @@ phutil_register_library_map(array(
'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php', 'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php',
'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php', 'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php',
'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php', 'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php',
'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php', 'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php',
'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php', 'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php',
@ -1227,8 +1231,15 @@ phutil_register_library_map(array(
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php', 'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php', 'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php',
'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php', 'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php',
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php', 'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.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', 'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php', 'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php', 'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
@ -1306,11 +1317,14 @@ phutil_register_library_map(array(
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
'HarbormasterLogWorker' => 'applications/harbormaster/worker/HarbormasterLogWorker.php',
'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php', 'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php',
'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php', 'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
'HarbormasterManagementRebuildLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php',
'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php', 'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php',
'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php', 'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php', 'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php',
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php', 'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php', 'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
@ -3030,7 +3044,6 @@ phutil_register_library_map(array(
'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php', 'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php', 'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php',
'PhabricatorFilesComposeAvatarExample' => 'applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.php',
'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php', 'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php',
'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php', 'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php', 'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
@ -5859,10 +5872,12 @@ phutil_register_library_map(array(
'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBranchesController' => 'DiffusionController', 'DiffusionCommitBranchesController' => 'DiffusionController',
'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitController' => 'DiffusionController', 'DiffusionCommitController' => 'DiffusionController',
'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField',
@ -6011,9 +6026,11 @@ phutil_register_library_map(array(
'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField',
@ -6505,11 +6522,20 @@ phutil_register_library_map(array(
'HarbormasterBuildLog' => array( 'HarbormasterBuildLog' => array(
'HarbormasterDAO', 'HarbormasterDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
), ),
'HarbormasterBuildLogChunk' => 'HarbormasterDAO', 'HarbormasterBuildLogChunk' => 'HarbormasterDAO',
'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator', 'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator',
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildLogRenderController' => 'HarbormasterController',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildLogView' => 'AphrontView',
'HarbormasterBuildLogViewController' => 'HarbormasterController',
'HarbormasterBuildMessage' => array( 'HarbormasterBuildMessage' => array(
'HarbormasterDAO', 'HarbormasterDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
@ -6609,11 +6635,14 @@ phutil_register_library_map(array(
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView', 'HarbormasterLintPropertyView' => 'AphrontView',
'HarbormasterLogWorker' => 'HarbormasterWorker',
'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRebuildLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterMessageType' => 'Phobject', 'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
@ -8598,7 +8627,6 @@ phutil_register_library_map(array(
'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorFilesBuiltinFile' => 'Phobject', 'PhabricatorFilesBuiltinFile' => 'Phobject',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesComposeAvatarExample' => 'PhabricatorUIExample',
'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',

View file

@ -49,6 +49,54 @@ final class AphrontRequest extends Phobject {
return idx($this->uriData, $key, $default); 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( public function setApplicationConfiguration(
$application_configuration) { $application_configuration) {
$this->applicationConfiguration = $application_configuration; $this->applicationConfiguration = $application_configuration;

View file

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

View file

@ -7,9 +7,11 @@ abstract class AphrontResponse extends Phobject {
private $canCDN; private $canCDN;
private $responseCode = 200; private $responseCode = 200;
private $lastModified = null; private $lastModified = null;
private $contentSecurityPolicyURIs;
private $disableContentSecurityPolicy;
protected $frameable; protected $frameable;
public function setRequest($request) { public function setRequest($request) {
$this->request = $request; $this->request = $request;
return $this; return $this;
@ -19,6 +21,33 @@ abstract class AphrontResponse extends Phobject {
return $this->request; 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 )------------------------------------------------------------ */ /* -( 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; 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) { public function setCacheDurationInSeconds($duration) {
$this->cacheable = $duration; $this->cacheable = $duration;
return $this; return $this;

View file

@ -24,10 +24,12 @@ final class PhabricatorAuthSSHKeyGenerateController
$keys = PhabricatorSSHKeyGenerator::generateKeypair(); $keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys; list($public_key, $private_key) = $keys;
$key_name = $default_name.'.key';
$file = PhabricatorFile::newFromFileData( $file = PhabricatorFile::newFromFileData(
$private_key, $private_key,
array( array(
'name' => $default_name.'.key', 'name' => $key_name,
'ttl.relative' => phutil_units('10 minutes in seconds'), 'ttl.relative' => phutil_units('10 minutes in seconds'),
'viewPolicy' => $viewer->getPHID(), 'viewPolicy' => $viewer->getPHID(),
)); ));
@ -62,25 +64,33 @@ final class PhabricatorAuthSSHKeyGenerateController
->setContentSourceFromRequest($request) ->setContentSourceFromRequest($request)
->applyTransactions($key, $xactions); ->applyTransactions($key, $xactions);
// NOTE: We're disabling workflow on submit so the download works. We're $download_link = phutil_tag(
// disabling workflow on cancel so the page reloads, showing the new 'a',
// key. 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() return $this->newDialog()
->setTitle(pht('Download Private Key')) ->setTitle(pht('Download Private Key'))
->setDisableWorkflowOnCancel(true)
->setDisableWorkflowOnSubmit(true)
->setSubmitURI($file->getDownloadURI())
->appendParagraph( ->appendParagraph(
pht( pht(
'A keypair has been generated, and the public key has been '. 'A keypair has been generated, and the public key has been '.
'added as a recognized key. Use the button below to download '. 'added as a recognized key.'))
'the private key.')) ->appendParagraph($download_link)
->appendParagraph( ->appendParagraph(
pht( pht(
'After you download the private key, it will be destroyed. '. 'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.')) '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')); ->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( return phabricator_form(
$viewer, $viewer,
array( array(
@ -505,4 +512,8 @@ abstract class PhabricatorAuthProvider extends Phobject {
throw new PhutilMethodNotImplementedException(); throw new PhutilMethodNotImplementedException();
} }
protected function getContentSecurityPolicyFormActions() {
return array();
}
} }

View file

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

View file

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

View file

@ -17,6 +17,7 @@ final class CelerityStaticResourceResponse extends Phobject {
private $behaviors = array(); private $behaviors = array();
private $hasRendered = array(); private $hasRendered = array();
private $postprocessorKey; private $postprocessorKey;
private $contentSecurityPolicyURIs = array();
public function __construct() { public function __construct() {
if (isset($_REQUEST['__metablock__'])) { if (isset($_REQUEST['__metablock__'])) {
@ -37,6 +38,15 @@ final class CelerityStaticResourceResponse extends Phobject {
return $this->metadataBlock.'_'.$id; return $this->metadataBlock.'_'.$id;
} }
public function addContentSecurityPolicyURI($kind, $uri) {
$this->contentSecurityPolicyURIs[$kind][] = $uri;
return $this;
}
public function getContentSecurityPolicyURIMap() {
return $this->contentSecurityPolicyURIs;
}
public function getMetadataBlock() { public function getMetadataBlock() {
return $this->metadataBlock; return $this->metadataBlock;
} }
@ -196,23 +206,16 @@ final class CelerityStaticResourceResponse extends Phobject {
$type)); $type));
} }
public function renderHTMLFooter() { public function renderHTMLFooter($is_frameable) {
$this->metadataLocked = true; $this->metadataLocked = true;
$data = array(); $merge_data = array(
if ($this->metadata) { 'block' => $this->metadataBlock,
$json_metadata = AphrontResponse::encodeJSONForHTTPResponse( 'data' => $this->metadata,
$this->metadata); );
$this->metadata = array(); $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.');';
$onload = array(); $behavior_lists = array();
if ($this->behaviors) { if ($this->behaviors) {
$behaviors = $this->behaviors; $behaviors = $this->behaviors;
$this->behaviors = array(); $this->behaviors = array();
@ -241,24 +244,52 @@ final class CelerityStaticResourceResponse extends Phobject {
if (!$group) { if (!$group) {
continue; continue;
} }
$group_json = AphrontResponse::encodeJSONForHTTPResponse( $behavior_lists[] = $group;
$group);
$onload[] = 'JX.initBehaviors('.$group_json.')';
} }
} }
if ($onload) { $initializers = array();
foreach ($onload as $func) {
$data[] = 'JX.onload(function(){'.$func.'});'; // 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) { if ($is_frameable) {
$data = implode("\n", $data); $initializers[] = array(
return self::renderInlineScript($data); '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 { } else {
return ''; $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) { public static function renderInlineScript($data) {

View file

@ -106,6 +106,11 @@ abstract class CelerityResourceController extends PhabricatorController {
$response = id(new AphrontFileResponse()) $response = id(new AphrontFileResponse())
->setMimeType($type_map[$type]); ->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'); $range = AphrontRequest::getHTTPHeader('Range');
if (strlen($range)) { if (strlen($range)) {

View file

@ -1046,7 +1046,7 @@ final class DiffusionServeController extends DiffusionController {
// <https://github.com/github/git-lfs/issues/1088> // <https://github.com/github/git-lfs/issues/1088>
$no_authorization = 'Basic '.base64_encode('none'); $no_authorization = 'Basic '.base64_encode('none');
$get_uri = $file->getCDNURI(); $get_uri = $file->getCDNURI('data');
$actions['download'] = array( $actions['download'] = array(
'href' => $get_uri, 'href' => $get_uri,
'header' => array( '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) { public function getHeraldFieldValue($object) {
$viewer = $this->getAdapter()->getViewer();
$commit = $object; $commit = $object;
$repository = $object->getRepository(); $repository = $object->getRepository();
@ -19,7 +21,7 @@ final class DiffusionCommitBranchesHeraldField
); );
$result = id(new ConduitCall('diffusion.branchquery', $params)) $result = id(new ConduitCall('diffusion.branchquery', $params))
->setUser(PhabricatorUser::getOmnipotentUser()) ->setUser($viewer)
->execute(); ->execute();
$refs = DiffusionRepositoryRef::loadAllFromDictionaries($result); $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'; const FIELDCONST = 'diffusion.pre.content.pusher.projects';
public function getHeraldFieldName() { public function getHeraldFieldName() {
return pht('Pusher projects'); return pht("Pusher's projects");
} }
public function getHeraldFieldValue($object) { public function getHeraldFieldValue($object) {

View file

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

View file

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

View file

@ -333,8 +333,12 @@ final class PhabricatorImageTransformer extends Phobject {
return null; 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(); ob_start();
$result = imagepng($image, null, 9); $result = imagepng($image, null, 6);
$output = ob_get_clean(); $output = ob_get_clean();
if (!$result) { if (!$result) {

View file

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

View file

@ -7,8 +7,79 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
private $color; private $color;
private $border; private $border;
private $maps = array();
const VERSION = 'v1'; 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) { public function setIcon($icon) {
$this->icon = $icon; $this->icon = $icon;
return $this; return $this;
@ -46,15 +117,22 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
} }
public function getBuiltinDisplayName() { public function getBuiltinDisplayName() {
$icon = $this->getIcon(); return $this->getImageDisplayName(
$color = $this->getColor(); $this->getIcon(),
$border = implode(',', $this->getBorder()); $this->getColor(),
$this->getBorder());
}
private function getImageDisplayName($icon, $color, $border) {
$border = implode(',', $border);
return "{$icon}-{$color}-{$border}.png"; return "{$icon}-{$color}-{$border}.png";
} }
public function loadBuiltinFileData() { public function loadBuiltinFileData() {
return $this->composeImage( return $this->composeImage(
$this->getColor(), $this->getIcon(), $this->getBorder()); $this->getColor(),
$this->getIcon(),
$this->getBorder());
} }
private function composeImage($color, $image, $border) { private function composeImage($color, $image, $border) {
@ -68,7 +146,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
$color_const = hexdec(trim($color, '#')); $color_const = hexdec(trim($color, '#'));
$true_border = self::rgba2gd($border); $true_border = self::rgba2gd($border);
$image_map = self::getImageMap(); $image_map = $this->getMap('image');
$data = Filesystem::readFile($image_map[$image]); $data = Filesystem::readFile($image_map[$image]);
$img = imagecreatefromstring($data); $img = imagecreatefromstring($data);
@ -114,7 +192,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
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 = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/'; $root = $root.'/resources/builtin/alphanumeric/';
@ -131,64 +209,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
return $map; return $map;
} }
public function getUniqueProfileImage($username) { private function newPackMap() {
$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() {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/'; $root = $root.'/resources/builtin/alphanumeric/';
@ -196,28 +217,24 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
->withType('d') ->withType('d')
->withFollowSymlinks(false) ->withFollowSymlinks(false)
->find(); ->find();
$map = array_values($map);
return array_values($map); return $map;
} }
public static function getBorderMap() { private function newBorderMap() {
return array(
$map = array(
array(0, 0, 0, 0), array(0, 0, 0, 0),
array(0, 0, 0, 0.3), array(0, 0, 0, 0.3),
array(255, 255, 255, 0.4), array(255, 255, 255, 0.4),
array(255, 255, 255, 0.7), array(255, 255, 255, 0.7),
); );
return $map;
} }
public static function getColorMap() { private function newColorMap() {
// // Via: http://tools.medialab.sciences-po.fr/iwanthue/
// Generated Colors
// http://tools.medialab.sciences-po.fr/iwanthue/ return array(
//
$map = array(
'#335862', '#335862',
'#2d5192', '#2d5192',
'#3c5da0', '#3c5da0',
@ -447,7 +464,6 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
'#335862', '#335862',
'#335862', '#335862',
); );
return $map;
} }
} }

View file

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

View file

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

View file

@ -144,7 +144,7 @@ final class PhabricatorEmbedFileRemarkupRule
$existing_xform = $file->getTransform($preview_key); $existing_xform = $file->getTransform($preview_key);
if ($existing_xform) { if ($existing_xform) {
$xform_uri = $existing_xform->getCDNURI(); $xform_uri = $existing_xform->getCDNURI('data');
} else { } else {
$xform_uri = $file->getURIForTransform($xform); $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.')); pht('You must save a file before you can generate a view URI.'));
} }
return $this->getCDNURI(); return $this->getCDNURI('data');
}
public function getCDNURI($request_kind) {
if (($request_kind !== 'data') &&
($request_kind !== 'download')) {
throw new Exception(
pht(
'Unknown file content request kind "%s".',
$request_kind));
} }
public function getCDNURI() {
$name = self::normalizeFileName($this->getName()); $name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name); $name = phutil_escape_uri($name);
$parts = array(); $parts = array();
$parts[] = 'file'; $parts[] = 'file';
$parts[] = 'data'; $parts[] = $request_kind;
// If this is an instanced install, add the instance identifier to the URI. // 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 // Instanced configurations behind a CDN may not be able to control the
@ -861,9 +869,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
} }
public function getDownloadURI() { public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI())) return $this->getCDNURI('download');
->setQueryParam('download', true);
return (string)$uri;
} }
public function getURIForTransform(PhabricatorFileTransform $transform) { public function getURIForTransform(PhabricatorFileTransform $transform) {
@ -1469,6 +1475,16 @@ final class PhabricatorFile extends PhabricatorFileDAO
->setURI($uri); ->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) { public function attachTransforms(array $map) {
$this->transforms = $map; $this->transforms = $map;
return $this; 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', 'circleci/' => 'HarbormasterCircleCIHookController',
'buildkite/' => 'HarbormasterBuildkiteHookController', '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->setLines($lines);
$log_view->setStart($start); $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()) $header = id(new PHUIHeaderView())
->setHeader(pht( ->setHeader(pht(
'Build Log %d (%s - %s)', 'Build Log %d (%s - %s)',
$log->getID(), $log->getID(),
$log->getLogSource(), $log->getLogSource(),
$log->getLogType())) $log->getLogType()))
->addActionLink($prototype_view)
->setSubheader($this->createLogHeader($build, $log)) ->setSubheader($this->createLogHeader($build, $log))
->setUser($viewer); ->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) { foreach ($handles as $phid => $handle) {
$build_log = $objects[$phid]; $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; return $this;
} }
public function newResultObject() {
return new HarbormasterBuildLog();
}
protected function loadPage() { protected function loadPage() {
$table = new HarbormasterBuildLog(); return $this->loadStandardPage($this->newResultObject());
$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);
} }
protected function willFilterPage(array $page) { protected function willFilterPage(array $page) {
@ -63,33 +56,31 @@ final class HarbormasterBuildLogQuery
return $page; return $page;
} }
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array(); $where = parent::buildWhereClauseParts($conn);
if ($this->ids) { if ($this->ids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn,
'id IN (%Ld)', 'id IN (%Ld)',
$this->ids); $this->ids);
} }
if ($this->phids) { if ($this->phids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn,
'phid IN (%Ls)', 'phid IN (%Ls)',
$this->phids); $this->phids);
} }
if ($this->buildTargetPHIDs) { if ($this->buildTargetPHIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn,
'buildTargetPHID IN (%Ls)', 'buildTargetPHID IN (%Ls)',
$this->buildTargetPHIDs); $this->buildTargetPHIDs);
} }
$where[] = $this->buildPagingClause($conn_r); return $where;
return $this->formatWhereClause($where);
} }
public function getQueryApplicationClass() { 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 final class HarbormasterBuildLog
extends HarbormasterDAO extends HarbormasterDAO
implements PhabricatorPolicyInterface { implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $buildTargetPHID; protected $buildTargetPHID;
protected $logSource; protected $logSource;
protected $logType; protected $logType;
protected $duration; protected $duration;
protected $live; protected $live;
protected $filePHID;
protected $byteLength;
protected $chunkFormat;
protected $lineMap = array();
private $buildTarget = self::ATTACHABLE; private $buildTarget = self::ATTACHABLE;
private $rope; private $rope;
private $isOpen; private $isOpen;
private $lock;
const CHUNK_BYTE_LIMIT = 102400; const CHUNK_BYTE_LIMIT = 1048576;
public function __construct() { public function __construct() {
$this->rope = new PhutilRope(); $this->rope = new PhutilRope();
@ -24,6 +32,12 @@ final class HarbormasterBuildLog
if ($this->isOpen) { if ($this->isOpen) {
$this->closeBuildLog(); $this->closeBuildLog();
} }
if ($this->lock) {
if ($this->lock->isLocked()) {
$this->lock->unlock();
}
}
} }
public static function initializeNewBuildLog( public static function initializeNewBuildLog(
@ -32,43 +46,29 @@ final class HarbormasterBuildLog
return id(new HarbormasterBuildLog()) return id(new HarbormasterBuildLog())
->setBuildTargetPHID($build_target->getPHID()) ->setBuildTargetPHID($build_target->getPHID())
->setDuration(null) ->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) ->setLive(1)
->save(); ->setByteLength(0)
->setChunkFormat(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
} }
public function closeBuildLog() { public function scheduleRebuild($force) {
if (!$this->isOpen) { PhabricatorWorker::scheduleTask(
throw new Exception(pht('This build log is not open!')); 'HarbormasterLogWorker',
array(
'logPHID' => $this->getPHID(),
'force' => $force,
),
array(
'objectPHID' => $this->getPHID(),
));
} }
if ($this->canCompressLog()) {
$this->compressLog();
}
$start = $this->getDateCreated();
$now = PhabricatorTime::getNow();
return $this
->setDuration($now - $start)
->setLive(0)
->save();
}
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
self::CONFIG_AUX_PHID => true, self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'lineMap' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
// T6203/NULLABILITY // T6203/NULLABILITY
// It seems like these should be non-nullable? All logs should have a // It seems like these should be non-nullable? All logs should have a
@ -78,6 +78,9 @@ final class HarbormasterBuildLog
'duration' => 'uint32?', 'duration' => 'uint32?',
'live' => 'bool', 'live' => 'bool',
'filePHID' => 'phid?',
'byteLength' => 'uint64',
'chunkFormat' => 'text32',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_buildtarget' => array( 'key_buildtarget' => array(
@ -105,9 +108,366 @@ final class HarbormasterBuildLog
return pht('Build Log'); 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()) { 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; $content = (string)$content;
@ -155,126 +515,160 @@ final class HarbormasterBuildLog
$append_data = $rope->getPrefixBytes($data_limit); $append_data = $rope->getPrefixBytes($data_limit);
$data_size = strlen($append_data); $data_size = strlen($append_data);
$this->openTransaction();
if ($append_id) { if ($append_id) {
queryfx( queryfx(
$conn_w, $conn_w,
'UPDATE %T SET chunk = CONCAT(chunk, %B), size = %d WHERE id = %d', 'UPDATE %T SET
chunk = CONCAT(chunk, %B),
size = %d,
tailOffset = headOffset + %d
WHERE
id = %d',
$chunk_table, $chunk_table,
$append_data, $append_data,
$prefix_size + $data_size, $prefix_size + $data_size,
$prefix_size + $data_size,
$append_id); $append_id);
} else { } else {
$this->writeChunk($encoding_text, $data_size, $append_data); $this->writeChunk(
$encoding_text,
$this->getByteLength(),
$data_size,
$append_data);
} }
$this->updateLineMap($append_data);
$this->save();
$this->saveTransaction();
$rope->removeBytesFromHead($data_size); $rope->removeBytesFromHead($data_size);
} }
} }
public function newChunkIterator() { public function updateLineMap($append_data, $marker_distance = null) {
return id(new HarbormasterBuildLogChunkIterator($this)) $this->byteLength += strlen($append_data);
->setPageSize(32);
if (!$marker_distance) {
$marker_distance = (self::CHUNK_BYTE_LIMIT / 2);
} }
private function loadLastChunkInfo() { if (!$this->lineMap) {
$chunk_table = new HarbormasterBuildLogChunk(); $this->lineMap = array(
$conn_w = $chunk_table->establishConnection('w'); array(),
0,
return queryfx_one( 0,
$conn_w, null,
'SELECT id, size, encoding FROM %T WHERE logID = %d );
ORDER BY id DESC LIMIT 1',
$chunk_table->getTableName(),
$this->getID());
} }
public function getLogText() { list($map, $map_bytes, $line_count, $prefix) = $this->lineMap;
// TODO: Remove this method since it won't scale for big logs.
$all_chunks = $this->newChunkIterator(); $buffer = $append_data;
$full_text = array(); if ($prefix) {
foreach ($all_chunks as $chunk) { $prefix = base64_decode($prefix);
$full_text[] = $chunk->getChunkDisplayText(); $buffer = $prefix.$buffer;
} }
return implode('', $full_text); if ($map) {
list($last_marker, $last_count) = last($map);
} else {
$last_marker = 0;
$last_count = 0;
} }
private function canCompressLog() { $max_utf8_width = 8;
return function_exists('gzdeflate'); $next_marker = $last_marker + $marker_distance;
}
public function compressLog() { $pos = 0;
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP); $len = strlen($buffer);
} while (true) {
// If we only have a few bytes left in the buffer, leave it as a prefix
public function decompressLog() { // for next time.
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT); if (($len - $pos) <= ($max_utf8_width * 2)) {
} $prefix = substr($buffer, $pos);
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']);
}
$byte_limit = self::CHUNK_BYTE_LIMIT;
$rope = new PhutilRope();
$this->openTransaction();
foreach ($chunks as $chunk) {
$rope->append($chunk->getChunkDisplayText());
$chunk->delete();
while ($rope->getByteLength() > $byte_limit) {
$this->writeEncodedChunk($rope, $byte_limit, $mode);
}
}
while ($rope->getByteLength()) {
$this->writeEncodedChunk($rope, $byte_limit, $mode);
}
$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; break;
case HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP: }
$data = gzdeflate($data);
if ($data === false) { // The next slice we're going to look at is the smaller of:
throw new Exception(pht('Failed to gzdeflate() log data!')); //
// - 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; break;
default:
throw new Exception(pht('Unknown chunk encoding "%s"!', $mode));
} }
$this->writeChunk($mode, $size, $data); // If the next character is 0x7F or lower, or between 0xC2 and 0xF4,
// we're not slicing in the middle of a UTF8 character.
$rope->removeBytesFromHead($size); $ord = ord($next);
if ($ord <= 0x7F || ($ord >= 0xC2 && $ord <= 0xF4)) {
break;
} }
private function writeChunk($encoding, $raw_size, $data) { $slice_length--;
return id(new HarbormasterBuildLogChunk()) }
->setLogID($this->getID())
->setEncoding($encoding) $slice = substr($buffer, $pos, $slice_length);
->setSize($raw_size) $pos += $slice_length;
->setChunk($data)
->save(); $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;
}
}
$this->lineMap = array(
$map,
$map_bytes,
$line_count,
base64_encode($prefix),
);
return $this;
} }
@ -299,8 +693,88 @@ final class HarbormasterBuildLog
public function describeAutomaticCapability($capability) { public function describeAutomaticCapability($capability) {
return pht( 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 $logID;
protected $encoding; protected $encoding;
protected $headOffset;
protected $tailOffset;
protected $size; protected $size;
protected $chunk; protected $chunk;
@ -20,6 +22,8 @@ final class HarbormasterBuildLogChunk
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'logID' => 'id', 'logID' => 'id',
'encoding' => 'text32', 'encoding' => 'text32',
'headOffset' => 'uint64',
'tailOffset' => 'uint64',
// T6203/NULLABILITY // T6203/NULLABILITY
// Both the type and nullability of this column are crazily wrong. // Both the type and nullability of this column are crazily wrong.
@ -28,8 +32,8 @@ final class HarbormasterBuildLogChunk
'chunk' => 'bytes', 'chunk' => 'bytes',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_log' => array( 'key_offset' => array(
'columns' => array('logID'), 'columns' => array('logID', 'headOffset', 'tailOffset'),
), ),
), ),
) + parent::getConfiguration(); ) + parent::getConfiguration();

View file

@ -5,6 +5,7 @@ final class HarbormasterBuildLogChunkIterator
private $log; private $log;
private $cursor; private $cursor;
private $asString;
private $min = 0; private $min = 0;
private $max = PHP_INT_MAX; private $max = PHP_INT_MAX;
@ -27,6 +28,11 @@ final class HarbormasterBuildLogChunkIterator
return $this; return $this;
} }
public function setAsString($as_string) {
$this->asString = $as_string;
return $this;
}
protected function loadPage() { protected function loadPage() {
if ($this->cursor > $this->max) { if ($this->cursor > $this->max) {
return array(); return array();
@ -43,7 +49,11 @@ final class HarbormasterBuildLogChunkIterator
$this->cursor = last($results)->getID() + 1; $this->cursor = last($results)->getID() + 1;
} }
if ($this->asString) {
return mpull($results, 'getChunkDisplayText');
} else {
return $results; 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 { final class PhabricatorPasteViewController extends PhabricatorPasteController {
private $highlightMap;
public function shouldAllowPublic() { public function shouldAllowPublic() {
return true; 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) { public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer(); $viewer = $request->getViewer();
$id = $request->getURIData('id'); $id = $request->getURIData('id');
@ -40,11 +20,18 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController {
return new Aphront404Response(); return new Aphront404Response();
} }
$lines = $request->getURILineRange('lines', 1000);
if ($lines) {
$map = range($lines[0], $lines[1]);
} else {
$map = array();
}
$header = $this->buildHeaderView($paste); $header = $this->buildHeaderView($paste);
$curtain = $this->buildCurtain($paste); $curtain = $this->buildCurtain($paste);
$subheader = $this->buildSubheaderView($paste); $subheader = $this->buildSubheaderView($paste);
$source_code = $this->buildSourceCodeView($paste, $this->highlightMap); $source_code = $this->buildSourceCodeView($paste, $map);
require_celerity_resource('paste-css'); require_celerity_resource('paste-css');

View file

@ -45,21 +45,10 @@ final class PhabricatorUserProfileImageCacheType
$generate_users[] = $user; $generate_users[] = $user;
} }
// Generate Files for anyone without a default $generator = new PhabricatorFilesComposeAvatarBuiltinFile();
foreach ($generate_users as $generate_user) { foreach ($generate_users as $user) {
$generate_user_phid = $generate_user->getPHID(); $file = $generator->updateUser($user);
$generate_username = $generate_user->getUsername(); $file_phids[$user->getPHID()] = $file->getPHID();
$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();
} }
if ($file_phids) { if ($file_phids) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -512,15 +512,7 @@ final class PhabricatorApplicationSearchController
->setURI($job->getMonitorURI()); ->setURI($job->getMonitorURI());
} else { } else {
$file = $export_engine->exportFile(); $file = $export_engine->exportFile();
return $file->newDownloadResponse();
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'));
} }
} }
} }
@ -535,12 +527,18 @@ final class PhabricatorApplicationSearchController
->setValue($format_key) ->setValue($format_key)
->setOptions($format_options)); ->setOptions($format_options));
if ($is_large_export) {
$submit_button = pht('Continue');
} else {
$submit_button = pht('Download Data');
}
return $this->newDialog() return $this->newDialog()
->setTitle(pht('Export Results')) ->setTitle(pht('Export Results'))
->setErrors($errors) ->setErrors($errors)
->appendForm($export_form) ->appendForm($export_form)
->addCancelButton($cancel_uri) ->addCancelButton($cancel_uri)
->addSubmitButton(pht('Continue')); ->addSubmitButton($submit_button);
} }
private function processEditRequest() { 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')); ->setName(pht('Temporary File Expired'));
} else { } else {
$actions[] = id(new PhabricatorActionView()) $actions[] = id(new PhabricatorActionView())
->setRenderAsForm(true)
->setHref($file->getDownloadURI()) ->setHref($file->getDownloadURI())
->setIcon('fa-download') ->setIcon('fa-download')
->setName(pht('Download Data Export')); ->setName(pht('Download Data Export'));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -279,35 +279,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
} }
} }
$icon = id(new PHUIIconView()) Javelin::initBehavior('lightbox-attachments');
->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('aphront-form-disable-on-submit'); Javelin::initBehavior('aphront-form-disable-on-submit');
@ -610,10 +582,13 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
array( array(
'websocketURI' => (string)$client_uri, 'websocketURI' => (string)$client_uri,
) + $this->buildAphlictListenConfigData()); ) + $this->buildAphlictListenConfigData());
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('connect-src', $client_uri);
} }
} }
$tail[] = $response->renderHTMLFooter(); $tail[] = $response->renderHTMLFooter($this->getFrameable());
return $tail; return $tail;
} }
@ -860,6 +835,19 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
$blacklist[] = $application->getQuicksandURIPatternBlacklist(); $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); return array_mergev($blacklist);
} }
@ -903,9 +891,17 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
->setContent($content); ->setContent($content);
} else { } else {
$content = $this->render(); $content = $this->render();
$response = id(new AphrontWebpageResponse()) $response = id(new AphrontWebpageResponse())
->setContent($content) ->setContent($content)
->setFrameable($this->getFrameable()); ->setFrameable($this->getFrameable());
$static = CelerityAPI::getStaticResourceResponse();
foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) {
foreach ($uris as $uri) {
$response->addContentSecurityPolicyURI($kind, $uri);
}
}
} }
return $response; return $response;

View file

@ -30,3 +30,140 @@
text-overflow: ellipsis; text-overflow: ellipsis;
color: {$lightgreytext}; 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 { .phabricator-source-line {
background-color: {$paste.highlight}; background-color: {$paste.highlight};
text-align: right; text-align: right;
padding: 2px 6px 1px 12px;
border-right: 1px solid {$paste.border}; border-right: 1px solid {$paste.border};
color: {$sh-yellowtext}; color: {$sh-yellowtext};
@ -48,17 +47,23 @@
th.phabricator-source-line a { th.phabricator-source-line a {
color: {$darkbluetext}; color: {$darkbluetext};
display: block;
padding: 2px 6px 1px 12px;
} }
th.phabricator-source-line:hover { th.phabricator-source-line a:hover {
background: {$paste.border}; background: {$paste.border};
cursor: pointer; text-decoration: none;
} }
.phabricator-source-highlight { .phabricator-source-highlight {
background: {$paste.highlight}; background: {$paste.highlight};
} }
.phabricator-source-highlight th.phabricator-source-line {
background: {$paste.border};
}
.phabricator-source-code-summary { .phabricator-source-code-summary {
padding-bottom: 8px; padding-bottom: 8px;
} }

View file

@ -517,6 +517,36 @@ JX.install('Stratcom', {
return len ? this._execContext[len - 1].event : null; 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 * Merge metadata. You must call this (even if you have no metadata) to
@ -542,7 +572,6 @@ JX.install('Stratcom', {
} else { } else {
this._data[block] = data; this._data[block] = data;
if (block === 0) { if (block === 0) {
JX.Stratcom.ready = true;
JX.flushHoldingQueue('install-init', function(fn) { JX.flushHoldingQueue('install-init', function(fn) {
fn(); fn();
}); });

View file

@ -46,25 +46,51 @@
makeHoldingQueue('behavior'); makeHoldingQueue('behavior');
makeHoldingQueue('install-init'); makeHoldingQueue('install-init');
window.__DEV__ = window.__DEV__ || 0;
var loaded = false; var loaded = false;
var onload = []; var onload = [];
var master_event_queue = []; var master_event_queue = [];
var root = document.documentElement; var root = document.documentElement;
var has_add_event_listener = !!root.addEventListener; var has_add_event_listener = !!root.addEventListener;
window.__DEV__ = !!root.getAttribute('data-developer-mode');
JX.__rawEventQueue = function(what) { JX.__rawEventQueue = function(what) {
master_event_queue.push(what); master_event_queue.push(what);
// Evade static analysis - JX.Stratcom var ii;
var Stratcom = JX['Stratcom']; var Stratcom = JX['Stratcom'];
if (Stratcom && Stratcom.ready) {
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 // Empty the queue now so that exceptions don't cause us to repeatedly
// try to handle events. // try to handle events.
var local_queue = master_event_queue; var local_queue = master_event_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]; var evt = local_queue[ii];
// Sometimes IE gives us events which throw when ".type" is accessed; // Sometimes IE gives us events which throw when ".type" is accessed;
@ -72,11 +98,10 @@
// figure out where these are coming from. // figure out where these are coming from.
try { var test = evt.type; } catch (x) { continue; } 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 // NOTE: Firefox interprets "document.body.id = null" as the string
// literal "null". // literal "null".
document.body && (document.body.id = ''); document.body && (document.body.id = '');
loaded = true;
for (var jj = 0; jj < onload.length; jj++) { for (var jj = 0; jj < onload.length; jj++) {
onload[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 // 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. // reload, so we need to take this branch if the key is present.
if (r && (typeof r.redirect != 'undefined')) { 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(); JX.$U(r.redirect).go();
} else if (r && r.dialog) { } else if (r && r.dialog) {
this._push(); 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 * phabricator-busy
*/ */
JX.behavior('lightbox-attachments', function (config) { JX.behavior('lightbox-attachments', function() {
var lightbox = null; var lightbox = null;
var prev = null; var prev = null;
var next = null; var next = null;
var shown = false; var shown = false;
var downloadForm = JX.$H(config.downloadForm).getFragment().firstChild;
var lightbox_id = config.lightbox_id;
function _toggleComment(e) { function _toggleComment(e) {
e.kill(); e.kill();
shown = !shown; shown = !shown;
JX.DOM.alterClass(JX.$(lightbox_id), 'comment-panel-open', shown); JX.DOM.alterClass(lightbox, 'comment-panel-open', shown);
} }
function markCommentsLoading(loading) { function markCommentsLoading(loading) {
@ -48,6 +47,12 @@ JX.behavior('lightbox-attachments', function (config) {
return; 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(); e.kill();
var mainFrame = JX.$('main-page-frame'); var mainFrame = JX.$('main-page-frame');
@ -82,8 +87,7 @@ JX.behavior('lightbox-attachments', function (config) {
var img_uri = ''; var img_uri = '';
var img = ''; var img = '';
var extra_status = ''; 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) { if (target_data.viewable) {
img_uri = target_data.uri; img_uri = target_data.uri;
var alt_name = ''; var alt_name = '';
@ -114,7 +118,7 @@ JX.behavior('lightbox-attachments', function (config) {
{ {
className : 'lightbox-icon-frame', className : 'lightbox-icon-frame',
sigil : 'lightbox-download-submit', sigil : 'lightbox-download-submit',
href : '#', href : target_data.dUri,
}, },
[ imgIcon, nameElement ] [ imgIcon, nameElement ]
); );
@ -138,12 +142,12 @@ JX.behavior('lightbox-attachments', function (config) {
); );
var commentClass = (shown) ? 'comment-panel-open' : ''; var commentClass = (shown) ? 'comment-panel-open' : '';
lightbox = lightbox =
JX.$N('div', JX.$N('div',
{ {
className : 'lightbox-attachment ' + commentClass, className : 'lightbox-attachment ' + commentClass,
sigil : 'lightbox-attachment', sigil : 'lightbox-attachment'
id : lightbox_id
}, },
[imgFrame, commentFrame] [imgFrame, commentFrame]
); );
@ -161,12 +165,17 @@ JX.behavior('lightbox-attachments', function (config) {
] ]
); );
var downloadSpan = var download_icon = new JX.PHUIXIconView()
JX.$N('span', .setIcon('fa-download phui-icon-circle-icon')
.getNode();
var download_button = JX.$N(
'a',
{ {
className : 'lightbox-download' className: 'lightbox-download phui-icon-circle hover-sky',
} href: target_data.dUri
); },
download_icon);
var commentIcon = new JX.PHUIXIconView() var commentIcon = new JX.PHUIXIconView()
.setIcon('fa-comments phui-icon-circle-icon') .setIcon('fa-comments phui-icon-circle-icon')
@ -180,6 +189,7 @@ JX.behavior('lightbox-attachments', function (config) {
}, },
commentIcon commentIcon
); );
var closeIcon = new JX.PHUIXIconView() var closeIcon = new JX.PHUIXIconView()
.setIcon('fa-times phui-icon-circle-icon') .setIcon('fa-times phui-icon-circle-icon')
.getNode(); .getNode();
@ -190,12 +200,13 @@ JX.behavior('lightbox-attachments', function (config) {
href : '#' href : '#'
}, },
closeIcon); closeIcon);
var statusHTML = var statusHTML =
JX.$N('div', JX.$N('div',
{ {
className : 'lightbox-status' className : 'lightbox-status'
}, },
[statusSpan, closeButton, commentButton, downloadSpan] [statusSpan, closeButton, commentButton, download_button]
); );
JX.DOM.appendContent(lightbox, statusHTML); JX.DOM.appendContent(lightbox, statusHTML);
JX.DOM.listen(closeButton, 'click', null, closeLightBox); 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.DOM.alterClass(document.body, 'lightbox-attached', true);
JX.Mask.show('jx-dark-mask'); JX.Mask.show('jx-dark-mask');
downloadForm.action = target_data.dUri;
downloadSpan.appendChild(downloadForm);
document.body.appendChild(lightbox); document.body.appendChild(lightbox);
if (img_uri) { if (img_uri) {
@ -365,23 +373,12 @@ JX.behavior('lightbox-attachments', function (config) {
'lightbox-comment-form', 'lightbox-comment-form',
_sendMessage); _sendMessage);
var _startDownload = function(e) {
e.kill();
var form = JX.$('lightbox-download-form');
form.submit();
};
var _startPageDownload = function(e) { var _startPageDownload = function(e) {
e.kill(); e.kill();
var form = e.getNode('tag:form'); var form = e.getNode('tag:form');
form.submit(); form.submit();
}; };
JX.Stratcom.listen(
'click',
'lightbox-download-submit',
_startDownload);
JX.Stratcom.listen( JX.Stratcom.listen(
'click', 'click',
'embed-download-form', 'embed-download-form',

View file

@ -10,6 +10,7 @@ JX.behavior('phabricator-line-linker', function() {
var origin = null; var origin = null;
var target = null; var target = null;
var root = null; var root = null;
var highlighted = null;
var editor_link = null; var editor_link = null;
try { try {
@ -19,49 +20,112 @@ JX.behavior('phabricator-line-linker', function() {
} }
function getRowNumber(tr) { 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); return +(th.textContent || th.innerText);
} }
JX.Stratcom.listen( JX.Stratcom.listen(
'mousedown', ['click', 'mousedown'],
'phabricator-source-line', ['phabricator-source', 'tag:tr', 'tag:th', 'tag:a'],
function(e) { function(e) {
if (!e.isNormalMouseEvent()) { if (!e.isNormalMouseEvent()) {
return; return;
} }
origin = e.getNode('tag:tr');
target = origin;
root = e.getNode('phabricator-source');
e.kill();
});
JX.Stratcom.listen( // Make sure the link we clicked is actually a line number in a source
'click', // table, not some kind of link in some element embedded inside the
'phabricator-source-line', // table. The row's immediate ancestor table needs to be the table with
function(e) { // 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(); 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) { var highlight = function(e) {
if (!origin || e.getNode('phabricator-source') !== root) { if (!origin) {
return;
}
if (e.getNode('phabricator-source') !== root) {
return; return;
} }
target = e.getNode('tag:tr'); target = e.getNode('tag:tr');
var highlighting = false; var min;
var source = null; var max;
var trs = JX.DOM.scry(root, 'tr');
for (var i = 0; i < trs.length; i++) { // NOTE: We're using position to figure out which order these rows are in,
if (!highlighting && (trs[i] === origin || trs[i] === target)) { // not row numbers. We do this because Harbormaster build logs may have
highlighting = true; // multiple rows with the same row number.
source = trs[i];
if (JX.$V(origin).y <= JX.$V(target).y) {
min = origin;
max = target;
} else {
min = target;
max = origin;
} }
JX.DOM.alterClass(trs[i], 'phabricator-source-highlight', highlighting);
if (trs[i] === (source === origin ? target : origin)) { // If we haven't changed highlighting, we don't have a list of highlighted
highlighting = false; // 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]);
} }
} }
// 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;
}
}; };
JX.Stratcom.listen('mouseover', 'phabricator-source', highlight); JX.Stratcom.listen('mouseover', 'phabricator-source', highlight);
@ -75,21 +139,27 @@ JX.behavior('phabricator-line-linker', function() {
} }
highlight(e); highlight(e);
e.kill();
var o = getRowNumber(origin); var o = getRowNumber(origin);
var t = getRowNumber(target); var t = getRowNumber(target);
var lines = (o == t ? o : Math.min(o, t) + '-' + Math.max(o, t)); var uri = JX.Stratcom.getData(root).uri;
var th = JX.DOM.find(origin, 'th', 'phabricator-source-line');
var uri = JX.DOM.find(th, 'a').href;
uri = uri.replace(/(.*\$)\d+/, '$1' + lines);
origin = null; origin = null;
target = 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); JX.History.replace(uri);
if (editor_link) {
if (editor_link.href) { if (editor_link.href) {
var editdata = JX.Stratcom.getData(editor_link); var editdata = JX.Stratcom.getData(editor_link);
editor_link.href = editdata.link_template.replace('%25l', o); editor_link.href = editdata.link_template.replace('%25l', o);
} }
}
}); });
}); });