1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 08:42:41 +01:00

Merge branch 'master' into phutil_tag

(Sync.)
This commit is contained in:
epriestley 2013-02-07 08:08:01 -08:00
commit 11bb8db970
80 changed files with 2727 additions and 1188 deletions

View file

@ -1015,17 +1015,6 @@ return array(
// interact with the revisions.
'differential.anonymous-access' => false,
// If you set this to true, revision author email address information will
// be exposed in Conduit. This is useful for Arcanist.
//
// For example, consider the "arc patch DX" workflow which needs to ask
// Differential for the revision DX. This data often should contain
// the author's email address, eg "George Washington
// <gwashinton@example.com>" when DX is a git or mercurial revision. If this
// option is false, Differential defaults to the best it can, something like
// "George Washington" or "gwashington".
'differential.expose-emails-prudently' => false,
// List of file regexps that should be treated as if they are generated by
// an automatic process, and thus get hidden by default in differential.
'differential.generated-paths' => array(

View file

@ -7,12 +7,12 @@
],
"handlers" : [
"PhabricatorIRCProtocolHandler",
"PhabricatorIRCObjectNameHandler",
"PhabricatorIRCSymbolHandler",
"PhabricatorIRCLogHandler",
"PhabricatorIRCWhatsNewHandler",
"PhabricatorIRCDifferentialNotificationHandler",
"PhabricatorIRCMacroHandler"
"PhabricatorBotObjectNameHandler",
"PhabricatorBotSymbolHandler",
"PhabricatorBotLogHandler",
"PhabricatorBotWhatsNewHandler",
"PhabricatorBotDifferentialNotificationHandler",
"PhabricatorBotMacroHandler"
],
"conduit.uri" : null,

View file

@ -0,0 +1,6 @@
ALTER TABLE {$NAMESPACE}_conpherence.conpherence_thread
DROP imagePHID,
ADD imagePHIDs LONGTEXT COLLATE utf8_bin NOT NULL AFTER title;
UPDATE {$NAMESPACE}_conpherence.conpherence_thread
SET imagePHIDs = '{}' WHERE imagePHIDs = '';

View file

@ -646,7 +646,7 @@ celerity_register_resource_map(array(
),
'aphront-form-view-css' =>
array(
'uri' => '/res/1e191b83/rsrc/css/aphront/form-view.css',
'uri' => '/res/ec323d34/rsrc/css/aphront/form-view.css',
'type' => 'css',
'requires' =>
array(
@ -737,7 +737,7 @@ celerity_register_resource_map(array(
),
'conpherence-header-pane-css' =>
array(
'uri' => '/res/4b8aebd2/rsrc/css/application/conpherence/header-pane.css',
'uri' => '/res/11c32adc/rsrc/css/application/conpherence/header-pane.css',
'type' => 'css',
'requires' =>
array(
@ -764,7 +764,7 @@ celerity_register_resource_map(array(
),
'conpherence-update-css' =>
array(
'uri' => '/res/8e4757b5/rsrc/css/application/conpherence/update.css',
'uri' => '/res/92094ed7/rsrc/css/application/conpherence/update.css',
'type' => 'css',
'requires' =>
array(
@ -773,7 +773,7 @@ celerity_register_resource_map(array(
),
'conpherence-widget-pane-css' =>
array(
'uri' => '/res/b3e6a558/rsrc/css/application/conpherence/widget-pane.css',
'uri' => '/res/6e5755bb/rsrc/css/application/conpherence/widget-pane.css',
'type' => 'css',
'requires' =>
array(
@ -1031,6 +1031,19 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/behavior-tokenizer.js',
),
'javelin-behavior-aphront-crop' =>
array(
'uri' => '/res/cda1eace/rsrc/js/application/core/behavior-crop.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-vector',
3 => 'javelin-magical-init',
),
'disk' => '/rsrc/js/application/core/behavior-crop.js',
),
'javelin-behavior-aphront-drag-and-drop' =>
array(
'uri' => '/res/3d809b40/rsrc/js/application/core/behavior-drag-and-drop.js',
@ -1095,6 +1108,19 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js',
),
'javelin-behavior-conpherence-drag-and-drop-photo' =>
array(
'uri' => '/res/9e3eb1cd/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-workflow',
3 => 'phabricator-drag-and-drop-file-upload',
),
'disk' => '/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js',
),
'javelin-behavior-conpherence-init' =>
array(
'uri' => '/res/bd911b43/rsrc/js/application/conpherence/behavior-init.js',
@ -1109,7 +1135,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-conpherence-menu' =>
array(
'uri' => '/res/9d21fb86/rsrc/js/application/conpherence/behavior-menu.js',
'uri' => '/res/986774a0/rsrc/js/application/conpherence/behavior-menu.js',
'type' => 'js',
'requires' =>
array(
@ -1124,7 +1150,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-conpherence-widget-pane' =>
array(
'uri' => '/res/f3e0dbba/rsrc/js/application/conpherence/behavior-widget-pane.js',
'uri' => '/res/4fb51b46/rsrc/js/application/conpherence/behavior-widget-pane.js',
'type' => 'js',
'requires' =>
array(
@ -1148,7 +1174,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-dark-console' =>
array(
'uri' => '/res/52444d4e/rsrc/js/application/core/behavior-dark-console.js',
'uri' => '/res/ae7f15ce/rsrc/js/application/core/behavior-dark-console.js',
'type' => 'js',
'requires' =>
array(
@ -1269,7 +1295,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-differential-keyboard-navigation' =>
array(
'uri' => '/res/a7798465/rsrc/js/application/differential/behavior-keyboard-nav.js',
'uri' => '/res/89e93cc9/rsrc/js/application/differential/behavior-keyboard-nav.js',
'type' => 'js',
'requires' =>
array(
@ -1282,7 +1308,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-differential-populate' =>
array(
'uri' => '/res/71effec4/rsrc/js/application/differential/behavior-populate.js',
'uri' => '/res/526c2615/rsrc/js/application/differential/behavior-populate.js',
'type' => 'js',
'requires' =>
array(
@ -1292,7 +1318,8 @@ celerity_register_resource_map(array(
3 => 'javelin-dom',
4 => 'javelin-stratcom',
5 => 'javelin-behavior-device',
6 => 'phabricator-tooltip',
6 => 'javelin-vector',
7 => 'phabricator-tooltip',
),
'disk' => '/rsrc/js/application/differential/behavior-populate.js',
),
@ -1640,9 +1667,21 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/core/behavior-autofocus.js',
),
'javelin-behavior-phabricator-file-tree' =>
array(
'uri' => '/res/e9c96597/rsrc/js/application/core/behavior-file-tree.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'phabricator-keyboard-shortcut',
2 => 'javelin-stratcom',
),
'disk' => '/rsrc/js/application/core/behavior-file-tree.js',
),
'javelin-behavior-phabricator-home-reveal-tiles' =>
array(
'uri' => '/res/7230ca0c/rsrc/js/application/core/behavior-home-reveal-tiles.js',
'uri' => '/res/b3c5cea9/rsrc/js/application/core/behavior-home-reveal-tiles.js',
'type' => 'js',
'requires' =>
array(
@ -1680,7 +1719,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-phabricator-nav' =>
array(
'uri' => '/res/cec31f3f/rsrc/js/application/core/behavior-phabricator-nav.js',
'uri' => '/res/222b9329/rsrc/js/application/core/behavior-phabricator-nav.js',
'type' => 'js',
'requires' =>
array(
@ -1827,12 +1866,16 @@ celerity_register_resource_map(array(
),
'javelin-behavior-pholio-mock-view' =>
array(
'uri' => '/res/10fbdca1/rsrc/js/application/pholio/behavior-pholio-mock-view.js',
'uri' => '/res/518a169e/rsrc/js/application/pholio/behavior-pholio-mock-view.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-stratcom',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-magical-init',
5 => 'javelin-request',
),
'disk' => '/rsrc/js/application/pholio/behavior-pholio-mock-view.js',
),
@ -2015,7 +2058,7 @@ celerity_register_resource_map(array(
),
'javelin-event' =>
array(
'uri' => '/res/3815b473/rsrc/js/javelin/core/Event.js',
'uri' => '/res/e6582051/rsrc/js/javelin/core/Event.js',
'type' => 'js',
'requires' =>
array(
@ -2671,7 +2714,7 @@ celerity_register_resource_map(array(
),
'phabricator-header-view-css' =>
array(
'uri' => '/res/88ef478c/rsrc/css/layout/phabricator-header-view.css',
'uri' => '/res/585b771c/rsrc/css/layout/phabricator-header-view.css',
'type' => 'css',
'requires' =>
array(
@ -3167,7 +3210,7 @@ celerity_register_resource_map(array(
),
'pholio-css' =>
array(
'uri' => '/res/f101ad7c/rsrc/css/application/pholio/pholio.css',
'uri' => '/res/9de6e0b2/rsrc/css/application/pholio/pholio.css',
'type' => 'css',
'requires' =>
array(
@ -3293,7 +3336,7 @@ celerity_register_resource_map(array(
),
'sprite-conpher-css' =>
array(
'uri' => '/res/f640f0c5/rsrc/css/sprite-conph.css',
'uri' => '/res/89821322/rsrc/css/sprite-conph.css',
'type' => 'css',
'requires' =>
array(
@ -3357,7 +3400,7 @@ celerity_register_resource_map(array(
), array(
'packages' =>
array(
'0e6165d0' =>
'4b569463' =>
array(
'name' => 'core.pkg.css',
'symbols' =>
@ -3400,10 +3443,10 @@ celerity_register_resource_map(array(
35 => 'phabricator-object-item-list-view-css',
36 => 'global-drag-and-drop-css',
),
'uri' => '/res/pkg/0e6165d0/core.pkg.css',
'uri' => '/res/pkg/4b569463/core.pkg.css',
'type' => 'css',
),
'ff199687' =>
'bc0774e5' =>
array(
'name' => 'core.pkg.js',
'symbols' =>
@ -3442,10 +3485,10 @@ celerity_register_resource_map(array(
31 => 'javelin-behavior-global-drag-and-drop',
32 => 'javelin-behavior-phabricator-home-reveal-tiles',
),
'uri' => '/res/pkg/ff199687/core.pkg.js',
'uri' => '/res/pkg/bc0774e5/core.pkg.js',
'type' => 'js',
),
'74593df4' =>
'3e0098ea' =>
array(
'name' => 'darkconsole.pkg.js',
'symbols' =>
@ -3453,7 +3496,7 @@ celerity_register_resource_map(array(
0 => 'javelin-behavior-dark-console',
1 => 'javelin-behavior-error-log',
),
'uri' => '/res/pkg/74593df4/darkconsole.pkg.js',
'uri' => '/res/pkg/3e0098ea/darkconsole.pkg.js',
'type' => 'js',
),
'8aaacd1b' =>
@ -3478,7 +3521,7 @@ celerity_register_resource_map(array(
'uri' => '/res/pkg/8aaacd1b/differential.pkg.css',
'type' => 'css',
),
'9dae5f20' =>
'95d0d865' =>
array(
'name' => 'differential.pkg.js',
'symbols' =>
@ -3503,7 +3546,7 @@ celerity_register_resource_map(array(
17 => 'javelin-behavior-differential-toggle-files',
18 => 'javelin-behavior-differential-user-select',
),
'uri' => '/res/pkg/9dae5f20/differential.pkg.js',
'uri' => '/res/pkg/95d0d865/differential.pkg.js',
'type' => 'js',
),
'c8ce2d88' =>
@ -3529,7 +3572,7 @@ celerity_register_resource_map(array(
'uri' => '/res/pkg/f96657b8/diffusion.pkg.js',
'type' => 'js',
),
'1c6f020b' =>
'd466c034' =>
array(
'name' => 'javelin.pkg.js',
'symbols' =>
@ -3554,7 +3597,7 @@ celerity_register_resource_map(array(
17 => 'javelin-typeahead-ondemand-source',
18 => 'javelin-tokenizer',
),
'uri' => '/res/pkg/1c6f020b/javelin.pkg.js',
'uri' => '/res/pkg/d466c034/javelin.pkg.js',
'type' => 'js',
),
'e30a3fa8' =>
@ -3588,20 +3631,20 @@ celerity_register_resource_map(array(
'reverse' =>
array(
'aphront-attached-file-view-css' => 'e30a3fa8',
'aphront-crumbs-view-css' => '0e6165d0',
'aphront-dialog-view-css' => '0e6165d0',
'aphront-error-view-css' => '0e6165d0',
'aphront-form-view-css' => '0e6165d0',
'aphront-list-filter-view-css' => '0e6165d0',
'aphront-pager-view-css' => '0e6165d0',
'aphront-panel-view-css' => '0e6165d0',
'aphront-table-view-css' => '0e6165d0',
'aphront-tokenizer-control-css' => '0e6165d0',
'aphront-tooltip-css' => '0e6165d0',
'aphront-typeahead-control-css' => '0e6165d0',
'aphront-crumbs-view-css' => '4b569463',
'aphront-dialog-view-css' => '4b569463',
'aphront-error-view-css' => '4b569463',
'aphront-form-view-css' => '4b569463',
'aphront-list-filter-view-css' => '4b569463',
'aphront-pager-view-css' => '4b569463',
'aphront-panel-view-css' => '4b569463',
'aphront-table-view-css' => '4b569463',
'aphront-tokenizer-control-css' => '4b569463',
'aphront-tooltip-css' => '4b569463',
'aphront-typeahead-control-css' => '4b569463',
'differential-changeset-view-css' => '8aaacd1b',
'differential-core-view-css' => '8aaacd1b',
'differential-inline-comment-editor' => '9dae5f20',
'differential-inline-comment-editor' => '95d0d865',
'differential-local-commits-view-css' => '8aaacd1b',
'differential-results-table-css' => '8aaacd1b',
'differential-revision-add-comment-css' => '8aaacd1b',
@ -3612,117 +3655,117 @@ celerity_register_resource_map(array(
'differential-table-of-contents-css' => '8aaacd1b',
'diffusion-commit-view-css' => 'c8ce2d88',
'diffusion-icons-css' => 'c8ce2d88',
'global-drag-and-drop-css' => '0e6165d0',
'global-drag-and-drop-css' => '4b569463',
'inline-comment-summary-css' => '8aaacd1b',
'javelin-aphlict' => 'ff199687',
'javelin-behavior' => '1c6f020b',
'javelin-behavior-aphlict-dropdown' => 'ff199687',
'javelin-behavior-aphlict-listen' => 'ff199687',
'javelin-behavior-aphront-basic-tokenizer' => 'ff199687',
'javelin-behavior-aphront-drag-and-drop' => '9dae5f20',
'javelin-behavior-aphront-drag-and-drop-textarea' => '9dae5f20',
'javelin-behavior-aphront-form-disable-on-submit' => 'ff199687',
'javelin-aphlict' => 'bc0774e5',
'javelin-behavior' => 'd466c034',
'javelin-behavior-aphlict-dropdown' => 'bc0774e5',
'javelin-behavior-aphlict-listen' => 'bc0774e5',
'javelin-behavior-aphront-basic-tokenizer' => 'bc0774e5',
'javelin-behavior-aphront-drag-and-drop' => '95d0d865',
'javelin-behavior-aphront-drag-and-drop-textarea' => '95d0d865',
'javelin-behavior-aphront-form-disable-on-submit' => 'bc0774e5',
'javelin-behavior-audit-preview' => 'f96657b8',
'javelin-behavior-dark-console' => '74593df4',
'javelin-behavior-device' => 'ff199687',
'javelin-behavior-differential-accept-with-errors' => '9dae5f20',
'javelin-behavior-differential-add-reviewers-and-ccs' => '9dae5f20',
'javelin-behavior-differential-comment-jump' => '9dae5f20',
'javelin-behavior-differential-diff-radios' => '9dae5f20',
'javelin-behavior-differential-dropdown-menus' => '9dae5f20',
'javelin-behavior-differential-edit-inline-comments' => '9dae5f20',
'javelin-behavior-differential-feedback-preview' => '9dae5f20',
'javelin-behavior-differential-keyboard-navigation' => '9dae5f20',
'javelin-behavior-differential-populate' => '9dae5f20',
'javelin-behavior-differential-show-more' => '9dae5f20',
'javelin-behavior-differential-toggle-files' => '9dae5f20',
'javelin-behavior-differential-user-select' => '9dae5f20',
'javelin-behavior-dark-console' => '3e0098ea',
'javelin-behavior-device' => 'bc0774e5',
'javelin-behavior-differential-accept-with-errors' => '95d0d865',
'javelin-behavior-differential-add-reviewers-and-ccs' => '95d0d865',
'javelin-behavior-differential-comment-jump' => '95d0d865',
'javelin-behavior-differential-diff-radios' => '95d0d865',
'javelin-behavior-differential-dropdown-menus' => '95d0d865',
'javelin-behavior-differential-edit-inline-comments' => '95d0d865',
'javelin-behavior-differential-feedback-preview' => '95d0d865',
'javelin-behavior-differential-keyboard-navigation' => '95d0d865',
'javelin-behavior-differential-populate' => '95d0d865',
'javelin-behavior-differential-show-more' => '95d0d865',
'javelin-behavior-differential-toggle-files' => '95d0d865',
'javelin-behavior-differential-user-select' => '95d0d865',
'javelin-behavior-diffusion-commit-graph' => 'f96657b8',
'javelin-behavior-diffusion-pull-lastmodified' => 'f96657b8',
'javelin-behavior-error-log' => '74593df4',
'javelin-behavior-global-drag-and-drop' => 'ff199687',
'javelin-behavior-konami' => 'ff199687',
'javelin-behavior-lightbox-attachments' => 'ff199687',
'javelin-behavior-error-log' => '3e0098ea',
'javelin-behavior-global-drag-and-drop' => 'bc0774e5',
'javelin-behavior-konami' => 'bc0774e5',
'javelin-behavior-lightbox-attachments' => 'bc0774e5',
'javelin-behavior-maniphest-batch-selector' => '7707de41',
'javelin-behavior-maniphest-subpriority-editor' => '7707de41',
'javelin-behavior-maniphest-transaction-controls' => '7707de41',
'javelin-behavior-maniphest-transaction-expand' => '7707de41',
'javelin-behavior-maniphest-transaction-preview' => '7707de41',
'javelin-behavior-phabricator-active-nav' => 'ff199687',
'javelin-behavior-phabricator-autofocus' => 'ff199687',
'javelin-behavior-phabricator-home-reveal-tiles' => 'ff199687',
'javelin-behavior-phabricator-keyboard-shortcuts' => 'ff199687',
'javelin-behavior-phabricator-nav' => 'ff199687',
'javelin-behavior-phabricator-object-selector' => '9dae5f20',
'javelin-behavior-phabricator-oncopy' => 'ff199687',
'javelin-behavior-phabricator-remarkup-assist' => 'ff199687',
'javelin-behavior-phabricator-search-typeahead' => 'ff199687',
'javelin-behavior-phabricator-tooltips' => 'ff199687',
'javelin-behavior-phabricator-watch-anchor' => 'ff199687',
'javelin-behavior-refresh-csrf' => 'ff199687',
'javelin-behavior-repository-crossreference' => '9dae5f20',
'javelin-behavior-toggle-class' => 'ff199687',
'javelin-behavior-workflow' => 'ff199687',
'javelin-dom' => '1c6f020b',
'javelin-event' => '1c6f020b',
'javelin-install' => '1c6f020b',
'javelin-json' => '1c6f020b',
'javelin-mask' => '1c6f020b',
'javelin-request' => '1c6f020b',
'javelin-resource' => '1c6f020b',
'javelin-stratcom' => '1c6f020b',
'javelin-tokenizer' => '1c6f020b',
'javelin-typeahead' => '1c6f020b',
'javelin-typeahead-normalizer' => '1c6f020b',
'javelin-typeahead-ondemand-source' => '1c6f020b',
'javelin-typeahead-preloaded-source' => '1c6f020b',
'javelin-typeahead-source' => '1c6f020b',
'javelin-uri' => '1c6f020b',
'javelin-util' => '1c6f020b',
'javelin-vector' => '1c6f020b',
'javelin-workflow' => '1c6f020b',
'lightbox-attachment-css' => '0e6165d0',
'javelin-behavior-phabricator-active-nav' => 'bc0774e5',
'javelin-behavior-phabricator-autofocus' => 'bc0774e5',
'javelin-behavior-phabricator-home-reveal-tiles' => 'bc0774e5',
'javelin-behavior-phabricator-keyboard-shortcuts' => 'bc0774e5',
'javelin-behavior-phabricator-nav' => 'bc0774e5',
'javelin-behavior-phabricator-object-selector' => '95d0d865',
'javelin-behavior-phabricator-oncopy' => 'bc0774e5',
'javelin-behavior-phabricator-remarkup-assist' => 'bc0774e5',
'javelin-behavior-phabricator-search-typeahead' => 'bc0774e5',
'javelin-behavior-phabricator-tooltips' => 'bc0774e5',
'javelin-behavior-phabricator-watch-anchor' => 'bc0774e5',
'javelin-behavior-refresh-csrf' => 'bc0774e5',
'javelin-behavior-repository-crossreference' => '95d0d865',
'javelin-behavior-toggle-class' => 'bc0774e5',
'javelin-behavior-workflow' => 'bc0774e5',
'javelin-dom' => 'd466c034',
'javelin-event' => 'd466c034',
'javelin-install' => 'd466c034',
'javelin-json' => 'd466c034',
'javelin-mask' => 'd466c034',
'javelin-request' => 'd466c034',
'javelin-resource' => 'd466c034',
'javelin-stratcom' => 'd466c034',
'javelin-tokenizer' => 'd466c034',
'javelin-typeahead' => 'd466c034',
'javelin-typeahead-normalizer' => 'd466c034',
'javelin-typeahead-ondemand-source' => 'd466c034',
'javelin-typeahead-preloaded-source' => 'd466c034',
'javelin-typeahead-source' => 'd466c034',
'javelin-uri' => 'd466c034',
'javelin-util' => 'd466c034',
'javelin-vector' => 'd466c034',
'javelin-workflow' => 'd466c034',
'lightbox-attachment-css' => '4b569463',
'maniphest-task-summary-css' => 'e30a3fa8',
'maniphest-transaction-detail-css' => 'e30a3fa8',
'phabricator-busy' => 'ff199687',
'phabricator-busy' => 'bc0774e5',
'phabricator-content-source-view-css' => '8aaacd1b',
'phabricator-core-buttons-css' => '0e6165d0',
'phabricator-core-css' => '0e6165d0',
'phabricator-crumbs-view-css' => '0e6165d0',
'phabricator-directory-css' => '0e6165d0',
'phabricator-drag-and-drop-file-upload' => '9dae5f20',
'phabricator-dropdown-menu' => 'ff199687',
'phabricator-file-upload' => 'ff199687',
'phabricator-filetree-view-css' => '0e6165d0',
'phabricator-flag-css' => '0e6165d0',
'phabricator-form-view-css' => '0e6165d0',
'phabricator-header-view-css' => '0e6165d0',
'phabricator-jump-nav' => '0e6165d0',
'phabricator-keyboard-shortcut' => 'ff199687',
'phabricator-keyboard-shortcut-manager' => 'ff199687',
'phabricator-main-menu-view' => '0e6165d0',
'phabricator-menu-item' => 'ff199687',
'phabricator-nav-view-css' => '0e6165d0',
'phabricator-notification' => 'ff199687',
'phabricator-notification-css' => '0e6165d0',
'phabricator-notification-menu-css' => '0e6165d0',
'phabricator-object-item-list-view-css' => '0e6165d0',
'phabricator-core-buttons-css' => '4b569463',
'phabricator-core-css' => '4b569463',
'phabricator-crumbs-view-css' => '4b569463',
'phabricator-directory-css' => '4b569463',
'phabricator-drag-and-drop-file-upload' => '95d0d865',
'phabricator-dropdown-menu' => 'bc0774e5',
'phabricator-file-upload' => 'bc0774e5',
'phabricator-filetree-view-css' => '4b569463',
'phabricator-flag-css' => '4b569463',
'phabricator-form-view-css' => '4b569463',
'phabricator-header-view-css' => '4b569463',
'phabricator-jump-nav' => '4b569463',
'phabricator-keyboard-shortcut' => 'bc0774e5',
'phabricator-keyboard-shortcut-manager' => 'bc0774e5',
'phabricator-main-menu-view' => '4b569463',
'phabricator-menu-item' => 'bc0774e5',
'phabricator-nav-view-css' => '4b569463',
'phabricator-notification' => 'bc0774e5',
'phabricator-notification-css' => '4b569463',
'phabricator-notification-menu-css' => '4b569463',
'phabricator-object-item-list-view-css' => '4b569463',
'phabricator-object-selector-css' => '8aaacd1b',
'phabricator-paste-file-upload' => 'ff199687',
'phabricator-prefab' => 'ff199687',
'phabricator-paste-file-upload' => 'bc0774e5',
'phabricator-prefab' => 'bc0774e5',
'phabricator-project-tag-css' => 'e30a3fa8',
'phabricator-remarkup-css' => '0e6165d0',
'phabricator-shaped-request' => '9dae5f20',
'phabricator-side-menu-view-css' => '0e6165d0',
'phabricator-standard-page-view' => '0e6165d0',
'phabricator-textareautils' => 'ff199687',
'phabricator-tooltip' => 'ff199687',
'phabricator-transaction-view-css' => '0e6165d0',
'phabricator-zindex-css' => '0e6165d0',
'sprite-apps-large-css' => '0e6165d0',
'sprite-gradient-css' => '0e6165d0',
'sprite-icon-css' => '0e6165d0',
'sprite-menu-css' => '0e6165d0',
'syntax-highlighting-css' => '0e6165d0',
'phabricator-remarkup-css' => '4b569463',
'phabricator-shaped-request' => '95d0d865',
'phabricator-side-menu-view-css' => '4b569463',
'phabricator-standard-page-view' => '4b569463',
'phabricator-textareautils' => 'bc0774e5',
'phabricator-tooltip' => 'bc0774e5',
'phabricator-transaction-view-css' => '4b569463',
'phabricator-zindex-css' => '4b569463',
'sprite-apps-large-css' => '4b569463',
'sprite-gradient-css' => '4b569463',
'sprite-icon-css' => '4b569463',
'sprite-menu-css' => '4b569463',
'syntax-highlighting-css' => '4b569463',
),
));

View file

@ -32,6 +32,7 @@ phutil_register_library_map(array(
'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
'AphrontFormCropControl' => 'view/form/control/AphrontFormCropControl.php',
'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
'AphrontFormDragAndDropUploadControl' => 'view/form/control/AphrontFormDragAndDropUploadControl.php',
@ -87,6 +88,7 @@ phutil_register_library_map(array(
'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php',
'AphrontView' => 'view/AphrontView.php',
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
'AuditPeopleMenuEventListener' => 'applications/audit/events/AuditPeopleMenuEventListener.php',
'CelerityAPI' => 'infrastructure/celerity/CelerityAPI.php',
'CelerityPhabricatorResourceController' => 'infrastructure/celerity/CelerityPhabricatorResourceController.php',
'CelerityResourceController' => 'infrastructure/celerity/CelerityResourceController.php',
@ -197,12 +199,15 @@ phutil_register_library_map(array(
'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php',
'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php',
'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php',
'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.php',
'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php',
'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php',
'ConpherencePeopleMenuEventListener' => 'applications/conpherence/events/ConpherencePeopleMenuEventListener.php',
'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php',
'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php',
@ -307,6 +312,7 @@ phutil_register_library_map(array(
'DifferentialNewDiffMail' => 'applications/differential/mail/DifferentialNewDiffMail.php',
'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php',
'DifferentialPeopleMenuEventListener' => 'applications/differential/events/DifferentialPeopleMenuEventListener.php',
'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php',
'DifferentialReplyHandler' => 'applications/differential/DifferentialReplyHandler.php',
'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php',
@ -420,6 +426,7 @@ phutil_register_library_map(array(
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
'DiffusionPeopleMenuEventListener' => 'applications/diffusion/events/DiffusionPeopleMenuEventListener.php',
'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php',
'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php',
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
@ -563,6 +570,7 @@ phutil_register_library_map(array(
'ManiphestDefaultTaskExtensions' => 'applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php',
'ManiphestEdgeEventListener' => 'applications/maniphest/event/ManiphestEdgeEventListener.php',
'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php',
'ManiphestPeopleMenuEventListener' => 'applications/maniphest/event/ManiphestPeopleMenuEventListener.php',
'ManiphestReplyHandler' => 'applications/maniphest/ManiphestReplyHandler.php',
'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
'ManiphestSavedQuery' => 'applications/maniphest/storage/ManiphestSavedQuery.php',
@ -695,6 +703,18 @@ phutil_register_library_map(array(
'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php',
'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php',
'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php',
'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
'PhabricatorBotDifferentialNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDifferentialNotificationHandler.php',
'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php',
'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php',
'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php',
'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php',
'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php',
'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php',
'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php',
'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
'PhabricatorButtonsExample' => 'applications/uiexample/examples/PhabricatorButtonsExample.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
@ -707,6 +727,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php',
'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php',
'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php',
'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
@ -877,6 +898,7 @@ phutil_register_library_map(array(
'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php',
'PhabricatorFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorFileStorageEngineSelector.php',
'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php',
'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php',
'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php',
@ -910,17 +932,9 @@ phutil_register_library_map(array(
'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php',
'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
'PhabricatorIRCBot' => 'infrastructure/daemon/irc/PhabricatorIRCBot.php',
'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php',
'PhabricatorIRCFeedNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCFeedNotificationHandler.php',
'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php',
'PhabricatorIRCLogHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php',
'PhabricatorIRCMacroHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php',
'PhabricatorIRCMessage' => 'infrastructure/daemon/irc/PhabricatorIRCMessage.php',
'PhabricatorIRCObjectNameHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php',
'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php',
'PhabricatorIRCSymbolHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCSymbolHandler.php',
'PhabricatorIRCWhatsNewHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php',
'PhabricatorIRCBot' => 'infrastructure/daemon/bot/PhabricatorIRCBot.php',
'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php',
'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php',
'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php',
'PhabricatorInfrastructureTestCase' => 'infrastructure/__tests__/PhabricatorInfrastructureTestCase.php',
'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
@ -1291,6 +1305,7 @@ phutil_register_library_map(array(
'PhabricatorTagView' => 'view/layout/PhabricatorTagView.php',
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
'PhabricatorTimelineCursor' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php',
'PhabricatorTimelineDAO' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php',
@ -1394,6 +1409,7 @@ phutil_register_library_map(array(
'PholioController' => 'applications/pholio/controller/PholioController.php',
'PholioDAO' => 'applications/pholio/storage/PholioDAO.php',
'PholioImage' => 'applications/pholio/storage/PholioImage.php',
'PholioInlineSaveController' => 'applications/pholio/controller/PholioInlineSaveController.php',
'PholioMock' => 'applications/pholio/storage/PholioMock.php',
'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php',
'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php',
@ -1521,6 +1537,7 @@ phutil_register_library_map(array(
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormCropControl' => 'AphrontFormControl',
'AphrontFormDateControl' => 'AphrontFormControl',
'AphrontFormDividerControl' => 'AphrontFormControl',
'AphrontFormDragAndDropUploadControl' => 'AphrontFormControl',
@ -1572,6 +1589,7 @@ phutil_register_library_map(array(
'AphrontUsageException' => 'AphrontException',
'AphrontView' => 'Phobject',
'AphrontWebpageResponse' => 'AphrontHTMLResponse',
'AuditPeopleMenuEventListener' => 'PhutilEventListener',
'CelerityPhabricatorResourceController' => 'CelerityResourceController',
'CelerityResourceController' => 'PhabricatorController',
'CelerityResourceGraph' => 'AbstractDirectedGraph',
@ -1672,12 +1690,15 @@ phutil_register_library_map(array(
'ConpherenceController' => 'PhabricatorController',
'ConpherenceDAO' => 'PhabricatorLiskDAO',
'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl',
'ConpherenceImageData' => 'ConpherenceConstants',
'ConpherenceListController' => 'ConpherenceController',
'ConpherenceMenuItemView' => 'AphrontTagView',
'ConpherenceNewController' => 'ConpherenceController',
'ConpherenceParticipant' => 'ConpherenceDAO',
'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipationStatus' => 'ConpherenceConstants',
'ConpherencePeopleMenuEventListener' => 'PhutilEventListener',
'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
'ConpherenceThread' =>
array(
@ -1778,6 +1799,7 @@ phutil_register_library_map(array(
'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail',
'DifferentialParseRenderTestCase' => 'PhabricatorTestCase',
'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification',
'DifferentialPeopleMenuEventListener' => 'PhutilEventListener',
'DifferentialPrimaryPaneView' => 'AphrontView',
'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler',
'DifferentialResultsTableView' => 'AphrontView',
@ -1873,6 +1895,7 @@ phutil_register_library_map(array(
'DiffusionPathCompleteController' => 'DiffusionController',
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
'DiffusionPathValidateController' => 'DiffusionController',
'DiffusionPeopleMenuEventListener' => 'PhutilEventListener',
'DiffusionRawDiffQuery' => 'DiffusionQuery',
'DiffusionRepositoryController' => 'DiffusionController',
'DiffusionSetupException' => 'AphrontUsageException',
@ -1985,6 +2008,7 @@ phutil_register_library_map(array(
'ManiphestDefaultTaskExtensions' => 'ManiphestTaskExtensions',
'ManiphestEdgeEventListener' => 'PhutilEventListener',
'ManiphestExportController' => 'ManiphestController',
'ManiphestPeopleMenuEventListener' => 'PhutilEventListener',
'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler',
'ManiphestReportController' => 'ManiphestController',
'ManiphestSavedQuery' => 'ManiphestDAO',
@ -2131,6 +2155,15 @@ phutil_register_library_map(array(
'PhabricatorBarePageExample' => 'PhabricatorUIExample',
'PhabricatorBarePageView' => 'AphrontPageView',
'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation',
'PhabricatorBot' => 'PhabricatorDaemon',
'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
'PhabricatorBotDifferentialNotificationHandler' => 'PhabricatorBotHandler',
'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler',
'PhabricatorBotLogHandler' => 'PhabricatorBotHandler',
'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler',
'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler',
'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler',
'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
'PhabricatorButtonsExample' => 'PhabricatorUIExample',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
@ -2142,6 +2175,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController',
'PhabricatorCampfireProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
@ -2311,6 +2345,7 @@ phutil_register_library_map(array(
'PhabricatorFileShortcutController' => 'PhabricatorFileController',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileStorageConfigurationException' => 'Exception',
'PhabricatorFileTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTransformController' => 'PhabricatorFileController',
'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileUploadException' => 'Exception',
@ -2340,14 +2375,8 @@ phutil_register_library_map(array(
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorIRCBot' => 'PhabricatorDaemon',
'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCFeedNotificationHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCMacroHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCObjectNameHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCProtocolHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCSymbolHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCWhatsNewHandler' => 'PhabricatorIRCHandler',
'PhabricatorIRCProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
'PhabricatorIRCProtocolHandler' => 'PhabricatorBotHandler',
'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase',
'PhabricatorInlineCommentController' => 'PhabricatorController',
'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface',
@ -2683,6 +2712,7 @@ phutil_register_library_map(array(
'PhabricatorTagView' => 'AphrontView',
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
'PhabricatorTestCase' => 'ArcanistPhutilTestCase',
'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorTestWorker' => 'PhabricatorWorker',
'PhabricatorTimelineCursor' => 'PhabricatorTimelineDAO',
'PhabricatorTimelineDAO' => 'PhabricatorLiskDAO',
@ -2797,6 +2827,7 @@ phutil_register_library_map(array(
0 => 'PholioDAO',
1 => 'PhabricatorMarkupInterface',
),
'PholioInlineSaveController' => 'PholioController',
'PholioMock' =>
array(
0 => 'PholioDAO',

View file

@ -18,6 +18,12 @@ final class PhabricatorApplicationAudit extends PhabricatorApplication {
return PhabricatorEnv::getDoclink('article/Audit_User_Guide.html');
}
public function getEventListeners() {
return array(
new AuditPeopleMenuEventListener()
);
}
public function getRoutes() {
return array(
'/audit/' => array(

View file

@ -0,0 +1,38 @@
<?php
final class AuditPeopleMenuEventListener extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU:
$this->handleMenuEvent($event);
break;
}
}
private function handleMenuEvent($event) {
$viewer = $event->getUser();
$menu = $event->getValue('menu');
$person = $event->getValue('person');
$username = phutil_escape_uri($person->getUsername());
$href = '/audit/view/author/'.$username.'/';
$name = pht('Commits');
$menu->addMenuItemToLabel('activity',
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setName($name)
->setHref($href)
->setKey($name)
);
$event->setValue('menu', $menu);
}
}

View file

@ -65,20 +65,6 @@ final class PhabricatorSetupCheckDatabase extends PhabricatorSetupCheck {
return;
}
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$mode_string = queryfx_one($conn_raw, "SELECT @@sql_mode");
$modes = explode(',', $mode_string['@@sql_mode']);
if (!in_array('STRICT_ALL_TABLES', $modes)) {
$message = pht(
"The global sql_mode is not set to 'STRICT_ALL_TABLES'. It is ".
"recommended that you set this mode while developing Phabricator.");
$this->newIssue('mysql.mode')
->setName(pht('MySQL STRICT_ALL_TABLES mode not set.'))
->setMessage($message);
}
}
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$databases = queryfx_all($conn_raw, 'SHOW DATABASES');

View file

@ -24,6 +24,31 @@ final class PhabricatorSetupCheckMySQL extends PhabricatorSetupCheck {
->setName(pht('Small MySQL "max_allowed_packet"'))
->setMessage($message);
}
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$mode_string = queryfx_one($conn_raw, "SELECT @@sql_mode");
$modes = explode(',', $mode_string['@@sql_mode']);
if (!in_array('STRICT_ALL_TABLES', $modes)) {
$summary = pht(
"MySQL is not in strict mode, but should be for Phabricator ".
"development.");
$message = pht(
"This install is in developer mode, but the global sql_mode is not ".
"set to 'STRICT_ALL_TABLES'. It is recommended that you set this ".
"mode while developing Phabricator. Strict mode will promote some ".
"query warnings to errors, and ensure you don't miss them during ".
"development. You can find more information about this mode (and ".
"how to configure it) in the MySQL manual.");
$this->newIssue('mysql.mode')
->setName(pht('MySQL STRICT_ALL_TABLES Mode Not Set'))
->addPhabricatorConfig('phabricator.developer-mode')
->setSummary($summary)
->setMessage($message);
}
}
}
}

View file

@ -33,6 +33,12 @@ final class PhabricatorApplicationConpherence extends PhabricatorApplication {
return self::GROUP_COMMUNICATION;
}
public function getEventListeners() {
return array(
new ConpherencePeopleMenuEventListener(),
);
}
public function getRoutes() {
return array(
'/conpherence/' => array(

View file

@ -0,0 +1,11 @@
<?php
final class ConpherenceImageData extends ConpherenceConstants {
const SIZE_ORIG = 'original';
const SIZE_HEAD = 'header';
const HEAD_WIDTH = 120;
const HEAD_HEIGHT = 80;
}

View file

@ -8,6 +8,7 @@ final class ConpherenceTransactionType extends ConpherenceConstants {
const TYPE_FILES = 'files';
const TYPE_TITLE = 'title';
const TYPE_PICTURE = 'picture';
const TYPE_PICTURE_CROP = 'picture-crop';
const TYPE_PARTICIPANTS = 'participants';
}

View file

@ -131,7 +131,10 @@ abstract class ConpherenceController extends PhabricatorController {
$user = $this->getRequest()->getUser();
foreach ($conpherences as $conpherence) {
$uri = $this->getApplicationURI('view/'.$conpherence->getID().'/');
$data = $conpherence->getDisplayData($user);
$data = $conpherence->getDisplayData(
$user,
null
);
$title = $data['title'];
$subtitle = $data['subtitle'];
$unread_count = $data['unread_count'];
@ -206,6 +209,7 @@ abstract class ConpherenceController extends PhabricatorController {
'messages' => 'conpherence-messages',
'widgets_pane' => 'conpherence-widget-pane',
'form_pane' => 'conpherence-form',
'menu_pane' => 'conpherence-menu',
'fancy_ajax' => (bool) $this->getSelectedConpherencePHID()
)
);
@ -219,6 +223,14 @@ abstract class ConpherenceController extends PhabricatorController {
'form_pane' => 'conpherence-form'
)
);
Javelin::initBehavior('conpherence-drag-and-drop-photo',
array(
'target' => 'conpherence-header-pane',
'form_pane' => 'conpherence-form',
'upload_uri' => '/file/dropupload/',
'activated_class' => 'conpherence-header-upload-photo',
)
);
}
}

View file

@ -30,6 +30,8 @@ final class ConpherenceUpdateController extends
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->withIDs(array($conpherence_id))
->needOrigPics(true)
->needHeaderPics(true)
->executeOne();
$supported_formats = PhabricatorFile::getTransformableImageFormats();
@ -59,31 +61,63 @@ final class ConpherenceUpdateController extends
break;
case 'metadata':
$xactions = array();
$images = $request->getArr('image');
if ($images) {
// just take the first one
$file_phid = reset($images);
$file = id(new PhabricatorFileQuery())
$top = $request->getInt('image_y');
$left = $request->getInt('image_x');
$file_id = $request->getInt('file_id');
if ($file_id) {
$orig_file = id(new PhabricatorFileQuery())
->setViewer($user)
->withPHIDs(array($file_phid))
->withIDs(array($file_id))
->executeOne();
$okay = $file->isTransformableImage();
$okay = $orig_file->isTransformableImage();
if ($okay) {
$xformer = new PhabricatorImageTransformer();
$xformed = $xformer->executeThumbTransform(
$file,
$x = 50,
$y = 50);
$image_phid = $xformed->getPHID();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE)
->setNewValue($image_phid);
->setNewValue($orig_file->getPHID());
// do 2 transformations "crudely"
$xformer = new PhabricatorImageTransformer();
$header_file = $xformer->executeConpherenceTransform(
$orig_file,
0,
0,
ConpherenceImageData::HEAD_WIDTH,
ConpherenceImageData::HEAD_HEIGHT
);
// this is handled outside the editor for now. no particularly
// good reason to move it inside
$conpherence->setImagePHIDs(
array(
ConpherenceImageData::SIZE_HEAD => $header_file->getPHID(),
)
);
$conpherence->setImages(
array(
ConpherenceImageData::SIZE_HEAD => $header_file,
)
);
} else {
$e_file[] = $file;
$e_file[] = $orig_file;
$errors[] =
pht('This server only supports these image formats: %s.',
implode(', ', $supported_formats));
}
} else if ($top !== null || $left !== null) {
$file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
$xformer = new PhabricatorImageTransformer();
$xformed = $xformer->executeConpherenceTransform(
$file,
$top,
$left,
ConpherenceImageData::HEAD_WIDTH,
ConpherenceImageData::HEAD_HEIGHT
);
$image_phid = $xformed->getPHID();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransactionType::TYPE_PICTURE_CROP
)
->setNewValue($image_phid);
}
$title = $request->getStr('title');
if ($title != $conpherence->getTitle()) {
@ -131,24 +165,42 @@ final class ConpherenceUpdateController extends
->setLabel(pht('Title'))
->setName('title')
->setValue($conpherence->getTitle())
)
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Image'))
->setValue(phutil_tag(
'img',
array(
'src' => $conpherence->loadImageURI(),
))
)
)
->appendChild(
id(new AphrontFormDragAndDropUploadControl())
->setLabel(pht('Change Image'))
->setName('image')
->setValue($e_file)
->setCaption('Supported formats: '.implode(', ', $supported_formats))
);
$image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
if ($image) {
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Image'))
->setValue(phutil_tag(
'img',
array(
'src' =>
$conpherence->loadImageURI(ConpherenceImageData::SIZE_HEAD),
))
)
)
->appendChild(
id(new AphrontFormCropControl())
->setLabel(pht('Crop Image'))
->setValue($image)
->setWidth(ConpherenceImageData::HEAD_WIDTH)
->setHeight(ConpherenceImageData::HEAD_HEIGHT)
)
->appendChild(
id(new ConpherenceFormDragAndDropUploadControl())
->setLabel(pht('Change Image'))
);
} else {
$form
->appendChild(
id(new ConpherenceFormDragAndDropUploadControl())
->setLabel(pht('Image'))
);
}
require_celerity_resource('conpherence-update-css');
return id(new AphrontDialogResponse())

View file

@ -45,6 +45,7 @@ final class ConpherenceViewController extends
->setViewer($user)
->withIDs(array($conpherence_id))
->needWidgetData(true)
->needHeaderPics(true)
->executeOne();
$this->setConpherence($conpherence);
@ -67,23 +68,34 @@ final class ConpherenceViewController extends
require_celerity_resource('conpherence-header-pane-css');
$user = $this->getRequest()->getUser();
$conpherence = $this->getConpherence();
$display_data = $conpherence->getDisplayData($user);
$display_data = $conpherence->getDisplayData(
$user,
ConpherenceImageData::SIZE_HEAD
);
$edit_href = $this->getApplicationURI('update/'.$conpherence->getID().'/');
$class_mod = $display_data['image_class'];
$header =
phutil_tag(
'div',
array(
'class' => 'upload-photo'
),
pht('Drop photo here to change this Conpherence photo.')
).
javelin_tag(
'a',
array(
'class' => 'edit',
'href' => $edit_href,
'sigil' => 'workflow',
'sigil' => 'workflow edit-action',
),
''
).
phutil_tag(
'div',
array(
'class' => 'header-image',
'class' => $class_mod.'header-image',
'style' => 'background-image: url('.$display_data['image'].');'
),
''
@ -91,14 +103,14 @@ final class ConpherenceViewController extends
phutil_tag(
'div',
array(
'class' => 'title',
'class' => $class_mod.'title',
),
$display_data['title']
).
phutil_tag(
'div',
array(
'class' => 'subtitle',
'class' => $class_mod.'subtitle',
),
$display_data['subtitle']
);
@ -114,6 +126,18 @@ final class ConpherenceViewController extends
$rendered_transactions = array();
$transactions = $conpherence->getTransactions();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user);
foreach ($transactions as $transaction) {
if ($transaction->getComment()) {
$engine->addObject(
$transaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
foreach ($transactions as $transaction) {
if ($transaction->shouldHide()) {
continue;
@ -122,6 +146,7 @@ final class ConpherenceViewController extends
->setUser($user)
->setConpherenceTransaction($transaction)
->setHandles($handles)
->setMarkupEngine($engine)
->render();
}
$transactions = implode(' ', $rendered_transactions);

View file

@ -48,6 +48,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$types[] = ConpherenceTransactionType::TYPE_TITLE;
$types[] = ConpherenceTransactionType::TYPE_PICTURE;
$types[] = ConpherenceTransactionType::TYPE_PICTURE_CROP;
$types[] = ConpherenceTransactionType::TYPE_PARTICIPANTS;
$types[] = ConpherenceTransactionType::TYPE_FILES;
@ -62,7 +63,9 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
case ConpherenceTransactionType::TYPE_TITLE:
return $object->getTitle();
case ConpherenceTransactionType::TYPE_PICTURE:
return $object->getImagePHID();
return $object->getImagePHID(ConpherenceImageData::SIZE_ORIG);
case ConpherenceTransactionType::TYPE_PICTURE_CROP:
return $object->getImagePHID(ConpherenceImageData::SIZE_HEAD);
case ConpherenceTransactionType::TYPE_PARTICIPANTS:
return $object->getParticipantPHIDs();
case ConpherenceTransactionType::TYPE_FILES:
@ -77,6 +80,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
switch ($xaction->getTransactionType()) {
case ConpherenceTransactionType::TYPE_TITLE:
case ConpherenceTransactionType::TYPE_PICTURE:
case ConpherenceTransactionType::TYPE_PICTURE_CROP:
return $xaction->getNewValue();
case ConpherenceTransactionType::TYPE_PARTICIPANTS:
case ConpherenceTransactionType::TYPE_FILES:
@ -93,7 +97,16 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$object->setTitle($xaction->getNewValue());
break;
case ConpherenceTransactionType::TYPE_PICTURE:
$object->setImagePHID($xaction->getNewValue());
$object->setImagePHID(
$xaction->getNewValue(),
ConpherenceImageData::SIZE_ORIG
);
break;
case ConpherenceTransactionType::TYPE_PICTURE_CROP:
$object->setImagePHID(
$xaction->getNewValue(),
ConpherenceImageData::SIZE_HEAD
);
break;
}
}

View file

@ -0,0 +1,38 @@
<?php
final class ConpherencePeopleMenuEventListener extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU:
$this->handleMenuEvent($event);
break;
}
}
private function handleMenuEvent($event) {
$viewer = $event->getUser();
$menu = $event->getValue('menu');
$person = $event->getValue('person');
$conpherence_uri =
new PhutilURI('/conpherence/new/?participant='.$person->getPHID());
$name = pht('Conpherence');
$menu->addMenuItemBefore('activity',
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setName($name)
->setHref($conpherence_uri)
->setKey($name)
);
$event->setValue('menu', $menu);
}
}

View file

@ -9,6 +9,18 @@ final class ConpherenceThreadQuery
private $phids;
private $ids;
private $needWidgetData;
private $needHeaderPics;
private $needOrigPics;
public function needOrigPics($need_orig_pics) {
$this->needOrigPics = $need_orig_pics;
return $this;
}
public function needHeaderPics($need_header_pics) {
$this->needHeaderPics = $need_header_pics;
return $this;
}
public function needWidgetData($need_widget_data) {
$this->needWidgetData = $need_widget_data;
@ -47,6 +59,12 @@ final class ConpherenceThreadQuery
if ($this->needWidgetData) {
$this->loadWidgetData($conpherences);
}
if ($this->needOrigPics) {
$this->loadOrigPics($conpherences);
}
if ($this->needHeaderPics) {
$this->loadHeaderPics($conpherences);
}
}
return $conpherences;
@ -187,4 +205,39 @@ final class ConpherenceThreadQuery
return $this;
}
private function loadOrigPics(array $conpherences) {
return $this->loadPics(
$conpherences,
ConpherenceImageData::SIZE_ORIG
);
}
private function loadHeaderPics(array $conpherences) {
return $this->loadPics(
$conpherences,
ConpherenceImageData::SIZE_HEAD
);
}
private function loadPics(array $conpherences, $size) {
$conpherence_pic_phids = array();
foreach ($conpherences as $conpherence) {
$phid = $conpherence->getImagePHID($size);
if ($phid) {
$conpherence_pic_phids[$conpherence->getPHID()] = $phid;
}
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->withPHIDs($conpherence_pic_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($conpherence_pic_phids as $conpherence_phid => $pic_phid) {
$conpherences[$conpherence_phid]->setImage($files[$pic_phid], $size);
}
return $this;
}
}

View file

@ -9,7 +9,7 @@ final class ConpherenceThread extends ConpherenceDAO
protected $id;
protected $phid;
protected $title;
protected $imagePHID;
protected $imagePHIDs = array();
protected $mailKey;
private $participants;
@ -17,10 +17,14 @@ final class ConpherenceThread extends ConpherenceDAO
private $handles;
private $filePHIDs;
private $widgetData;
private $images = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'imagePHIDs' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
@ -36,6 +40,33 @@ final class ConpherenceThread extends ConpherenceDAO
return parent::save();
}
public function getImagePHID($size) {
$image_phids = $this->getImagePHIDs();
return idx($image_phids, $size);
}
public function setImagePHID($phid, $size) {
$image_phids = $this->getImagePHIDs();
$image_phids[$size] = $phid;
return $this->setImagePHIDs($image_phids);
}
public function getImage($size) {
$images = $this->getImages();
return idx($images, $size);
}
public function setImage(PhabricatorFile $file, $size) {
$files = $this->getImages();
$files[$size] = $file;
return $this->setImages($files);
}
public function setImages(array $files) {
$this->images = $files;
return $this;
}
private function getImages() {
return $this->images;
}
public function attachParticipants(array $participants) {
assert_instances_of($participants, 'ConpherenceParticipant');
$this->participants = $participants;
@ -112,59 +143,36 @@ final class ConpherenceThread extends ConpherenceDAO
return $this->widgetData;
}
public function loadImageURI() {
$src_phid = $this->getImagePHID();
public function loadImageURI($size) {
$file = $this->getImage($size);
if ($src_phid) {
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid);
if ($file) {
return $file->getBestURI();
}
if ($file) {
return $file->getBestURI();
}
return PhabricatorUser::getDefaultProfileImageURI();
}
public function getDisplayData(PhabricatorUser $user) {
public function getDisplayData(PhabricatorUser $user, $size) {
$transactions = $this->getTransactions();
$latest_transaction = end($transactions);
$latest_participant = $latest_transaction->getAuthorPHID();
$handles = $this->getHandles();
$latest_handle = $handles[$latest_participant];
if ($this->getImagePHID()) {
$img_src = $this->loadImageURI();
} else {
$img_src = $latest_handle->getImageURI();
}
$title = $this->getTitle();
if (!$title) {
$title = $latest_handle->getName();
unset($handles[$latest_participant]);
}
unset($handles[$user->getPHID()]);
$subtitle = '';
$count = 0;
$final = false;
foreach ($handles as $handle) {
if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) {
continue;
}
if ($subtitle) {
if ($final) {
$subtitle .= '...';
break;
} else {
$subtitle .= ', ';
}
}
$subtitle .= $handle->getName();
$count++;
$final = $count == 3;
$handles = $this->getHandles();
// we don't want to show the user unless they are babbling to themselves
if (count($handles) > 1) {
unset($handles[$user->getPHID()]);
}
$participants = $this->getParticipants();
$user_participation = $participants[$user->getPHID()];
$latest_transaction = null;
$title = $this->getTitle();
$subtitle = '';
$img_src = null;
$img_class = null;
if ($this->getImagePHID($size)) {
$img_src = $this->getImage($size)->getBestURI();
$img_class = 'custom-';
}
$unread_count = 0;
$max_count = 10;
$snippet = null;
@ -186,9 +194,28 @@ final class ConpherenceThread extends ConpherenceDAO
$transaction->getComment()->getContent(),
48
);
if ($transaction->getAuthorPHID() == $user->getPHID()) {
$snippet = "\xE2\x86\xB0 " . $snippet;
}
}
// fallthrough intentionally here
case ConpherenceTransactionType::TYPE_FILES:
if (!$latest_transaction) {
$latest_transaction = $transaction;
}
$latest_participant_phid = $transaction->getAuthorPHID();
if ((!$title || !$img_src) &&
$latest_participant_phid != $user->getPHID()) {
$latest_handle = $handles[$latest_participant_phid];
if (!$img_src) {
$img_src = $latest_handle->getImageURI();
}
if (!$title) {
$title = $latest_handle->getName();
// (maybs) used the pic, definitely used the name -- discard
unset($handles[$latest_participant_phid]);
}
}
if ($behind_transaction_phid) {
$unread_count++;
if ($transaction->getPHID() == $behind_transaction_phid) {
@ -210,12 +237,45 @@ final class ConpherenceThread extends ConpherenceDAO
$unread_count = $max_count.'+';
}
// This happens if the user has been babbling, maybs just to themselves,
// but enough un-responded to transactions for our SQL limit would
// hit this too... Also happens on new threads since only the first
// author has participated.
// ...so just pick a different handle in these cases.
$some_handle = reset($handles);
if (!$img_src) {
$img_src = $some_handle->getImageURI();
}
if (!$title) {
$title = $some_handle->getName();
}
$count = 0;
$final = false;
foreach ($handles as $handle) {
if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) {
continue;
}
if ($subtitle) {
if ($final) {
$subtitle .= '...';
break;
} else {
$subtitle .= ', ';
}
}
$subtitle .= $handle->getName();
$count++;
$final = $count == 3;
}
return array(
'title' => $title,
'subtitle' => $subtitle,
'unread_count' => $unread_count,
'epoch' => $latest_transaction->getDateCreated(),
'image' => $img_src,
'image_class' => $img_class,
'snippet' => $snippet,
);
}

View file

@ -31,6 +31,7 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction {
case ConpherenceTransactionType::TYPE_PICTURE:
return false;
case ConpherenceTransactionType::TYPE_FILES:
case ConpherenceTransactionType::TYPE_PICTURE_CROP:
return true;
}
@ -107,12 +108,10 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction {
$phids[] = $this->getAuthorPHID();
switch ($this->getTransactionType()) {
case ConpherenceTransactionType::TYPE_PICTURE:
$phids[] = $new;
break;
case ConpherenceTransactionType::TYPE_TITLE:
case ConpherenceTransactionType::TYPE_FILES:
break;
case ConpherenceTransactionType::TYPE_PARTICIPANTS:
case ConpherenceTransactionType::TYPE_FILES:
$phids = array_merge($phids, $this->getOldValue());
$phids = array_merge($phids, $this->getNewValue());
break;

View file

@ -0,0 +1,42 @@
<?php
final class ConpherenceFormDragAndDropUploadControl extends AphrontFormControl {
private $dropID;
public function setDropID($drop_id) {
$this->dropID = $drop_id;
return $this;
}
public function getDropID() {
return $this->dropID;
}
protected function getCustomControlClass() {
return null;
}
protected function renderInput() {
$drop_id = celerity_generate_unique_node_id();
Javelin::initBehavior('conpherence-drag-and-drop-photo',
array(
'target' => $drop_id,
'form_pane' => 'conpherence-form',
'upload_uri' => '/file/dropupload/',
'activated_class' => 'conpherence-dialogue-upload-photo',
)
);
require_celerity_resource('conpherence-update-css');
return phutil_tag(
'div',
array(
'id' => $drop_id,
'class' => 'conpherence-dialogue-drag-photo',
),
pht('Drag and drop an image here to upload it.')
);
}
}

View file

@ -7,6 +7,12 @@ final class ConpherenceTransactionView extends AphrontView {
private $conpherenceTransaction;
private $handles;
private $markupEngine;
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
@ -37,8 +43,11 @@ final class ConpherenceTransactionView extends AphrontView {
$content = null;
$content_class = null;
$content = null;
switch ($transaction->getTransactionType()) {
case ConpherenceTransactionType::TYPE_TITLE:
case ConpherenceTransactionType::TYPE_PICTURE:
case ConpherenceTransactionType::TYPE_PICTURE_CROP:
$content = $transaction->getTitle();
$transaction_view->addClass('conpherence-edited');
break;
@ -47,12 +56,13 @@ final class ConpherenceTransactionView extends AphrontView {
break;
case ConpherenceTransactionType::TYPE_PICTURE:
$img = $transaction->getHandle($transaction->getNewValue());
$content = $transaction->getTitle() .
$content = array(
$transaction->getTitle(),
phutil_tag(
'img',
array(
'src' => $img->getImageURI()
));
)));
$transaction_view->addClass('conpherence-edited');
break;
case ConpherenceTransactionType::TYPE_PARTICIPANTS:
@ -65,18 +75,9 @@ final class ConpherenceTransactionView extends AphrontView {
PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
array($comment->getContent())
);
$markup_field = ConpherenceTransactionComment::MARKUP_FIELD_COMMENT;
$engine = id(new PhabricatorMarkupEngine())
->setViewer($this->getUser());
$engine->addObject(
$content = $this->markupEngine->getOutput(
$comment,
$markup_field
);
$engine->process();
$content = $engine->getOutput(
$comment,
$markup_field
);
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
$content_class = 'conpherence-message phabricator-remarkup';
$transaction_view
->setImageURI($author->getImageURI())
@ -90,7 +91,7 @@ final class ConpherenceTransactionView extends AphrontView {
array(
'class' => $content_class
),
new PhutilSafeHTML($content))
$this->renderHTMLView($content))
);
return $transaction_view->render();

View file

@ -28,6 +28,12 @@ final class PhabricatorApplicationDifferential extends PhabricatorApplication {
return "\xE2\x9A\x99";
}
public function getEventListeners() {
return array(
new DifferentialPeopleMenuEventListener()
);
}
public function getRoutes() {
return array(
'/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController',

View file

@ -72,7 +72,6 @@ final class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod {
$project_name = null;
}
$basic_dict['projectName'] = $project_name;
$basic_dict['author'] = $diff->loadAuthorInformation();
return $basic_dict;
}

View file

@ -127,28 +127,6 @@ final class PhabricatorDifferentialConfigOptions
"If you set this to true, users won't need to login to view ".
"Differential revisions. Anonymous users will have read-only ".
"access and won't be able to interact with the revisions.")),
$this->newOption('differential.expose-emails-prudently', 'bool', false)
->setBoolOptions(
array(
pht("Expose revision author email address via Conduit"),
pht("Don't expose revision author email address via Conduit"),
))
->setSummary(
pht(
"Determines whether or not the author's email address should be ".
"exposed via Conduit."))
->setDescription(
pht(
"If you set this to true, revision author email address ".
"information will be exposed in Conduit. This is useful for ".
"Arcanist.\n\n".
"For example, consider the 'arc patch DX' workflow which needs ".
"to ask Differential for the revision DX. This data often should ".
"contain the author's email address, eg 'George Washington ".
"<gwashinton@example.com>' when DX is a git or mercurial ".
"revision. If this option is false, Differential defaults to the ".
"best it can, something like 'George Washington' or ".
"'gwashington'.")),
$this->newOption('differential.generated-paths', 'list<string>', array())
->setSummary(pht("File regexps to treat as automatically generated."))
->setDescription(

View file

@ -0,0 +1,38 @@
<?php
final class DifferentialPeopleMenuEventListener extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU:
$this->handleMenuEvent($event);
break;
}
}
private function handleMenuEvent($event) {
$viewer = $event->getUser();
$menu = $event->getValue('menu');
$person = $event->getValue('person');
$username = phutil_escape_uri($person->getUserName());
$href = '/differential/filter/revisions/'.$username.'/';
$name = pht('Revisions');
$menu->addMenuItemToLabel('activity',
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setHref($href)
->setName($name)
->setKey($name)
);
$event->setValue('menu', $menu);
}
}

View file

@ -240,55 +240,17 @@ final class DifferentialDiff extends DifferentialDAO {
$this->getID());
foreach ($properties as $property) {
$dict['properties'][$property->getName()] = $property->getData();
if ($property->getName() == 'local:commits') {
foreach ($property->getData() as $commit) {
$dict['authorName'] = $commit['author'];
$dict['authorEmail'] = $commit['authorEmail'];
break;
}
}
}
return $dict;
}
/**
* Figures out the right author information for a given diff based on the
* repository and Phabricator configuration settings.
*
* Git is particularly finicky as it requires author information to be in
* the format "George Washington <gwashington@example.com>" to
* consistently work. If the Phabricator instance isn't configured to
* expose emails prudently, then we are unable to get any author information
* for git.
*/
public function loadAuthorInformation() {
$author = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $this->getAuthorPHID());
$use_emails =
PhabricatorEnv::getEnvConfig('differential.expose-emails-prudently');
switch ($this->getSourceControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if (!$use_emails) {
$author_info = '';
} else {
$author_info = $this->getFullAuthorInfo($author);
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if (!$use_emails) {
$author_info = $author->getUsername();
} else {
$author_info = $this->getFullAuthorInfo($author);
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
default:
$author_info = $author->getUsername();
break;
}
return $author_info;
}
private function getFullAuthorInfo(PhabricatorUser $author) {
return sprintf('%s <%s>',
$author->getRealName(),
$author->loadPrimaryEmailAddress());
}
}

View file

@ -24,6 +24,12 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication {
);
}
public function getEventListeners() {
return array(
new DiffusionPeopleMenuEventListener()
);
}
public function getRoutes() {
return array(
'/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)'

View file

@ -0,0 +1,37 @@
<?php
final class DiffusionPeopleMenuEventListener extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU:
$this->handleMenuEvent($event);
break;
}
}
private function handleMenuEvent($event) {
$viewer = $event->getUser();
$menu = $event->getValue('menu');
$person_phid = $event->getValue('person')->getPHID();
$href = '/diffusion/lint/?owner[0]='.$person_phid;
$name = pht('Lint Messages');
$menu->addMenuItemToLabel('activity',
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setHref($href)
->setName($name)
->setKey($name)
);
$event->setValue('menu', $menu);
}
}

View file

@ -56,6 +56,29 @@ final class PhabricatorImageTransformer {
));
}
public function executeConpherenceTransform(
PhabricatorFile $file,
$top,
$left,
$width,
$height
) {
$image = $this->crasslyCropTo(
$file,
$top,
$left,
$width,
$height
);
return PhabricatorFile::newFromFileData(
$image,
array(
'name' => 'conpherence-'.$file->getName(),
)
);
}
private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) {
$data = $file->loadFileData();
@ -80,6 +103,30 @@ final class PhabricatorImageTransformer {
return $this->saveImageDataInAnyFormat($img, $file->getMimeType());
}
private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) {
$data = $file->loadFileData();
$src = imagecreatefromstring($data);
$dst = $this->getBlankDestinationFile($w, $h);
$scale = self::getScaleForCrop($file, $w, $h);
$orig_x = $left / $scale;
$orig_y = $top / $scale;
$orig_w = $w / $scale;
$orig_h = $h / $scale;
imagecopyresampled(
$dst,
$src,
0, 0,
$orig_x, $orig_y,
$w, $h,
$orig_w, $orig_h
);
return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
}
/**
* Very crudely scale an image up or down to an exact size.
*/
@ -92,15 +139,21 @@ final class PhabricatorImageTransformer {
return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
}
private function getBlankDestinationFile($dx, $dy) {
$dst = imagecreatetruecolor($dx, $dy);
imagesavealpha($dst, true);
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
return $dst;
}
private function applyScaleTo($src, $dx, $dy) {
$x = imagesx($src);
$y = imagesy($src);
$scale = min(($dx / $x), ($dy / $y), 1);
$dst = imagecreatetruecolor($dx, $dy);
imagesavealpha($dst, true);
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
$dst = $this->getBlankDestinationFile($dx, $dy);
$sdx = $scale * $x;
$sdy = $scale * $y;
@ -141,6 +194,27 @@ final class PhabricatorImageTransformer {
);
}
public static function getScaleForCrop(
PhabricatorFile $file,
$des_width,
$des_height) {
$metadata = $file->getMetadata();
$width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH];
$height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT];
if ($height < $des_height) {
$scale = $height / $des_height;
} else if ($width < $des_width) {
$scale = $width / $des_width;
} else {
$scale_x = $des_width / $width;
$scale_y = $des_height / $height;
$scale = max($scale_x, $scale_y);
}
return $scale;
}
private function generatePreview(PhabricatorFile $file, $size) {
$data = $file->loadFileData();
$src = imagecreatefromstring($data);
@ -153,9 +227,7 @@ final class PhabricatorImageTransformer {
$sdx = $dimensions['sdx'];
$sdy = $dimensions['sdy'];
$dst = imagecreatetruecolor($dx, $dy);
imagesavealpha($dst, true);
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
$dst = $this->getBlankDestinationFile($dx, $dy);
imagecopyresampled(
$dst,

View file

@ -0,0 +1,36 @@
<?php
/**
* Test storage engine. Does not actually store files. Used for unit tests.
*
* @group filestorage
*/
final class PhabricatorTestStorageEngine
extends PhabricatorFileStorageEngine {
private static $storage = array();
private static $nextHandle = 1;
public function getEngineIdentifier() {
return 'unit-test';
}
public function writeFile($data, array $params) {
AphrontWriteGuard::willWrite();
self::$storage[self::$nextHandle] = $data;
return self::$nextHandle++;
}
public function readFile($handle) {
if (isset(self::$storage[$handle])) {
return self::$storage[$handle];
}
throw new Exception("No such file with handle '{$handle}'!");
}
public function deleteFile($handle) {
AphrontWriteGuard::willWrite();
unset(self::$storage[$handle]);
}
}

View file

@ -134,9 +134,16 @@ final class PhabricatorFile extends PhabricatorFileDAO
public static function newFromFileData($data, array $params = array()) {
$selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector');
$engines = $selector->selectStorageEngines($data, $params);
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$selector = PhabricatorEnv::newObjectFromConfig(
'storage.engine-selector');
$engines = $selector->selectStorageEngines($data, $params);
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception("No valid storage engines are available!");
}

View file

@ -0,0 +1,61 @@
<?php
final class PhabricatorFileTestCase extends PhabricatorTestCase {
public function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testFileStorageReadWrite() {
$engine = new PhabricatorTestStorageEngine();
$data = Filesystem::readRandomCharacters(64);
$params = array(
'name' => 'test.dat',
'storageEngines' => array(
$engine,
),
);
$file = PhabricatorFile::newFromFileData($data, $params);
// Test that the storage engine worked, and was the target of the write. We
// don't actually care what the data is (future changes may compress or
// encrypt it), just that it exists in the test storage engine.
$engine->readFile($file->getStorageHandle());
// Now test that we get the same data back out.
$this->assertEqual($data, $file->loadFileData());
}
public function testFileStorageDelete() {
$engine = new PhabricatorTestStorageEngine();
$data = Filesystem::readRandomCharacters(64);
$params = array(
'name' => 'test.dat',
'storageEngines' => array(
$engine,
),
);
$file = PhabricatorFile::newFromFileData($data, $params);
$handle = $file->getStorageHandle();
$file->delete();
$caught = null;
try {
$engine->readFile($handle);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(true, $caught instanceof Exception);
}
}

View file

@ -36,6 +36,12 @@ final class PhabricatorApplicationManiphest extends PhabricatorApplication {
return $this->getBaseURI().'task/create/';
}
public function getEventListeners() {
return array(
new ManiphestPeopleMenuEventListener()
);
}
public function getRoutes() {
return array(
'/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController',

View file

@ -0,0 +1,36 @@
<?php
final class ManiphestPeopleMenuEventListener extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU:
$this->handleMenuEvent($event);
break;
}
}
private function handleMenuEvent($event) {
$viewer = $event->getUser();
$menu = $event->getValue('menu');
$person_phid = $event->getValue('person')->getPHID();
$href = '/maniphest/view/action/?users='.$person_phid;
$name = pht('Tasks');
$menu->addMenuItemToLabel('activity',
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setHref($href)
->setName($name)
->setKey($name)
);
$event->setValue('menu', $menu);
}
}

View file

@ -15,7 +15,7 @@ extends PhabricatorAuthController {
$current_user = $request->getUser();
$server = new PhabricatorOAuthServer();
$client_phid = $request->getStr('client_id');
$scope = $request->getStr('scope');
$scope = $request->getStr('scope', array());
$redirect_uri = $request->getStr('redirect_uri');
$state = $request->getStr('state');
$response_type = $request->getStr('response_type');
@ -63,10 +63,8 @@ extends PhabricatorAuthController {
return $response;
}
$uri = $redirect_uri;
$access_token_uri = $uri;
} else {
$uri = new PhutilURI($client->getRedirectURI());
$access_token_uri = null;
}
// we've now validated this request enough overall such that we
// can safely redirect to the client with the response
@ -121,7 +119,7 @@ extends PhabricatorAuthController {
if ($return_auth_code) {
// step 1 -- generate authorization code
$auth_code =
$server->generateAuthorizationCode($access_token_uri);
$server->generateAuthorizationCode($uri);
// step 2 return it
$content = array(

View file

@ -16,6 +16,21 @@ final class PhabricatorPeopleProfileController
return $this->profileUser;
}
private function getMainFilters($username) {
return array(
array(
'key' => 'feed',
'name' => pht('Feed'),
'href' => '/p/'.$username.'/feed/'
),
array(
'key' => 'about',
'name' => pht('About'),
'href' => '/p/'.$username.'/about/'
)
);
}
public function processRequest() {
$viewer = $this->getRequest()->getUser();
@ -39,40 +54,14 @@ final class PhabricatorPeopleProfileController
}
$username = phutil_escape_uri($user->getUserName());
$external_arrow = "\xE2\x86\x97";
$menu = new PhabricatorMenuView();
foreach ($this->getMainFilters($username) as $filter) {
$menu->newLink($filter['name'], $filter['href'], $filter['key']);
}
$conpherence_uri =
new PhutilURI('/conpherence/new/?participant='.$user->getPHID());
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/p/'.$username.'/'));
$nav->addFilter('feed', 'Feed');
$nav->addMenuItem(
id(new PhabricatorMenuItemView())
->setName(pht('Conpherence').' '.$external_arrow)
->setHref($conpherence_uri)
);
$nav->addFilter('about', 'About');
$nav->addLabel('Activity');
$nav->addFilter(
null,
"Revisions {$external_arrow}",
'/differential/filter/revisions/'.$username.'/');
$nav->addFilter(
null,
"Tasks {$external_arrow}",
'/maniphest/view/action/?users='.$user->getPHID());
$nav->addFilter(
null,
"Commits {$external_arrow}",
'/audit/view/author/'.$username.'/');
$nav->addFilter(
null,
"Lint Messages {$external_arrow}",
'/diffusion/lint/?owner[0]='.$user->getPHID());
$menu->newLabel(pht('Activity'), 'activity');
// NOTE: applications install the various links through PhabricatorEvent
// listeners
$oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere(
'userID = %d',
@ -92,18 +81,34 @@ final class PhabricatorPeopleProfileController
continue;
}
$name = $provider->getProviderName().' Profile';
$name = pht('%s Profile', $provider->getProviderName());
$href = $oauths[$provider_key]->getAccountURI();
if ($href) {
if (!$added_label) {
$nav->addLabel('Linked Accounts');
$menu->newLabel(pht('Linked Accounts'), 'linked_accounts');
$added_label = true;
}
$nav->addFilter(null, $name.' '.$external_arrow, $href);
$menu->addMenuItem(
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setName($name)
->setHref($href)
->setType(PhabricatorMenuItemView::TYPE_LINK)
);
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU,
array(
'menu' => $menu,
'person' => $user,
));
$event->setUser($viewer);
PhutilEventEngine::dispatchEvent($event);
$nav = AphrontSideNavFilterView::newFromMenu($event->getValue('menu'));
$this->page = $nav->selectFilter($this->page, 'feed');
switch ($this->page) {
@ -141,14 +146,19 @@ final class PhabricatorPeopleProfileController
$header->appendChild($content);
if ($user->getPHID() == $viewer->getPHID()) {
$nav->addFilter(null, 'Edit Profile...', '/settings/panel/profile/');
$nav->addFilter(
null,
pht('Edit Profile...'),
'/settings/panel/profile/'
);
}
if ($viewer->getIsAdmin()) {
$nav->addFilter(
null,
'Administrate User...',
'/people/edit/'.$user->getID().'/');
pht('Administrate User...'),
'/people/edit/'.$user->getID().'/'
);
}
return $this->buildApplicationPage(
@ -162,7 +172,10 @@ final class PhabricatorPeopleProfileController
$blurb = nonempty(
$profile->getBlurb(),
'//Nothing is known about this rare specimen.//');
'//'.
pht('Nothing is known about this rare specimen.')
.'//'
);
$engine = PhabricatorMarkupEngine::newProfileMarkupEngine();
$blurb = phutil_safe_html($engine->markupText($blurb));

View file

@ -413,9 +413,6 @@ final class PhabricatorObjectHandleData {
$handle->setName($file->getName());
$handle->setURI($file->getBestURI());
$handle->setComplete(true);
if ($file->isViewableImage()) {
$handle->setImageURI($file->getBestURI());
}
}
$handles[$phid] = $handle;
}

View file

@ -43,6 +43,7 @@ final class PhabricatorApplicationPholio extends PhabricatorApplication {
'new/' => 'PholioMockEditController',
'edit/(?P<id>\d+)/' => 'PholioMockEditController',
'comment/(?P<id>\d+)/' => 'PholioMockCommentController',
'inline/(?P<id>\d+)/' => 'PholioInlineSaveController',
),
);
}

View file

@ -4,5 +4,6 @@ final class PholioTransactionType extends PholioConstants {
const TYPE_NAME = 'name';
const TYPE_DESCRIPTION = 'description';
const TYPE_INLINE = 'inline';
}

View file

@ -0,0 +1,54 @@
<?php
/**
* @group pholio
*/
final class PholioInlineSaveController extends PholioController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$mock = id(new PholioMockQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($request->getInt('mockID')))
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$draft = id(new PholioTransactionComment());
$draft->setImageID($request->getInt('imageID'));
$draft->setX($request->getInt('startX'));
$draft->setY($request->getInt('startY'));
$draft->setCommentVersion(1);
$draft->setAuthorPHID($user->getPHID());
$draft->setEditPolicy($user->getPHID());
$draft->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
$draft->setContentSource($content_source);
$draft->setWidth($request->getInt('endX') - $request->getInt('startX'));
$draft->setHeight($request->getInt('endY') - $request->getInt('startY'));
$draft->setContent($request->getStr('comment'));
$draft->save();
return id(new AphrontAjaxResponse())->setContent(array());
}
}

View file

@ -22,6 +22,7 @@ final class PholioMockCommentController extends PholioController {
$mock = id(new PholioMockQuery())
->setViewer($user)
->withIDs(array($this->id))
->needImages(true)
->executeOne();
if (!$mock) {
@ -49,6 +50,18 @@ final class PholioMockCommentController extends PholioController {
id(new PholioTransactionComment())
->setContent($comment));
$inlineComments = id(new PholioTransactionComment())->loadAllWhere(
'authorphid = %s AND transactionphid IS NULL AND imageid IN (%Ld)',
$user->getPHID(),
mpull($mock->getImages(), 'getID')
);
foreach ($inlineComments as $inlineComment) {
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioTransactionType::TYPE_INLINE)
->attachComment($inlineComment);
}
$editor = id(new PholioMockEditor())
->setActor($user)
->setContentSource($content_source)

View file

@ -14,6 +14,7 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
$types[] = PholioTransactionType::TYPE_NAME;
$types[] = PholioTransactionType::TYPE_DESCRIPTION;
$types[] = PholioTransactionType::TYPE_INLINE;
return $types;
}
@ -40,6 +41,18 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
return true;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {

View file

@ -15,7 +15,9 @@ final class PholioMockImagesView extends AphrontView {
$main_image_id = celerity_generate_unique_node_id();
require_celerity_resource('javelin-behavior-pholio-mock-view');
$config = array('mainID' => $main_image_id);
$config = array(
'mainID' => $main_image_id,
'mockID' => $this->mock->getID());
Javelin::initBehavior('pholio-mock-view', $config);
$mockview = "";

View file

@ -75,9 +75,12 @@ final class PhabricatorApplicationTransactionCommentEditor
"Transaction must have a PHID before calling applyEdit()!");
}
if ($comment->getPHID()) {
throw new Exception(
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
if ($comment->getPHID()) {
throw new Exception(
"Transaction comment must not yet have a PHID!");
}
}
if (!$this->getContentSource()) {

View file

@ -0,0 +1,132 @@
<?php
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*
* @group irc
*/
final class PhabricatorBot extends PhabricatorDaemon {
private $handlers;
private $conduit;
private $config;
private $pollFrequency;
public function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception("usage: PhabricatorBot <json_config_file>");
}
$json_raw = Filesystem::readFile($argv[0]);
$config = json_decode($json_raw, true);
if (!is_array($config)) {
throw new Exception("File '{$argv[0]}' is not valid JSON!");
}
$nick = idx($config, 'nick', 'phabot');
$handlers = idx($config, 'handlers', array());
$protocol_adapter_class = idx(
$config,
'protocol-adapter',
'PhabricatorIRCProtocolAdapter');
$this->pollFrequency = idx($config, 'poll-frequency', 1);
$this->config = $config;
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/api/');
$conduit_uri = (string)$conduit_uri;
$conduit = new ConduitClient($conduit_uri);
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'PhabricatorBot',
'clientVersion' => '1.0',
'clientDescription' => php_uname('n').':'.$nick,
'user' => $conduit_user,
'certificate' => $conduit_cert,
));
$this->conduit = $conduit;
}
// Instantiate Protocol Adapter, for now follow same technique as
// handler instantiation
$this->protocolAdapter = newv($protocol_adapter_class, array());
$this->protocolAdapter
->setConfig($this->config)
->connect();
$this->runLoop();
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
private function runLoop() {
do {
$this->stillWorking();
$messages = $this->protocolAdapter->getNextMessages($this->pollFrequency);
if (count($messages) > 0) {
foreach ($messages as $message) {
$this->routeMessage($message);
}
}
foreach ($this->handlers as $handler) {
$handler->runBackgroundTasks();
}
} while (true);
}
public function writeMessage(PhabricatorBotMessage $message) {
return $this->protocolAdapter->writeMessage($message);
}
private function routeMessage(PhabricatorBotMessage $message) {
$ignore = $this->getConfig('ignore');
if ($ignore && in_array($message->getSenderNickName(), $ignore)) {
return;
}
foreach ($this->handlers as $handler) {
try {
$handler->receiveMessage($message);
} catch (Exception $ex) {
phlog($ex);
}
}
}
public function getConduit() {
if (empty($this->conduit)) {
throw new Exception(
"This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
"'conduit.user' and 'conduit.cert' in the configuration to connect.");
}
return $this->conduit;
}
}

View file

@ -0,0 +1,68 @@
<?php
final class PhabricatorBotMessage {
private $sender;
private $command;
private $body;
private $target;
private $public;
public function __construct() {
// By default messages are public
$this->public = true;
}
public function setSender($sender) {
$this->sender = $sender;
return $this;
}
public function getSender() {
return $this->sender;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
public function setBody($body) {
$this->body = $body;
return $this;
}
public function getBody() {
return $this->body;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function getTarget() {
return $this->target;
}
public function isPublic() {
return $this->public;
}
public function setPublic($is_public) {
$this->public = $is_public;
return $this;
}
public function getReplyTo() {
if ($this->public) {
return $this->target;
} else {
return $this->sender;
}
}
}

View file

@ -0,0 +1,11 @@
<?php
/**
* Placeholder to let people know that the bot has been renamed
*/
final class PhabricatorIRCBot extends PhabricatorDaemon {
public function run() {
throw new Exception(
"This daemon has been deprecated, use `PhabricatorBot` instead.");
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* Defines the api for protocol adapters for @{class:PhabricatorBot}
*/
abstract class PhabricatorBaseProtocolAdapter {
protected $config;
public function setConfig($config) {
$this->config = $config;
return $this;
}
/**
* Performs any connection logic necessary for the protocol
*/
abstract public function connect();
/**
* This is the spout for messages coming in from the protocol.
* This will be called in the main event loop of the bot daemon
* So if if doesn't implement some sort of blocking timeout
* (e.g. select-based socket polling), it should at least sleep
* for some period of time in order to not overwhelm the processor.
*
* @param Int $poll_frequency The number of seconds between polls
*/
abstract public function getNextMessages($poll_frequency);
/**
* This is the output mechanism for the protocol.
*
* @param PhabricatorBotMessage $message The message to write
*/
abstract public function writeMessage(PhabricatorBotMessage $message);
}

View file

@ -0,0 +1,201 @@
<?php
final class PhabricatorCampfireProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
private $readBuffers;
private $authtoken;
private $server;
private $readHandles;
private $multiHandle;
private $active;
private $rooms;
public function connect() {
$this->server = idx($this->config, 'server');
$this->authtoken = idx($this->config, 'authtoken');
$ssl = idx($this->config, 'ssl', false);
$this->rooms = idx($this->config, 'join');
// First, join the room
if (!$this->rooms) {
throw new Exception("Not configured to join any rooms!");
}
$this->readBuffers = array();
// Set up our long poll in a curl multi request so we can
// continue running while it executes in the background
$this->multiHandle = curl_multi_init();
$this->readHandles = array();
foreach ($this->rooms as $room_id) {
$this->joinRoom($room_id);
// Set up the curl stream for reading
$url = ($ssl) ? "https://" : "http://";
$url .= "streaming.campfirenow.com/room/{$room_id}/live.json";
$this->readHandle[$url] = curl_init();
curl_setopt($this->readHandle[$url], CURLOPT_URL, $url);
curl_setopt($this->readHandle[$url], CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->readHandle[$url], CURLOPT_FOLLOWLOCATION, 1);
curl_setopt(
$this->readHandle[$url],
CURLOPT_USERPWD,
$this->authtoken.':x');
curl_setopt(
$this->readHandle[$url],
CURLOPT_HTTPHEADER,
array("Content-type: application/json"));
curl_setopt(
$this->readHandle[$url],
CURLOPT_WRITEFUNCTION,
array($this, 'read'));
curl_setopt($this->readHandle[$url], CURLOPT_BUFFERSIZE, 128);
curl_setopt($this->readHandle[$url], CURLOPT_TIMEOUT, 0);
curl_multi_add_handle($this->multiHandle, $this->readHandle[$url]);
// Initialize read buffer
$this->readBuffers[$url] = '';
}
$this->active = null;
$this->blockingMultiExec();
}
// This is our callback for the background curl multi-request.
// Puts the data read in on the readBuffer for processing.
private function read($ch, $data) {
$info = curl_getinfo($ch);
$length = strlen($data);
$this->readBuffers[$info['url']] .= $data;
return $length;
}
private function blockingMultiExec() {
do {
$status = curl_multi_exec($this->multiHandle, $this->active);
} while ($status == CURLM_CALL_MULTI_PERFORM);
// Check for errors
if ($status != CURLM_OK) {
throw new Exception(
"Phabricator Bot had a problem reading from campfire.");
}
}
public function getNextMessages($poll_frequency) {
$messages = array();
if (!$this->active) {
throw new Exception("Phabricator Bot stopped reading from campfire.");
}
// Prod our http request
curl_multi_select($this->multiHandle, $poll_frequency);
$this->blockingMultiExec();
// Process anything waiting on the read buffer
while ($m = $this->processReadBuffer()) {
$messages[] = $m;
}
return $messages;
}
private function processReadBuffer() {
foreach ($this->readBuffers as $url => &$buffer) {
$until = strpos($buffer, "}\r");
if ($until == false) {
continue;
}
$message = substr($buffer, 0, $until + 1);
$buffer = substr($buffer, $until + 2);
$m_obj = json_decode($message, true);
return id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($m_obj['room_id'])
->setBody($m_obj['body']);
}
// If we're here, there's nothing to process
return false;
}
public function writeMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$this->speak(
$message->getBody(),
$message->getTarget());
break;
}
}
private function joinRoom($room_id) {
$this->performPost("/room/{$room_id}/join.json");
}
private function leaveRoom($room_id) {
$this->performPost("/room/{$room_id}/leave.json");
}
private function speak($message, $room_id) {
$this->performPost(
"/room/{$room_id}/speak.json",
array(
'message' => array(
'type' => 'TextMessage',
'body' => $message)));
}
private function performPost($endpoint, $data = Null) {
$url = $this->server.$endpoint;
$payload = json_encode($data);
// cURL init & config
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_USERPWD, $this->authtoken . ':x');
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array("Content-type: application/json"));
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$output = curl_exec($ch);
curl_close($ch);
$output = trim($output);
if (strlen($output)) {
return json_decode($output);
}
return true;
}
public function __destruct() {
if ($this->rooms) {
foreach ($this->rooms as $room_id) {
$this->leaveRoom($room_id);
}
}
if ($this->readHandles) {
foreach ($this->readHandles as $read_handle) {
curl_multi_remove_handle($this->multiHandle, $read_handle);
curl_close($read_handle);
}
}
curl_multi_close($this->multiHandle);
}
}

View file

@ -0,0 +1,221 @@
<?php
final class PhabricatorIRCProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
private $socket;
private $writeBuffer;
private $readBuffer;
// Hash map of command translations
public static $commandTranslations = array(
'PRIVMSG' => 'MESSAGE');
public function connect() {
$nick = idx($this->config, 'nick', 'phabot');
$server = idx($this->config, 'server');
$port = idx($this->config, 'port', 6667);
$pass = idx($this->config, 'pass');
$ssl = idx($this->config, 'ssl', false);
$user = idx($this->config, 'user', $nick);
if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
throw new Exception(
"Nickname '{$nick}' is invalid!");
}
$errno = null;
$error = null;
if (!$ssl) {
$socket = fsockopen($server, $port, $errno, $error);
} else {
$socket = fsockopen('ssl://'.$server, $port, $errno, $error);
}
if (!$socket) {
throw new Exception("Failed to connect, #{$errno}: {$error}");
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception("Failed to set stream nonblocking.");
}
$this->socket = $socket;
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('USER')
->setBody("{$user} 0 * :{$user}"));
if ($pass) {
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('PASS')
->setBody("{$pass}"));
}
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('NICK')
->setBody("{$nick}"));
}
public function getNextMessages($poll_frequency) {
$messages = array();
$read = array($this->socket);
if (strlen($this->writeBuffer)) {
$write = array($this->socket);
} else {
$write = array();
}
$except = array();
$ok = @stream_select($read, $write, $except, $timeout_sec = 1);
if ($ok === false) {
throw new Exception(
"socket_select() failed: ".socket_strerror(socket_last_error()));
}
if ($read) {
// Test for connection termination; in PHP, fread() off a nonblocking,
// closed socket is empty string.
if (feof($this->socket)) {
// This indicates the connection was terminated on the other side,
// just exit via exception and let the overseer restart us after a
// delay so we can reconnect.
throw new Exception("Remote host closed connection.");
}
do {
$data = fread($this->socket, 4096);
if ($data === false) {
throw new Exception("fread() failed!");
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand("LOG")
->setBody(">>> ".$data);
$this->readBuffer .= $data;
}
} while (strlen($data));
}
if ($write) {
do {
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
throw new Exception("fwrite() failed!");
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand("LOG")
->setBody(">>> ".substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
}
while ($m = $this->processReadBuffer()) {
$messages[] = $m;
}
return $messages;
}
private function write($message) {
$this->writeBuffer .= $message;
return $this;
}
public function writeMessage(PhabricatorBotMessage $message) {
$irc_command = $this->getIRCCommand($message->getCommand());
switch ($message->getCommand()) {
case 'MESSAGE':
$data = $irc_command.' '.
$message->getTarget().' :'.
$message->getBody()."\r\n";
break;
default:
$data = $irc_command.' '.
$message->getBody()."\r\n";
break;
}
return $this->write($data);
}
private function processReadBuffer() {
$until = strpos($this->readBuffer, "\r\n");
if ($until === false) {
return false;
}
$message = substr($this->readBuffer, 0, $until);
$this->readBuffer = substr($this->readBuffer, $until + 2);
$pattern =
'/^'.
'(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
$matches = null;
if (!preg_match($pattern, $message, $matches)) {
throw new Exception("Unexpected message from server: {$message}");
}
$command = $this->getBotCommand($matches['command']);
list($target, $body) = $this->parseMessageData($command, $matches['data']);
$bot_message = id(new PhabricatorBotMessage())
->setSender(idx($matches, 'sender'))
->setCommand($command)
->setTarget($target)
->setBody($body);
if (!empty($target) && strncmp($target, '#', 1) !== 0) {
$bot_message->setPublic(false);
}
return $bot_message;
}
private function getBotCommand($irc_command) {
if (isset(self::$commandTranslations[$irc_command])) {
return self::$commandTranslations[$irc_command];
}
// We have no translation for this command, use as-is
return $irc_command;
}
private function getIRCCommand($original_bot_command) {
foreach (self::$commandTranslations as $irc_command=>$bot_command) {
if ($bot_command === $original_bot_command) {
return $irc_command;
}
}
return $original_bot_command;
}
private function parseMessageData($command, $data) {
switch ($command) {
case 'MESSAGE':
$matches = null;
if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) {
return array(
$matches[1],
rtrim($matches[2], "\r\n"));
}
break;
}
// By default we assume there is no target, only a body
return array(
null,
$data);
}
public function __destruct() {
$this->write("QUIT Goodbye.\r\n");
fclose($this->socket);
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* Logs messages to stdout
*/
final class PhabricatorBotDebugLogHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'LOG':
echo addcslashes(
$message->getBody(),
"\0..\37\177..\377");
echo "\n";
break;
}
}
}

View file

@ -3,12 +3,12 @@
/**
* @group irc
*/
final class PhabricatorIRCDifferentialNotificationHandler
extends PhabricatorIRCHandler {
final class PhabricatorBotDifferentialNotificationHandler
extends PhabricatorBotHandler {
private $skippedOldEvents;
public function receiveMessage(PhabricatorIRCMessage $message) {
public function receiveMessage(PhabricatorBotMessage $message) {
return;
}
@ -39,11 +39,16 @@ final class PhabricatorIRCDifferentialNotificationHandler
$verb = DifferentialAction::getActionPastTenseVerb($data['action']);
$actor_name = $handles[$actor_phid]->getName();
$message = "{$actor_name} {$verb} revision D".$data['revision_id'].".";
$message_body =
"{$actor_name} {$verb} revision D".$data['revision_id'].".";
$channels = $this->getConfig('notification.channels', array());
foreach ($channels as $channel) {
$this->write('PRIVMSG', "{$channel} :{$message}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($message_body));
}
}
}

View file

@ -5,8 +5,8 @@
*
* @group irc
*/
final class PhabricatorIRCFeedNotificationHandler
extends PhabricatorIRCHandler {
final class PhabricatorBotFeedNotificationHandler
extends PhabricatorBotHandler {
private $startupDelay = 30;
private $lastSeenChronoKey = 0;
@ -82,7 +82,7 @@ final class PhabricatorIRCFeedNotificationHandler
return false;
}
public function receiveMessage(PhabricatorIRCMessage $message) {
public function receiveMessage(PhabricatorBotMessage $message) {
return;
}
@ -150,7 +150,11 @@ final class PhabricatorIRCFeedNotificationHandler
$channels = $this->getConfig('join');
foreach ($channels as $channel) {
$this->write('PRIVMSG', "{$channel} :{$story['text']}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($story['text']));
}
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* Responds to IRC messages. You plug a bunch of these into a
* @{class:PhabricatorBot} to give it special behavior.
*
* @group irc
*/
abstract class PhabricatorBotHandler {
private $bot;
final public function __construct(PhabricatorBot $irc_bot) {
$this->bot = $irc_bot;
}
final protected function writeMessage(PhabricatorBotMessage $message) {
$this->bot->writeMessage($message);
return $this;
}
final protected function getConduit() {
return $this->bot->getConduit();
}
final protected function getConfig($key, $default = null) {
return $this->bot->getConfig($key, $default);
}
final protected function getURI($path) {
$base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
$base_uri->setPath($path);
return (string)$base_uri;
}
abstract public function receiveMessage(PhabricatorBotMessage $message);
public function runBackgroundTasks() {
return;
}
public function replyTo($original_message, $body) {
if ($original_message->getCommand() != 'MESSAGE') {
throw new Exception(
"Handler is trying to reply to something which is not a message!");
}
$reply = id(new PhabricatorBotMessage())
->setCommand('MESSAGE');
if ($original_message->isPublic()) {
// This is a public target, like a chatroom. Send the response to the
// chatroom.
$reply->setTarget($original_message->getTarget());
} else {
// This is a private target, like a private message. Send the response
// back to the sender (presumably, we are the target).
$reply->setTarget($original_message->getSender())
->setPublic(false);
}
$reply->setBody($body);
return $this->writeMessage($reply);
}
}

View file

@ -5,19 +5,19 @@
*
* @group irc
*/
final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler {
final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
private $futures = array();
public function receiveMessage(PhabricatorIRCMessage $message) {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
case 'MESSAGE':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
if (!$this->isChannelName($reply_to)) {
if (!$message->isPublic()) {
// Don't log private messages, although maybe we should for debugging?
break;
}
@ -27,8 +27,8 @@ final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler {
'channel' => $reply_to,
'type' => 'mesg',
'epoch' => time(),
'author' => $message->getSenderNickname(),
'message' => $message->getMessageText(),
'author' => $message->getSender(),
'message' => $message->getBody(),
),
);
@ -46,7 +46,7 @@ final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler {
$tell = false;
foreach ($prompts as $prompt) {
if (preg_match($prompt, $message->getMessageText())) {
if (preg_match($prompt, $message->getBody())) {
$tell = true;
break;
}
@ -55,7 +55,8 @@ final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler {
if ($tell) {
$response = $this->getURI(
'/chatlog/channel/'.phutil_escape_uri($reply_to).'/');
$this->write('PRIVMSG', "{$reply_to} :{$response}");
$this->replyTo($message, $response);
}
break;

View file

@ -3,7 +3,7 @@
/**
* @group irc
*/
final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler {
final class PhabricatorBotMacroHandler extends PhabricatorBotHandler {
private $macros;
private $regexp;
@ -40,44 +40,44 @@ final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler {
return true;
}
public function receiveMessage(PhabricatorIRCMessage $message) {
public function receiveMessage(PhabricatorBotMessage $message) {
if (!$this->init()) {
return;
}
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$matches = null;
if (!preg_match($this->regexp, $message, $matches)) {
return;
}
$macro = $matches[1];
$ascii = idx($this->macros[$macro], 'ascii');
if ($ascii === false) {
return;
}
if (!$ascii) {
$this->macros[$macro]['ascii'] = $this->rasterize(
$this->macros[$macro],
$this->getConfig('macro.size', 48),
$this->getConfig('macro.aspect', 0.66));
$ascii = $this->macros[$macro]['ascii'];
}
foreach ($ascii as $line) {
$this->buffer[$reply_to][] = $line;
}
case 'MESSAGE':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message_body = $message->getBody();
$matches = null;
if (!preg_match($this->regexp, $message_body, $matches)) {
return;
}
$macro = $matches[1];
$ascii = idx($this->macros[$macro], 'ascii');
if ($ascii === false) {
return;
}
if (!$ascii) {
$this->macros[$macro]['ascii'] = $this->rasterize(
$this->macros[$macro],
$this->getConfig('macro.size', 48),
$this->getConfig('macro.aspect', 0.66));
$ascii = $this->macros[$macro]['ascii'];
}
foreach ($ascii as $line) {
$this->buffer[$reply_to][] = $line;
}
break;
}
}
@ -92,7 +92,11 @@ final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler {
continue;
}
foreach ($lines as $key => $line) {
$this->write('PRIVMSG', "{$channel} :{$line}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($line));
unset($this->buffer[$channel][$key]);
break 2;
}

View file

@ -0,0 +1,195 @@
<?php
/**
* Looks for Dxxxx, Txxxx and links to them.
*
* @group irc
*/
final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler {
/**
* Map of PHIDs to the last mention of them (as an epoch timestamp); prevents
* us from spamming chat when a single object is discussed.
*/
private $recentlyMentioned = array();
public function receiveMessage(PhabricatorBotMessage $original_message) {
switch ($original_message->getCommand()) {
case 'MESSAGE':
$message = $original_message->getBody();
$matches = null;
$pattern =
'@'.
'(?<!/)(?:^|\b)'. // Negative lookbehind prevent matching "/D123".
'(D|T|P|V|F)(\d+)'.
'(?:\b|$)'.
'@';
$revision_ids = array();
$task_ids = array();
$paste_ids = array();
$commit_names = array();
$vote_ids = array();
$file_ids = array();
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
switch ($match[1]) {
case 'D':
$revision_ids[] = $match[2];
break;
case 'T':
$task_ids[] = $match[2];
break;
case 'P':
$paste_ids[] = $match[2];
break;
case 'V':
$vote_ids[] = $match[2];
break;
case 'F':
$file_ids[] = $match[2];
break;
}
}
}
$pattern =
'@'.
'(?<!/)(?:^|\b)'.
'(r[A-Z]+[0-9a-z]{1,40})'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$commit_names[] = $match[1];
}
}
$output = array();
if ($revision_ids) {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => $revision_ids,
));
$revisions = array_select_keys(
ipull($revisions, null, 'id'),
$revision_ids
);
foreach ($revisions as $revision) {
$output[$revision['phid']] =
'D'.$revision['id'].' '.$revision['title'].' - '.
$revision['uri'];
}
}
if ($task_ids) {
foreach ($task_ids as $task_id) {
if ($task_id == 1000) {
$output[1000] = 'T1000: A nanomorph mimetic poly-alloy'
.'(liquid metal) assassin controlled by Skynet: '
.'http://en.wikipedia.org/wiki/T-1000';
continue;
}
$task = $this->getConduit()->callMethodSynchronous(
'maniphest.info',
array(
'task_id' => $task_id,
));
$output[$task['phid']] = 'T'.$task['id'].': '.$task['title'].
' (Priority: '.$task['priority'].') - '.$task['uri'];
}
}
if ($vote_ids) {
foreach ($vote_ids as $vote_id) {
$vote = $this->getConduit()->callMethodSynchronous(
'slowvote.info',
array(
'poll_id' => $vote_id,
));
$output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question'].
' Come Vote '.$vote['uri'];
}
}
if ($file_ids) {
foreach ($file_ids as $file_id) {
$file = $this->getConduit()->callMethodSynchronous(
'file.info',
array(
'id' => $file_id,
));
$output[$file['phid']] = $file['objectName'].": ".$file['uri']." - ".
$file['name'];
}
}
if ($paste_ids) {
foreach ($paste_ids as $paste_id) {
$paste = $this->getConduit()->callMethodSynchronous(
'paste.info',
array(
'paste_id' => $paste_id,
));
// Eventually I'd like to show the username of the paster as well,
// however that will need something like a user.username_from_phid
// since we (ideally) want to keep the bot to Conduit calls...and
// not call to Phabricator-specific stuff (like actually loading
// the User object and fetching his/her username.)
$output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.
$paste['title'];
if ($paste['language']) {
$output[$paste['phid']] .= ' ('.$paste['language'].')';
}
}
}
if ($commit_names) {
$commits = $this->getConduit()->callMethodSynchronous(
'diffusion.getcommits',
array(
'commits' => $commit_names,
));
foreach ($commits as $commit) {
if (isset($commit['error'])) {
continue;
}
$output[$commit['commitPHID']] = $commit['uri'];
}
}
foreach ($output as $phid => $description) {
// Don't mention the same object more than once every 10 minutes
// in public channels, so we avoid spamming the chat over and over
// again for discsussions of a specific revision, for example.
$reply_to = $original_message->getReplyTo();
if (empty($this->recentlyMentioned[$reply_to])) {
$this->recentlyMentioned[$reply_to] = array();
}
$quiet_until = idx(
$this->recentlyMentioned[$reply_to],
$phid,
0) + (60 * 10);
if (time() < $quiet_until) {
// Remain quiet on this channel.
continue;
}
$this->recentlyMentioned[$reply_to][$phid] = time();
$this->replyTo($original_message, $description);
}
break;
}
}
}

View file

@ -0,0 +1,50 @@
<?php
/**
* Watches for "where is <symbol>?"
*
* @group irc
*/
final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$text = $message->getBody();
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
break;
}
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
if (count($results) > 1) {
$response = "Multiple symbols named '{$symbol}': {$default_uri}";
} else if (count($results) == 1) {
$result = head($results);
$response =
$result['type'].' '.
$result['name'].' '.
'('.$result['language'].'): '.
nonempty($result['uri'], $default_uri);
} else {
$response = "No symbol '{$symbol}' found anywhere.";
}
$this->replyTo($message, $response);
break;
}
}
}

View file

@ -5,23 +5,23 @@
*
* @group irc
*/
final class PhabricatorIRCWhatsNewHandler extends PhabricatorIRCHandler {
final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler {
private $floodblock = 0;
public function receiveMessage(PhabricatorIRCMessage $message) {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
case 'MESSAGE':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$message_body = $message->getBody();
$prompt = '~what( i|\')?s new\?~i';
if (preg_match($prompt, $message)) {
if (preg_match($prompt, $message_body)) {
if (time() < $this->floodblock) {
return;
}
@ -108,9 +108,15 @@ final class PhabricatorIRCWhatsNewHandler extends PhabricatorIRCHandler {
$gray = $color.'15';
$bold = chr(2);
$reset = chr(15);
$content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}".
"{$title}{$reset} - {$gray}{$uri}{$reset}";
$this->write('PRIVMSG',"{$reply_to} :{$content}");
// Disabling irc-specific styling, at least for now
// $content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}".
// "{$title}{$reset} - {$gray}{$uri}{$reset}";
$content = "{$user} {$action} {$title} - {$uri}";
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($reply_to)
->setBody($content));
}
return;
}

View file

@ -0,0 +1,42 @@
<?php
/**
* Implements the base IRC protocol so servers don't kick you off.
*
* @group irc
*/
final class PhabricatorIRCProtocolHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case '422': // Error - no MOTD
case '376': // End of MOTD
$nickpass = $this->getConfig('nickpass');
if ($nickpass) {
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget('nickserv')
->setBody("IDENTIFY {$nickpass}"));
}
$join = $this->getConfig('join');
if (!$join) {
throw new Exception("Not configured to join any channels!");
}
foreach ($join as $channel) {
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('JOIN')
->setBody($channel));
}
break;
case 'PING':
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('PONG')
->setBody($message->getBody()));
break;
}
}
}

View file

@ -1,250 +0,0 @@
<?php
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*
* @group irc
*/
final class PhabricatorIRCBot extends PhabricatorDaemon {
private $socket;
private $handlers;
private $writeBuffer;
private $readBuffer;
private $conduit;
private $config;
public function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception("usage: PhabricatorIRCBot <json_config_file>");
}
$json_raw = Filesystem::readFile($argv[0]);
$config = json_decode($json_raw, true);
if (!is_array($config)) {
throw new Exception("File '{$argv[0]}' is not valid JSON!");
}
$server = idx($config, 'server');
$port = idx($config, 'port', 6667);
$handlers = idx($config, 'handlers', array());
$pass = idx($config, 'pass');
$nick = idx($config, 'nick', 'phabot');
$user = idx($config, 'user', $nick);
$ssl = idx($config, 'ssl', false);
$nickpass = idx($config, 'nickpass');
$this->config = $config;
if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
throw new Exception(
"Nickname '{$nick}' is invalid!");
}
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/api/');
$conduit_uri = (string)$conduit_uri;
$conduit = new ConduitClient($conduit_uri);
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'PhabricatorIRCBot',
'clientVersion' => '1.0',
'clientDescription' => php_uname('n').':'.$nick,
'user' => $conduit_user,
'certificate' => $conduit_cert,
));
$this->conduit = $conduit;
}
$errno = null;
$error = null;
if (!$ssl) {
$socket = fsockopen($server, $port, $errno, $error);
} else {
$socket = fsockopen('ssl://'.$server, $port, $errno, $error);
}
if (!$socket) {
throw new Exception("Failed to connect, #{$errno}: {$error}");
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception("Failed to set stream nonblocking.");
}
$this->socket = $socket;
$this->writeCommand('USER', "{$user} 0 * :{$user}");
if ($pass) {
$this->writeCommand('PASS', "{$pass}");
}
$this->writeCommand('NICK', "{$nick}");
$this->runSelectLoop();
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
private function runSelectLoop() {
do {
$this->stillWorking();
$read = array($this->socket);
if (strlen($this->writeBuffer)) {
$write = array($this->socket);
} else {
$write = array();
}
$except = array();
$ok = @stream_select($read, $write, $except, $timeout_sec = 1);
if ($ok === false) {
throw new Exception(
"socket_select() failed: ".socket_strerror(socket_last_error()));
}
if ($read) {
// Test for connection termination; in PHP, fread() off a nonblocking,
// closed socket is empty string.
if (feof($this->socket)) {
// This indicates the connection was terminated on the other side,
// just exit via exception and let the overseer restart us after a
// delay so we can reconnect.
throw new Exception("Remote host closed connection.");
}
do {
$data = fread($this->socket, 4096);
if ($data === false) {
throw new Exception("fread() failed!");
} else {
$this->debugLog(true, $data);
$this->readBuffer .= $data;
}
} while (strlen($data));
}
if ($write) {
do {
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
throw new Exception("fwrite() failed!");
} else {
$this->debugLog(false, substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
}
do {
$routed_message = $this->processReadBuffer();
} while ($routed_message);
foreach ($this->handlers as $handler) {
$handler->runBackgroundTasks();
}
} while (true);
}
private function write($message) {
$this->writeBuffer .= $message;
return $this;
}
public function writeCommand($command, $message) {
return $this->write($command.' '.$message."\r\n");
}
private function processReadBuffer() {
$until = strpos($this->readBuffer, "\r\n");
if ($until === false) {
return false;
}
$message = substr($this->readBuffer, 0, $until);
$this->readBuffer = substr($this->readBuffer, $until + 2);
$pattern =
'/^'.
'(?:(?P<sender>:(\S+)) )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
$matches = null;
if (!preg_match($pattern, $message, $matches)) {
throw new Exception("Unexpected message from server: {$message}");
}
$irc_message = new PhabricatorIRCMessage(
idx($matches, 'sender'),
$matches['command'],
$matches['data']);
$this->routeMessage($irc_message);
return true;
}
private function routeMessage(PhabricatorIRCMessage $message) {
$ignore = $this->getConfig('ignore');
if ($ignore && in_array($message->getSenderNickName(), $ignore)) {
return;
}
foreach ($this->handlers as $handler) {
try {
$handler->receiveMessage($message);
} catch (Exception $ex) {
phlog($ex);
}
}
}
public function __destruct() {
$this->write("QUIT Goodbye.\r\n");
fclose($this->socket);
}
private function debugLog($is_read, $message) {
if ($this->getTraceMode()) {
echo $is_read ? '<<< ' : '>>> ';
echo addcslashes($message, "\0..\37\177..\377");
echo "\n";
}
}
public function getConduit() {
if (empty($this->conduit)) {
throw new Exception(
"This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
"'conduit.user' and 'conduit.cert' in the configuration to connect.");
}
return $this->conduit;
}
}

View file

@ -1,72 +0,0 @@
<?php
final class PhabricatorIRCMessage {
private $sender;
private $command;
private $data;
public function __construct($sender, $command, $data) {
$this->sender = $sender;
$this->command = $command;
$this->data = $data;
}
public function getRawSender() {
return $this->sender;
}
public function getRawData() {
return $this->data;
}
public function getCommand() {
return $this->command;
}
public function getReplyTo() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$target = $this->getTarget();
if ($target[0] == '#') {
return $target;
}
break;
}
return null;
}
public function getSenderNickname() {
$nick = $this->getRawSender();
$nick = ltrim($nick, ':');
$nick = head(explode('!', $nick));
return $nick;
}
public function getTarget() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$matches = null;
$raw = $this->getRawData();
if (preg_match('/^(\S+)\s/', $raw, $matches)) {
return $matches[1];
}
break;
}
return null;
}
public function getMessageText() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$matches = null;
$raw = $this->getRawData();
if (preg_match('/^\S+\s+:?(.*)$/', $raw, $matches)) {
return rtrim($matches[1], "\r\n");
}
break;
}
return null;
}
}

View file

@ -1,46 +0,0 @@
<?php
/**
* Responds to IRC messages. You plug a bunch of these into a
* @{class:PhabricatorIRCBot} to give it special behavior.
*
* @group irc
*/
abstract class PhabricatorIRCHandler {
private $bot;
final public function __construct(PhabricatorIRCBot $irc_bot) {
$this->bot = $irc_bot;
}
final protected function write($command, $message) {
$this->bot->writeCommand($command, $message);
return $this;
}
final protected function getConduit() {
return $this->bot->getConduit();
}
final protected function getConfig($key, $default = null) {
return $this->bot->getConfig($key, $default);
}
final protected function getURI($path) {
$base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
$base_uri->setPath($path);
return (string)$base_uri;
}
final protected function isChannelName($name) {
return (strncmp($name, '#', 1) === 0);
}
abstract public function receiveMessage(PhabricatorIRCMessage $message);
public function runBackgroundTasks() {
return;
}
}

View file

@ -1,199 +0,0 @@
<?php
/**
* Looks for Dxxxx, Txxxx and links to them.
*
* @group irc
*/
final class PhabricatorIRCObjectNameHandler extends PhabricatorIRCHandler {
/**
* Map of PHIDs to the last mention of them (as an epoch timestamp); prevents
* us from spamming chat when a single object is discussed.
*/
private $recentlyMentioned = array();
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$matches = null;
$pattern =
'@'.
'(?<!/)(?:^|\b)'. // Negative lookbehind prevent matching "/D123".
'(D|T|P|V|F)(\d+)'.
'(?:\b|$)'.
'@';
$revision_ids = array();
$task_ids = array();
$paste_ids = array();
$commit_names = array();
$vote_ids = array();
$file_ids = array();
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
switch ($match[1]) {
case 'D':
$revision_ids[] = $match[2];
break;
case 'T':
$task_ids[] = $match[2];
break;
case 'P':
$paste_ids[] = $match[2];
break;
case 'V':
$vote_ids[] = $match[2];
break;
case 'F':
$file_ids[] = $match[2];
break;
}
}
}
$pattern =
'@'.
'(?<!/)(?:^|\b)'.
'(r[A-Z]+[0-9a-z]{1,40})'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$commit_names[] = $match[1];
}
}
$output = array();
if ($revision_ids) {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => $revision_ids,
));
$revisions = array_select_keys(
ipull($revisions, null, 'id'),
$revision_ids
);
foreach ($revisions as $revision) {
$output[$revision['phid']] =
'D'.$revision['id'].' '.$revision['title'].' - '.
$revision['uri'];
}
}
if ($task_ids) {
foreach ($task_ids as $task_id) {
if ($task_id == 1000) {
$output[1000] = 'T1000: A nanomorph mimetic poly-alloy'
.'(liquid metal) assassin controlled by Skynet: '
.'http://en.wikipedia.org/wiki/T-1000';
continue;
}
$task = $this->getConduit()->callMethodSynchronous(
'maniphest.info',
array(
'task_id' => $task_id,
));
$output[$task['phid']] = 'T'.$task['id'].': '.$task['title'].
' (Priority: '.$task['priority'].') - '.$task['uri'];
}
}
if ($vote_ids) {
foreach ($vote_ids as $vote_id) {
$vote = $this->getConduit()->callMethodSynchronous(
'slowvote.info',
array(
'poll_id' => $vote_id,
));
$output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question'].
' Come Vote '.$vote['uri'];
}
}
if ($file_ids) {
foreach ($file_ids as $file_id) {
$file = $this->getConduit()->callMethodSynchronous(
'file.info',
array(
'id' => $file_id,
));
$output[$file['phid']] = $file['objectName'].": ".$file['uri']." - ".
$file['name'];
}
}
if ($paste_ids) {
foreach ($paste_ids as $paste_id) {
$paste = $this->getConduit()->callMethodSynchronous(
'paste.info',
array(
'paste_id' => $paste_id,
));
// Eventually I'd like to show the username of the paster as well,
// however that will need something like a user.username_from_phid
// since we (ideally) want to keep the bot to Conduit calls...and
// not call to Phabricator-specific stuff (like actually loading
// the User object and fetching his/her username.)
$output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.
$paste['title'];
if ($paste['language']) {
$output[$paste['phid']] .= ' ('.$paste['language'].')';
}
}
}
if ($commit_names) {
$commits = $this->getConduit()->callMethodSynchronous(
'diffusion.getcommits',
array(
'commits' => $commit_names,
));
foreach ($commits as $commit) {
if (isset($commit['error'])) {
continue;
}
$output[$commit['commitPHID']] = $commit['uri'];
}
}
foreach ($output as $phid => $description) {
// Don't mention the same object more than once every 10 minutes
// in public channels, so we avoid spamming the chat over and over
// again for discsussions of a specific revision, for example.
if (empty($this->recentlyMentioned[$reply_to])) {
$this->recentlyMentioned[$reply_to] = array();
}
$quiet_until = idx(
$this->recentlyMentioned[$reply_to],
$phid,
0) + (60 * 10);
if (time() < $quiet_until) {
// Remain quiet on this channel.
continue;
}
$this->recentlyMentioned[$reply_to][$phid] = time();
$this->write('PRIVMSG', "{$reply_to} :{$description}");
}
break;
}
}
}

View file

@ -1,32 +0,0 @@
<?php
/**
* Implements the base IRC protocol so servers don't kick you off.
*
* @group irc
*/
final class PhabricatorIRCProtocolHandler extends PhabricatorIRCHandler {
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case '422': // Error - no MOTD
case '376': // End of MOTD
$nickpass = $this->getConfig('nickpass');
if ($nickpass) {
$this->write('PRIVMSG', "nickserv :IDENTIFY {$nickpass}");
}
$join = $this->getConfig('join');
if (!$join) {
throw new Exception("Not configured to join any channels!");
}
foreach ($join as $channel) {
$this->write('JOIN', $channel);
}
break;
case 'PING':
$this->write('PONG', $message->getRawData());
break;
}
}
}

View file

@ -1,55 +0,0 @@
<?php
/**
* Watches for "where is <symbol>?"
*
* @group irc
*/
final class PhabricatorIRCSymbolHandler extends PhabricatorIRCHandler {
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$text = $message->getMessageText();
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
break;
}
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
if (count($results) > 1) {
$response = "Multiple symbols named '{$symbol}': {$default_uri}";
} else if (count($results) == 1) {
$result = head($results);
$response =
$result['type'].' '.
$result['name'].' '.
'('.$result['language'].'): '.
nonempty($result['uri'], $default_uri);
} else {
$response = "No symbol '{$symbol}' found anywhere.";
}
$this->write('PRIVMSG', "{$reply_to} :{$response}");
break;
}
}
}

View file

@ -30,4 +30,5 @@ final class PhabricatorEventType extends PhutilEventType {
const TYPE_UI_DDIDRENDEROBJECT = 'ui.didRenderObject';
const TYPE_UI_DIDRENDEROBJECTS = 'ui.didRenderObjects';
const TYPE_PEOPLE_DIDRENDERMENU = 'people.didRenderMenu';
}

View file

@ -50,37 +50,41 @@ final class PhabricatorRemarkupRuleEmbedFile
$file_name = coalesce($options['name'], $file->getName());
$options['name'] = $file_name;
$is_viewable_image = $file->isViewableImage();
$attrs = array();
switch ((string)$options['size']) {
case 'full':
$attrs['src'] = $file->getBestURI();
$options['image_class'] = null;
$file_data = $file->getMetadata();
$height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT);
if ($height) {
$attrs['height'] = $height;
}
$width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH);
if ($width) {
$attrs['width'] = $width;
}
break;
case 'thumb':
default:
$attrs['src'] = $file->getPreview220URI();
$dimensions =
PhabricatorImageTransformer::getPreviewDimensions($file, 220);
$attrs['width'] = $dimensions['sdx'];
$attrs['height'] = $dimensions['sdy'];
$options['image_class'] = 'phabricator-remarkup-embed-image';
break;
if ($is_viewable_image) {
switch ((string)$options['size']) {
case 'full':
$attrs['src'] = $file->getBestURI();
$options['image_class'] = null;
$file_data = $file->getMetadata();
$height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT);
if ($height) {
$attrs['height'] = $height;
}
$width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH);
if ($width) {
$attrs['width'] = $width;
}
break;
case 'thumb':
default:
$attrs['src'] = $file->getPreview220URI();
$dimensions =
PhabricatorImageTransformer::getPreviewDimensions($file, 220);
$attrs['width'] = $dimensions['sdx'];
$attrs['height'] = $dimensions['sdy'];
$options['image_class'] = 'phabricator-remarkup-embed-image';
break;
}
}
$bundle['attrs'] = $attrs;
$bundle['options'] = $options;
$bundle['meta'] = array(
'phid' => $file->getPHID(),
'viewable' => $file->isViewableImage(),
'viewable' => $is_viewable_image,
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'name' => $options['name'],

View file

@ -1105,6 +1105,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20130201.revisionunsubscribed.sql'),
),
'20130131.conpherencepics.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130131.conpherencepics.sql'),
),
);
}

View file

@ -0,0 +1,99 @@
<?php
final class AphrontFormCropControl extends AphrontFormControl {
private $width = 50;
private $height = 50;
public function setHeight($height) {
$this->height = $height;
return $this;
}
public function getHeight() {
return $this->height;
}
public function setWidth($width) {
$this->width = $width;
return $this;
}
public function getWidth() {
return $this->width;
}
protected function getCustomControlClass() {
return 'aphront-form-crop';
}
protected function renderInput() {
$file = $this->getValue();
if ($file === null) {
return phutil_render_tag(
'img',
array(
'src' => PhabricatorUser::getDefaultProfileImageURI()
),
''
);
}
$c_id = celerity_generate_unique_node_id();
$metadata = $file->getMetadata();
$scale = PhabricatorImageTransformer::getScaleForCrop(
$file,
$this->getWidth(),
$this->getHeight()
);
Javelin::initBehavior(
'aphront-crop',
array(
'cropBoxID' => $c_id,
'width' => $this->getWidth(),
'height' => $this->getHeight(),
'scale' => $scale,
'imageH' => $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT],
'imageW' => $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH],
)
);
return javelin_render_tag(
'div',
array(
'id' => $c_id,
'sigil' => 'crop-box',
'mustcapture' => true,
'class' => 'crop-box'
),
javelin_render_tag(
'img',
array(
'src' => $file->getBestURI(),
'class' => 'crop-image',
'sigil' => 'crop-image'
),
''
).
javelin_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'image_x',
'sigil' => 'crop-x',
),
''
).
javelin_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'image_y',
'sigil' => 'crop-y',
),
''
)
);
}
}

View file

@ -8,22 +8,30 @@ final class PhabricatorMenuView extends AphrontTagView {
return false;
}
public function newLabel($name) {
public function newLabel($name, $key = null) {
$item = id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_LABEL)
->setName($name);
if ($key !== null) {
$item->setKey($key);
}
$this->addMenuItem($item);
return $item;
}
public function newLink($name, $href) {
public function newLink($name, $href, $key = null) {
$item = id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_LINK)
->setName($name)
->setHref($href);
if ($key !== null) {
$item->setKey($key);
}
$this->addMenuItem($item);
return $item;

View file

@ -231,6 +231,18 @@ table.aphront-form-control-checkbox-layout th {
border-color: #669966;
}
.aphront-form-crop .crop-box {
cursor: move;
overflow: hidden;
}
.aphront-form-crop .crop-box .crop-image {
position: relative;
top: 0px;
left: 0px;
}
.calendar-button {
display: inline;
background: url(/rsrc/image/icon/fatcow/calendar_edit.png)

View file

@ -29,6 +29,14 @@
width: 50px;
}
.conpherence-header-pane .custom-header-image {
position: absolute;
top: 0px;
left: 0px;
height: 80px;
width: 120px;
}
.conpherence-header-pane .title {
position: relative;
font-size: 16px;
@ -39,6 +47,16 @@
overflow-x: auto;
}
.conpherence-header-pane .custom-title {
position: relative;
font-size: 16px;
font-weight: bold;
left: 132px;
top: 21px;
max-width: 80%;
overflow-x: auto;
}
.conpherence-header-pane .subtitle {
position: relative;
left: 77px;
@ -47,4 +65,34 @@
max-width: 80%;
}
.conpherence-header-pane .custom-subtitle {
position: relative;
left: 132px;
top: 21px;
color: #bfbfbf;
max-width: 80%;
}
.conpherence-header-pane .upload-photo {
display: none;
}
.conpherence-header-upload-photo .upload-photo {
display: block;
width: 100%;
padding: 32px;
font-size: 16px;
background: #99ff99;
border-color: #669966;
}
.conpherence-header-upload-photo .edit,
.conpherence-header-upload-photo .header-image,
.conpherence-header-upload-photo .custom-header-image,
.conpherence-header-upload-photo .title,
.conpherence-header-upload-photo .custom-title,
.conpherence-header-upload-photo .subtitle,
.conpherence-header-upload-photo .custom-subtitle {
display: none;
}

View file

@ -5,3 +5,13 @@
.phabricator-standard-page-body .aphront-dialog-view {
margin: 20px auto 0px auto;
}
.aphront-dialog-view .conpherence-dialogue-drag-photo {
border: 1px dashed #bfbfbf;
padding: 10px 0px 10px 10px;
}
.aphront-dialog-view .conpherence-dialogue-upload-photo {
background: #99ff99;
border-color: #669966;
}

View file

@ -21,9 +21,18 @@
display: inline-block;
}
.pholio-mock-select {
border: 1px solid #FF0000;
position: absolute;
.pholio-mock-select-border {
position: absolute;
background: #ffffff;
opacity: 0.25;
box-sizing: border-box;
border: 1px solid #000000;
}
.pholio-mock-select-fill {
position: absolute;
border: 1px dashed #ffffff;
box-sizing: border-box;
}
.pholio-mock-wrapper {

View file

@ -0,0 +1,39 @@
/**
* @provides javelin-behavior-conpherence-drag-and-drop-photo
* @requires javelin-behavior
* javelin-dom
* javelin-workflow
* phabricator-drag-and-drop-file-upload
*/
JX.behavior('conpherence-drag-and-drop-photo', function(config) {
var target = JX.$(config.target);
var form_pane = JX.$(config.form_pane);
function onupload(f) {
var data = {
'file_id' : f.getID(),
'action' : 'metadata'
};
var form = JX.DOM.find(form_pane, 'form');
var workflow = JX.Workflow.newFromForm(form, data);
workflow.start();
}
if (JX.PhabricatorDragAndDropFileUpload.isSupported()) {
var drop = new JX.PhabricatorDragAndDropFileUpload(target)
.setURI(config.upload_uri);
drop.listen('didBeginDrag', function(e) {
JX.DOM.alterClass(target, config.activated_class, true);
});
drop.listen('didEndDrag', function(e) {
JX.DOM.alterClass(target, config.activated_class, false);
});
drop.listen('didUpload', onupload);
drop.start();
}
});

View file

@ -19,14 +19,21 @@ JX.behavior('conpherence-menu', function(config) {
var messagesRoot = JX.$(config.messages);
var formRoot = JX.$(config.form_pane);
var widgetsRoot = JX.$(config.widgets_pane);
var menuRoot = JX.$(config.menu_pane);
JX.DOM.setContent(headerRoot, header);
JX.DOM.setContent(messagesRoot, messages);
messagesRoot.scrollTop = messagesRoot.scrollHeight;
JX.DOM.setContent(formRoot, form);
JX.DOM.setContent(widgetsRoot, widgets);
for (var i = 0; i < context.parentNode.childNodes.length; i++) {
var current = context.parentNode.childNodes[i];
var conpherences = JX.DOM.scry(
menuRoot,
'a',
'conpherence-menu-click'
);
for (var i = 0; i < conpherences.length; i++) {
var current = conpherences[i];
if (current.id == context.id) {
JX.DOM.alterClass(current, 'conpherence-selected', true);
JX.DOM.alterClass(current, 'hide-unread-count', true);

View file

@ -0,0 +1,94 @@
/**
* @provides javelin-behavior-aphront-crop
* @requires javelin-behavior
* javelin-dom
* javelin-vector
* javelin-magical-init
*/
JX.behavior('aphront-crop', function(config) {
var dragging = false;
var startX, startY;
var finalX, finalY;
var dScale = config.scale;
var cropBox = JX.$(config.cropBoxID);
var basePos = JX.$V(cropBox);
cropBox.style.height = config.height + 'px';
cropBox.style.width = config.width + 'px';
var baseD = JX.$V(config.width, config.height);
var image = JX.DOM.find(cropBox, 'img', 'crop-image');
image.style.height = (config.imageH * config.scale) + 'px';
image.style.width = (config.imageW * config.scale) + 'px';
var imageD = JX.$V(
config.imageW * config.scale,
config.imageH * config.scale
);
var minLeft = baseD.x - imageD.x;
var minTop = baseD.y - imageD.y;
var minScale = Math.min(
config.width / config.imageW,
config.height / config.imageH,
1
);
var maxScale = Math.max(
config.imageW / config.width,
config.imageH / config.height,
2
);
var ondrag = function(e) {
e.kill();
dragging = true;
var p = JX.$V(e);
startX = p.x;
startY = p.y;
};
var onmove = function(e) {
if (!dragging) {
return;
}
e.kill();
var p = JX.$V(e);
var dx = startX - p.x;
var dy = startY - p.y;
var imagePos = JX.$V(image);
var moveLeft = imagePos.x - basePos.x - dx;
var moveTop = imagePos.y - basePos.y - dy;
image.style.left = Math.min(Math.max(minLeft, moveLeft), 0) + 'px';
image.style.top = Math.min(Math.max(minTop, moveTop), 0) + 'px';
// reset these; a new beginning!
startX = p.x;
startY = p.y;
// save off where we are right now
imagePos = JX.$V(image);
finalX = Math.abs(imagePos.x - basePos.x);
finalY = Math.abs(imagePos.y - basePos.y);
JX.DOM.find(cropBox, 'input', 'crop-x').value = finalX;
JX.DOM.find(cropBox, 'input', 'crop-y').value = finalY;
};
var ondrop = function(e) {
if (!dragging) {
return;
}
dragging = false;
};
// NOTE: Javelin does not dispatch mousemove by default.
JX.enableDispatch(cropBox, 'mousemove');
JX.DOM.listen(cropBox, 'mousedown', [], ondrag);
JX.DOM.listen(cropBox, 'mousemove', [], onmove);
JX.DOM.listen(cropBox, 'mouseup', [], ondrop);
JX.DOM.listen(cropBox, 'mouseout', [], ondrop);
});

View file

@ -4,7 +4,8 @@
* javelin-stratcom
* javelin-dom
* javelin-vector
* javelin-event
* javelin-magical-init
* javelin-request
*/
JX.behavior('pholio-mock-view', function(config) {
var is_dragging = false;
@ -13,7 +14,9 @@ JX.behavior('pholio-mock-view', function(config) {
var imageData;
var startPos;
var endPos;
var selection;
var selection_border;
var selection_fill;
JX.Stratcom.listen(
'click', // Listen for clicks...
@ -35,26 +38,28 @@ JX.behavior('pholio-mock-view', function(config) {
});
function draw_rectangle(node, current, init) {
JX.$V(
Math.abs(current.x-init.x),
Math.abs(current.y-init.y))
.setDim(node);
function draw_rectangle(nodes, current, init) {
for (var ii = 0; ii < nodes.length; ii++) {
var node = nodes[ii];
JX.$V(
(current.x-init.x < 0) ? current.x:init.x,
(current.y-init.y < 0) ? current.y:init.y)
.setPos(node);
JX.$V(
Math.abs(current.x-init.x),
Math.abs(current.y-init.y))
.setDim(node);
JX.$V(
(current.x-init.x < 0) ? current.x:init.x,
(current.y-init.y < 0) ? current.y:init.y)
.setPos(node);
}
}
function getRealXY(parent, point) {
var pos = {x: (point.x - parent.x), y: (point.y - parent.y)};
var dim = JX.Vector.getDim(image);
if (pos.x < 0) pos.x = 0;
else if (pos.x > image.clientWidth) pos.x = image.clientWidth - 1;
if (pos.y < 0) pos.y = 0;
else if (pos.y > image.clientHeight) pos.y = image.clientHeight - 2;
pos.x = Math.max(0, Math.min(pos.x, dim.x));
pos.y = Math.max(0, Math.min(pos.y, dim.y));
return pos;
}
@ -72,17 +77,18 @@ JX.behavior('pholio-mock-view', function(config) {
startPos = getRealXY(JX.$V(wrapper),JX.$V(e));
selection = JX.$N(
selection_border = JX.$N(
'div',
{className: 'pholio-mock-select'}
);
{className: 'pholio-mock-select-border'});
selection_fill = JX.$N(
'div',
{className: 'pholio-mock-select-fill'});
JX.$V(startPos.x,startPos.y).setPos(selection);
JX.DOM.appendContent(wrapper, selection);
JX.$V(startPos.x, startPos.y).setPos(selection_border);
JX.$V(startPos.x, startPos.y).setPos(selection_fill);
JX.DOM.appendContent(wrapper, [selection_border, selection_fill]);
});
JX.enableDispatch(document.body, 'mousemove');
@ -91,7 +97,10 @@ JX.behavior('pholio-mock-view', function(config) {
return;
}
draw_rectangle(selection, getRealXY(JX.$V(wrapper), JX.$V(e)), startPos);
draw_rectangle(
[selection_border, selection_fill],
getRealXY(JX.$V(wrapper),
JX.$V(e)), startPos);
});
JX.Stratcom.listen(
@ -107,17 +116,31 @@ JX.behavior('pholio-mock-view', function(config) {
comment = window.prompt("Add your comment");
if (comment == null || comment == "") {
selection.remove();
JX.DOM.remove(selection_border);
JX.DOM.remove(selection_fill);
return;
}
selection.title = comment;
selection_fill.title = comment;
console.log("ImageID: " + imageData['imageID'] +
", coords: (" + Math.min(startPos.x, endPos.x) + "," +
Math.min(startPos.y, endPos.y) + ") -> (" +
Math.max(startPos.x,endPos.x) + "," + Math.max(startPos.y,endPos.y) +
"), comment: " + comment);
var saveURL = "/pholio/inline/" + imageData['imageID'] + "/";
var inlineComment = new JX.Request(saveURL, function(r) {
});
var commentToAdd = {
mockID: config.mockID,
imageID: imageData['imageID'],
startX: Math.min(startPos.x, endPos.x),
startY: Math.min(startPos.y, endPos.y),
endX: Math.max(startPos.x,endPos.x),
endY: Math.max(startPos.y,endPos.y),
comment: comment};
inlineComment.addData(commentToAdd);
inlineComment.send();
});