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:
commit
ae7236c7a4
77 changed files with 3567 additions and 670 deletions
|
@ -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',
|
||||||
|
|
2
resources/sql/autopatches/20180222.log.01.filephid.sql
Normal file
2
resources/sql/autopatches/20180222.log.01.filephid.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
|
||||||
|
ADD filePHID VARBINARY(64);
|
2
resources/sql/autopatches/20180223.log.01.bytelength.sql
Normal file
2
resources/sql/autopatches/20180223.log.01.bytelength.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
|
||||||
|
ADD byteLength BIGINT UNSIGNED NOT NULL;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
|
||||||
|
ADD chunkFormat VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};
|
|
@ -0,0 +1,2 @@
|
||||||
|
UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildlog
|
||||||
|
SET chunkFormat = 'text' WHERE chunkFormat = '';
|
2
resources/sql/autopatches/20180223.log.04.linemap.sql
Normal file
2
resources/sql/autopatches/20180223.log.04.linemap.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildlog
|
||||||
|
ADD lineMap LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
|
|
@ -0,0 +1,2 @@
|
||||||
|
UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildlog
|
||||||
|
SET lineMap = '[]' WHERE lineMap = '';
|
5
resources/sql/autopatches/20180228.log.01.offset.sql
Normal file
5
resources/sql/autopatches/20180228.log.01.offset.sql
Normal 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;
|
|
@ -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',
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 )-------------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -298,6 +298,7 @@ abstract class PhabricatorController extends AphrontController {
|
||||||
->setContent(
|
->setContent(
|
||||||
array(
|
array(
|
||||||
'redirect' => $response->getURI(),
|
'redirect' => $response->getURI(),
|
||||||
|
'close' => $response->getCloseDialogBeforeRedirect(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)/.*',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
101
src/applications/harbormaster/view/HarbormasterBuildLogView.php
Normal file
101
src/applications/harbormaster/view/HarbormasterBuildLogView.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
105
src/applications/harbormaster/worker/HarbormasterLogWorker.php
Normal file
105
src/applications/harbormaster/worker/HarbormasterLogWorker.php
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)),
|
)),
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
31
webroot/rsrc/externals/javelin/core/Stratcom.js
vendored
31
webroot/rsrc/externals/javelin/core/Stratcom.js
vendored
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
39
webroot/rsrc/externals/javelin/core/init.js
vendored
39
webroot/rsrc/externals/javelin/core/init.js
vendored
|
@ -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]();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
});
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue