Merge branch 'master' into phutil_tag
(Sync.)
This commit is contained in:
		| @@ -1015,17 +1015,6 @@ return array( | |||||||
|   // interact with the revisions. |   // interact with the revisions. | ||||||
|   'differential.anonymous-access' => false, |   '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 |   // 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. |   // an automatic process, and thus get hidden by default in differential. | ||||||
|   'differential.generated-paths' => array( |   'differential.generated-paths' => array( | ||||||
|   | |||||||
| @@ -7,12 +7,12 @@ | |||||||
|   ], |   ], | ||||||
|   "handlers" : [ |   "handlers" : [ | ||||||
|     "PhabricatorIRCProtocolHandler", |     "PhabricatorIRCProtocolHandler", | ||||||
|     "PhabricatorIRCObjectNameHandler", |     "PhabricatorBotObjectNameHandler", | ||||||
|     "PhabricatorIRCSymbolHandler", |     "PhabricatorBotSymbolHandler", | ||||||
|     "PhabricatorIRCLogHandler", |     "PhabricatorBotLogHandler", | ||||||
|     "PhabricatorIRCWhatsNewHandler", |     "PhabricatorBotWhatsNewHandler", | ||||||
|     "PhabricatorIRCDifferentialNotificationHandler", |     "PhabricatorBotDifferentialNotificationHandler", | ||||||
|     "PhabricatorIRCMacroHandler" |     "PhabricatorBotMacroHandler" | ||||||
|   ], |   ], | ||||||
|  |  | ||||||
|   "conduit.uri" : null, |   "conduit.uri" : null, | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								resources/sql/patches/20130131.conpherencepics.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								resources/sql/patches/20130131.conpherencepics.sql
									
									
									
									
									
										Normal 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 = ''; | ||||||
| @@ -646,7 +646,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'aphront-form-view-css' => |   'aphront-form-view-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/1e191b83/rsrc/css/aphront/form-view.css', |     'uri' => '/res/ec323d34/rsrc/css/aphront/form-view.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -737,7 +737,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'conpherence-header-pane-css' => |   'conpherence-header-pane-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/4b8aebd2/rsrc/css/application/conpherence/header-pane.css', |     'uri' => '/res/11c32adc/rsrc/css/application/conpherence/header-pane.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -764,7 +764,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'conpherence-update-css' => |   'conpherence-update-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/8e4757b5/rsrc/css/application/conpherence/update.css', |     'uri' => '/res/92094ed7/rsrc/css/application/conpherence/update.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -773,7 +773,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'conpherence-widget-pane-css' => |   'conpherence-widget-pane-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/b3e6a558/rsrc/css/application/conpherence/widget-pane.css', |     'uri' => '/res/6e5755bb/rsrc/css/application/conpherence/widget-pane.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1031,6 +1031,19 @@ celerity_register_resource_map(array( | |||||||
|     ), |     ), | ||||||
|     'disk' => '/rsrc/js/application/core/behavior-tokenizer.js', |     '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' => |   'javelin-behavior-aphront-drag-and-drop' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/3d809b40/rsrc/js/application/core/behavior-drag-and-drop.js', |     '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', |     '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' => |   'javelin-behavior-conpherence-init' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/bd911b43/rsrc/js/application/conpherence/behavior-init.js', |     'uri' => '/res/bd911b43/rsrc/js/application/conpherence/behavior-init.js', | ||||||
| @@ -1109,7 +1135,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-conpherence-menu' => |   'javelin-behavior-conpherence-menu' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/9d21fb86/rsrc/js/application/conpherence/behavior-menu.js', |     'uri' => '/res/986774a0/rsrc/js/application/conpherence/behavior-menu.js', | ||||||
|     'type' => 'js', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1124,7 +1150,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-conpherence-widget-pane' => |   'javelin-behavior-conpherence-widget-pane' => | ||||||
|   array( |   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', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1148,7 +1174,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-dark-console' => |   'javelin-behavior-dark-console' => | ||||||
|   array( |   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', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1269,7 +1295,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-differential-keyboard-navigation' => |   'javelin-behavior-differential-keyboard-navigation' => | ||||||
|   array( |   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', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1282,7 +1308,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-differential-populate' => |   'javelin-behavior-differential-populate' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/71effec4/rsrc/js/application/differential/behavior-populate.js', |     'uri' => '/res/526c2615/rsrc/js/application/differential/behavior-populate.js', | ||||||
|     'type' => 'js', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1292,7 +1318,8 @@ celerity_register_resource_map(array( | |||||||
|       3 => 'javelin-dom', |       3 => 'javelin-dom', | ||||||
|       4 => 'javelin-stratcom', |       4 => 'javelin-stratcom', | ||||||
|       5 => 'javelin-behavior-device', |       5 => 'javelin-behavior-device', | ||||||
|       6 => 'phabricator-tooltip', |       6 => 'javelin-vector', | ||||||
|  |       7 => 'phabricator-tooltip', | ||||||
|     ), |     ), | ||||||
|     'disk' => '/rsrc/js/application/differential/behavior-populate.js', |     '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', |     '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' => |   'javelin-behavior-phabricator-home-reveal-tiles' => | ||||||
|   array( |   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', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1680,7 +1719,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-phabricator-nav' => |   'javelin-behavior-phabricator-nav' => | ||||||
|   array( |   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', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -1827,12 +1866,16 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-behavior-pholio-mock-view' => |   'javelin-behavior-pholio-mock-view' => | ||||||
|   array( |   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', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
|       0 => 'javelin-behavior', |       0 => 'javelin-behavior', | ||||||
|       1 => 'javelin-stratcom', |       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', |     'disk' => '/rsrc/js/application/pholio/behavior-pholio-mock-view.js', | ||||||
|   ), |   ), | ||||||
| @@ -2015,7 +2058,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'javelin-event' => |   'javelin-event' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/3815b473/rsrc/js/javelin/core/Event.js', |     'uri' => '/res/e6582051/rsrc/js/javelin/core/Event.js', | ||||||
|     'type' => 'js', |     'type' => 'js', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -2671,7 +2714,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'phabricator-header-view-css' => |   'phabricator-header-view-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/88ef478c/rsrc/css/layout/phabricator-header-view.css', |     'uri' => '/res/585b771c/rsrc/css/layout/phabricator-header-view.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -3167,7 +3210,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'pholio-css' => |   'pholio-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/f101ad7c/rsrc/css/application/pholio/pholio.css', |     'uri' => '/res/9de6e0b2/rsrc/css/application/pholio/pholio.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -3293,7 +3336,7 @@ celerity_register_resource_map(array( | |||||||
|   ), |   ), | ||||||
|   'sprite-conpher-css' => |   'sprite-conpher-css' => | ||||||
|   array( |   array( | ||||||
|     'uri' => '/res/f640f0c5/rsrc/css/sprite-conph.css', |     'uri' => '/res/89821322/rsrc/css/sprite-conph.css', | ||||||
|     'type' => 'css', |     'type' => 'css', | ||||||
|     'requires' => |     'requires' => | ||||||
|     array( |     array( | ||||||
| @@ -3357,7 +3400,7 @@ celerity_register_resource_map(array( | |||||||
| ), array( | ), array( | ||||||
|   'packages' => |   'packages' => | ||||||
|   array( |   array( | ||||||
|     '0e6165d0' => |     '4b569463' => | ||||||
|     array( |     array( | ||||||
|       'name' => 'core.pkg.css', |       'name' => 'core.pkg.css', | ||||||
|       'symbols' => |       'symbols' => | ||||||
| @@ -3400,10 +3443,10 @@ celerity_register_resource_map(array( | |||||||
|         35 => 'phabricator-object-item-list-view-css', |         35 => 'phabricator-object-item-list-view-css', | ||||||
|         36 => 'global-drag-and-drop-css', |         36 => 'global-drag-and-drop-css', | ||||||
|       ), |       ), | ||||||
|       'uri' => '/res/pkg/0e6165d0/core.pkg.css', |       'uri' => '/res/pkg/4b569463/core.pkg.css', | ||||||
|       'type' => 'css', |       'type' => 'css', | ||||||
|     ), |     ), | ||||||
|     'ff199687' => |     'bc0774e5' => | ||||||
|     array( |     array( | ||||||
|       'name' => 'core.pkg.js', |       'name' => 'core.pkg.js', | ||||||
|       'symbols' => |       'symbols' => | ||||||
| @@ -3442,10 +3485,10 @@ celerity_register_resource_map(array( | |||||||
|         31 => 'javelin-behavior-global-drag-and-drop', |         31 => 'javelin-behavior-global-drag-and-drop', | ||||||
|         32 => 'javelin-behavior-phabricator-home-reveal-tiles', |         32 => 'javelin-behavior-phabricator-home-reveal-tiles', | ||||||
|       ), |       ), | ||||||
|       'uri' => '/res/pkg/ff199687/core.pkg.js', |       'uri' => '/res/pkg/bc0774e5/core.pkg.js', | ||||||
|       'type' => 'js', |       'type' => 'js', | ||||||
|     ), |     ), | ||||||
|     '74593df4' => |     '3e0098ea' => | ||||||
|     array( |     array( | ||||||
|       'name' => 'darkconsole.pkg.js', |       'name' => 'darkconsole.pkg.js', | ||||||
|       'symbols' => |       'symbols' => | ||||||
| @@ -3453,7 +3496,7 @@ celerity_register_resource_map(array( | |||||||
|         0 => 'javelin-behavior-dark-console', |         0 => 'javelin-behavior-dark-console', | ||||||
|         1 => 'javelin-behavior-error-log', |         1 => 'javelin-behavior-error-log', | ||||||
|       ), |       ), | ||||||
|       'uri' => '/res/pkg/74593df4/darkconsole.pkg.js', |       'uri' => '/res/pkg/3e0098ea/darkconsole.pkg.js', | ||||||
|       'type' => 'js', |       'type' => 'js', | ||||||
|     ), |     ), | ||||||
|     '8aaacd1b' => |     '8aaacd1b' => | ||||||
| @@ -3478,7 +3521,7 @@ celerity_register_resource_map(array( | |||||||
|       'uri' => '/res/pkg/8aaacd1b/differential.pkg.css', |       'uri' => '/res/pkg/8aaacd1b/differential.pkg.css', | ||||||
|       'type' => 'css', |       'type' => 'css', | ||||||
|     ), |     ), | ||||||
|     '9dae5f20' => |     '95d0d865' => | ||||||
|     array( |     array( | ||||||
|       'name' => 'differential.pkg.js', |       'name' => 'differential.pkg.js', | ||||||
|       'symbols' => |       'symbols' => | ||||||
| @@ -3503,7 +3546,7 @@ celerity_register_resource_map(array( | |||||||
|         17 => 'javelin-behavior-differential-toggle-files', |         17 => 'javelin-behavior-differential-toggle-files', | ||||||
|         18 => 'javelin-behavior-differential-user-select', |         18 => 'javelin-behavior-differential-user-select', | ||||||
|       ), |       ), | ||||||
|       'uri' => '/res/pkg/9dae5f20/differential.pkg.js', |       'uri' => '/res/pkg/95d0d865/differential.pkg.js', | ||||||
|       'type' => 'js', |       'type' => 'js', | ||||||
|     ), |     ), | ||||||
|     'c8ce2d88' => |     'c8ce2d88' => | ||||||
| @@ -3529,7 +3572,7 @@ celerity_register_resource_map(array( | |||||||
|       'uri' => '/res/pkg/f96657b8/diffusion.pkg.js', |       'uri' => '/res/pkg/f96657b8/diffusion.pkg.js', | ||||||
|       'type' => 'js', |       'type' => 'js', | ||||||
|     ), |     ), | ||||||
|     '1c6f020b' => |     'd466c034' => | ||||||
|     array( |     array( | ||||||
|       'name' => 'javelin.pkg.js', |       'name' => 'javelin.pkg.js', | ||||||
|       'symbols' => |       'symbols' => | ||||||
| @@ -3554,7 +3597,7 @@ celerity_register_resource_map(array( | |||||||
|         17 => 'javelin-typeahead-ondemand-source', |         17 => 'javelin-typeahead-ondemand-source', | ||||||
|         18 => 'javelin-tokenizer', |         18 => 'javelin-tokenizer', | ||||||
|       ), |       ), | ||||||
|       'uri' => '/res/pkg/1c6f020b/javelin.pkg.js', |       'uri' => '/res/pkg/d466c034/javelin.pkg.js', | ||||||
|       'type' => 'js', |       'type' => 'js', | ||||||
|     ), |     ), | ||||||
|     'e30a3fa8' => |     'e30a3fa8' => | ||||||
| @@ -3588,20 +3631,20 @@ celerity_register_resource_map(array( | |||||||
|   'reverse' => |   'reverse' => | ||||||
|   array( |   array( | ||||||
|     'aphront-attached-file-view-css' => 'e30a3fa8', |     'aphront-attached-file-view-css' => 'e30a3fa8', | ||||||
|     'aphront-crumbs-view-css' => '0e6165d0', |     'aphront-crumbs-view-css' => '4b569463', | ||||||
|     'aphront-dialog-view-css' => '0e6165d0', |     'aphront-dialog-view-css' => '4b569463', | ||||||
|     'aphront-error-view-css' => '0e6165d0', |     'aphront-error-view-css' => '4b569463', | ||||||
|     'aphront-form-view-css' => '0e6165d0', |     'aphront-form-view-css' => '4b569463', | ||||||
|     'aphront-list-filter-view-css' => '0e6165d0', |     'aphront-list-filter-view-css' => '4b569463', | ||||||
|     'aphront-pager-view-css' => '0e6165d0', |     'aphront-pager-view-css' => '4b569463', | ||||||
|     'aphront-panel-view-css' => '0e6165d0', |     'aphront-panel-view-css' => '4b569463', | ||||||
|     'aphront-table-view-css' => '0e6165d0', |     'aphront-table-view-css' => '4b569463', | ||||||
|     'aphront-tokenizer-control-css' => '0e6165d0', |     'aphront-tokenizer-control-css' => '4b569463', | ||||||
|     'aphront-tooltip-css' => '0e6165d0', |     'aphront-tooltip-css' => '4b569463', | ||||||
|     'aphront-typeahead-control-css' => '0e6165d0', |     'aphront-typeahead-control-css' => '4b569463', | ||||||
|     'differential-changeset-view-css' => '8aaacd1b', |     'differential-changeset-view-css' => '8aaacd1b', | ||||||
|     'differential-core-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-local-commits-view-css' => '8aaacd1b', | ||||||
|     'differential-results-table-css' => '8aaacd1b', |     'differential-results-table-css' => '8aaacd1b', | ||||||
|     'differential-revision-add-comment-css' => '8aaacd1b', |     'differential-revision-add-comment-css' => '8aaacd1b', | ||||||
| @@ -3612,117 +3655,117 @@ celerity_register_resource_map(array( | |||||||
|     'differential-table-of-contents-css' => '8aaacd1b', |     'differential-table-of-contents-css' => '8aaacd1b', | ||||||
|     'diffusion-commit-view-css' => 'c8ce2d88', |     'diffusion-commit-view-css' => 'c8ce2d88', | ||||||
|     'diffusion-icons-css' => 'c8ce2d88', |     'diffusion-icons-css' => 'c8ce2d88', | ||||||
|     'global-drag-and-drop-css' => '0e6165d0', |     'global-drag-and-drop-css' => '4b569463', | ||||||
|     'inline-comment-summary-css' => '8aaacd1b', |     'inline-comment-summary-css' => '8aaacd1b', | ||||||
|     'javelin-aphlict' => 'ff199687', |     'javelin-aphlict' => 'bc0774e5', | ||||||
|     'javelin-behavior' => '1c6f020b', |     'javelin-behavior' => 'd466c034', | ||||||
|     'javelin-behavior-aphlict-dropdown' => 'ff199687', |     'javelin-behavior-aphlict-dropdown' => 'bc0774e5', | ||||||
|     'javelin-behavior-aphlict-listen' => 'ff199687', |     'javelin-behavior-aphlict-listen' => 'bc0774e5', | ||||||
|     'javelin-behavior-aphront-basic-tokenizer' => 'ff199687', |     'javelin-behavior-aphront-basic-tokenizer' => 'bc0774e5', | ||||||
|     'javelin-behavior-aphront-drag-and-drop' => '9dae5f20', |     'javelin-behavior-aphront-drag-and-drop' => '95d0d865', | ||||||
|     'javelin-behavior-aphront-drag-and-drop-textarea' => '9dae5f20', |     'javelin-behavior-aphront-drag-and-drop-textarea' => '95d0d865', | ||||||
|     'javelin-behavior-aphront-form-disable-on-submit' => 'ff199687', |     'javelin-behavior-aphront-form-disable-on-submit' => 'bc0774e5', | ||||||
|     'javelin-behavior-audit-preview' => 'f96657b8', |     'javelin-behavior-audit-preview' => 'f96657b8', | ||||||
|     'javelin-behavior-dark-console' => '74593df4', |     'javelin-behavior-dark-console' => '3e0098ea', | ||||||
|     'javelin-behavior-device' => 'ff199687', |     'javelin-behavior-device' => 'bc0774e5', | ||||||
|     'javelin-behavior-differential-accept-with-errors' => '9dae5f20', |     'javelin-behavior-differential-accept-with-errors' => '95d0d865', | ||||||
|     'javelin-behavior-differential-add-reviewers-and-ccs' => '9dae5f20', |     'javelin-behavior-differential-add-reviewers-and-ccs' => '95d0d865', | ||||||
|     'javelin-behavior-differential-comment-jump' => '9dae5f20', |     'javelin-behavior-differential-comment-jump' => '95d0d865', | ||||||
|     'javelin-behavior-differential-diff-radios' => '9dae5f20', |     'javelin-behavior-differential-diff-radios' => '95d0d865', | ||||||
|     'javelin-behavior-differential-dropdown-menus' => '9dae5f20', |     'javelin-behavior-differential-dropdown-menus' => '95d0d865', | ||||||
|     'javelin-behavior-differential-edit-inline-comments' => '9dae5f20', |     'javelin-behavior-differential-edit-inline-comments' => '95d0d865', | ||||||
|     'javelin-behavior-differential-feedback-preview' => '9dae5f20', |     'javelin-behavior-differential-feedback-preview' => '95d0d865', | ||||||
|     'javelin-behavior-differential-keyboard-navigation' => '9dae5f20', |     'javelin-behavior-differential-keyboard-navigation' => '95d0d865', | ||||||
|     'javelin-behavior-differential-populate' => '9dae5f20', |     'javelin-behavior-differential-populate' => '95d0d865', | ||||||
|     'javelin-behavior-differential-show-more' => '9dae5f20', |     'javelin-behavior-differential-show-more' => '95d0d865', | ||||||
|     'javelin-behavior-differential-toggle-files' => '9dae5f20', |     'javelin-behavior-differential-toggle-files' => '95d0d865', | ||||||
|     'javelin-behavior-differential-user-select' => '9dae5f20', |     'javelin-behavior-differential-user-select' => '95d0d865', | ||||||
|     'javelin-behavior-diffusion-commit-graph' => 'f96657b8', |     'javelin-behavior-diffusion-commit-graph' => 'f96657b8', | ||||||
|     'javelin-behavior-diffusion-pull-lastmodified' => 'f96657b8', |     'javelin-behavior-diffusion-pull-lastmodified' => 'f96657b8', | ||||||
|     'javelin-behavior-error-log' => '74593df4', |     'javelin-behavior-error-log' => '3e0098ea', | ||||||
|     'javelin-behavior-global-drag-and-drop' => 'ff199687', |     'javelin-behavior-global-drag-and-drop' => 'bc0774e5', | ||||||
|     'javelin-behavior-konami' => 'ff199687', |     'javelin-behavior-konami' => 'bc0774e5', | ||||||
|     'javelin-behavior-lightbox-attachments' => 'ff199687', |     'javelin-behavior-lightbox-attachments' => 'bc0774e5', | ||||||
|     'javelin-behavior-maniphest-batch-selector' => '7707de41', |     'javelin-behavior-maniphest-batch-selector' => '7707de41', | ||||||
|     'javelin-behavior-maniphest-subpriority-editor' => '7707de41', |     'javelin-behavior-maniphest-subpriority-editor' => '7707de41', | ||||||
|     'javelin-behavior-maniphest-transaction-controls' => '7707de41', |     'javelin-behavior-maniphest-transaction-controls' => '7707de41', | ||||||
|     'javelin-behavior-maniphest-transaction-expand' => '7707de41', |     'javelin-behavior-maniphest-transaction-expand' => '7707de41', | ||||||
|     'javelin-behavior-maniphest-transaction-preview' => '7707de41', |     'javelin-behavior-maniphest-transaction-preview' => '7707de41', | ||||||
|     'javelin-behavior-phabricator-active-nav' => 'ff199687', |     'javelin-behavior-phabricator-active-nav' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-autofocus' => 'ff199687', |     'javelin-behavior-phabricator-autofocus' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-home-reveal-tiles' => 'ff199687', |     'javelin-behavior-phabricator-home-reveal-tiles' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-keyboard-shortcuts' => 'ff199687', |     'javelin-behavior-phabricator-keyboard-shortcuts' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-nav' => 'ff199687', |     'javelin-behavior-phabricator-nav' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-object-selector' => '9dae5f20', |     'javelin-behavior-phabricator-object-selector' => '95d0d865', | ||||||
|     'javelin-behavior-phabricator-oncopy' => 'ff199687', |     'javelin-behavior-phabricator-oncopy' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-remarkup-assist' => 'ff199687', |     'javelin-behavior-phabricator-remarkup-assist' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-search-typeahead' => 'ff199687', |     'javelin-behavior-phabricator-search-typeahead' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-tooltips' => 'ff199687', |     'javelin-behavior-phabricator-tooltips' => 'bc0774e5', | ||||||
|     'javelin-behavior-phabricator-watch-anchor' => 'ff199687', |     'javelin-behavior-phabricator-watch-anchor' => 'bc0774e5', | ||||||
|     'javelin-behavior-refresh-csrf' => 'ff199687', |     'javelin-behavior-refresh-csrf' => 'bc0774e5', | ||||||
|     'javelin-behavior-repository-crossreference' => '9dae5f20', |     'javelin-behavior-repository-crossreference' => '95d0d865', | ||||||
|     'javelin-behavior-toggle-class' => 'ff199687', |     'javelin-behavior-toggle-class' => 'bc0774e5', | ||||||
|     'javelin-behavior-workflow' => 'ff199687', |     'javelin-behavior-workflow' => 'bc0774e5', | ||||||
|     'javelin-dom' => '1c6f020b', |     'javelin-dom' => 'd466c034', | ||||||
|     'javelin-event' => '1c6f020b', |     'javelin-event' => 'd466c034', | ||||||
|     'javelin-install' => '1c6f020b', |     'javelin-install' => 'd466c034', | ||||||
|     'javelin-json' => '1c6f020b', |     'javelin-json' => 'd466c034', | ||||||
|     'javelin-mask' => '1c6f020b', |     'javelin-mask' => 'd466c034', | ||||||
|     'javelin-request' => '1c6f020b', |     'javelin-request' => 'd466c034', | ||||||
|     'javelin-resource' => '1c6f020b', |     'javelin-resource' => 'd466c034', | ||||||
|     'javelin-stratcom' => '1c6f020b', |     'javelin-stratcom' => 'd466c034', | ||||||
|     'javelin-tokenizer' => '1c6f020b', |     'javelin-tokenizer' => 'd466c034', | ||||||
|     'javelin-typeahead' => '1c6f020b', |     'javelin-typeahead' => 'd466c034', | ||||||
|     'javelin-typeahead-normalizer' => '1c6f020b', |     'javelin-typeahead-normalizer' => 'd466c034', | ||||||
|     'javelin-typeahead-ondemand-source' => '1c6f020b', |     'javelin-typeahead-ondemand-source' => 'd466c034', | ||||||
|     'javelin-typeahead-preloaded-source' => '1c6f020b', |     'javelin-typeahead-preloaded-source' => 'd466c034', | ||||||
|     'javelin-typeahead-source' => '1c6f020b', |     'javelin-typeahead-source' => 'd466c034', | ||||||
|     'javelin-uri' => '1c6f020b', |     'javelin-uri' => 'd466c034', | ||||||
|     'javelin-util' => '1c6f020b', |     'javelin-util' => 'd466c034', | ||||||
|     'javelin-vector' => '1c6f020b', |     'javelin-vector' => 'd466c034', | ||||||
|     'javelin-workflow' => '1c6f020b', |     'javelin-workflow' => 'd466c034', | ||||||
|     'lightbox-attachment-css' => '0e6165d0', |     'lightbox-attachment-css' => '4b569463', | ||||||
|     'maniphest-task-summary-css' => 'e30a3fa8', |     'maniphest-task-summary-css' => 'e30a3fa8', | ||||||
|     'maniphest-transaction-detail-css' => 'e30a3fa8', |     'maniphest-transaction-detail-css' => 'e30a3fa8', | ||||||
|     'phabricator-busy' => 'ff199687', |     'phabricator-busy' => 'bc0774e5', | ||||||
|     'phabricator-content-source-view-css' => '8aaacd1b', |     'phabricator-content-source-view-css' => '8aaacd1b', | ||||||
|     'phabricator-core-buttons-css' => '0e6165d0', |     'phabricator-core-buttons-css' => '4b569463', | ||||||
|     'phabricator-core-css' => '0e6165d0', |     'phabricator-core-css' => '4b569463', | ||||||
|     'phabricator-crumbs-view-css' => '0e6165d0', |     'phabricator-crumbs-view-css' => '4b569463', | ||||||
|     'phabricator-directory-css' => '0e6165d0', |     'phabricator-directory-css' => '4b569463', | ||||||
|     'phabricator-drag-and-drop-file-upload' => '9dae5f20', |     'phabricator-drag-and-drop-file-upload' => '95d0d865', | ||||||
|     'phabricator-dropdown-menu' => 'ff199687', |     'phabricator-dropdown-menu' => 'bc0774e5', | ||||||
|     'phabricator-file-upload' => 'ff199687', |     'phabricator-file-upload' => 'bc0774e5', | ||||||
|     'phabricator-filetree-view-css' => '0e6165d0', |     'phabricator-filetree-view-css' => '4b569463', | ||||||
|     'phabricator-flag-css' => '0e6165d0', |     'phabricator-flag-css' => '4b569463', | ||||||
|     'phabricator-form-view-css' => '0e6165d0', |     'phabricator-form-view-css' => '4b569463', | ||||||
|     'phabricator-header-view-css' => '0e6165d0', |     'phabricator-header-view-css' => '4b569463', | ||||||
|     'phabricator-jump-nav' => '0e6165d0', |     'phabricator-jump-nav' => '4b569463', | ||||||
|     'phabricator-keyboard-shortcut' => 'ff199687', |     'phabricator-keyboard-shortcut' => 'bc0774e5', | ||||||
|     'phabricator-keyboard-shortcut-manager' => 'ff199687', |     'phabricator-keyboard-shortcut-manager' => 'bc0774e5', | ||||||
|     'phabricator-main-menu-view' => '0e6165d0', |     'phabricator-main-menu-view' => '4b569463', | ||||||
|     'phabricator-menu-item' => 'ff199687', |     'phabricator-menu-item' => 'bc0774e5', | ||||||
|     'phabricator-nav-view-css' => '0e6165d0', |     'phabricator-nav-view-css' => '4b569463', | ||||||
|     'phabricator-notification' => 'ff199687', |     'phabricator-notification' => 'bc0774e5', | ||||||
|     'phabricator-notification-css' => '0e6165d0', |     'phabricator-notification-css' => '4b569463', | ||||||
|     'phabricator-notification-menu-css' => '0e6165d0', |     'phabricator-notification-menu-css' => '4b569463', | ||||||
|     'phabricator-object-item-list-view-css' => '0e6165d0', |     'phabricator-object-item-list-view-css' => '4b569463', | ||||||
|     'phabricator-object-selector-css' => '8aaacd1b', |     'phabricator-object-selector-css' => '8aaacd1b', | ||||||
|     'phabricator-paste-file-upload' => 'ff199687', |     'phabricator-paste-file-upload' => 'bc0774e5', | ||||||
|     'phabricator-prefab' => 'ff199687', |     'phabricator-prefab' => 'bc0774e5', | ||||||
|     'phabricator-project-tag-css' => 'e30a3fa8', |     'phabricator-project-tag-css' => 'e30a3fa8', | ||||||
|     'phabricator-remarkup-css' => '0e6165d0', |     'phabricator-remarkup-css' => '4b569463', | ||||||
|     'phabricator-shaped-request' => '9dae5f20', |     'phabricator-shaped-request' => '95d0d865', | ||||||
|     'phabricator-side-menu-view-css' => '0e6165d0', |     'phabricator-side-menu-view-css' => '4b569463', | ||||||
|     'phabricator-standard-page-view' => '0e6165d0', |     'phabricator-standard-page-view' => '4b569463', | ||||||
|     'phabricator-textareautils' => 'ff199687', |     'phabricator-textareautils' => 'bc0774e5', | ||||||
|     'phabricator-tooltip' => 'ff199687', |     'phabricator-tooltip' => 'bc0774e5', | ||||||
|     'phabricator-transaction-view-css' => '0e6165d0', |     'phabricator-transaction-view-css' => '4b569463', | ||||||
|     'phabricator-zindex-css' => '0e6165d0', |     'phabricator-zindex-css' => '4b569463', | ||||||
|     'sprite-apps-large-css' => '0e6165d0', |     'sprite-apps-large-css' => '4b569463', | ||||||
|     'sprite-gradient-css' => '0e6165d0', |     'sprite-gradient-css' => '4b569463', | ||||||
|     'sprite-icon-css' => '0e6165d0', |     'sprite-icon-css' => '4b569463', | ||||||
|     'sprite-menu-css' => '0e6165d0', |     'sprite-menu-css' => '4b569463', | ||||||
|     'syntax-highlighting-css' => '0e6165d0', |     'syntax-highlighting-css' => '4b569463', | ||||||
|   ), |   ), | ||||||
| )); | )); | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ phutil_register_library_map(array( | |||||||
|     'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', |     'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', | ||||||
|     'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', |     'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', | ||||||
|     'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', |     'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', | ||||||
|  |     'AphrontFormCropControl' => 'view/form/control/AphrontFormCropControl.php', | ||||||
|     'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php', |     'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php', | ||||||
|     'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php', |     'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php', | ||||||
|     'AphrontFormDragAndDropUploadControl' => 'view/form/control/AphrontFormDragAndDropUploadControl.php', |     'AphrontFormDragAndDropUploadControl' => 'view/form/control/AphrontFormDragAndDropUploadControl.php', | ||||||
| @@ -87,6 +88,7 @@ phutil_register_library_map(array( | |||||||
|     'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php', |     'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php', | ||||||
|     'AphrontView' => 'view/AphrontView.php', |     'AphrontView' => 'view/AphrontView.php', | ||||||
|     'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', |     'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', | ||||||
|  |     'AuditPeopleMenuEventListener' => 'applications/audit/events/AuditPeopleMenuEventListener.php', | ||||||
|     'CelerityAPI' => 'infrastructure/celerity/CelerityAPI.php', |     'CelerityAPI' => 'infrastructure/celerity/CelerityAPI.php', | ||||||
|     'CelerityPhabricatorResourceController' => 'infrastructure/celerity/CelerityPhabricatorResourceController.php', |     'CelerityPhabricatorResourceController' => 'infrastructure/celerity/CelerityPhabricatorResourceController.php', | ||||||
|     'CelerityResourceController' => 'infrastructure/celerity/CelerityResourceController.php', |     'CelerityResourceController' => 'infrastructure/celerity/CelerityResourceController.php', | ||||||
| @@ -197,12 +199,15 @@ phutil_register_library_map(array( | |||||||
|     'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', |     'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php', | ||||||
|     'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', |     'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', | ||||||
|     'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.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', |     'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php', | ||||||
|     'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', |     'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', | ||||||
|     'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', |     'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', | ||||||
|     'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', |     'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', | ||||||
|     'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', |     'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', | ||||||
|     'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', |     'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', | ||||||
|  |     'ConpherencePeopleMenuEventListener' => 'applications/conpherence/events/ConpherencePeopleMenuEventListener.php', | ||||||
|     'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php', |     'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php', | ||||||
|     'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php', |     'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php', | ||||||
|     'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', |     'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', | ||||||
| @@ -307,6 +312,7 @@ phutil_register_library_map(array( | |||||||
|     'DifferentialNewDiffMail' => 'applications/differential/mail/DifferentialNewDiffMail.php', |     'DifferentialNewDiffMail' => 'applications/differential/mail/DifferentialNewDiffMail.php', | ||||||
|     'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php', |     'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php', | ||||||
|     'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php', |     'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php', | ||||||
|  |     'DifferentialPeopleMenuEventListener' => 'applications/differential/events/DifferentialPeopleMenuEventListener.php', | ||||||
|     'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php', |     'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php', | ||||||
|     'DifferentialReplyHandler' => 'applications/differential/DifferentialReplyHandler.php', |     'DifferentialReplyHandler' => 'applications/differential/DifferentialReplyHandler.php', | ||||||
|     'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php', |     'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php', | ||||||
| @@ -420,6 +426,7 @@ phutil_register_library_map(array( | |||||||
|     'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php', |     'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php', | ||||||
|     'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php', |     'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php', | ||||||
|     'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php', |     'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php', | ||||||
|  |     'DiffusionPeopleMenuEventListener' => 'applications/diffusion/events/DiffusionPeopleMenuEventListener.php', | ||||||
|     'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php', |     'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php', | ||||||
|     'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php', |     'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php', | ||||||
|     'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', |     'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', | ||||||
| @@ -563,6 +570,7 @@ phutil_register_library_map(array( | |||||||
|     'ManiphestDefaultTaskExtensions' => 'applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php', |     'ManiphestDefaultTaskExtensions' => 'applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php', | ||||||
|     'ManiphestEdgeEventListener' => 'applications/maniphest/event/ManiphestEdgeEventListener.php', |     'ManiphestEdgeEventListener' => 'applications/maniphest/event/ManiphestEdgeEventListener.php', | ||||||
|     'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php', |     'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php', | ||||||
|  |     'ManiphestPeopleMenuEventListener' => 'applications/maniphest/event/ManiphestPeopleMenuEventListener.php', | ||||||
|     'ManiphestReplyHandler' => 'applications/maniphest/ManiphestReplyHandler.php', |     'ManiphestReplyHandler' => 'applications/maniphest/ManiphestReplyHandler.php', | ||||||
|     'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php', |     'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php', | ||||||
|     'ManiphestSavedQuery' => 'applications/maniphest/storage/ManiphestSavedQuery.php', |     'ManiphestSavedQuery' => 'applications/maniphest/storage/ManiphestSavedQuery.php', | ||||||
| @@ -695,6 +703,18 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php', |     'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php', | ||||||
|     'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php', |     'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php', | ||||||
|     'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/PhabricatorBaseEnglishTranslation.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', |     'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', | ||||||
|     'PhabricatorButtonsExample' => 'applications/uiexample/examples/PhabricatorButtonsExample.php', |     'PhabricatorButtonsExample' => 'applications/uiexample/examples/PhabricatorButtonsExample.php', | ||||||
|     'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', |     'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', | ||||||
| @@ -707,6 +727,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', |     'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', | ||||||
|     'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', |     'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', | ||||||
|     'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php', |     'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php', | ||||||
|  |     'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php', | ||||||
|     'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', |     'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', | ||||||
|     'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', |     'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', | ||||||
|     'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', |     'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', | ||||||
| @@ -877,6 +898,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php', |     'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php', | ||||||
|     'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php', |     'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php', | ||||||
|     'PhabricatorFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorFileStorageEngineSelector.php', |     'PhabricatorFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorFileStorageEngineSelector.php', | ||||||
|  |     'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php', | ||||||
|     'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php', |     'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php', | ||||||
|     'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php', |     'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php', | ||||||
|     'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', |     'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', | ||||||
| @@ -910,17 +932,9 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php', |     'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php', | ||||||
|     'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', |     'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', | ||||||
|     'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', |     'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', | ||||||
|     'PhabricatorIRCBot' => 'infrastructure/daemon/irc/PhabricatorIRCBot.php', |     'PhabricatorIRCBot' => 'infrastructure/daemon/bot/PhabricatorIRCBot.php', | ||||||
|     'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php', |     'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php', | ||||||
|     'PhabricatorIRCFeedNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCFeedNotificationHandler.php', |     'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.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', |  | ||||||
|     'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', |     'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', | ||||||
|     'PhabricatorInfrastructureTestCase' => 'infrastructure/__tests__/PhabricatorInfrastructureTestCase.php', |     'PhabricatorInfrastructureTestCase' => 'infrastructure/__tests__/PhabricatorInfrastructureTestCase.php', | ||||||
|     'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', |     'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', | ||||||
| @@ -1291,6 +1305,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorTagView' => 'view/layout/PhabricatorTagView.php', |     'PhabricatorTagView' => 'view/layout/PhabricatorTagView.php', | ||||||
|     'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', |     'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', | ||||||
|     'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', |     'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', | ||||||
|  |     'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php', | ||||||
|     'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php', |     'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php', | ||||||
|     'PhabricatorTimelineCursor' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php', |     'PhabricatorTimelineCursor' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php', | ||||||
|     'PhabricatorTimelineDAO' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php', |     'PhabricatorTimelineDAO' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php', | ||||||
| @@ -1394,6 +1409,7 @@ phutil_register_library_map(array( | |||||||
|     'PholioController' => 'applications/pholio/controller/PholioController.php', |     'PholioController' => 'applications/pholio/controller/PholioController.php', | ||||||
|     'PholioDAO' => 'applications/pholio/storage/PholioDAO.php', |     'PholioDAO' => 'applications/pholio/storage/PholioDAO.php', | ||||||
|     'PholioImage' => 'applications/pholio/storage/PholioImage.php', |     'PholioImage' => 'applications/pholio/storage/PholioImage.php', | ||||||
|  |     'PholioInlineSaveController' => 'applications/pholio/controller/PholioInlineSaveController.php', | ||||||
|     'PholioMock' => 'applications/pholio/storage/PholioMock.php', |     'PholioMock' => 'applications/pholio/storage/PholioMock.php', | ||||||
|     'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php', |     'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php', | ||||||
|     'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php', |     'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php', | ||||||
| @@ -1521,6 +1537,7 @@ phutil_register_library_map(array( | |||||||
|     'AphrontFileResponse' => 'AphrontResponse', |     'AphrontFileResponse' => 'AphrontResponse', | ||||||
|     'AphrontFormCheckboxControl' => 'AphrontFormControl', |     'AphrontFormCheckboxControl' => 'AphrontFormControl', | ||||||
|     'AphrontFormControl' => 'AphrontView', |     'AphrontFormControl' => 'AphrontView', | ||||||
|  |     'AphrontFormCropControl' => 'AphrontFormControl', | ||||||
|     'AphrontFormDateControl' => 'AphrontFormControl', |     'AphrontFormDateControl' => 'AphrontFormControl', | ||||||
|     'AphrontFormDividerControl' => 'AphrontFormControl', |     'AphrontFormDividerControl' => 'AphrontFormControl', | ||||||
|     'AphrontFormDragAndDropUploadControl' => 'AphrontFormControl', |     'AphrontFormDragAndDropUploadControl' => 'AphrontFormControl', | ||||||
| @@ -1572,6 +1589,7 @@ phutil_register_library_map(array( | |||||||
|     'AphrontUsageException' => 'AphrontException', |     'AphrontUsageException' => 'AphrontException', | ||||||
|     'AphrontView' => 'Phobject', |     'AphrontView' => 'Phobject', | ||||||
|     'AphrontWebpageResponse' => 'AphrontHTMLResponse', |     'AphrontWebpageResponse' => 'AphrontHTMLResponse', | ||||||
|  |     'AuditPeopleMenuEventListener' => 'PhutilEventListener', | ||||||
|     'CelerityPhabricatorResourceController' => 'CelerityResourceController', |     'CelerityPhabricatorResourceController' => 'CelerityResourceController', | ||||||
|     'CelerityResourceController' => 'PhabricatorController', |     'CelerityResourceController' => 'PhabricatorController', | ||||||
|     'CelerityResourceGraph' => 'AbstractDirectedGraph', |     'CelerityResourceGraph' => 'AbstractDirectedGraph', | ||||||
| @@ -1672,12 +1690,15 @@ phutil_register_library_map(array( | |||||||
|     'ConpherenceController' => 'PhabricatorController', |     'ConpherenceController' => 'PhabricatorController', | ||||||
|     'ConpherenceDAO' => 'PhabricatorLiskDAO', |     'ConpherenceDAO' => 'PhabricatorLiskDAO', | ||||||
|     'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', |     'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', | ||||||
|  |     'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl', | ||||||
|  |     'ConpherenceImageData' => 'ConpherenceConstants', | ||||||
|     'ConpherenceListController' => 'ConpherenceController', |     'ConpherenceListController' => 'ConpherenceController', | ||||||
|     'ConpherenceMenuItemView' => 'AphrontTagView', |     'ConpherenceMenuItemView' => 'AphrontTagView', | ||||||
|     'ConpherenceNewController' => 'ConpherenceController', |     'ConpherenceNewController' => 'ConpherenceController', | ||||||
|     'ConpherenceParticipant' => 'ConpherenceDAO', |     'ConpherenceParticipant' => 'ConpherenceDAO', | ||||||
|     'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', |     'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', | ||||||
|     'ConpherenceParticipationStatus' => 'ConpherenceConstants', |     'ConpherenceParticipationStatus' => 'ConpherenceConstants', | ||||||
|  |     'ConpherencePeopleMenuEventListener' => 'PhutilEventListener', | ||||||
|     'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler', |     'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler', | ||||||
|     'ConpherenceThread' => |     'ConpherenceThread' => | ||||||
|     array( |     array( | ||||||
| @@ -1778,6 +1799,7 @@ phutil_register_library_map(array( | |||||||
|     'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', |     'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', | ||||||
|     'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', |     'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', | ||||||
|     'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification', |     'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification', | ||||||
|  |     'DifferentialPeopleMenuEventListener' => 'PhutilEventListener', | ||||||
|     'DifferentialPrimaryPaneView' => 'AphrontView', |     'DifferentialPrimaryPaneView' => 'AphrontView', | ||||||
|     'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler', |     'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler', | ||||||
|     'DifferentialResultsTableView' => 'AphrontView', |     'DifferentialResultsTableView' => 'AphrontView', | ||||||
| @@ -1873,6 +1895,7 @@ phutil_register_library_map(array( | |||||||
|     'DiffusionPathCompleteController' => 'DiffusionController', |     'DiffusionPathCompleteController' => 'DiffusionController', | ||||||
|     'DiffusionPathQueryTestCase' => 'PhabricatorTestCase', |     'DiffusionPathQueryTestCase' => 'PhabricatorTestCase', | ||||||
|     'DiffusionPathValidateController' => 'DiffusionController', |     'DiffusionPathValidateController' => 'DiffusionController', | ||||||
|  |     'DiffusionPeopleMenuEventListener' => 'PhutilEventListener', | ||||||
|     'DiffusionRawDiffQuery' => 'DiffusionQuery', |     'DiffusionRawDiffQuery' => 'DiffusionQuery', | ||||||
|     'DiffusionRepositoryController' => 'DiffusionController', |     'DiffusionRepositoryController' => 'DiffusionController', | ||||||
|     'DiffusionSetupException' => 'AphrontUsageException', |     'DiffusionSetupException' => 'AphrontUsageException', | ||||||
| @@ -1985,6 +2008,7 @@ phutil_register_library_map(array( | |||||||
|     'ManiphestDefaultTaskExtensions' => 'ManiphestTaskExtensions', |     'ManiphestDefaultTaskExtensions' => 'ManiphestTaskExtensions', | ||||||
|     'ManiphestEdgeEventListener' => 'PhutilEventListener', |     'ManiphestEdgeEventListener' => 'PhutilEventListener', | ||||||
|     'ManiphestExportController' => 'ManiphestController', |     'ManiphestExportController' => 'ManiphestController', | ||||||
|  |     'ManiphestPeopleMenuEventListener' => 'PhutilEventListener', | ||||||
|     'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler', |     'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler', | ||||||
|     'ManiphestReportController' => 'ManiphestController', |     'ManiphestReportController' => 'ManiphestController', | ||||||
|     'ManiphestSavedQuery' => 'ManiphestDAO', |     'ManiphestSavedQuery' => 'ManiphestDAO', | ||||||
| @@ -2131,6 +2155,15 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorBarePageExample' => 'PhabricatorUIExample', |     'PhabricatorBarePageExample' => 'PhabricatorUIExample', | ||||||
|     'PhabricatorBarePageView' => 'AphrontPageView', |     'PhabricatorBarePageView' => 'AphrontPageView', | ||||||
|     'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation', |     'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation', | ||||||
|  |     'PhabricatorBot' => 'PhabricatorDaemon', | ||||||
|  |     'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotDifferentialNotificationHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotLogHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler', | ||||||
|  |     'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler', | ||||||
|     'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', |     'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', | ||||||
|     'PhabricatorButtonsExample' => 'PhabricatorUIExample', |     'PhabricatorButtonsExample' => 'PhabricatorUIExample', | ||||||
|     'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', |     'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', | ||||||
| @@ -2142,6 +2175,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', |     'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', | ||||||
|     'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', |     'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController', |     'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController', | ||||||
|  |     'PhabricatorCampfireProtocolAdapter' => 'PhabricatorBaseProtocolAdapter', | ||||||
|     'PhabricatorChangesetResponse' => 'AphrontProxyResponse', |     'PhabricatorChangesetResponse' => 'AphrontProxyResponse', | ||||||
|     'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', |     'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', | ||||||
|     'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', |     'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', | ||||||
| @@ -2311,6 +2345,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorFileShortcutController' => 'PhabricatorFileController', |     'PhabricatorFileShortcutController' => 'PhabricatorFileController', | ||||||
|     'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', |     'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', | ||||||
|     'PhabricatorFileStorageConfigurationException' => 'Exception', |     'PhabricatorFileStorageConfigurationException' => 'Exception', | ||||||
|  |     'PhabricatorFileTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorFileTransformController' => 'PhabricatorFileController', |     'PhabricatorFileTransformController' => 'PhabricatorFileController', | ||||||
|     'PhabricatorFileUploadController' => 'PhabricatorFileController', |     'PhabricatorFileUploadController' => 'PhabricatorFileController', | ||||||
|     'PhabricatorFileUploadException' => 'Exception', |     'PhabricatorFileUploadException' => 'Exception', | ||||||
| @@ -2340,14 +2375,8 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorHelpController' => 'PhabricatorController', |     'PhabricatorHelpController' => 'PhabricatorController', | ||||||
|     'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', |     'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', | ||||||
|     'PhabricatorIRCBot' => 'PhabricatorDaemon', |     'PhabricatorIRCBot' => 'PhabricatorDaemon', | ||||||
|     'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler', |     'PhabricatorIRCProtocolAdapter' => 'PhabricatorBaseProtocolAdapter', | ||||||
|     'PhabricatorIRCFeedNotificationHandler' => 'PhabricatorIRCHandler', |     'PhabricatorIRCProtocolHandler' => 'PhabricatorBotHandler', | ||||||
|     'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler', |  | ||||||
|     'PhabricatorIRCMacroHandler' => 'PhabricatorIRCHandler', |  | ||||||
|     'PhabricatorIRCObjectNameHandler' => 'PhabricatorIRCHandler', |  | ||||||
|     'PhabricatorIRCProtocolHandler' => 'PhabricatorIRCHandler', |  | ||||||
|     'PhabricatorIRCSymbolHandler' => 'PhabricatorIRCHandler', |  | ||||||
|     'PhabricatorIRCWhatsNewHandler' => 'PhabricatorIRCHandler', |  | ||||||
|     'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', |     'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorInlineCommentController' => 'PhabricatorController', |     'PhabricatorInlineCommentController' => 'PhabricatorController', | ||||||
|     'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface', |     'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface', | ||||||
| @@ -2683,6 +2712,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorTagView' => 'AphrontView', |     'PhabricatorTagView' => 'AphrontView', | ||||||
|     'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', |     'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', | ||||||
|     'PhabricatorTestCase' => 'ArcanistPhutilTestCase', |     'PhabricatorTestCase' => 'ArcanistPhutilTestCase', | ||||||
|  |     'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine', | ||||||
|     'PhabricatorTestWorker' => 'PhabricatorWorker', |     'PhabricatorTestWorker' => 'PhabricatorWorker', | ||||||
|     'PhabricatorTimelineCursor' => 'PhabricatorTimelineDAO', |     'PhabricatorTimelineCursor' => 'PhabricatorTimelineDAO', | ||||||
|     'PhabricatorTimelineDAO' => 'PhabricatorLiskDAO', |     'PhabricatorTimelineDAO' => 'PhabricatorLiskDAO', | ||||||
| @@ -2797,6 +2827,7 @@ phutil_register_library_map(array( | |||||||
|       0 => 'PholioDAO', |       0 => 'PholioDAO', | ||||||
|       1 => 'PhabricatorMarkupInterface', |       1 => 'PhabricatorMarkupInterface', | ||||||
|     ), |     ), | ||||||
|  |     'PholioInlineSaveController' => 'PholioController', | ||||||
|     'PholioMock' => |     'PholioMock' => | ||||||
|     array( |     array( | ||||||
|       0 => 'PholioDAO', |       0 => 'PholioDAO', | ||||||
|   | |||||||
| @@ -18,6 +18,12 @@ final class PhabricatorApplicationAudit extends PhabricatorApplication { | |||||||
|     return PhabricatorEnv::getDoclink('article/Audit_User_Guide.html'); |     return PhabricatorEnv::getDoclink('article/Audit_User_Guide.html'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getEventListeners() { | ||||||
|  |     return array( | ||||||
|  |       new AuditPeopleMenuEventListener() | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getRoutes() { |   public function getRoutes() { | ||||||
|     return array( |     return array( | ||||||
|       '/audit/' => array( |       '/audit/' => array( | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -65,20 +65,6 @@ final class PhabricatorSetupCheckDatabase extends PhabricatorSetupCheck { | |||||||
|       return; |       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'); |     $namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace'); | ||||||
|  |  | ||||||
|     $databases = queryfx_all($conn_raw, 'SHOW DATABASES'); |     $databases = queryfx_all($conn_raw, 'SHOW DATABASES'); | ||||||
|   | |||||||
| @@ -24,6 +24,31 @@ final class PhabricatorSetupCheckMySQL extends PhabricatorSetupCheck { | |||||||
|         ->setName(pht('Small MySQL "max_allowed_packet"')) |         ->setName(pht('Small MySQL "max_allowed_packet"')) | ||||||
|         ->setMessage($message); |         ->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); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,6 +33,12 @@ final class PhabricatorApplicationConpherence extends PhabricatorApplication { | |||||||
|     return self::GROUP_COMMUNICATION; |     return self::GROUP_COMMUNICATION; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getEventListeners() { | ||||||
|  |     return array( | ||||||
|  |       new ConpherencePeopleMenuEventListener(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getRoutes() { |   public function getRoutes() { | ||||||
|     return array( |     return array( | ||||||
|       '/conpherence/' => array( |       '/conpherence/' => array( | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -8,6 +8,7 @@ final class ConpherenceTransactionType extends ConpherenceConstants { | |||||||
|   const TYPE_FILES           = 'files'; |   const TYPE_FILES           = 'files'; | ||||||
|   const TYPE_TITLE           = 'title'; |   const TYPE_TITLE           = 'title'; | ||||||
|   const TYPE_PICTURE         = 'picture'; |   const TYPE_PICTURE         = 'picture'; | ||||||
|  |   const TYPE_PICTURE_CROP    = 'picture-crop'; | ||||||
|   const TYPE_PARTICIPANTS    = 'participants'; |   const TYPE_PARTICIPANTS    = 'participants'; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -131,7 +131,10 @@ abstract class ConpherenceController extends PhabricatorController { | |||||||
|     $user = $this->getRequest()->getUser(); |     $user = $this->getRequest()->getUser(); | ||||||
|     foreach ($conpherences as $conpherence) { |     foreach ($conpherences as $conpherence) { | ||||||
|       $uri = $this->getApplicationURI('view/'.$conpherence->getID().'/'); |       $uri = $this->getApplicationURI('view/'.$conpherence->getID().'/'); | ||||||
|       $data = $conpherence->getDisplayData($user); |       $data = $conpherence->getDisplayData( | ||||||
|  |         $user, | ||||||
|  |         null | ||||||
|  |       ); | ||||||
|       $title = $data['title']; |       $title = $data['title']; | ||||||
|       $subtitle = $data['subtitle']; |       $subtitle = $data['subtitle']; | ||||||
|       $unread_count = $data['unread_count']; |       $unread_count = $data['unread_count']; | ||||||
| @@ -206,6 +209,7 @@ abstract class ConpherenceController extends PhabricatorController { | |||||||
|         'messages' => 'conpherence-messages', |         'messages' => 'conpherence-messages', | ||||||
|         'widgets_pane' => 'conpherence-widget-pane', |         'widgets_pane' => 'conpherence-widget-pane', | ||||||
|         'form_pane' => 'conpherence-form', |         'form_pane' => 'conpherence-form', | ||||||
|  |         'menu_pane' => 'conpherence-menu', | ||||||
|         'fancy_ajax' => (bool) $this->getSelectedConpherencePHID() |         'fancy_ajax' => (bool) $this->getSelectedConpherencePHID() | ||||||
|       ) |       ) | ||||||
|     ); |     ); | ||||||
| @@ -219,6 +223,14 @@ abstract class ConpherenceController extends PhabricatorController { | |||||||
|         'form_pane' => 'conpherence-form' |         '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', | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,6 +30,8 @@ final class ConpherenceUpdateController extends | |||||||
|     $conpherence = id(new ConpherenceThreadQuery()) |     $conpherence = id(new ConpherenceThreadQuery()) | ||||||
|       ->setViewer($user) |       ->setViewer($user) | ||||||
|       ->withIDs(array($conpherence_id)) |       ->withIDs(array($conpherence_id)) | ||||||
|  |       ->needOrigPics(true) | ||||||
|  |       ->needHeaderPics(true) | ||||||
|       ->executeOne(); |       ->executeOne(); | ||||||
|     $supported_formats = PhabricatorFile::getTransformableImageFormats(); |     $supported_formats = PhabricatorFile::getTransformableImageFormats(); | ||||||
|  |  | ||||||
| @@ -59,31 +61,63 @@ final class ConpherenceUpdateController extends | |||||||
|           break; |           break; | ||||||
|         case 'metadata': |         case 'metadata': | ||||||
|           $xactions = array(); |           $xactions = array(); | ||||||
|           $images = $request->getArr('image'); |           $top = $request->getInt('image_y'); | ||||||
|           if ($images) { |           $left = $request->getInt('image_x'); | ||||||
|             // just take the first one |           $file_id = $request->getInt('file_id'); | ||||||
|             $file_phid = reset($images); |           if ($file_id) { | ||||||
|             $file = id(new PhabricatorFileQuery()) |             $orig_file = id(new PhabricatorFileQuery()) | ||||||
|               ->setViewer($user) |               ->setViewer($user) | ||||||
|               ->withPHIDs(array($file_phid)) |               ->withIDs(array($file_id)) | ||||||
|               ->executeOne(); |               ->executeOne(); | ||||||
|             $okay = $file->isTransformableImage(); |             $okay = $orig_file->isTransformableImage(); | ||||||
|             if ($okay) { |             if ($okay) { | ||||||
|               $xformer = new PhabricatorImageTransformer(); |  | ||||||
|               $xformed = $xformer->executeThumbTransform( |  | ||||||
|                 $file, |  | ||||||
|                 $x = 50, |  | ||||||
|                 $y = 50); |  | ||||||
|               $image_phid = $xformed->getPHID(); |  | ||||||
|               $xactions[] = id(new ConpherenceTransaction()) |               $xactions[] = id(new ConpherenceTransaction()) | ||||||
|                 ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE) |                 ->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 { |             } else { | ||||||
|               $e_file[] = $file; |               $e_file[] = $orig_file; | ||||||
|               $errors[] = |               $errors[] = | ||||||
|                 pht('This server only supports these image formats: %s.', |                 pht('This server only supports these image formats: %s.', | ||||||
|                   implode(', ', $supported_formats)); |                   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'); |           $title = $request->getStr('title'); | ||||||
|           if ($title != $conpherence->getTitle()) { |           if ($title != $conpherence->getTitle()) { | ||||||
| @@ -131,25 +165,43 @@ final class ConpherenceUpdateController extends | |||||||
|         ->setLabel(pht('Title')) |         ->setLabel(pht('Title')) | ||||||
|         ->setName('title') |         ->setName('title') | ||||||
|         ->setValue($conpherence->getTitle()) |         ->setValue($conpherence->getTitle()) | ||||||
|       ) |       ); | ||||||
|  |  | ||||||
|  |     $image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); | ||||||
|  |     if ($image) { | ||||||
|  |       $form | ||||||
|         ->appendChild( |         ->appendChild( | ||||||
|           id(new AphrontFormMarkupControl()) |           id(new AphrontFormMarkupControl()) | ||||||
|           ->setLabel(pht('Image')) |           ->setLabel(pht('Image')) | ||||||
|           ->setValue(phutil_tag( |           ->setValue(phutil_tag( | ||||||
|             'img', |             'img', | ||||||
|             array( |             array( | ||||||
|             'src' => $conpherence->loadImageURI(), |               'src' => | ||||||
|  |               $conpherence->loadImageURI(ConpherenceImageData::SIZE_HEAD), | ||||||
|               )) |               )) | ||||||
|             ) |             ) | ||||||
|           ) |           ) | ||||||
|           ->appendChild( |           ->appendChild( | ||||||
|         id(new AphrontFormDragAndDropUploadControl()) |             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')) |             ->setLabel(pht('Change Image')) | ||||||
|         ->setName('image') |  | ||||||
|         ->setValue($e_file) |  | ||||||
|         ->setCaption('Supported formats: '.implode(', ', $supported_formats)) |  | ||||||
|           ); |           ); | ||||||
|  |  | ||||||
|  |     } else { | ||||||
|  |  | ||||||
|  |       $form | ||||||
|  |         ->appendChild( | ||||||
|  |           id(new ConpherenceFormDragAndDropUploadControl()) | ||||||
|  |           ->setLabel(pht('Image')) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     require_celerity_resource('conpherence-update-css'); |     require_celerity_resource('conpherence-update-css'); | ||||||
|     return id(new AphrontDialogResponse()) |     return id(new AphrontDialogResponse()) | ||||||
|       ->setDialog( |       ->setDialog( | ||||||
|   | |||||||
| @@ -45,6 +45,7 @@ final class ConpherenceViewController extends | |||||||
|       ->setViewer($user) |       ->setViewer($user) | ||||||
|       ->withIDs(array($conpherence_id)) |       ->withIDs(array($conpherence_id)) | ||||||
|       ->needWidgetData(true) |       ->needWidgetData(true) | ||||||
|  |       ->needHeaderPics(true) | ||||||
|       ->executeOne(); |       ->executeOne(); | ||||||
|     $this->setConpherence($conpherence); |     $this->setConpherence($conpherence); | ||||||
|  |  | ||||||
| @@ -67,23 +68,34 @@ final class ConpherenceViewController extends | |||||||
|     require_celerity_resource('conpherence-header-pane-css'); |     require_celerity_resource('conpherence-header-pane-css'); | ||||||
|     $user = $this->getRequest()->getUser(); |     $user = $this->getRequest()->getUser(); | ||||||
|     $conpherence = $this->getConpherence(); |     $conpherence = $this->getConpherence(); | ||||||
|     $display_data = $conpherence->getDisplayData($user); |     $display_data = $conpherence->getDisplayData( | ||||||
|  |       $user, | ||||||
|  |       ConpherenceImageData::SIZE_HEAD | ||||||
|  |     ); | ||||||
|     $edit_href = $this->getApplicationURI('update/'.$conpherence->getID().'/'); |     $edit_href = $this->getApplicationURI('update/'.$conpherence->getID().'/'); | ||||||
|  |     $class_mod = $display_data['image_class']; | ||||||
|  |  | ||||||
|     $header = |     $header = | ||||||
|  |     phutil_tag( | ||||||
|  |       'div', | ||||||
|  |       array( | ||||||
|  |         'class' => 'upload-photo' | ||||||
|  |       ), | ||||||
|  |       pht('Drop photo here to change this Conpherence photo.') | ||||||
|  |     ). | ||||||
|     javelin_tag( |     javelin_tag( | ||||||
|       'a', |       'a', | ||||||
|       array( |       array( | ||||||
|         'class' => 'edit', |         'class' => 'edit', | ||||||
|         'href' => $edit_href, |         'href' => $edit_href, | ||||||
|         'sigil' => 'workflow', |         'sigil' => 'workflow edit-action', | ||||||
|       ), |       ), | ||||||
|       '' |       '' | ||||||
|     ). |     ). | ||||||
|     phutil_tag( |     phutil_tag( | ||||||
|       'div', |       'div', | ||||||
|       array( |       array( | ||||||
|         'class' => 'header-image', |         'class' => $class_mod.'header-image', | ||||||
|         'style' => 'background-image: url('.$display_data['image'].');' |         'style' => 'background-image: url('.$display_data['image'].');' | ||||||
|       ), |       ), | ||||||
|       '' |       '' | ||||||
| @@ -91,14 +103,14 @@ final class ConpherenceViewController extends | |||||||
|     phutil_tag( |     phutil_tag( | ||||||
|       'div', |       'div', | ||||||
|       array( |       array( | ||||||
|         'class' => 'title', |         'class' => $class_mod.'title', | ||||||
|       ), |       ), | ||||||
|       $display_data['title'] |       $display_data['title'] | ||||||
|     ). |     ). | ||||||
|     phutil_tag( |     phutil_tag( | ||||||
|       'div', |       'div', | ||||||
|       array( |       array( | ||||||
|         'class' => 'subtitle', |         'class' => $class_mod.'subtitle', | ||||||
|       ), |       ), | ||||||
|       $display_data['subtitle'] |       $display_data['subtitle'] | ||||||
|     ); |     ); | ||||||
| @@ -114,6 +126,18 @@ final class ConpherenceViewController extends | |||||||
|     $rendered_transactions = array(); |     $rendered_transactions = array(); | ||||||
|  |  | ||||||
|     $transactions = $conpherence->getTransactions(); |     $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) { |     foreach ($transactions as $transaction) { | ||||||
|       if ($transaction->shouldHide()) { |       if ($transaction->shouldHide()) { | ||||||
|         continue; |         continue; | ||||||
| @@ -122,6 +146,7 @@ final class ConpherenceViewController extends | |||||||
|         ->setUser($user) |         ->setUser($user) | ||||||
|         ->setConpherenceTransaction($transaction) |         ->setConpherenceTransaction($transaction) | ||||||
|         ->setHandles($handles) |         ->setHandles($handles) | ||||||
|  |         ->setMarkupEngine($engine) | ||||||
|         ->render(); |         ->render(); | ||||||
|     } |     } | ||||||
|     $transactions = implode(' ', $rendered_transactions); |     $transactions = implode(' ', $rendered_transactions); | ||||||
|   | |||||||
| @@ -48,6 +48,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { | |||||||
|  |  | ||||||
|     $types[] = ConpherenceTransactionType::TYPE_TITLE; |     $types[] = ConpherenceTransactionType::TYPE_TITLE; | ||||||
|     $types[] = ConpherenceTransactionType::TYPE_PICTURE; |     $types[] = ConpherenceTransactionType::TYPE_PICTURE; | ||||||
|  |     $types[] = ConpherenceTransactionType::TYPE_PICTURE_CROP; | ||||||
|     $types[] = ConpherenceTransactionType::TYPE_PARTICIPANTS; |     $types[] = ConpherenceTransactionType::TYPE_PARTICIPANTS; | ||||||
|     $types[] = ConpherenceTransactionType::TYPE_FILES; |     $types[] = ConpherenceTransactionType::TYPE_FILES; | ||||||
|  |  | ||||||
| @@ -62,7 +63,9 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { | |||||||
|       case ConpherenceTransactionType::TYPE_TITLE: |       case ConpherenceTransactionType::TYPE_TITLE: | ||||||
|         return $object->getTitle(); |         return $object->getTitle(); | ||||||
|       case ConpherenceTransactionType::TYPE_PICTURE: |       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: |       case ConpherenceTransactionType::TYPE_PARTICIPANTS: | ||||||
|         return $object->getParticipantPHIDs(); |         return $object->getParticipantPHIDs(); | ||||||
|       case ConpherenceTransactionType::TYPE_FILES: |       case ConpherenceTransactionType::TYPE_FILES: | ||||||
| @@ -77,6 +80,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { | |||||||
|     switch ($xaction->getTransactionType()) { |     switch ($xaction->getTransactionType()) { | ||||||
|       case ConpherenceTransactionType::TYPE_TITLE: |       case ConpherenceTransactionType::TYPE_TITLE: | ||||||
|       case ConpherenceTransactionType::TYPE_PICTURE: |       case ConpherenceTransactionType::TYPE_PICTURE: | ||||||
|  |       case ConpherenceTransactionType::TYPE_PICTURE_CROP: | ||||||
|         return $xaction->getNewValue(); |         return $xaction->getNewValue(); | ||||||
|       case ConpherenceTransactionType::TYPE_PARTICIPANTS: |       case ConpherenceTransactionType::TYPE_PARTICIPANTS: | ||||||
|       case ConpherenceTransactionType::TYPE_FILES: |       case ConpherenceTransactionType::TYPE_FILES: | ||||||
| @@ -93,7 +97,16 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { | |||||||
|         $object->setTitle($xaction->getNewValue()); |         $object->setTitle($xaction->getNewValue()); | ||||||
|         break; |         break; | ||||||
|       case ConpherenceTransactionType::TYPE_PICTURE: |       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; |         break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -9,6 +9,18 @@ final class ConpherenceThreadQuery | |||||||
|   private $phids; |   private $phids; | ||||||
|   private $ids; |   private $ids; | ||||||
|   private $needWidgetData; |   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) { |   public function needWidgetData($need_widget_data) { | ||||||
|     $this->needWidgetData = $need_widget_data; |     $this->needWidgetData = $need_widget_data; | ||||||
| @@ -47,6 +59,12 @@ final class ConpherenceThreadQuery | |||||||
|       if ($this->needWidgetData) { |       if ($this->needWidgetData) { | ||||||
|         $this->loadWidgetData($conpherences); |         $this->loadWidgetData($conpherences); | ||||||
|       } |       } | ||||||
|  |       if ($this->needOrigPics) { | ||||||
|  |         $this->loadOrigPics($conpherences); | ||||||
|  |       } | ||||||
|  |       if ($this->needHeaderPics) { | ||||||
|  |         $this->loadHeaderPics($conpherences); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return $conpherences; |     return $conpherences; | ||||||
| @@ -187,4 +205,39 @@ final class ConpherenceThreadQuery | |||||||
|     return $this; |     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; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ final class ConpherenceThread extends ConpherenceDAO | |||||||
|   protected $id; |   protected $id; | ||||||
|   protected $phid; |   protected $phid; | ||||||
|   protected $title; |   protected $title; | ||||||
|   protected $imagePHID; |   protected $imagePHIDs = array(); | ||||||
|   protected $mailKey; |   protected $mailKey; | ||||||
|  |  | ||||||
|   private $participants; |   private $participants; | ||||||
| @@ -17,10 +17,14 @@ final class ConpherenceThread extends ConpherenceDAO | |||||||
|   private $handles; |   private $handles; | ||||||
|   private $filePHIDs; |   private $filePHIDs; | ||||||
|   private $widgetData; |   private $widgetData; | ||||||
|  |   private $images = array(); | ||||||
|  |  | ||||||
|   public function getConfiguration() { |   public function getConfiguration() { | ||||||
|     return array( |     return array( | ||||||
|       self::CONFIG_AUX_PHID => true, |       self::CONFIG_AUX_PHID => true, | ||||||
|  |       self::CONFIG_SERIALIZATION => array( | ||||||
|  |         'imagePHIDs'  => self::SERIALIZATION_JSON, | ||||||
|  |       ), | ||||||
|     ) + parent::getConfiguration(); |     ) + parent::getConfiguration(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -36,6 +40,33 @@ final class ConpherenceThread extends ConpherenceDAO | |||||||
|     return parent::save(); |     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) { |   public function attachParticipants(array $participants) { | ||||||
|     assert_instances_of($participants, 'ConpherenceParticipant'); |     assert_instances_of($participants, 'ConpherenceParticipant'); | ||||||
|     $this->participants = $participants; |     $this->participants = $participants; | ||||||
| @@ -112,59 +143,36 @@ final class ConpherenceThread extends ConpherenceDAO | |||||||
|     return $this->widgetData; |     return $this->widgetData; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function loadImageURI() { |   public function loadImageURI($size) { | ||||||
|     $src_phid = $this->getImagePHID(); |     $file = $this->getImage($size); | ||||||
|  |  | ||||||
|     if ($src_phid) { |  | ||||||
|       $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); |  | ||||||
|     if ($file) { |     if ($file) { | ||||||
|       return $file->getBestURI(); |       return $file->getBestURI(); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return PhabricatorUser::getDefaultProfileImageURI(); |     return PhabricatorUser::getDefaultProfileImageURI(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function getDisplayData(PhabricatorUser $user) { |   public function getDisplayData(PhabricatorUser $user, $size) { | ||||||
|     $transactions = $this->getTransactions(); |     $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 = ''; |     $handles = $this->getHandles(); | ||||||
|     $count = 0; |     // we don't want to show the user unless they are babbling to themselves | ||||||
|     $final = false; |     if (count($handles) > 1) { | ||||||
|     foreach ($handles as $handle) { |       unset($handles[$user->getPHID()]); | ||||||
|       if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) { |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       if ($subtitle) { |  | ||||||
|         if ($final) { |  | ||||||
|           $subtitle .= '...'; |  | ||||||
|           break; |  | ||||||
|         } else { |  | ||||||
|           $subtitle .= ', '; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       $subtitle .= $handle->getName(); |  | ||||||
|       $count++; |  | ||||||
|       $final = $count == 3; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $participants = $this->getParticipants(); |     $participants = $this->getParticipants(); | ||||||
|     $user_participation = $participants[$user->getPHID()]; |     $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; |     $unread_count = 0; | ||||||
|     $max_count = 10; |     $max_count = 10; | ||||||
|     $snippet = null; |     $snippet = null; | ||||||
| @@ -186,9 +194,28 @@ final class ConpherenceThread extends ConpherenceDAO | |||||||
|               $transaction->getComment()->getContent(), |               $transaction->getComment()->getContent(), | ||||||
|               48 |               48 | ||||||
|             ); |             ); | ||||||
|  |             if ($transaction->getAuthorPHID() == $user->getPHID()) { | ||||||
|  |               $snippet = "\xE2\x86\xB0  " . $snippet; | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|           // fallthrough intentionally here |           // fallthrough intentionally here | ||||||
|         case ConpherenceTransactionType::TYPE_FILES: |         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) { |           if ($behind_transaction_phid) { | ||||||
|             $unread_count++; |             $unread_count++; | ||||||
|             if ($transaction->getPHID() == $behind_transaction_phid) { |             if ($transaction->getPHID() == $behind_transaction_phid) { | ||||||
| @@ -210,12 +237,45 @@ final class ConpherenceThread extends ConpherenceDAO | |||||||
|       $unread_count = $max_count.'+'; |       $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( |     return array( | ||||||
|       'title' => $title, |       'title' => $title, | ||||||
|       'subtitle' => $subtitle, |       'subtitle' => $subtitle, | ||||||
|       'unread_count' => $unread_count, |       'unread_count' => $unread_count, | ||||||
|       'epoch' => $latest_transaction->getDateCreated(), |       'epoch' => $latest_transaction->getDateCreated(), | ||||||
|       'image' => $img_src, |       'image' => $img_src, | ||||||
|  |       'image_class' => $img_class, | ||||||
|       'snippet' => $snippet, |       'snippet' => $snippet, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction { | |||||||
|       case ConpherenceTransactionType::TYPE_PICTURE: |       case ConpherenceTransactionType::TYPE_PICTURE: | ||||||
|         return false; |         return false; | ||||||
|       case ConpherenceTransactionType::TYPE_FILES: |       case ConpherenceTransactionType::TYPE_FILES: | ||||||
|  |       case ConpherenceTransactionType::TYPE_PICTURE_CROP: | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -107,12 +108,10 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction { | |||||||
|     $phids[] = $this->getAuthorPHID(); |     $phids[] = $this->getAuthorPHID(); | ||||||
|     switch ($this->getTransactionType()) { |     switch ($this->getTransactionType()) { | ||||||
|       case ConpherenceTransactionType::TYPE_PICTURE: |       case ConpherenceTransactionType::TYPE_PICTURE: | ||||||
|         $phids[] = $new; |  | ||||||
|         break; |  | ||||||
|       case ConpherenceTransactionType::TYPE_TITLE: |       case ConpherenceTransactionType::TYPE_TITLE: | ||||||
|  |       case ConpherenceTransactionType::TYPE_FILES: | ||||||
|         break; |         break; | ||||||
|       case ConpherenceTransactionType::TYPE_PARTICIPANTS: |       case ConpherenceTransactionType::TYPE_PARTICIPANTS: | ||||||
|       case ConpherenceTransactionType::TYPE_FILES: |  | ||||||
|         $phids = array_merge($phids, $this->getOldValue()); |         $phids = array_merge($phids, $this->getOldValue()); | ||||||
|         $phids = array_merge($phids, $this->getNewValue()); |         $phids = array_merge($phids, $this->getNewValue()); | ||||||
|         break; |         break; | ||||||
|   | |||||||
| @@ -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.') | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -7,6 +7,12 @@ final class ConpherenceTransactionView extends AphrontView { | |||||||
|  |  | ||||||
|   private $conpherenceTransaction; |   private $conpherenceTransaction; | ||||||
|   private $handles; |   private $handles; | ||||||
|  |   private $markupEngine; | ||||||
|  |  | ||||||
|  |   public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) { | ||||||
|  |     $this->markupEngine = $markup_engine; | ||||||
|  |     return $this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function setHandles(array $handles) { |   public function setHandles(array $handles) { | ||||||
|     assert_instances_of($handles, 'PhabricatorObjectHandle'); |     assert_instances_of($handles, 'PhabricatorObjectHandle'); | ||||||
| @@ -37,8 +43,11 @@ final class ConpherenceTransactionView extends AphrontView { | |||||||
|  |  | ||||||
|     $content = null; |     $content = null; | ||||||
|     $content_class = null; |     $content_class = null; | ||||||
|  |     $content = null; | ||||||
|     switch ($transaction->getTransactionType()) { |     switch ($transaction->getTransactionType()) { | ||||||
|       case ConpherenceTransactionType::TYPE_TITLE: |       case ConpherenceTransactionType::TYPE_TITLE: | ||||||
|  |       case ConpherenceTransactionType::TYPE_PICTURE: | ||||||
|  |       case ConpherenceTransactionType::TYPE_PICTURE_CROP: | ||||||
|         $content = $transaction->getTitle(); |         $content = $transaction->getTitle(); | ||||||
|         $transaction_view->addClass('conpherence-edited'); |         $transaction_view->addClass('conpherence-edited'); | ||||||
|         break; |         break; | ||||||
| @@ -47,12 +56,13 @@ final class ConpherenceTransactionView extends AphrontView { | |||||||
|         break; |         break; | ||||||
|       case ConpherenceTransactionType::TYPE_PICTURE: |       case ConpherenceTransactionType::TYPE_PICTURE: | ||||||
|         $img = $transaction->getHandle($transaction->getNewValue()); |         $img = $transaction->getHandle($transaction->getNewValue()); | ||||||
|         $content = $transaction->getTitle() . |         $content = array( | ||||||
|  |           $transaction->getTitle(), | ||||||
|           phutil_tag( |           phutil_tag( | ||||||
|             'img', |             'img', | ||||||
|             array( |             array( | ||||||
|               'src' => $img->getImageURI() |               'src' => $img->getImageURI() | ||||||
|             )); |             ))); | ||||||
|         $transaction_view->addClass('conpherence-edited'); |         $transaction_view->addClass('conpherence-edited'); | ||||||
|         break; |         break; | ||||||
|       case ConpherenceTransactionType::TYPE_PARTICIPANTS: |       case ConpherenceTransactionType::TYPE_PARTICIPANTS: | ||||||
| @@ -65,18 +75,9 @@ final class ConpherenceTransactionView extends AphrontView { | |||||||
|           PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( |           PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( | ||||||
|             array($comment->getContent()) |             array($comment->getContent()) | ||||||
|           ); |           ); | ||||||
|         $markup_field = ConpherenceTransactionComment::MARKUP_FIELD_COMMENT; |         $content = $this->markupEngine->getOutput( | ||||||
|         $engine = id(new PhabricatorMarkupEngine()) |  | ||||||
|           ->setViewer($this->getUser()); |  | ||||||
|         $engine->addObject( |  | ||||||
|           $comment, |           $comment, | ||||||
|           $markup_field |           PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); | ||||||
|         ); |  | ||||||
|         $engine->process(); |  | ||||||
|         $content = $engine->getOutput( |  | ||||||
|           $comment, |  | ||||||
|           $markup_field |  | ||||||
|         ); |  | ||||||
|         $content_class = 'conpherence-message phabricator-remarkup'; |         $content_class = 'conpherence-message phabricator-remarkup'; | ||||||
|         $transaction_view |         $transaction_view | ||||||
|           ->setImageURI($author->getImageURI()) |           ->setImageURI($author->getImageURI()) | ||||||
| @@ -90,7 +91,7 @@ final class ConpherenceTransactionView extends AphrontView { | |||||||
|         array( |         array( | ||||||
|           'class' => $content_class |           'class' => $content_class | ||||||
|         ), |         ), | ||||||
|         new PhutilSafeHTML($content)) |         $this->renderHTMLView($content)) | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|     return $transaction_view->render(); |     return $transaction_view->render(); | ||||||
|   | |||||||
| @@ -28,6 +28,12 @@ final class PhabricatorApplicationDifferential extends PhabricatorApplication { | |||||||
|     return "\xE2\x9A\x99"; |     return "\xE2\x9A\x99"; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getEventListeners() { | ||||||
|  |     return array( | ||||||
|  |       new DifferentialPeopleMenuEventListener() | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getRoutes() { |   public function getRoutes() { | ||||||
|     return array( |     return array( | ||||||
|       '/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController', |       '/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController', | ||||||
|   | |||||||
| @@ -72,7 +72,6 @@ final class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod { | |||||||
|       $project_name = null; |       $project_name = null; | ||||||
|     } |     } | ||||||
|     $basic_dict['projectName'] = $project_name; |     $basic_dict['projectName'] = $project_name; | ||||||
|     $basic_dict['author'] = $diff->loadAuthorInformation(); |  | ||||||
|  |  | ||||||
|     return $basic_dict; |     return $basic_dict; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -127,28 +127,6 @@ final class PhabricatorDifferentialConfigOptions | |||||||
|             "If you set this to true, users won't need to login to view ". |             "If you set this to true, users won't need to login to view ". | ||||||
|             "Differential revisions. Anonymous users will have read-only ". |             "Differential revisions. Anonymous users will have read-only ". | ||||||
|             "access and won't be able to interact with the revisions.")), |             "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()) |       $this->newOption('differential.generated-paths', 'list<string>', array()) | ||||||
|         ->setSummary(pht("File regexps to treat as automatically generated.")) |         ->setSummary(pht("File regexps to treat as automatically generated.")) | ||||||
|         ->setDescription( |         ->setDescription( | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -240,55 +240,17 @@ final class DifferentialDiff extends DifferentialDAO { | |||||||
|       $this->getID()); |       $this->getID()); | ||||||
|     foreach ($properties as $property) { |     foreach ($properties as $property) { | ||||||
|       $dict['properties'][$property->getName()] = $property->getData(); |       $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; |     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()); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,6 +24,12 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getEventListeners() { | ||||||
|  |     return array( | ||||||
|  |       new DiffusionPeopleMenuEventListener() | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getRoutes() { |   public function getRoutes() { | ||||||
|     return array( |     return array( | ||||||
|       '/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)' |       '/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)' | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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) { |   private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) { | ||||||
|     $data = $file->loadFileData(); |     $data = $file->loadFileData(); | ||||||
| @@ -80,6 +103,30 @@ final class PhabricatorImageTransformer { | |||||||
|     return $this->saveImageDataInAnyFormat($img, $file->getMimeType()); |     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. |    * 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()); |     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) { |   private function applyScaleTo($src, $dx, $dy) { | ||||||
|     $x = imagesx($src); |     $x = imagesx($src); | ||||||
|     $y = imagesy($src); |     $y = imagesy($src); | ||||||
|  |  | ||||||
|     $scale = min(($dx / $x), ($dy / $y), 1); |     $scale = min(($dx / $x), ($dy / $y), 1); | ||||||
|  |  | ||||||
|     $dst = imagecreatetruecolor($dx, $dy); |     $dst = $this->getBlankDestinationFile($dx, $dy); | ||||||
|     imagesavealpha($dst, true); |  | ||||||
|     imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); |  | ||||||
|  |  | ||||||
|     $sdx = $scale * $x; |     $sdx = $scale * $x; | ||||||
|     $sdy = $scale * $y; |     $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) { |   private function generatePreview(PhabricatorFile $file, $size) { | ||||||
|     $data = $file->loadFileData(); |     $data = $file->loadFileData(); | ||||||
|     $src = imagecreatefromstring($data); |     $src = imagecreatefromstring($data); | ||||||
| @@ -153,9 +227,7 @@ final class PhabricatorImageTransformer { | |||||||
|     $sdx = $dimensions['sdx']; |     $sdx = $dimensions['sdx']; | ||||||
|     $sdy = $dimensions['sdy']; |     $sdy = $dimensions['sdy']; | ||||||
|  |  | ||||||
|     $dst = imagecreatetruecolor($dx, $dy); |     $dst = $this->getBlankDestinationFile($dx, $dy); | ||||||
|     imagesavealpha($dst, true); |  | ||||||
|     imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); |  | ||||||
|  |  | ||||||
|     imagecopyresampled( |     imagecopyresampled( | ||||||
|       $dst, |       $dst, | ||||||
|   | |||||||
| @@ -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]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -134,9 +134,16 @@ final class PhabricatorFile extends PhabricatorFileDAO | |||||||
|  |  | ||||||
|  |  | ||||||
|   public static function newFromFileData($data, array $params = array()) { |   public static function newFromFileData($data, array $params = array()) { | ||||||
|     $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector'); |  | ||||||
|  |  | ||||||
|  |     if (isset($params['storageEngines'])) { | ||||||
|  |       $engines = $params['storageEngines']; | ||||||
|  |     } else { | ||||||
|  |       $selector = PhabricatorEnv::newObjectFromConfig( | ||||||
|  |         'storage.engine-selector'); | ||||||
|       $engines = $selector->selectStorageEngines($data, $params); |       $engines = $selector->selectStorageEngines($data, $params); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     assert_instances_of($engines, 'PhabricatorFileStorageEngine'); | ||||||
|     if (!$engines) { |     if (!$engines) { | ||||||
|       throw new Exception("No valid storage engines are available!"); |       throw new Exception("No valid storage engines are available!"); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -36,6 +36,12 @@ final class PhabricatorApplicationManiphest extends PhabricatorApplication { | |||||||
|     return $this->getBaseURI().'task/create/'; |     return $this->getBaseURI().'task/create/'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getEventListeners() { | ||||||
|  |     return array( | ||||||
|  |       new ManiphestPeopleMenuEventListener() | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getRoutes() { |   public function getRoutes() { | ||||||
|     return array( |     return array( | ||||||
|       '/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController', |       '/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController', | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -15,7 +15,7 @@ extends PhabricatorAuthController { | |||||||
|     $current_user  = $request->getUser(); |     $current_user  = $request->getUser(); | ||||||
|     $server        = new PhabricatorOAuthServer(); |     $server        = new PhabricatorOAuthServer(); | ||||||
|     $client_phid   = $request->getStr('client_id'); |     $client_phid   = $request->getStr('client_id'); | ||||||
|     $scope         = $request->getStr('scope'); |     $scope         = $request->getStr('scope', array()); | ||||||
|     $redirect_uri  = $request->getStr('redirect_uri'); |     $redirect_uri  = $request->getStr('redirect_uri'); | ||||||
|     $state         = $request->getStr('state'); |     $state         = $request->getStr('state'); | ||||||
|     $response_type = $request->getStr('response_type'); |     $response_type = $request->getStr('response_type'); | ||||||
| @@ -63,10 +63,8 @@ extends PhabricatorAuthController { | |||||||
|           return $response; |           return $response; | ||||||
|         } |         } | ||||||
|         $uri              = $redirect_uri; |         $uri              = $redirect_uri; | ||||||
|         $access_token_uri = $uri; |  | ||||||
|       } else { |       } else { | ||||||
|         $uri              = new PhutilURI($client->getRedirectURI()); |         $uri              = new PhutilURI($client->getRedirectURI()); | ||||||
|         $access_token_uri = null; |  | ||||||
|       } |       } | ||||||
|       // we've now validated this request enough overall such that we |       // we've now validated this request enough overall such that we | ||||||
|       // can safely redirect to the client with the response |       // can safely redirect to the client with the response | ||||||
| @@ -121,7 +119,7 @@ extends PhabricatorAuthController { | |||||||
|       if ($return_auth_code) { |       if ($return_auth_code) { | ||||||
|         // step 1 -- generate authorization code |         // step 1 -- generate authorization code | ||||||
|         $auth_code = |         $auth_code = | ||||||
|           $server->generateAuthorizationCode($access_token_uri); |           $server->generateAuthorizationCode($uri); | ||||||
|  |  | ||||||
|         // step 2 return it |         // step 2 return it | ||||||
|         $content = array( |         $content = array( | ||||||
|   | |||||||
| @@ -16,6 +16,21 @@ final class PhabricatorPeopleProfileController | |||||||
|     return $this->profileUser; |     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() { |   public function processRequest() { | ||||||
|  |  | ||||||
|     $viewer = $this->getRequest()->getUser(); |     $viewer = $this->getRequest()->getUser(); | ||||||
| @@ -39,40 +54,14 @@ final class PhabricatorPeopleProfileController | |||||||
|     } |     } | ||||||
|     $username = phutil_escape_uri($user->getUserName()); |     $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 = |     $menu->newLabel(pht('Activity'), 'activity'); | ||||||
|       new PhutilURI('/conpherence/new/?participant='.$user->getPHID()); |     // NOTE: applications install the various links through PhabricatorEvent | ||||||
|     $nav = new AphrontSideNavFilterView(); |     // listeners | ||||||
|     $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()); |  | ||||||
|  |  | ||||||
|     $oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere( |     $oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere( | ||||||
|       'userID = %d', |       'userID = %d', | ||||||
| @@ -92,18 +81,34 @@ final class PhabricatorPeopleProfileController | |||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       $name = $provider->getProviderName().' Profile'; |       $name = pht('%s Profile', $provider->getProviderName()); | ||||||
|       $href = $oauths[$provider_key]->getAccountURI(); |       $href = $oauths[$provider_key]->getAccountURI(); | ||||||
|  |  | ||||||
|       if ($href) { |       if ($href) { | ||||||
|         if (!$added_label) { |         if (!$added_label) { | ||||||
|           $nav->addLabel('Linked Accounts'); |           $menu->newLabel(pht('Linked Accounts'), 'linked_accounts'); | ||||||
|           $added_label = true; |           $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'); |     $this->page = $nav->selectFilter($this->page, 'feed'); | ||||||
|  |  | ||||||
|     switch ($this->page) { |     switch ($this->page) { | ||||||
| @@ -141,14 +146,19 @@ final class PhabricatorPeopleProfileController | |||||||
|     $header->appendChild($content); |     $header->appendChild($content); | ||||||
|  |  | ||||||
|     if ($user->getPHID() == $viewer->getPHID()) { |     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()) { |     if ($viewer->getIsAdmin()) { | ||||||
|       $nav->addFilter( |       $nav->addFilter( | ||||||
|         null, |         null, | ||||||
|         'Administrate User...', |         pht('Administrate User...'), | ||||||
|         '/people/edit/'.$user->getID().'/'); |         '/people/edit/'.$user->getID().'/' | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return $this->buildApplicationPage( |     return $this->buildApplicationPage( | ||||||
| @@ -162,7 +172,10 @@ final class PhabricatorPeopleProfileController | |||||||
|  |  | ||||||
|     $blurb = nonempty( |     $blurb = nonempty( | ||||||
|       $profile->getBlurb(), |       $profile->getBlurb(), | ||||||
|       '//Nothing is known about this rare specimen.//'); |       '//'. | ||||||
|  |       pht('Nothing is known about this rare specimen.') | ||||||
|  |       .'//' | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     $engine = PhabricatorMarkupEngine::newProfileMarkupEngine(); |     $engine = PhabricatorMarkupEngine::newProfileMarkupEngine(); | ||||||
|     $blurb = phutil_safe_html($engine->markupText($blurb)); |     $blurb = phutil_safe_html($engine->markupText($blurb)); | ||||||
|   | |||||||
| @@ -413,9 +413,6 @@ final class PhabricatorObjectHandleData { | |||||||
|               $handle->setName($file->getName()); |               $handle->setName($file->getName()); | ||||||
|               $handle->setURI($file->getBestURI()); |               $handle->setURI($file->getBestURI()); | ||||||
|               $handle->setComplete(true); |               $handle->setComplete(true); | ||||||
|               if ($file->isViewableImage()) { |  | ||||||
|                 $handle->setImageURI($file->getBestURI()); |  | ||||||
|               } |  | ||||||
|             } |             } | ||||||
|             $handles[$phid] = $handle; |             $handles[$phid] = $handle; | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ final class PhabricatorApplicationPholio extends PhabricatorApplication { | |||||||
|         'new/'                  => 'PholioMockEditController', |         'new/'                  => 'PholioMockEditController', | ||||||
|         'edit/(?P<id>\d+)/'     => 'PholioMockEditController', |         'edit/(?P<id>\d+)/'     => 'PholioMockEditController', | ||||||
|         'comment/(?P<id>\d+)/'  => 'PholioMockCommentController', |         'comment/(?P<id>\d+)/'  => 'PholioMockCommentController', | ||||||
|  |         'inline/(?P<id>\d+)/'   => 'PholioInlineSaveController', | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -4,5 +4,6 @@ final class PholioTransactionType extends PholioConstants { | |||||||
|  |  | ||||||
|   const TYPE_NAME         = 'name'; |   const TYPE_NAME         = 'name'; | ||||||
|   const TYPE_DESCRIPTION  = 'description'; |   const TYPE_DESCRIPTION  = 'description'; | ||||||
|  |   const TYPE_INLINE  = 'inline'; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -22,6 +22,7 @@ final class PholioMockCommentController extends PholioController { | |||||||
|     $mock = id(new PholioMockQuery()) |     $mock = id(new PholioMockQuery()) | ||||||
|       ->setViewer($user) |       ->setViewer($user) | ||||||
|       ->withIDs(array($this->id)) |       ->withIDs(array($this->id)) | ||||||
|  |       ->needImages(true) | ||||||
|       ->executeOne(); |       ->executeOne(); | ||||||
|  |  | ||||||
|     if (!$mock) { |     if (!$mock) { | ||||||
| @@ -49,6 +50,18 @@ final class PholioMockCommentController extends PholioController { | |||||||
|         id(new PholioTransactionComment()) |         id(new PholioTransactionComment()) | ||||||
|           ->setContent($comment)); |           ->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()) |     $editor = id(new PholioMockEditor()) | ||||||
|       ->setActor($user) |       ->setActor($user) | ||||||
|       ->setContentSource($content_source) |       ->setContentSource($content_source) | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor { | |||||||
|  |  | ||||||
|     $types[] = PholioTransactionType::TYPE_NAME; |     $types[] = PholioTransactionType::TYPE_NAME; | ||||||
|     $types[] = PholioTransactionType::TYPE_DESCRIPTION; |     $types[] = PholioTransactionType::TYPE_DESCRIPTION; | ||||||
|  |     $types[] = PholioTransactionType::TYPE_INLINE; | ||||||
|     return $types; |     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( |   protected function applyCustomInternalTransaction( | ||||||
|     PhabricatorLiskDAO $object, |     PhabricatorLiskDAO $object, | ||||||
|     PhabricatorApplicationTransaction $xaction) { |     PhabricatorApplicationTransaction $xaction) { | ||||||
|   | |||||||
| @@ -15,7 +15,9 @@ final class PholioMockImagesView extends AphrontView { | |||||||
|  |  | ||||||
|     $main_image_id = celerity_generate_unique_node_id(); |     $main_image_id = celerity_generate_unique_node_id(); | ||||||
|     require_celerity_resource('javelin-behavior-pholio-mock-view'); |     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); |     Javelin::initBehavior('pholio-mock-view', $config); | ||||||
|  |  | ||||||
|     $mockview = ""; |     $mockview = ""; | ||||||
|   | |||||||
| @@ -75,10 +75,13 @@ final class PhabricatorApplicationTransactionCommentEditor | |||||||
|         "Transaction must have a PHID before calling applyEdit()!"); |         "Transaction must have a PHID before calling applyEdit()!"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     $type_comment = PhabricatorTransactions::TYPE_COMMENT; | ||||||
|  |     if ($xaction->getTransactionType() == $type_comment) { | ||||||
|       if ($comment->getPHID()) { |       if ($comment->getPHID()) { | ||||||
|         throw new Exception( |         throw new Exception( | ||||||
|         "Transaction comment must not yet have a PHID!"); |         "Transaction comment must not yet have a PHID!"); | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!$this->getContentSource()) { |     if (!$this->getContentSource()) { | ||||||
|       throw new Exception( |       throw new Exception( | ||||||
|   | |||||||
							
								
								
									
										132
									
								
								src/infrastructure/daemon/bot/PhabricatorBot.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/infrastructure/daemon/bot/PhabricatorBot.php
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										68
									
								
								src/infrastructure/daemon/bot/PhabricatorBotMessage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/infrastructure/daemon/bot/PhabricatorBotMessage.php
									
									
									
									
									
										Normal 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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/infrastructure/daemon/bot/PhabricatorIRCBot.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/infrastructure/daemon/bot/PhabricatorIRCBot.php
									
									
									
									
									
										Normal 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."); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,12 +3,12 @@ | |||||||
| /** | /** | ||||||
|  * @group irc |  * @group irc | ||||||
|  */ |  */ | ||||||
| final class PhabricatorIRCDifferentialNotificationHandler | final class PhabricatorBotDifferentialNotificationHandler | ||||||
|   extends PhabricatorIRCHandler { | extends PhabricatorBotHandler { | ||||||
| 
 | 
 | ||||||
|   private $skippedOldEvents; |   private $skippedOldEvents; | ||||||
| 
 | 
 | ||||||
|   public function receiveMessage(PhabricatorIRCMessage $message) { |   public function receiveMessage(PhabricatorBotMessage $message) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -39,11 +39,16 @@ final class PhabricatorIRCDifferentialNotificationHandler | |||||||
|       $verb = DifferentialAction::getActionPastTenseVerb($data['action']); |       $verb = DifferentialAction::getActionPastTenseVerb($data['action']); | ||||||
| 
 | 
 | ||||||
|       $actor_name = $handles[$actor_phid]->getName(); |       $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()); |       $channels = $this->getConfig('notification.channels', array()); | ||||||
|       foreach ($channels as $channel) { |       foreach ($channels as $channel) { | ||||||
|         $this->write('PRIVMSG', "{$channel} :{$message}"); |         $this->writeMessage( | ||||||
|  |           id(new PhabricatorBotMessage()) | ||||||
|  |           ->setCommand('MESSAGE') | ||||||
|  |           ->setTarget($channel) | ||||||
|  |           ->setBody($message_body)); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -5,8 +5,8 @@ | |||||||
|  * |  * | ||||||
|  * @group irc |  * @group irc | ||||||
|  */ |  */ | ||||||
| final class PhabricatorIRCFeedNotificationHandler | final class PhabricatorBotFeedNotificationHandler | ||||||
|   extends PhabricatorIRCHandler { |   extends PhabricatorBotHandler { | ||||||
| 
 | 
 | ||||||
|   private $startupDelay = 30; |   private $startupDelay = 30; | ||||||
|   private $lastSeenChronoKey = 0; |   private $lastSeenChronoKey = 0; | ||||||
| @@ -82,7 +82,7 @@ final class PhabricatorIRCFeedNotificationHandler | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public function receiveMessage(PhabricatorIRCMessage $message) { |   public function receiveMessage(PhabricatorBotMessage $message) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -150,7 +150,11 @@ final class PhabricatorIRCFeedNotificationHandler | |||||||
| 
 | 
 | ||||||
|         $channels = $this->getConfig('join'); |         $channels = $this->getConfig('join'); | ||||||
|         foreach ($channels as $channel) { |         foreach ($channels as $channel) { | ||||||
|           $this->write('PRIVMSG', "{$channel} :{$story['text']}"); |           $this->writeMessage( | ||||||
|  |             id(new PhabricatorBotMessage()) | ||||||
|  |             ->setCommand('MESSAGE') | ||||||
|  |             ->setTarget($channel) | ||||||
|  |             ->setBody($story['text'])); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -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); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,19 +5,19 @@ | |||||||
|  * |  * | ||||||
|  * @group irc |  * @group irc | ||||||
|  */ |  */ | ||||||
| final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler { | final class PhabricatorBotLogHandler extends PhabricatorBotHandler { | ||||||
| 
 | 
 | ||||||
|   private $futures = array(); |   private $futures = array(); | ||||||
| 
 | 
 | ||||||
|   public function receiveMessage(PhabricatorIRCMessage $message) { |   public function receiveMessage(PhabricatorBotMessage $message) { | ||||||
| 
 | 
 | ||||||
|     switch ($message->getCommand()) { |     switch ($message->getCommand()) { | ||||||
|       case 'PRIVMSG': |       case 'MESSAGE': | ||||||
|         $reply_to = $message->getReplyTo(); |         $reply_to = $message->getReplyTo(); | ||||||
|         if (!$reply_to) { |         if (!$reply_to) { | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|         if (!$this->isChannelName($reply_to)) { |         if (!$message->isPublic()) { | ||||||
|           // Don't log private messages, although maybe we should for debugging?
 |           // Don't log private messages, although maybe we should for debugging?
 | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
| @@ -27,8 +27,8 @@ final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler { | |||||||
|             'channel' => $reply_to, |             'channel' => $reply_to, | ||||||
|             'type'    => 'mesg', |             'type'    => 'mesg', | ||||||
|             'epoch'   => time(), |             'epoch'   => time(), | ||||||
|             'author'  => $message->getSenderNickname(), |             'author'  => $message->getSender(), | ||||||
|             'message' => $message->getMessageText(), |             'message' => $message->getBody(), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
| @@ -46,7 +46,7 @@ final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler { | |||||||
| 
 | 
 | ||||||
|         $tell = false; |         $tell = false; | ||||||
|         foreach ($prompts as $prompt) { |         foreach ($prompts as $prompt) { | ||||||
|           if (preg_match($prompt, $message->getMessageText())) { |           if (preg_match($prompt, $message->getBody())) { | ||||||
|             $tell = true; |             $tell = true; | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
| @@ -55,7 +55,8 @@ final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler { | |||||||
|         if ($tell) { |         if ($tell) { | ||||||
|           $response = $this->getURI( |           $response = $this->getURI( | ||||||
|             '/chatlog/channel/'.phutil_escape_uri($reply_to).'/'); |             '/chatlog/channel/'.phutil_escape_uri($reply_to).'/'); | ||||||
|           $this->write('PRIVMSG', "{$reply_to} :{$response}"); | 
 | ||||||
|  |           $this->replyTo($message, $response); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         break; |         break; | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| /** | /** | ||||||
|  * @group irc |  * @group irc | ||||||
|  */ |  */ | ||||||
| final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler { | final class PhabricatorBotMacroHandler extends PhabricatorBotHandler { | ||||||
| 
 | 
 | ||||||
|   private $macros; |   private $macros; | ||||||
|   private $regexp; |   private $regexp; | ||||||
| @@ -40,22 +40,22 @@ final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler { | |||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public function receiveMessage(PhabricatorIRCMessage $message) { |   public function receiveMessage(PhabricatorBotMessage $message) { | ||||||
|     if (!$this->init()) { |     if (!$this->init()) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     switch ($message->getCommand()) { |     switch ($message->getCommand()) { | ||||||
|       case 'PRIVMSG': |     case 'MESSAGE': | ||||||
|       $reply_to = $message->getReplyTo(); |       $reply_to = $message->getReplyTo(); | ||||||
|       if (!$reply_to) { |       if (!$reply_to) { | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|         $message = $message->getMessageText(); |       $message_body = $message->getBody(); | ||||||
| 
 | 
 | ||||||
|       $matches = null; |       $matches = null; | ||||||
|         if (!preg_match($this->regexp, $message, $matches)) { |       if (!preg_match($this->regexp, $message_body, $matches)) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @@ -92,7 +92,11 @@ final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler { | |||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|       foreach ($lines as $key => $line) { |       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]); |         unset($this->buffer[$channel][$key]); | ||||||
|         break 2; |         break 2; | ||||||
|       } |       } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -5,23 +5,23 @@ | |||||||
|  * |  * | ||||||
|  * @group irc |  * @group irc | ||||||
|  */ |  */ | ||||||
| final class PhabricatorIRCWhatsNewHandler extends PhabricatorIRCHandler { | final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler { | ||||||
| 
 | 
 | ||||||
|   private $floodblock = 0; |   private $floodblock = 0; | ||||||
| 
 | 
 | ||||||
|   public function receiveMessage(PhabricatorIRCMessage $message) { |   public function receiveMessage(PhabricatorBotMessage $message) { | ||||||
| 
 | 
 | ||||||
|     switch ($message->getCommand()) { |     switch ($message->getCommand()) { | ||||||
|       case 'PRIVMSG': |       case 'MESSAGE': | ||||||
|         $reply_to = $message->getReplyTo(); |         $reply_to = $message->getReplyTo(); | ||||||
|         if (!$reply_to) { |         if (!$reply_to) { | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         $message = $message->getMessageText(); |         $message_body = $message->getBody(); | ||||||
| 
 | 
 | ||||||
|         $prompt = '~what( i|\')?s new\?~i'; |         $prompt = '~what( i|\')?s new\?~i'; | ||||||
|         if (preg_match($prompt, $message)) { |         if (preg_match($prompt, $message_body)) { | ||||||
|           if (time() < $this->floodblock) { |           if (time() < $this->floodblock) { | ||||||
|             return; |             return; | ||||||
|           } |           } | ||||||
| @@ -108,9 +108,15 @@ final class PhabricatorIRCWhatsNewHandler extends PhabricatorIRCHandler { | |||||||
|       $gray = $color.'15'; |       $gray = $color.'15'; | ||||||
|       $bold = chr(2); |       $bold = chr(2); | ||||||
|       $reset = chr(15); |       $reset = chr(15); | ||||||
|       $content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}". |       // Disabling irc-specific styling, at least for now
 | ||||||
|         "{$title}{$reset} - {$gray}{$uri}{$reset}"; |       // $content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}".
 | ||||||
|       $this->write('PRIVMSG',"{$reply_to} :{$content}"); |         // "{$title}{$reset} - {$gray}{$uri}{$reset}";
 | ||||||
|  |       $content = "{$user} {$action} {$title} - {$uri}"; | ||||||
|  |       $this->writeMessage( | ||||||
|  |         id(new PhabricatorBotMessage()) | ||||||
|  |         ->setCommand('MESSAGE') | ||||||
|  |         ->setTarget($reply_to) | ||||||
|  |         ->setBody($content)); | ||||||
|     } |     } | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -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; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -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; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -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; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -30,4 +30,5 @@ final class PhabricatorEventType extends PhutilEventType { | |||||||
|   const TYPE_UI_DDIDRENDEROBJECT            = 'ui.didRenderObject'; |   const TYPE_UI_DDIDRENDEROBJECT            = 'ui.didRenderObject'; | ||||||
|   const TYPE_UI_DIDRENDEROBJECTS            = 'ui.didRenderObjects'; |   const TYPE_UI_DIDRENDEROBJECTS            = 'ui.didRenderObjects'; | ||||||
|  |  | ||||||
|  |   const TYPE_PEOPLE_DIDRENDERMENU           = 'people.didRenderMenu'; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -50,7 +50,10 @@ final class PhabricatorRemarkupRuleEmbedFile | |||||||
|     $file_name = coalesce($options['name'], $file->getName()); |     $file_name = coalesce($options['name'], $file->getName()); | ||||||
|     $options['name'] = $file_name; |     $options['name'] = $file_name; | ||||||
|  |  | ||||||
|  |     $is_viewable_image = $file->isViewableImage(); | ||||||
|  |  | ||||||
|     $attrs = array(); |     $attrs = array(); | ||||||
|  |     if ($is_viewable_image) { | ||||||
|       switch ((string)$options['size']) { |       switch ((string)$options['size']) { | ||||||
|         case 'full': |         case 'full': | ||||||
|           $attrs['src'] = $file->getBestURI(); |           $attrs['src'] = $file->getBestURI(); | ||||||
| @@ -75,12 +78,13 @@ final class PhabricatorRemarkupRuleEmbedFile | |||||||
|           $options['image_class'] = 'phabricator-remarkup-embed-image'; |           $options['image_class'] = 'phabricator-remarkup-embed-image'; | ||||||
|           break; |           break; | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|     $bundle['attrs'] = $attrs; |     $bundle['attrs'] = $attrs; | ||||||
|     $bundle['options'] = $options; |     $bundle['options'] = $options; | ||||||
|  |  | ||||||
|     $bundle['meta'] = array( |     $bundle['meta'] = array( | ||||||
|       'phid'     => $file->getPHID(), |       'phid'     => $file->getPHID(), | ||||||
|       'viewable' => $file->isViewableImage(), |       'viewable' => $is_viewable_image, | ||||||
|       'uri'      => $file->getBestURI(), |       'uri'      => $file->getBestURI(), | ||||||
|       'dUri'     => $file->getDownloadURI(), |       'dUri'     => $file->getDownloadURI(), | ||||||
|       'name'     => $options['name'], |       'name'     => $options['name'], | ||||||
|   | |||||||
| @@ -1105,6 +1105,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { | |||||||
|         'type'    => 'sql', |         'type'    => 'sql', | ||||||
|         'name'    => $this->getPatchPath('20130201.revisionunsubscribed.sql'), |         'name'    => $this->getPatchPath('20130201.revisionunsubscribed.sql'), | ||||||
|       ), |       ), | ||||||
|  |       '20130131.conpherencepics.sql' => array( | ||||||
|  |         'type'    => 'sql', | ||||||
|  |         'name'    => $this->getPatchPath('20130131.conpherencepics.sql'), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										99
									
								
								src/view/form/control/AphrontFormCropControl.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/view/form/control/AphrontFormCropControl.php
									
									
									
									
									
										Normal 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', | ||||||
|  |         ), | ||||||
|  |         '' | ||||||
|  |       ) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -8,22 +8,30 @@ final class PhabricatorMenuView extends AphrontTagView { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function newLabel($name) { |   public function newLabel($name, $key = null) { | ||||||
|     $item = id(new PhabricatorMenuItemView()) |     $item = id(new PhabricatorMenuItemView()) | ||||||
|       ->setType(PhabricatorMenuItemView::TYPE_LABEL) |       ->setType(PhabricatorMenuItemView::TYPE_LABEL) | ||||||
|       ->setName($name); |       ->setName($name); | ||||||
|  |  | ||||||
|  |     if ($key !== null) { | ||||||
|  |       $item->setKey($key); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $this->addMenuItem($item); |     $this->addMenuItem($item); | ||||||
|  |  | ||||||
|     return $item; |     return $item; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function newLink($name, $href) { |   public function newLink($name, $href, $key = null) { | ||||||
|     $item = id(new PhabricatorMenuItemView()) |     $item = id(new PhabricatorMenuItemView()) | ||||||
|       ->setType(PhabricatorMenuItemView::TYPE_LINK) |       ->setType(PhabricatorMenuItemView::TYPE_LINK) | ||||||
|       ->setName($name) |       ->setName($name) | ||||||
|       ->setHref($href); |       ->setHref($href); | ||||||
|  |  | ||||||
|  |     if ($key !== null) { | ||||||
|  |       $item->setKey($key); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $this->addMenuItem($item); |     $this->addMenuItem($item); | ||||||
|  |  | ||||||
|     return $item; |     return $item; | ||||||
|   | |||||||
| @@ -231,6 +231,18 @@ table.aphront-form-control-checkbox-layout th { | |||||||
|   border-color: #669966; |   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 { | .calendar-button { | ||||||
|   display: inline; |   display: inline; | ||||||
|   background: url(/rsrc/image/icon/fatcow/calendar_edit.png) |   background: url(/rsrc/image/icon/fatcow/calendar_edit.png) | ||||||
|   | |||||||
| @@ -29,6 +29,14 @@ | |||||||
|   width: 50px; |   width: 50px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .conpherence-header-pane .custom-header-image { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0px; | ||||||
|  |   left: 0px; | ||||||
|  |   height: 80px; | ||||||
|  |   width: 120px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .conpherence-header-pane .title { | .conpherence-header-pane .title { | ||||||
|   position: relative; |   position: relative; | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
| @@ -39,6 +47,16 @@ | |||||||
|   overflow-x: auto; |   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 { | .conpherence-header-pane .subtitle { | ||||||
|   position: relative; |   position: relative; | ||||||
|   left: 77px; |   left: 77px; | ||||||
| @@ -47,4 +65,34 @@ | |||||||
|   max-width: 80%; |   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; | ||||||
|  | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,3 +5,13 @@ | |||||||
| .phabricator-standard-page-body .aphront-dialog-view { | .phabricator-standard-page-body .aphront-dialog-view { | ||||||
|   margin: 20px auto 0px auto; |   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; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -21,9 +21,18 @@ | |||||||
|   display: inline-block; |   display: inline-block; | ||||||
| } | } | ||||||
|  |  | ||||||
| .pholio-mock-select { | .pholio-mock-select-border { | ||||||
|     border: 1px solid #FF0000; |  | ||||||
|   position: absolute; |   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 { | .pholio-mock-wrapper { | ||||||
|   | |||||||
| @@ -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(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | }); | ||||||
|  |  | ||||||
| @@ -19,14 +19,21 @@ JX.behavior('conpherence-menu', function(config) { | |||||||
|     var messagesRoot = JX.$(config.messages); |     var messagesRoot = JX.$(config.messages); | ||||||
|     var formRoot = JX.$(config.form_pane); |     var formRoot = JX.$(config.form_pane); | ||||||
|     var widgetsRoot = JX.$(config.widgets_pane); |     var widgetsRoot = JX.$(config.widgets_pane); | ||||||
|  |     var menuRoot = JX.$(config.menu_pane); | ||||||
|     JX.DOM.setContent(headerRoot, header); |     JX.DOM.setContent(headerRoot, header); | ||||||
|     JX.DOM.setContent(messagesRoot, messages); |     JX.DOM.setContent(messagesRoot, messages); | ||||||
|     messagesRoot.scrollTop = messagesRoot.scrollHeight; |     messagesRoot.scrollTop = messagesRoot.scrollHeight; | ||||||
|     JX.DOM.setContent(formRoot, form); |     JX.DOM.setContent(formRoot, form); | ||||||
|     JX.DOM.setContent(widgetsRoot, widgets); |     JX.DOM.setContent(widgetsRoot, widgets); | ||||||
|  |  | ||||||
|     for (var i = 0; i < context.parentNode.childNodes.length; i++) { |     var conpherences = JX.DOM.scry( | ||||||
|       var current = context.parentNode.childNodes[i]; |       menuRoot, | ||||||
|  |       'a', | ||||||
|  |       'conpherence-menu-click' | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     for (var i = 0; i < conpherences.length; i++) { | ||||||
|  |       var current = conpherences[i]; | ||||||
|       if (current.id == context.id) { |       if (current.id == context.id) { | ||||||
|         JX.DOM.alterClass(current, 'conpherence-selected', true); |         JX.DOM.alterClass(current, 'conpherence-selected', true); | ||||||
|         JX.DOM.alterClass(current, 'hide-unread-count', true); |         JX.DOM.alterClass(current, 'hide-unread-count', true); | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								webroot/rsrc/js/application/core/behavior-crop.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								webroot/rsrc/js/application/core/behavior-crop.js
									
									
									
									
									
										Normal 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); | ||||||
|  |  | ||||||
|  | }); | ||||||
| @@ -4,7 +4,8 @@ | |||||||
|  *           javelin-stratcom |  *           javelin-stratcom | ||||||
|  *           javelin-dom |  *           javelin-dom | ||||||
|  *           javelin-vector |  *           javelin-vector | ||||||
|  *           javelin-event |  *           javelin-magical-init | ||||||
|  |  *           javelin-request | ||||||
|  */ |  */ | ||||||
| JX.behavior('pholio-mock-view', function(config) { | JX.behavior('pholio-mock-view', function(config) { | ||||||
|   var is_dragging = false; |   var is_dragging = false; | ||||||
| @@ -13,7 +14,9 @@ JX.behavior('pholio-mock-view', function(config) { | |||||||
|   var imageData; |   var imageData; | ||||||
|   var startPos; |   var startPos; | ||||||
|   var endPos; |   var endPos; | ||||||
|   var selection; |  | ||||||
|  |   var selection_border; | ||||||
|  |   var selection_fill; | ||||||
|  |  | ||||||
|   JX.Stratcom.listen( |   JX.Stratcom.listen( | ||||||
|     'click', // Listen for clicks... |     'click', // Listen for clicks... | ||||||
| @@ -35,7 +38,10 @@ JX.behavior('pholio-mock-view', function(config) { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |  | ||||||
|   function draw_rectangle(node, current, init) { |   function draw_rectangle(nodes, current, init) { | ||||||
|  |     for (var ii = 0; ii < nodes.length; ii++) { | ||||||
|  |       var node = nodes[ii]; | ||||||
|  |  | ||||||
|       JX.$V( |       JX.$V( | ||||||
|         Math.abs(current.x-init.x), |         Math.abs(current.x-init.x), | ||||||
|         Math.abs(current.y-init.y)) |         Math.abs(current.y-init.y)) | ||||||
| @@ -46,15 +52,14 @@ JX.behavior('pholio-mock-view', function(config) { | |||||||
|         (current.y-init.y < 0) ? current.y:init.y) |         (current.y-init.y < 0) ? current.y:init.y) | ||||||
|       .setPos(node); |       .setPos(node); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   function getRealXY(parent, point) { |   function getRealXY(parent, point) { | ||||||
|     var pos = {x: (point.x - parent.x), y: (point.y - parent.y)}; |     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; |     pos.x = Math.max(0, Math.min(pos.x, dim.x)); | ||||||
|     else if (pos.x > image.clientWidth) pos.x = image.clientWidth - 1; |     pos.y = Math.max(0, Math.min(pos.y, dim.y)); | ||||||
|  |  | ||||||
|     if (pos.y < 0) pos.y = 0; |  | ||||||
|     else if (pos.y > image.clientHeight) pos.y = image.clientHeight - 2; |  | ||||||
|  |  | ||||||
|     return pos; |     return pos; | ||||||
|   } |   } | ||||||
| @@ -72,17 +77,18 @@ JX.behavior('pholio-mock-view', function(config) { | |||||||
|  |  | ||||||
|     startPos = getRealXY(JX.$V(wrapper),JX.$V(e)); |     startPos = getRealXY(JX.$V(wrapper),JX.$V(e)); | ||||||
|  |  | ||||||
|     selection = JX.$N( |     selection_border = JX.$N( | ||||||
|       'div', |       '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.$V(startPos.x, startPos.y).setPos(selection_border); | ||||||
|  |     JX.$V(startPos.x, startPos.y).setPos(selection_fill); | ||||||
|     JX.DOM.appendContent(wrapper, selection); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     JX.DOM.appendContent(wrapper, [selection_border, selection_fill]); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   JX.enableDispatch(document.body, 'mousemove'); |   JX.enableDispatch(document.body, 'mousemove'); | ||||||
| @@ -91,7 +97,10 @@ JX.behavior('pholio-mock-view', function(config) { | |||||||
|       return; |       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( |   JX.Stratcom.listen( | ||||||
| @@ -107,17 +116,31 @@ JX.behavior('pholio-mock-view', function(config) { | |||||||
|  |  | ||||||
|       comment = window.prompt("Add your comment"); |       comment = window.prompt("Add your comment"); | ||||||
|       if (comment == null || comment == "") { |       if (comment == null || comment == "") { | ||||||
|         selection.remove(); |         JX.DOM.remove(selection_border); | ||||||
|  |         JX.DOM.remove(selection_fill); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       selection.title = comment; |       selection_fill.title = comment; | ||||||
|  |  | ||||||
|       console.log("ImageID: " + imageData['imageID'] + |       var saveURL = "/pholio/inline/" + imageData['imageID'] + "/"; | ||||||
|         ", coords: (" + Math.min(startPos.x, endPos.x) + "," + |  | ||||||
|         Math.min(startPos.y, endPos.y) + ") -> (" + |       var inlineComment = new JX.Request(saveURL, function(r) { | ||||||
|         Math.max(startPos.x,endPos.x) + "," + Math.max(startPos.y,endPos.y) + |  | ||||||
|         "), comment: " + comment); |       }); | ||||||
|  |  | ||||||
|  |       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(); | ||||||
|  |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 epriestley
					epriestley