diff --git a/webroot/rsrc/favicons/apple-touch-icon-120x120.png b/resources/builtin/favicon/default-120x120.png similarity index 100% rename from webroot/rsrc/favicons/apple-touch-icon-120x120.png rename to resources/builtin/favicon/default-120x120.png diff --git a/webroot/rsrc/favicons/favicon-128.png b/resources/builtin/favicon/default-128x128.png similarity index 100% rename from webroot/rsrc/favicons/favicon-128.png rename to resources/builtin/favicon/default-128x128.png diff --git a/webroot/rsrc/favicons/apple-touch-icon-152x152.png b/resources/builtin/favicon/default-152x152.png similarity index 100% rename from webroot/rsrc/favicons/apple-touch-icon-152x152.png rename to resources/builtin/favicon/default-152x152.png diff --git a/webroot/rsrc/favicons/apple-touch-icon-76x76.png b/resources/builtin/favicon/default-76x76.png similarity index 100% rename from webroot/rsrc/favicons/apple-touch-icon-76x76.png rename to resources/builtin/favicon/default-76x76.png diff --git a/resources/builtin/favicon/dot-pink-64x64.png b/resources/builtin/favicon/dot-pink-64x64.png new file mode 100644 index 0000000000..99595c8e1a Binary files /dev/null and b/resources/builtin/favicon/dot-pink-64x64.png differ diff --git a/resources/builtin/favicon/dot-red-64x64.png b/resources/builtin/favicon/dot-red-64x64.png new file mode 100644 index 0000000000..e727375be0 Binary files /dev/null and b/resources/builtin/favicon/dot-red-64x64.png differ diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 38343475d0..f5a5a57904 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,13 +10,11 @@ return array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', 'core.pkg.css' => 'c218ed53', - 'core.pkg.js' => '8b7400e7', - 'darkconsole.pkg.js' => '1f9a31bc', + 'core.pkg.js' => '8581cd02', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', 'diffusion.pkg.css' => 'a2d17c7d', 'diffusion.pkg.js' => '6134c5a1', - 'favicon.ico' => '30672e08', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '4d7e79c8', 'rsrc/audio/basic/alert.mp3' => '98461568', @@ -25,7 +23,7 @@ return array( 'rsrc/audio/basic/tap.mp3' => 'fc2fd796', 'rsrc/audio/basic/ting.mp3' => '17660001', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', - 'rsrc/css/aphront/dark-console.css' => 'f7b071f1', + 'rsrc/css/aphront/dark-console.css' => '0e14e8f6', 'rsrc/css/aphront/dialog-view.css' => '6bfc244b', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => '84cc6640', @@ -47,7 +45,7 @@ return array( 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '4615667b', 'rsrc/css/application/config/config-template.css' => '8f18fa41', - 'rsrc/css/application/config/setup-issue.css' => '7dae7f18', + 'rsrc/css/application/config/setup-issue.css' => '30ee0173', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', 'rsrc/css/application/conpherence/color.css' => 'abb4c358', 'rsrc/css/application/conpherence/durable-column.css' => '89ea6bef', @@ -270,28 +268,8 @@ return array( 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82', 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa', - 'rsrc/favicons/apple-touch-icon-114x114.png' => '12a24178', - 'rsrc/favicons/apple-touch-icon-120x120.png' => '0d1543c7', - 'rsrc/favicons/apple-touch-icon-144x144.png' => '8043b5a5', - 'rsrc/favicons/apple-touch-icon-152x152.png' => '65905ecd', - 'rsrc/favicons/apple-touch-icon-57x57.png' => '2bfc7b0a', - 'rsrc/favicons/apple-touch-icon-60x60.png' => '8ff52925', - 'rsrc/favicons/apple-touch-icon-72x72.png' => 'a2bb65d6', - 'rsrc/favicons/apple-touch-icon-76x76.png' => '2d061a11', - 'rsrc/favicons/favicon-128.png' => '72f7e812', 'rsrc/favicons/favicon-16x16.png' => 'fc6275ba', - 'rsrc/favicons/favicon-196x196.png' => '95db275e', - 'rsrc/favicons/favicon-32x32.png' => '5bd18b6c', - 'rsrc/favicons/favicon-96x96.png' => '7242c8e9', - 'rsrc/favicons/favicon-mention.ico' => '1fdd0fa4', - 'rsrc/favicons/favicon-message.ico' => '115bc010', - 'rsrc/favicons/favicon.ico' => 'cdb11121', 'rsrc/favicons/mask-icon.svg' => 'e132a80f', - 'rsrc/favicons/mstile-144x144.png' => '310c2ee5', - 'rsrc/favicons/mstile-150x150.png' => '74bf5133', - 'rsrc/favicons/mstile-310x150.png' => '4a49d3ee', - 'rsrc/favicons/mstile-310x310.png' => 'a52ab264', - 'rsrc/favicons/mstile-70x70.png' => '5edce7b8', 'rsrc/image/BFCFDA.png' => 'd5ec91f4', 'rsrc/image/actions/edit.png' => '2fc41442', 'rsrc/image/avatar.png' => '17d346a4', @@ -374,7 +352,7 @@ return array( 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => 'e1d4b11a', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2', - 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '4cc4f460', + 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '599a8f5f', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289', 'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443', @@ -412,7 +390,7 @@ return array( 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', - 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => 'e5822781', + 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', @@ -467,7 +445,7 @@ return array( 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', - 'rsrc/js/core/Notification.js' => '008faf9c', + 'rsrc/js/core/Notification.js' => '4f774dac', 'rsrc/js/core/Prefab.js' => '77b0ae28', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '320810c8', @@ -483,7 +461,6 @@ return array( 'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96', 'rsrc/js/core/behavior-device.js' => 'a3714c76', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22', - 'rsrc/js/core/behavior-error-log.js' => '6882e80a', 'rsrc/js/core/behavior-fancy-datepicker.js' => 'ecf4e799', 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', @@ -522,7 +499,7 @@ return array( 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/darkconsole/DarkLog.js' => 'c8e1ffe3', 'rsrc/js/core/darkconsole/DarkMessage.js' => 'c48cccdd', - 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '17bb8539', + 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '66888767', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', @@ -541,7 +518,7 @@ return array( 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', - 'aphront-dark-console-css' => 'f7b071f1', + 'aphront-dark-console-css' => '0e14e8f6', 'aphront-dialog-view-css' => '6bfc244b', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => '84cc6640', @@ -589,7 +566,7 @@ return array( 'javelin-aphlict' => 'e1d4b11a', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => 'caade6f2', - 'javelin-behavior-aphlict-listen' => '4cc4f460', + 'javelin-behavior-aphlict-listen' => '599a8f5f', 'javelin-behavior-aphlict-status' => '5e2634b9', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22', @@ -609,7 +586,7 @@ return array( 'javelin-behavior-conpherence-pontificate' => '55616e04', 'javelin-behavior-conpherence-search' => '9bbf3762', 'javelin-behavior-countdown-timer' => 'e4cc26b3', - 'javelin-behavior-dark-console' => '17bb8539', + 'javelin-behavior-dark-console' => '66888767', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '408bf173', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', @@ -629,12 +606,11 @@ return array( 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', - 'javelin-behavior-doorkeeper-tag' => 'e5822781', + 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', 'javelin-behavior-editengine-reorder-configs' => 'd7a74243', 'javelin-behavior-editengine-reorder-fields' => 'b59e1e96', - 'javelin-behavior-error-log' => '6882e80a', 'javelin-behavior-event-all-day' => 'b41537c9', 'javelin-behavior-fancy-datepicker' => 'ecf4e799', 'javelin-behavior-global-drag-and-drop' => '960f6a39', @@ -796,7 +772,7 @@ return array( 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-main-menu-view' => '1802a242', 'phabricator-nav-view-css' => 'a9e3e6d5', - 'phabricator-notification' => '008faf9c', + 'phabricator-notification' => '4f774dac', 'phabricator-notification-css' => '457861ec', 'phabricator-notification-menu-css' => '10685bd4', 'phabricator-object-selector-css' => '85ee8ce6', @@ -902,7 +878,7 @@ return array( 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', 'releeph-request-typeahead-css' => '667a48ae', - 'setup-issue-css' => '7dae7f18', + 'setup-issue-css' => '30ee0173', 'sprite-login-css' => '396f3c3a', 'sprite-tokens-css' => '9cdfd599', 'syntax-default-css' => '9923583c', @@ -918,13 +894,6 @@ return array( 'javelin-typeahead-preloaded-source', 'javelin-util', ), - '008faf9c' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'phabricator-notification-css', - ), '013ffff9' => array( 'javelin-install', 'javelin-util', @@ -1008,16 +977,6 @@ return array( 'aphront-typeahead-control-css', 'phui-tag-view-css', ), - '17bb8539' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-util', - 'javelin-dom', - 'javelin-request', - 'phabricator-keyboard-shortcut', - 'phabricator-darklog', - 'phabricator-darkmessage', - ), '1802a242' => array( 'phui-theme-css', ), @@ -1045,6 +1004,13 @@ return array( 'javelin-request', 'javelin-uri', ), + '1db13e70' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-json', + 'javelin-workflow', + 'javelin-magical-init', + ), '1f6794f6' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1286,20 +1252,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - '4cc4f460' => array( - 'javelin-behavior', - 'javelin-aphlict', - 'javelin-stratcom', - 'javelin-request', - 'javelin-uri', - 'javelin-dom', - 'javelin-json', - 'javelin-router', - 'javelin-util', - 'javelin-leader', - 'javelin-sound', - 'phabricator-notification', - ), '4d863052' => array( 'javelin-dom', 'javelin-util', @@ -1316,6 +1268,13 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + '4f774dac' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'phabricator-notification-css', + ), '503e17fd' => array( 'javelin-install', 'javelin-typeahead-source', @@ -1370,6 +1329,20 @@ return array( 'javelin-uri', 'phabricator-file-upload', ), + '599a8f5f' => array( + 'javelin-behavior', + 'javelin-aphlict', + 'javelin-stratcom', + 'javelin-request', + 'javelin-uri', + 'javelin-dom', + 'javelin-json', + 'javelin-router', + 'javelin-util', + 'javelin-leader', + 'javelin-sound', + 'phabricator-notification', + ), '59a7976a' => array( 'javelin-install', 'javelin-dom', @@ -1430,6 +1403,16 @@ return array( 'javelin-typeahead-ondemand-source', 'javelin-util', ), + 66888767 => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-util', + 'javelin-dom', + 'javelin-request', + 'phabricator-keyboard-shortcut', + 'phabricator-darklog', + 'phabricator-darkmessage', + ), '66a6def1' => array( 'javelin-behavior', 'javelin-dom', @@ -1443,9 +1426,6 @@ return array( 'javelin-dom', 'phabricator-notification', ), - '6882e80a' => array( - 'javelin-dom', - ), '68af71ca' => array( 'javelin-install', 'javelin-dom', @@ -2108,13 +2088,6 @@ return array( 'javelin-behavior', 'javelin-dom', ), - 'e5822781' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-json', - 'javelin-workflow', - 'javelin-magical-init', - ), 'e74b7517' => array( 'javelin-install', 'phuix-button-view', @@ -2364,10 +2337,6 @@ return array( 'javelin-behavior-user-menu', 'phabricator-favicon', ), - 'darkconsole.pkg.js' => array( - 'javelin-behavior-dark-console', - 'javelin-behavior-error-log', - ), 'differential.pkg.css' => array( 'differential-core-view-css', 'differential-changeset-view-css', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index abbf1d77e2..3cec508678 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -224,8 +224,4 @@ return array( 'javelin-behavior-maniphest-subpriority-editor', 'javelin-behavior-maniphest-list-editor', ), - 'darkconsole.pkg.js' => array( - 'javelin-behavior-dark-console', - 'javelin-behavior-error-log', - ), ); diff --git a/resources/sql/autopatches/20180312.reviewers.01.options.sql b/resources/sql/autopatches/20180312.reviewers.01.options.sql new file mode 100644 index 0000000000..159426614d --- /dev/null +++ b/resources/sql/autopatches/20180312.reviewers.01.options.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_reviewer + ADD options LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql b/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql new file mode 100644 index 0000000000..d509011f73 --- /dev/null +++ b/resources/sql/autopatches/20180312.reviewers.02.optionsdefault.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_differential.differential_reviewer + SET options = '{}' WHERE options = ''; diff --git a/scripts/install/install_ubuntu.sh b/scripts/install/install_ubuntu.sh index b3e678450d..5408cdb3e1 100755 --- a/scripts/install/install_ubuntu.sh +++ b/scripts/install/install_ubuntu.sh @@ -45,7 +45,7 @@ set +x sudo apt-get -qq update sudo apt-get install \ $GIT mysql-server apache2 dpkg-dev \ - php5 php5-mysql php5-gd php5-dev php5-curl php-apc php5-cli php5-json + php5 php5-mysqlnd php5-gd php5-dev php5-curl php-apc php5-cli php5-json # Enable mod_rewrite sudo a2enmod rewrite diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8677f47b6e..4caa7f563d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2055,6 +2055,8 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php', 'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php', 'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php', + 'PhabricatorApplicationTransactionWarningException' => 'applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php', + 'PhabricatorApplicationTransactionWarningResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php', 'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php', 'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php', 'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php', @@ -2953,6 +2955,8 @@ phutil_register_library_map(array( 'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php', + 'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php', + 'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php', 'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php', 'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php', 'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php', @@ -4331,7 +4335,6 @@ phutil_register_library_map(array( 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', - 'PhabricatorSystemFaviconController' => 'applications/system/controller/PhabricatorSystemFaviconController.php', 'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php', 'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php', 'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php', @@ -7483,6 +7486,8 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionView' => 'AphrontView', + 'PhabricatorApplicationTransactionWarningException' => 'Exception', + 'PhabricatorApplicationTransactionWarningResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationsApplication' => 'PhabricatorApplication', 'PhabricatorApplicationsController' => 'PhabricatorController', @@ -8512,6 +8517,8 @@ phutil_register_library_map(array( 'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', + 'PhabricatorFaviconRef' => 'Phobject', + 'PhabricatorFaviconRefQuery' => 'Phobject', 'PhabricatorFavoritesApplication' => 'PhabricatorApplication', 'PhabricatorFavoritesController' => 'PhabricatorController', 'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', @@ -10142,7 +10149,6 @@ phutil_register_library_map(array( 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', - 'PhabricatorSystemFaviconController' => 'PhabricatorController', 'PhabricatorSystemReadOnlyController' => 'PhabricatorController', 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', diff --git a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php index 5382a27b7a..cf3148b5be 100644 --- a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php @@ -67,6 +67,51 @@ final class PhabricatorPHPConfigSetupCheck extends PhabricatorSetupCheck { ->addPHPConfig('always_populate_raw_post_data'); } + if (!extension_loaded('mysqli')) { + $summary = pht( + 'Install the MySQLi extension to improve database behavior.'); + + $message = pht( + 'PHP is currently using the very old "mysql" extension to interact '. + 'with the database. You should install the newer "mysqli" extension '. + 'to improve behaviors (like error handling and query timeouts).'. + "\n\n". + 'Phabricator will work with the older extension, but upgrading to the '. + 'newer extension is recommended.'. + "\n\n". + 'You may be able to install the extension with a command like: %s', + + // NOTE: We're intentionally telling you to install "mysqlnd" here; on + // Ubuntu, there's no separate "mysqli" package. + phutil_tag('tt', array(), 'sudo apt-get install php5-mysqlnd')); + + $this->newIssue('php.mysqli') + ->setName(pht('MySQLi Extension Not Available')) + ->setSummary($summary) + ->setMessage($message); + } else if (!defined('MYSQLI_ASYNC')) { + $summary = pht( + 'Configure the MySQL Native Driver to improve database behavior.'); + + $message = pht( + 'PHP is currently using the older MySQL external driver instead of '. + 'the newer MySQL native driver. The older driver lacks options and '. + 'features (like support for query timeouts) which allow Phabricator '. + 'to interact better with the database.'. + "\n\n". + 'Phabricator will work with the older driver, but upgrading to the '. + 'native driver is recommended.'. + "\n\n". + 'You may be able to install the native driver with a command like: %s', + phutil_tag('tt', array(), 'sudo apt-get install php5-mysqlnd')); + + + $this->newIssue('php.myqlnd') + ->setName(pht('MySQL Native Driver Not Available')) + ->setSummary($summary) + ->setMessage($message); + } + } } diff --git a/src/applications/config/option/PhabricatorUIConfigOptions.php b/src/applications/config/option/PhabricatorUIConfigOptions.php index 7b2823f6ba..391ba7e123 100644 --- a/src/applications/config/option/PhabricatorUIConfigOptions.php +++ b/src/applications/config/option/PhabricatorUIConfigOptions.php @@ -64,6 +64,10 @@ EOJSON; "Phabricator logo in the site header.\n\n". " - **Wordmark**: Choose new text to display next to the logo. ". "By default, the header displays //Phabricator//.\n\n")), + $this->newOption('ui.favicons', 'wild', array()) + ->setSummary(pht('Customize favicons.')) + ->setDescription(pht('Customize favicons.')) + ->setLocked(true), $this->newOption('ui.footer-items', $footer_type, array()) ->setSummary( pht( diff --git a/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php b/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php index 0d9604dcba..213b578c75 100644 --- a/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php +++ b/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php @@ -39,10 +39,13 @@ final class DarkConsoleErrorLogPlugin extends DarkConsolePlugin { $file = $row['file']; $line = $row['line']; - $tag = phutil_tag( + $tag = javelin_tag( 'a', array( - 'onclick' => jsprintf('show_details(%d)', $index), + 'sigil' => 'darkconsole-expand', + 'meta' => array( + 'expandID' => 'row-details-'.$index, + ), ), $row['str'].' at ['.basename($file).':'.$line.']'); $rows[] = array($tag); diff --git a/src/applications/console/plugin/DarkConsoleServicesPlugin.php b/src/applications/console/plugin/DarkConsoleServicesPlugin.php index 7d779027da..a14ed4b541 100644 --- a/src/applications/console/plugin/DarkConsoleServicesPlugin.php +++ b/src/applications/console/plugin/DarkConsoleServicesPlugin.php @@ -29,6 +29,18 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { return false; } + public function didStartup() { + $should_analyze = self::isQueryAnalyzerRequested(); + + if ($should_analyze) { + PhutilServiceProfiler::getInstance() + ->setCollectStackTraces(true); + } + + return null; + } + + /** * @phutil-external-symbol class PhabricatorStartup */ @@ -266,6 +278,16 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { $info, $analysis, ); + + if ($row['trace']) { + $rows[] = array( + null, + null, + null, + $row['trace'], + null, + ); + } } $table = new AphrontTableView($rows); @@ -274,7 +296,7 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin { null, 'n', 'n', - 'wide', + 'wide prewrap', '', )); $table->setHeaders( diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index d04b0d684a..9882246608 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -154,59 +154,41 @@ final class DifferentialTransactionEditor $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST; - $is_sticky_accept = PhabricatorEnv::getEnvConfig( - 'differential.sticky-accept'); - - $downgrade_rejects = false; - $downgrade_accepts = false; + $want_downgrade = array(); + $must_downgrade = array(); if ($this->getIsCloseByCommit()) { // Never downgrade reviewers when we're closing a revision after a // commit. } else { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: - $downgrade_rejects = true; - if (!$is_sticky_accept) { - // If "sticky accept" is disabled, also downgrade the accepts. - $downgrade_accepts = true; - } + $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; + $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; break; case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE: - $downgrade_rejects = true; - if ((!$is_sticky_accept) || - (!$object->isChangePlanned())) { - // If the old state isn't "changes planned", downgrade the accepts. - // This exception allows an accepted revision to go through - // "Plan Changes" -> "Request Review" and return to "accepted" if - // the author didn't update the revision, essentially undoing the - // "Plan Changes". - $downgrade_accepts = true; + if (!$object->isChangePlanned()) { + // If the old state isn't "Changes Planned", downgrade the accepts + // even if they're sticky. + + // We don't downgrade for "Changes Planned" to allow an author to + // undo a "Plan Changes" by immediately following it up with a + // "Request Review". + $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; + $must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; } + $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; break; } } - $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED; - $new_reject = DifferentialReviewerStatus::STATUS_REJECTED; - $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER; - $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER; - - $downgrade = array(); - if ($downgrade_accepts) { - $downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; - } - - if ($downgrade_rejects) { - $downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; - } - - if ($downgrade) { + if ($want_downgrade) { $void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE; $results[] = id(new DifferentialTransaction()) ->setTransactionType($void_type) ->setIgnoreOnNoEffect(true) - ->setNewValue($downgrade); + ->setMetadataValue('void.force', $must_downgrade) + ->setNewValue($want_downgrade); } $is_commandeer = false; diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php index e3f9bdaf8d..823047f218 100644 --- a/src/applications/differential/storage/DifferentialReviewer.php +++ b/src/applications/differential/storage/DifferentialReviewer.php @@ -10,12 +10,16 @@ final class DifferentialReviewer protected $lastCommentDiffPHID; protected $lastActorPHID; protected $voidedPHID; + protected $options = array(); private $authority = array(); private $changesets = self::ATTACHABLE; protected function getConfiguration() { return array( + self::CONFIG_SERIALIZATION => array( + 'options' => self::SERIALIZATION_JSON, + ), self::CONFIG_COLUMN_SCHEMA => array( 'reviewerStatus' => 'text64', 'lastActionDiffPHID' => 'phid?', @@ -64,6 +68,15 @@ final class DifferentialReviewer return $this->assertAttached($this->changesets); } + public function setOption($key, $value) { + $this->options[$key] = $value; + return $this; + } + + public function getOption($key, $default = null) { + return idx($this->options, $key, $default); + } + public function isResigned() { $status_resigned = DifferentialReviewerStatus::STATUS_RESIGNED; return ($this->getReviewerStatus() == $status_resigned); diff --git a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php index 4169c3e4dc..561e57c7c4 100644 --- a/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php @@ -66,11 +66,6 @@ final class DifferentialRevisionCommandeerTransaction 'been closed. You can only commandeer open revisions.')); } - if ($object->isDraft()) { - throw new Exception( - pht('You can not commandeer a draft revision.')); - } - if ($this->isViewerRevisionAuthor($object, $viewer)) { throw new Exception( pht( diff --git a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php index b112eb638f..8aec75d2fb 100644 --- a/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionReviewTransaction.php @@ -164,7 +164,14 @@ abstract class DifferentialRevisionReviewTransaction DifferentialRevision $revision, PhabricatorUser $viewer, $value, - $status) { + $status, + array $reviewer_options = array()) { + + PhutilTypeSpec::checkMap( + $reviewer_options, + array( + 'sticky' => 'optional bool', + )); $map = array(); @@ -253,6 +260,8 @@ abstract class DifferentialRevisionReviewTransaction // accepted. $reviewer->setVoidedPHID(null); + $reviewer->setOption('sticky', idx($reviewer_options, 'sticky')); + try { $reviewer->save(); } catch (AphrontDuplicateKeyQueryException $ex) { diff --git a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php index 5073a9c464..331c637aad 100644 --- a/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionVoidTransaction.php @@ -16,21 +16,43 @@ final class DifferentialRevisionVoidTransaction } public function generateNewValue($object, $value) { - $table = new DifferentialReviewer(); - $table_name = $table->getTableName(); - $conn = $table->establishConnection('w'); - - $rows = queryfx_all( - $conn, - 'SELECT reviewerPHID FROM %T - WHERE revisionPHID = %s - AND voidedPHID IS NULL - AND reviewerStatus IN (%Ls)', - $table_name, + $reviewers = id(new DifferentialReviewer())->loadAllWhere( + 'revisionPHID = %s + AND voidedPHID IS NULL + AND reviewerStatus IN (%Ls)', $object->getPHID(), $value); - return ipull($rows, 'reviewerPHID'); + $must_downgrade = $this->getMetadataValue('void.force', array()); + $must_downgrade = array_fuse($must_downgrade); + + $default = PhabricatorEnv::getEnvConfig('differential.sticky-accept'); + foreach ($reviewers as $key => $reviewer) { + $status = $reviewer->getReviewerStatus(); + + // If this void is forced, always downgrade. For example, this happens + // when an author chooses "Request Review": existing reviews are always + // voided, even if they're sticky. + if (isset($must_downgrade[$status])) { + continue; + } + + // Otherwise, if this is a sticky accept, don't void it. Accepts may be + // explicitly sticky or unsticky, or they'll use the default value if + // no value is specified. + $is_sticky = $reviewer->getOption('sticky'); + $is_sticky = coalesce($is_sticky, $default); + + if ($status === DifferentialReviewerStatus::STATUS_ACCEPTED) { + if ($is_sticky) { + unset($reviewers[$key]); + continue; + } + } + } + + + return mpull($reviewers, 'getReviewerPHID'); } public function getTransactionHasEffect($object, $old, $new) { diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php index 199f049dce..920f2eeb91 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php @@ -97,6 +97,7 @@ final class DoorkeeperBridgeJIRA extends DoorkeeperBridge { $ref->setAttribute('title', idx($fields, 'summary')); $ref->setAttribute('description', idx($result, 'description')); + $ref->setAttribute('shortname', $result['key']); $obj = $ref->getExternalObject(); if ($obj->getID()) { diff --git a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php index 9957b82026..5a886cba3e 100644 --- a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php +++ b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php @@ -13,17 +13,13 @@ final class DoorkeeperTagsController extends PhabricatorController { } $refs = array(); - $id_map = array(); - foreach ($tags as $tag_spec) { + foreach ($tags as $key => $tag_spec) { $tag = $tag_spec['ref']; $ref = id(new DoorkeeperObjectRef()) ->setApplicationType($tag[0]) ->setApplicationDomain($tag[1]) ->setObjectType($tag[2]) ->setObjectID($tag[3]); - - $key = $ref->getObjectKey(); - $id_map[$key] = $tag_spec['id']; $refs[$key] = $ref; } @@ -43,19 +39,30 @@ final class DoorkeeperTagsController extends PhabricatorController { continue; } - $id = $id_map[$key]; + $tag_spec = $tags[$key]; + + $id = $tag_spec['id']; + $view = idx($tag_spec, 'view'); + + $is_short = ($view == 'short'); + + if ($is_short) { + $name = $ref->getShortName(); + } else { + $name = $ref->getFullName(); + } $tag = id(new PHUITagView()) ->setID($id) - ->setName($ref->getFullName()) + ->setName($name) ->setHref($uri) ->setType(PHUITagView::TYPE_OBJECT) ->setExternal(true) ->render(); $results[] = array( - 'id' => $id, - 'markup' => $tag, + 'id' => $id, + 'markup' => $tag, ); } diff --git a/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php b/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php index 3cf2a8dbe2..bbccc7a5d8 100644 --- a/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php +++ b/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php @@ -107,6 +107,13 @@ final class DoorkeeperObjectRef extends Phobject { pht('External Object')); } + public function getShortName() { + return coalesce( + $this->getAttribute('shortname'), + $this->getAttribute('name'), + pht('External Object')); + } + public function getObjectKey() { if (!$this->objectKey) { $this->objectKey = PhabricatorHash::digestForIndex( diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php index b24d53705c..9654d12bb8 100644 --- a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php +++ b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php @@ -4,11 +4,41 @@ abstract class DoorkeeperRemarkupRule extends PhutilRemarkupRule { const KEY_TAGS = 'doorkeeper.tags'; + const VIEW_FULL = 'full'; + const VIEW_SHORT = 'short'; + public function getPriority() { return 350.0; } protected function addDoorkeeperTag(array $spec) { + PhutilTypeSpec::checkMap( + $spec, + array( + 'href' => 'string', + 'tag' => 'map', + + 'name' => 'optional string', + 'view' => 'optional string', + )); + + $spec = $spec + array( + 'view' => self::VIEW_FULL, + ); + + $views = array( + self::VIEW_FULL, + self::VIEW_SHORT, + ); + $views = array_fuse($views); + if (!isset($views[$spec['view']])) { + throw new Exception( + pht( + 'Unsupported Doorkeeper tag view mode "%s". Supported modes are: %s.', + $spec['view'], + implode(', ', $views))); + } + $key = self::KEY_TAGS; $engine = $this->getEngine(); $token = $engine->storeText(get_class($this)); @@ -36,19 +66,26 @@ abstract class DoorkeeperRemarkupRule extends PhutilRemarkupRule { $refs = array(); foreach ($tags as $spec) { - $tag_id = celerity_generate_unique_node_id(); + $href = $spec['href']; + $name = idx($spec, 'name', $href); - $refs[] = array( - 'id' => $tag_id, - ) + $spec['tag']; + $this->assertFlatText($href); + $this->assertFlatText($name); if ($this->getEngine()->isTextMode()) { - $view = $spec['href']; + $view = "{$name} <{$href}>"; } else { + $tag_id = celerity_generate_unique_node_id(); + + $refs[] = array( + 'id' => $tag_id, + 'view' => $spec['view'], + ) + $spec['tag']; + $view = id(new PHUITagView()) ->setID($tag_id) - ->setName($this->assertFlatText($spec['href'])) - ->setHref($this->assertFlatText($spec['href'])) + ->setName($name) + ->setHref($href) ->setType(PHUITagView::TYPE_OBJECT) ->setExternal(true); } @@ -56,7 +93,9 @@ abstract class DoorkeeperRemarkupRule extends PhutilRemarkupRule { $engine->overwriteStoredText($spec['token'], $view); } - Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); + if ($refs) { + Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); + } $engine->setTextMetadata($key, array()); } diff --git a/src/applications/files/favicon/PhabricatorFaviconRef.php b/src/applications/files/favicon/PhabricatorFaviconRef.php new file mode 100644 index 0000000000..d6c2fd80a8 --- /dev/null +++ b/src/applications/files/favicon/PhabricatorFaviconRef.php @@ -0,0 +1,447 @@ +emblems = array(null, null, null, null); + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setWidth($width) { + $this->width = $width; + return $this; + } + + public function getWidth() { + return $this->width; + } + + public function setHeight($height) { + $this->height = $height; + return $this; + } + + public function getHeight() { + return $this->height; + } + + public function setEmblems(array $emblems) { + if (count($emblems) !== 4) { + throw new Exception( + pht( + 'Expected four elements in icon emblem list. To omit an emblem, '. + 'pass "null".')); + } + + $this->emblems = $emblems; + return $this; + } + + public function getEmblems() { + return $this->emblems; + } + + public function setURI($uri) { + $this->uri = $uri; + return $this; + } + + public function getURI() { + return $this->uri; + } + + public function setCacheKey($cache_key) { + $this->cacheKey = $cache_key; + return $this; + } + + public function getCacheKey() { + return $this->cacheKey; + } + + public function newDigest() { + return PhabricatorHash::digestForIndex(serialize($this->toDictionary())); + } + + public function toDictionary() { + return array( + 'width' => $this->width, + 'height' => $this->height, + 'emblems' => $this->emblems, + ); + } + + public static function newConfigurationDigest() { + $all_resources = self::getAllResources(); + + // Because we need to access this cache on every page, it's very sticky. + // Try to dirty it automatically if any relevant configuration changes. + $inputs = array( + 'resources' => $all_resources, + 'prod' => PhabricatorEnv::getProductionURI('/'), + 'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'), + 'havepng' => function_exists('imagepng'), + ); + + return PhabricatorHash::digestForIndex(serialize($inputs)); + } + + private static function getAllResources() { + $custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons'); + + foreach ($custom_resources as $key => $custom_resource) { + $custom_resources[$key] = array( + 'source-type' => 'file', + 'default' => false, + ) + $custom_resource; + } + + $builtin_resources = self::getBuiltinResources(); + + return array_merge($builtin_resources, $custom_resources); + } + + private static function getBuiltinResources() { + return array( + array( + 'source-type' => 'builtin', + 'source' => 'favicon/default-76x76.png', + 'version' => 1, + 'width' => 76, + 'height' => 76, + 'default' => true, + ), + array( + 'source-type' => 'builtin', + 'source' => 'favicon/default-120x120.png', + 'version' => 1, + 'width' => 120, + 'height' => 120, + 'default' => true, + ), + array( + 'source-type' => 'builtin', + 'source' => 'favicon/default-128x128.png', + 'version' => 1, + 'width' => 128, + 'height' => 128, + 'default' => true, + ), + array( + 'source-type' => 'builtin', + 'source' => 'favicon/default-152x152.png', + 'version' => 1, + 'width' => 152, + 'height' => 152, + 'default' => true, + ), + array( + 'source-type' => 'builtin', + 'source' => 'favicon/dot-pink-64x64.png', + 'version' => 1, + 'width' => 64, + 'height' => 64, + 'emblem' => 'dot-pink', + 'default' => true, + ), + array( + 'source-type' => 'builtin', + 'source' => 'favicon/dot-red-64x64.png', + 'version' => 1, + 'width' => 64, + 'height' => 64, + 'emblem' => 'dot-red', + 'default' => true, + ), + ); + } + + public function newURI() { + $dst_w = $this->getWidth(); + $dst_h = $this->getHeight(); + + $template = $this->newTemplateFile(null, $dst_w, $dst_h); + $template_file = $template['file']; + + $cache = $this->loadCachedFile($template_file); + if ($cache) { + return $cache->getViewURI(); + } + + $data = $this->newCompositedFavicon($template); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + $caught = null; + try { + $favicon_file = $this->newFaviconFile($data); + + $xform = id(new PhabricatorTransformedFile()) + ->setOriginalPHID($template_file->getPHID()) + ->setTransformedPHID($favicon_file->getPHID()) + ->setTransform($this->getCacheKey()); + + try { + $xform->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + unset($unguarded); + + $cache = $this->loadCachedFile($template_file); + if (!$cache) { + throw $ex; + } + + id(new PhabricatorDestructionEngine()) + ->destroyObject($favicon_file); + + return $cache->getViewURI(); + } + } catch (Exception $ex) { + $caught = $ex; + } + + unset($unguarded); + + if ($caught) { + throw $caught; + } + + return $favicon_file->getViewURI(); + } + + private function loadCachedFile(PhabricatorFile $template_file) { + $viewer = $this->getViewer(); + + $xform = id(new PhabricatorTransformedFile())->loadOneWhere( + 'originalPHID = %s AND transform = %s', + $template_file->getPHID(), + $this->getCacheKey()); + if (!$xform) { + return null; + } + + return id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($xform->getTransformedPHID())) + ->executeOne(); + } + + private function newCompositedFavicon($template) { + $dst_w = $this->getWidth(); + $dst_h = $this->getHeight(); + $src_w = $template['width']; + $src_h = $template['height']; + + $template_data = $template['file']->loadFileData(); + + if (!function_exists('imagecreatefromstring')) { + return $template_data; + } + + $src = @imagecreatefromstring($template_data); + if (!$src) { + return $template_data; + } + + $dst = imagecreatetruecolor($dst_w, $dst_h); + imagesavealpha($dst, true); + + $transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127); + imagefill($dst, 0, 0, $transparent); + + imagecopyresampled( + $dst, + $src, + 0, + 0, + 0, + 0, + $dst_w, + $dst_h, + $src_w, + $src_h); + + // Now, copy any icon emblems on top of the image. These are dots or other + // marks used to indicate status information. + $emblem_w = (int)floor(min($dst_w, $dst_h) / 2); + $emblem_h = $emblem_w; + foreach ($this->emblems as $key => $emblem) { + if ($emblem === null) { + continue; + } + + $emblem_template = $this->newTemplateFile( + $emblem, + $emblem_w, + $emblem_h); + + switch ($key) { + case 0: + $emblem_x = $dst_w - $emblem_w; + $emblem_y = 0; + break; + case 1: + $emblem_x = $dst_w - $emblem_w; + $emblem_y = $dst_h - $emblem_h; + break; + case 2: + $emblem_x = 0; + $emblem_y = $dst_h - $emblem_h; + break; + case 3: + $emblem_x = 0; + $emblem_y = 0; + break; + } + + $emblem_data = $emblem_template['file']->loadFileData(); + + $src = @imagecreatefromstring($emblem_data); + if (!$src) { + continue; + } + + imagecopyresampled( + $dst, + $src, + $emblem_x, + $emblem_y, + 0, + 0, + $emblem_w, + $emblem_h, + $emblem_template['width'], + $emblem_template['height']); + } + + return PhabricatorImageTransformer::saveImageDataInAnyFormat( + $dst, + 'image/png'); + } + + private function newTemplateFile($emblem, $width, $height) { + $all_resources = self::getAllResources(); + + $scores = array(); + $ratio = $width / $height; + foreach ($all_resources as $key => $resource) { + // We can't use an emblem resource for a different emblem, nor for an + // icon base. We also can't use an icon base as an emblem. That is, if + // we're looking for a picture of a red dot, we have to actually find + // a red dot, not just any image which happens to have a similar size. + if (idx($resource, 'emblem') !== $emblem) { + continue; + } + + $resource_width = $resource['width']; + $resource_height = $resource['height']; + + // Never use a resource with a different aspect ratio. + if (($resource_width / $resource_height) !== $ratio) { + continue; + } + + // Try to use custom resources instead of default resources. + if ($resource['default']) { + $default_score = 1; + } else { + $default_score = 0; + } + + $width_diff = ($resource_width - $width); + + // If we have to resize an image, we'd rather scale a larger image down + // than scale a smaller image up. + if ($width_diff < 0) { + $scale_score = 1; + } else { + $scale_score = 0; + } + + // Otherwise, we'd rather scale an image a little bit (ideally, zero) + // than scale an image a lot. + $width_score = abs($width_diff); + + $scores[$key] = id(new PhutilSortVector()) + ->addInt($default_score) + ->addInt($scale_score) + ->addInt($width_score); + } + + if (!$scores) { + if ($emblem === null) { + throw new Exception( + pht( + 'Found no background template resource for dimensions %dx%d.', + $width, + $height)); + } else { + throw new Exception( + pht( + 'Found no template resource (for emblem "%s") with dimensions '. + '%dx%d.', + $emblem, + $width, + $height)); + } + } + + $scores = msortv($scores, 'getSelf'); + $best_score = head_key($scores); + + $viewer = $this->getViewer(); + + $resource = $all_resources[$best_score]; + if ($resource['source-type'] === 'builtin') { + $file = PhabricatorFile::loadBuiltin($viewer, $resource['source']); + if (!$file) { + throw new Exception( + pht( + 'Failed to load favicon template builtin "%s".', + $resource['source'])); + } + } else { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($resource['source'])) + ->executeOne(); + if (!$file) { + throw new Exception( + pht( + 'Failed to load favicon template with PHID "%s".', + $resource['source'])); + } + } + + return array( + 'width' => $resource['width'], + 'height' => $resource['height'], + 'file' => $file, + ); + } + + private function newFaviconFile($data) { + return PhabricatorFile::newFromFileData( + $data, + array( + 'name' => 'favicon', + 'canCDN' => true, + )); + } + +} diff --git a/src/applications/files/favicon/PhabricatorFaviconRefQuery.php b/src/applications/files/favicon/PhabricatorFaviconRefQuery.php new file mode 100644 index 0000000000..f4e5556150 --- /dev/null +++ b/src/applications/files/favicon/PhabricatorFaviconRefQuery.php @@ -0,0 +1,55 @@ +refs = $refs; + return $this; + } + + public function execute() { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $refs = $this->refs; + + $config_digest = PhabricatorFaviconRef::newConfigurationDigest(); + + $ref_map = array(); + foreach ($refs as $ref) { + $ref_digest = $ref->newDigest(); + $ref_key = "favicon({$config_digest},{$ref_digest},8)"; + + $ref + ->setViewer($viewer) + ->setCacheKey($ref_key); + + $ref_map[$ref_key] = $ref; + } + + $cache = PhabricatorCaches::getImmutableCache(); + $ref_hits = $cache->getKeys(array_keys($ref_map)); + + foreach ($ref_hits as $ref_key => $ref_uri) { + $ref_map[$ref_key]->setURI($ref_uri); + unset($ref_map[$ref_key]); + } + + if ($ref_map) { + $new_map = array(); + foreach ($ref_map as $ref_key => $ref) { + $ref_uri = $ref->newURI(); + $ref->setURI($ref_uri); + $new_map[$ref_key] = $ref_uri; + } + + $cache->setKeys($new_map); + } + + return $refs; + } + + +} diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index 071ee03991..9f1659a7f3 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -152,61 +152,96 @@ final class PhabricatorFileQuery } protected function loadPage() { - $files = $this->loadStandardPage(new PhabricatorFile()); + $files = $this->loadStandardPage($this->newResultObject()); if (!$files) { return $files; } + // Figure out which files we need to load attached objects for. In most + // cases, we need to load attached objects to perform policy checks for + // files. + + // However, in some special cases where we know files will always be + // visible, we skip this. See T8478 and T13106. + $need_objects = array(); + $need_xforms = array(); + foreach ($files as $file) { + $always_visible = false; + + if ($file->getIsProfileImage()) { + $always_visible = true; + } + + if ($file->isBuiltin()) { + $always_visible = true; + } + + if ($always_visible) { + // We just treat these files as though they aren't attached to + // anything. This saves a query in common cases when we're loading + // profile images or builtins. We could be slightly more nuanced + // about this and distinguish between "not attached to anything" and + // "might be attached but policy checks don't need to care". + $file->attachObjectPHIDs(array()); + continue; + } + + $need_objects[] = $file; + $need_xforms[] = $file; + } + $viewer = $this->getViewer(); $is_omnipotent = $viewer->isOmnipotent(); - // We need to load attached objects to perform policy checks for files. - // First, load the edges. - - $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST; - $file_phids = mpull($files, 'getPHID'); - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($file_phids) - ->withEdgeTypes(array($edge_type)) - ->execute(); - + // If we have any files left which do need objects, load the edges now. $object_phids = array(); - foreach ($files as $file) { - $phids = array_keys($edges[$file->getPHID()][$edge_type]); - $file->attachObjectPHIDs($phids); + if ($need_objects) { + $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST; + $file_phids = mpull($need_objects, 'getPHID'); - if ($file->getIsProfileImage()) { - // If this is a profile image, don't bother loading related files. - // It will always be visible, and we can get into trouble if we try - // to load objects and end up stuck in a cycle. See T8478. - continue; - } + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($file_phids) + ->withEdgeTypes(array($edge_type)) + ->execute(); - if ($is_omnipotent) { - // If the viewer is omnipotent, we don't need to load the associated - // objects either since they can certainly see the object. Skipping - // this can improve performance and prevent cycles. - continue; - } + foreach ($need_objects as $file) { + $phids = array_keys($edges[$file->getPHID()][$edge_type]); + $file->attachObjectPHIDs($phids); - foreach ($phids as $phid) { - $object_phids[$phid] = true; + if ($is_omnipotent) { + // If the viewer is omnipotent, we don't need to load the associated + // objects either since the viewer can certainly see the object. + // Skipping this can improve performance and prevent cycles. This + // could possibly become part of the profile/builtin code above which + // short circuits attacment policy checks in cases where we know them + // to be unnecessary. + continue; + } + + foreach ($phids as $phid) { + $object_phids[$phid] = true; + } } } // If this file is a transform of another file, load that file too. If you // can see the original file, you can see the thumbnail. - // TODO: It might be nice to put this directly on PhabricatorFile and remove - // the PhabricatorTransformedFile table, which would be a little simpler. + // TODO: It might be nice to put this directly on PhabricatorFile and + // remove the PhabricatorTransformedFile table, which would be a little + // simpler. - $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( - 'transformedPHID IN (%Ls)', - $file_phids); - $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID'); - foreach ($xform_phids as $derived_phid => $original_phid) { - $object_phids[$original_phid] = true; + if ($need_xforms) { + $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( + 'transformedPHID IN (%Ls)', + mpull($need_xforms, 'getPHID')); + $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID'); + foreach ($xform_phids as $derived_phid => $original_phid) { + $object_phids[$original_phid] = true; + } + } else { + $xform_phids = array(); } $object_phids = array_keys($object_phids); diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 29fb606a4b..60b4ca0e74 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1152,7 +1152,6 @@ final class PhabricatorFile extends PhabricatorFileDAO $params = array( 'name' => $builtin->getBuiltinDisplayName(), - 'ttl.relative' => phutil_units('7 days in seconds'), 'canCDN' => true, 'builtin' => $key, ); @@ -1648,7 +1647,7 @@ final class PhabricatorFile extends PhabricatorFileDAO public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), - 'dataURI' => $this->getCDNURI(), + 'dataURI' => $this->getCDNURI('data'), 'size' => (int)$this->getByteSize(), ); } diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php index 103e6bce69..80be90b375 100644 --- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php +++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php @@ -71,7 +71,8 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication { ), 'build/' => array( $this->getQueryRoutePattern() => 'HarbormasterBuildListController', - '(?P\d+)/' => 'HarbormasterBuildViewController', + '(?P\d+)/(?:(?P\d+)/)?' + => 'HarbormasterBuildViewController', '(?Ppause|resume|restart|abort)/'. '(?P\d+)/(?:(?P[^/]+)/)?' => 'HarbormasterBuildActionController', diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 0e09b4d658..0646bd9309 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -8,7 +8,6 @@ final class HarbormasterBuildViewController $viewer = $request->getUser(); $id = $request->getURIData('id'); - $generation = $request->getInt('g'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) @@ -21,6 +20,7 @@ final class HarbormasterBuildViewController require_celerity_resource('harbormaster-css'); $title = pht('Build %d', $id); + $warnings = array(); $page_header = id(new PHUIHeaderView()) ->setHeader($title) @@ -28,7 +28,9 @@ final class HarbormasterBuildViewController ->setPolicyObject($build) ->setHeaderIcon('fa-cubes'); - if ($build->isRestarting()) { + $is_restarting = $build->isRestarting(); + + if ($is_restarting) { $page_header->setStatus( 'fa-exclamation-triangle', 'red', pht('Restarting')); } else if ($build->isPausing()) { @@ -42,19 +44,53 @@ final class HarbormasterBuildViewController 'fa-exclamation-triangle', 'red', pht('Aborting')); } + $max_generation = (int)$build->getBuildGeneration(); + if ($max_generation === 0) { + $min_generation = 0; + } else { + $min_generation = 1; + } + + if ($is_restarting) { + $max_generation = $max_generation + 1; + } + + $generation = $request->getURIData('generation'); + if ($generation === null) { + $generation = $max_generation; + } else { + $generation = (int)$generation; + } + + if ($generation < $min_generation || $generation > $max_generation) { + return new Aphront404Response(); + } + + if ($generation < $max_generation) { + $warnings[] = pht( + 'You are viewing an older run of this build. %s', + phutil_tag( + 'a', + array( + 'href' => $build->getURI(), + ), + pht('View Current Build'))); + } + + $curtain = $this->buildCurtainView($build); $properties = $this->buildPropertyList($build); + $history = $this->buildHistoryTable( + $build, + $generation, + $min_generation, + $max_generation); $crumbs = $this->buildApplicationCrumbs(); $this->addBuildableCrumb($crumbs, $build->getBuildable()); $crumbs->addTextCrumb($title); $crumbs->setBorder(true); - if ($generation === null || $generation > $build->getBuildGeneration() || - $generation < 0) { - $generation = $build->getBuildGeneration(); - } - $build_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->needBuildSteps(true) @@ -264,14 +300,25 @@ final class HarbormasterBuildViewController new HarbormasterBuildTransactionQuery()); $timeline->setShouldTerminate(true); + if ($warnings) { + $warnings = id(new PHUIInfoView()) + ->setErrors($warnings) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING); + } else { + $warnings = null; + } + $view = id(new PHUITwoColumnView()) ->setHeader($page_header) ->setCurtain($curtain) - ->setMainColumn(array( - $properties, - $targets, - $timeline, - )); + ->setMainColumn( + array( + $warnings, + $properties, + $history, + $targets, + $timeline, + )); return $this->newPage() ->setTitle($title) @@ -561,10 +608,6 @@ final class HarbormasterBuildViewController pht('Build Plan'), $handles[$build->getBuildPlanPHID()]->renderLink()); - $properties->addProperty( - pht('Restarts'), - $build->getBuildGeneration()); - $properties->addProperty( pht('Status'), $this->getStatus($build)); @@ -576,6 +619,53 @@ final class HarbormasterBuildViewController } + private function buildHistoryTable( + HarbormasterBuild $build, + $generation, + $min_generation, + $max_generation) { + + if ($max_generation === $min_generation) { + return null; + } + + $viewer = $this->getViewer(); + + $uri = $build->getURI(); + + $rows = array(); + $rowc = array(); + for ($ii = $max_generation; $ii >= $min_generation; $ii--) { + if ($generation == $ii) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + + $rows[] = array( + phutil_tag( + 'a', + array( + 'href' => $uri.$ii.'/', + ), + pht('Run %d', $ii)), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setColumnClasses( + array( + 'pri wide', + )) + ->setRowClasses($rowc); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('History')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + private function getStatus(HarbormasterBuild $build) { $status_view = new PHUIStatusListView(); diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 454fec7f37..f3589bfc53 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -147,7 +147,7 @@ final class HarbormasterBuildEngine extends Phobject { // If it is different, they will automatically stop what they're doing // and abort. - // Previously we used to delete targets, logs and artifacts here. Instead + // Previously we used to delete targets, logs and artifacts here. Instead, // leave them around so users can view previous generations of this build. } diff --git a/src/applications/harbormaster/event/HarbormasterUIEventListener.php b/src/applications/harbormaster/event/HarbormasterUIEventListener.php index 1227fbbe31..f5e6aee3e7 100644 --- a/src/applications/harbormaster/event/HarbormasterUIEventListener.php +++ b/src/applications/harbormaster/event/HarbormasterUIEventListener.php @@ -51,13 +51,22 @@ final class HarbormasterUIEventListener return; } - $buildable = id(new HarbormasterBuildableQuery()) - ->setViewer($viewer) - ->withManualBuildables(false) - ->withBuildablePHIDs(array($buildable_phid)) - ->needBuilds(true) - ->needTargets(true) - ->executeOne(); + try { + $buildable = id(new HarbormasterBuildableQuery()) + ->setViewer($viewer) + ->withManualBuildables(false) + ->withBuildablePHIDs(array($buildable_phid)) + ->needBuilds(true) + ->needTargets(true) + ->executeOne(); + } catch (PhabricatorPolicyException $ex) { + // TODO: See PHI430. When this query raises a policy exception, it + // fatals the whole page because it happens very late in execution, + // during final page rendering. If the viewer can't see the buildable or + // some of the builds, just hide this element for now. + return; + } + if (!$buildable) { return; } diff --git a/src/applications/macro/engine/PhabricatorMemeEngine.php b/src/applications/macro/engine/PhabricatorMemeEngine.php index f72a161171..175aa2fb46 100644 --- a/src/applications/macro/engine/PhabricatorMemeEngine.php +++ b/src/applications/macro/engine/PhabricatorMemeEngine.php @@ -236,7 +236,10 @@ final class PhabricatorMemeEngine extends Phobject { Filesystem::writeFile($output_name, $memed_frame_data); } - $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output); + $future = new ExecFuture( + 'convert -dispose background -loop 0 %Ls %s', + $output_files, + $output); $future->setTimeout(10)->resolvex(); return Filesystem::readFile($output); @@ -297,6 +300,9 @@ final class PhabricatorMemeEngine extends Phobject { $font_max = 72; $font_min = 5; + $margin_x = 16; + $margin_y = 16; + $last = null; $cursor = floor(($font_max + $font_min) / 2); $min = $font_min; @@ -321,12 +327,12 @@ final class PhabricatorMemeEngine extends Phobject { // text extends, for example if it has a "y". $descend = $box[3]; - if ($height > $dim_y) { + if (($height + $margin_y) > $dim_y) { $all_fit = false; break; } - if ($width > $dim_x) { + if (($width + $margin_x) > $dim_x) { $all_fit = false; break; } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index ecfb723f97..a11cc4d85f 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -284,9 +284,6 @@ final class ManiphestTaskDetailController extends ManiphestController { $edit_config = $edit_engine->loadDefaultEditConfiguration($task); $can_create = (bool)$edit_config; - $can_reassign = $edit_engine->hasEditAccessToTransaction( - ManiphestTaskOwnerTransaction::TRANSACTIONTYPE); - if ($can_create) { $form_key = $edit_config->getIdentifier(); $edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 955eba9972..98713078fb 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -357,29 +357,69 @@ final class PhabricatorProjectQuery } protected function didFilterPage(array $projects) { + $viewer = $this->getViewer(); + if ($this->needImages) { - $file_phids = mpull($projects, 'getProfileImagePHID'); - $file_phids = array_filter($file_phids); + $need_images = $projects; + + // First, try to load custom profile images for any projects with custom + // images. + $file_phids = array(); + foreach ($need_images as $key => $project) { + $image_phid = $project->getProfileImagePHID(); + if ($image_phid) { + $file_phids[$key] = $image_phid; + } + } + if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) - ->setViewer($this->getViewer()) + ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); - } else { - $files = array(); + + foreach ($file_phids as $key => $image_phid) { + $file = idx($files, $image_phid); + if (!$file) { + continue; + } + + $need_images[$key]->attachProfileImageFile($file); + unset($need_images[$key]); + } } - foreach ($projects as $project) { - $file = idx($files, $project->getProfileImagePHID()); - if (!$file) { - $builtin = PhabricatorProjectIconSet::getIconImage( - $project->getIcon()); - $file = PhabricatorFile::loadBuiltin($this->getViewer(), - 'projects/'.$builtin); + // For projects with default images, or projects where the custom image + // failed to load, load a builtin image. + if ($need_images) { + $builtin_map = array(); + $builtins = array(); + foreach ($need_images as $key => $project) { + $icon = $project->getIcon(); + + $builtin_name = PhabricatorProjectIconSet::getIconImage($icon); + $builtin_name = 'projects/'.$builtin_name; + + $builtin = id(new PhabricatorFilesOnDiskBuiltinFile()) + ->setName($builtin_name); + + $builtin_key = $builtin->getBuiltinFileKey(); + + $builtins[] = $builtin; + $builtin_map[$key] = $builtin_key; + } + + $builtin_files = PhabricatorFile::loadBuiltins( + $viewer, + $builtins); + + foreach ($need_images as $key => $project) { + $builtin_key = $builtin_map[$key]; + $builtin_file = $builtin_files[$builtin_key]; + $project->attachProfileImageFile($builtin_file); } - $project->attachProfileImageFile($file); } } diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index 452085fa9f..e38532c419 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -23,6 +23,14 @@ final class PhabricatorProjectSearchEngine id(new PhabricatorSearchTextField()) ->setLabel(pht('Name')) ->setKey('name'), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Slugs')) + ->setIsHidden(true) + ->setKey('slugs') + ->setDescription( + pht( + 'Search for projects with particular slugs. (Slugs are the same '. + 'as project hashtags.)')), id(new PhabricatorUsersSearchField()) ->setLabel(pht('Members')) ->setKey('memberPHIDs') @@ -81,6 +89,10 @@ final class PhabricatorProjectSearchEngine $query->withNameTokens($tokens); } + if ($map['slugs']) { + $query->withSlugs($map['slugs']); + } + if ($map['memberPHIDs']) { $query->withMemberPHIDs($map['memberPHIDs']); } diff --git a/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php b/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php index 7da35f3703..922f8c9651 100644 --- a/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php +++ b/src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php @@ -45,12 +45,19 @@ final class PhabricatorSubscriptionsSubscribersPolicyRule $this->subscribed[$viewer_phid] = array(); } - // Load the project PHIDs the user is a member of. + // Load the project PHIDs the user is a member of. We use the omnipotent + // user here because projects may themselves have "Subscribers" visibility + // policies and we don't want to get stuck in an infinite stack of + // recursive policy checks. See T13106. if (!isset($this->sourcePHIDs[$viewer_phid])) { - $source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $viewer_phid, - PhabricatorProjectMemberOfProjectEdgeType::EDGECONST); + $projects = id(new PhabricatorProjectQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($viewer_phid)) + ->execute(); + + $source_phids = mpull($projects, 'getPHID'); $source_phids[] = $viewer_phid; + $this->sourcePHIDs[$viewer_phid] = $source_phids; } diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php index 7ce2b4dbe0..0ec8f6a7a4 100644 --- a/src/applications/system/application/PhabricatorSystemApplication.php +++ b/src/applications/system/application/PhabricatorSystemApplication.php @@ -26,7 +26,6 @@ final class PhabricatorSystemApplication extends PhabricatorApplication { '/readonly/' => array( '(?P[^/]+)/' => 'PhabricatorSystemReadOnlyController', ), - '/favicon.ico' => 'PhabricatorSystemFaviconController', ); } diff --git a/src/applications/system/controller/PhabricatorSystemFaviconController.php b/src/applications/system/controller/PhabricatorSystemFaviconController.php deleted file mode 100644 index d9a31cbcb6..0000000000 --- a/src/applications/system/controller/PhabricatorSystemFaviconController.php +++ /dev/null @@ -1,19 +0,0 @@ -setContent($content) - ->setMimeType('image/x-icon') - ->setCacheDurationInSeconds(phutil_units('24 hours in seconds')) - ->setCanCDN(true); - } -} diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index de8139b145..f8f6f371bc 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1398,59 +1398,6 @@ abstract class PhabricatorEditEngine } - /** - * Test if the viewer could apply a certain type of change by using the - * normal "Edit" form. - * - * This method returns `true` if the user has access to an edit form and - * that edit form has a field which applied the specified transaction type, - * and that field is visible and editable for the user. - * - * For example, you can use it to test if a user is able to reassign tasks - * or not, prior to rendering dedicated UI for task reassignment. - * - * Note that this method does NOT test if the user can actually edit the - * current object, just if they have access to the related field. - * - * @param const Transaction type to test for. - * @return bool True if the user could "Edit" to apply the transaction type. - */ - final public function hasEditAccessToTransaction($xaction_type) { - $viewer = $this->getViewer(); - - $object = $this->getTargetObject(); - if (!$object) { - $object = $this->newEditableObject(); - } - - $config = $this->loadDefaultEditConfiguration($object); - if (!$config) { - return false; - } - - $fields = $this->buildEditFields($object); - - $field = null; - foreach ($fields as $form_field) { - $field_xaction_type = $form_field->getTransactionType(); - if ($field_xaction_type === $xaction_type) { - $field = $form_field; - break; - } - } - - if (!$field) { - return false; - } - - if (!$field->shouldReadValueFromSubmit()) { - return false; - } - - return true; - } - - public function newNUXButton($text) { $specs = $this->newCreateActionSpecifications(array()); $head = head($specs); @@ -1968,6 +1915,7 @@ abstract class PhabricatorEditEngine ->setContinueOnNoEffect($request->isContinueRequest()) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) + ->setRaiseWarnings(!$request->getBool('editEngine.warnings')) ->setIsPreview($is_preview); try { @@ -1980,6 +1928,10 @@ abstract class PhabricatorEditEngine return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); + } catch (PhabricatorApplicationTransactionWarningException $ex) { + return id(new PhabricatorApplicationTransactionWarningResponse()) + ->setCancelURI($view_uri) + ->setException($ex); } if (!$is_preview) { diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 1d02d0fdb0..26be0f87af 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -48,6 +48,7 @@ abstract class PhabricatorApplicationTransactionEditor private $mentionedPHIDs; private $continueOnNoEffect; private $continueOnMissingFields; + private $raiseWarnings; private $parentMessageID; private $heraldAdapter; private $heraldTranscript; @@ -273,6 +274,15 @@ abstract class PhabricatorApplicationTransactionEditor return $this->applicationEmail; } + public function setRaiseWarnings($raise_warnings) { + $this->raiseWarnings = $raise_warnings; + return $this; + } + + public function getRaiseWarnings() { + return $this->raiseWarnings; + } + public function getTransactionTypesForObject($object) { $old = $this->object; try { @@ -919,6 +929,19 @@ abstract class PhabricatorApplicationTransactionEditor throw new PhabricatorApplicationTransactionValidationException($errors); } + if ($this->raiseWarnings) { + $warnings = array(); + foreach ($xactions as $xaction) { + if ($this->hasWarnings($object, $xaction)) { + $warnings[] = $xaction; + } + } + if ($warnings) { + throw new PhabricatorApplicationTransactionWarningException( + $warnings); + } + } + $this->willApplyTransactions($object, $xactions); if ($object->getID()) { @@ -4277,4 +4300,28 @@ abstract class PhabricatorApplicationTransactionEditor } } + private function hasWarnings($object, $xaction) { + // TODO: For the moment, this is a very un-modular hack to support + // exactly one type of warning (mentioning users on a draft revision) + // that we want to show. See PHI433. + + if (!($object instanceof DifferentialRevision)) { + return false; + } + + if (!$object->isDraft()) { + return false; + } + + $type = $xaction->getTransactionType(); + if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) { + return false; + } + + // NOTE: This will currently warn even if you're only removing + // subscribers. + + return true; + } + } diff --git a/src/applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php b/src/applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php new file mode 100644 index 0000000000..e577071549 --- /dev/null +++ b/src/applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php @@ -0,0 +1,13 @@ +xactions = $xactions; + parent::__construct(); + } + +} diff --git a/src/applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php b/src/applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php new file mode 100644 index 0000000000..21b3ee61c8 --- /dev/null +++ b/src/applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php @@ -0,0 +1,58 @@ +cancelURI = $cancel_uri; + return $this; + } + + public function setException( + PhabricatorApplicationTransactionWarningException $exception) { + $this->exception = $exception; + return $this; + } + + protected function buildProxy() { + return new AphrontDialogResponse(); + } + + public function reduceProxyResponse() { + $request = $this->getRequest(); + + $title = pht('Warning: Unexpected Effects'); + + $head = pht( + 'This is a draft revision that will not publish any notifications '. + 'until the author requests review.'); + $tail = pht( + 'Mentioned or subscribed users will not be notified.'); + + $continue = pht('Tell No One'); + + $dialog = id(new AphrontDialogView()) + ->setViewer($request->getViewer()) + ->setTitle($title); + + $dialog->appendParagraph($head); + $dialog->appendParagraph($tail); + + $passthrough = $request->getPassthroughRequestParameters(); + foreach ($passthrough as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + $dialog + ->addHiddenInput('editEngine.warnings', 1) + ->addSubmitButton($continue) + ->addCancelButton($this->cancelURI); + + return $this->getProxy()->setDialog($dialog); + } + +} diff --git a/src/docs/user/installation_guide.diviner b/src/docs/user/installation_guide.diviner index 8feb8ae1d0..29895eb2a9 100644 --- a/src/docs/user/installation_guide.diviner +++ b/src/docs/user/installation_guide.diviner @@ -123,7 +123,7 @@ Otherwise, here's a general description of what you need to install: - MySQL Server (usually "mysqld" or "mysql-server") - PHP (usually "php") - Required PHP extensions: mbstring, iconv, mysql (or mysqli), curl, pcntl - (these might be something like "php-mysql" or "php5-mysql") + (these might be something like "php-mysql" or "php5-mysqlnd") - Optional PHP extensions: gd, apc (special instructions for APC are available below if you have difficulty installing it), xhprof (instructions below, you only need this if you are developing Phabricator) diff --git a/src/docs/user/support.diviner b/src/docs/user/support.diviner index 9dc06b15a3..c99b272de9 100644 --- a/src/docs/user/support.diviner +++ b/src/docs/user/support.diviner @@ -9,14 +9,23 @@ Overview This document describes available support resources. -The upstream provides active, free support for a narrow range of problems -(primarily, security issues and reproducible bugs). +The upstream provides free support for a narrow range of problems (primarily, +security issues and reproducible bugs) and paid support for virtually anything. The upstream does not provide free support for general problems with installing or configuring Phabricator. You may be able to get some help with these kinds of issues from the community. +Paid Support +============ + +If you'd like upstream support, see ((pacts)). + +This is the only way to request features and the only way to get guaranteed +answers from experts quickly. + + Reporting Security Vulnerabilities ================================== @@ -31,8 +40,7 @@ Reporting Bugs The upstream will accept **reproducible** bug reports in modern, first-party production code running in reasonable environments. Before submitting a bug -report you **must update** to the latest version of Phabricator. This reduces -support costs on the upstream, please be mindful. +report you **must update** to the latest version of Phabricator. To report bugs, see @{article:Contributing Bug Reports}. @@ -52,14 +60,10 @@ If you'd like to contribute to Phabricator, start with Installation and Setup Help =========================== -Installation and setup help is available from the upstream at consulting rates. -See [[ https://secure.phabricator.com/w/consulting/ | Consulting ]] for details. - -Helping individual installs navigate unique setup problems takes our time -away from developing Phabricator, so we can not offer this service for free. - You may be able to get free help with these issues from the -[[ https://phurl.io/u/discourse | community ]]. See below for details. +[[ https://phurl.io/u/discourse | community ]]. + +You can also pay us for support. See ((pacts)). Hosting @@ -67,8 +71,8 @@ Hosting The upstream offers Phabricator as a hosted service at [[ https://phacility.com | Phacility ]]. This simplifies setting up and -operating a Phabricator instance, and gives you access to a broader range -of upstream support services. +operating a Phabricator instance, and automatically gives you access to a +broader range of upstream support services. Running this service gives us a strong financial incentive to make installing and operating Phabricator as difficult as possible. Blinded by greed, we toil @@ -79,9 +83,10 @@ ourselves can hope to navigate. Phabricator Community ===================== -We provide hosting for a -[[ https://phurl.io/u/discourse | Discussion Forum ]] -where admins and users help and answer questions from other community members. -Upstream developers may occasionally participate, but this is mostly -a user to user community. If you run into general problems, but are not -interested in paid support, this is the main place to find help. +We provide hosting for a [[ https://phurl.io/u/discourse | Discussion Forum ]] +where admininstrators and users help and answer questions from other community +members. + +Upstream developers occasionally participate, but this is mostly a user to user +community. If you run into general problems, but are not interested in paid +support, this is the main place to find help. diff --git a/src/docs/user/userguide/mail_rules.diviner b/src/docs/user/userguide/mail_rules.diviner index 61bc3210e9..cdbbf1919d 100644 --- a/src/docs/user/userguide/mail_rules.diviner +++ b/src/docs/user/userguide/mail_rules.diviner @@ -43,6 +43,27 @@ The most useful header for routing is generally `X-Phabricator-Stamps`. This is a list of attributes which describe the object the mail is about and the actions which the mail informs you about. +Stamps and Gmail +================ + If you use a client which can not perform header matching (like Gmail), you can change the {nav Settings > Email Format > Send Stamps} setting to include the stamps in the mail body and then match them with body rules. + +When writing filter rules against mail stamps in Gmail, you should quote any +filters you want to apply. For example, specify rules like this, with quotes: + +> "author(@alice)" + +Note that Gmail will ignore some symbols when matching mail against filtering +rules, so you can get false positives if the body of the message includes text +like `author alice` (the same words in the same order, without the special +symbols). + +You'll also get false positives if the message body includes the text of a +mail stamp explicitly in a normal text field like a summary, description, or +comment. + +There's no way to avoid these false positives other than using a different +client with support for more powerful filtering rules, but these false +positives should normally be uncommon. diff --git a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php index fbbfa36805..31b4e52250 100644 --- a/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php @@ -279,6 +279,15 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { return $matches[0]; } + // If we're rendering a table of contents, just render the raw input. + // This could perhaps be handled more gracefully but it seems unusual to + // put something like "{P123}" in a header and it's not obvious what users + // expect? See T8845. + $engine = $this->getEngine(); + if ($engine->getState('toc')) { + return $matches[0]; + } + return $this->markupObject(array( 'type' => 'embed', 'id' => $matches[1], @@ -292,6 +301,12 @@ abstract class PhabricatorObjectRemarkupRule extends PhutilRemarkupRule { return $matches[0]; } + // If we're rendering a table of contents, just render the monogram. + $engine = $this->getEngine(); + if ($engine->getState('toc')) { + return $matches[0]; + } + return $this->markupObject(array( 'type' => 'ref', 'id' => $matches[1], diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php index afdc482170..de421d9e70 100644 --- a/src/view/page/PhabricatorBarePageView.php +++ b/src/view/page/PhabricatorBarePageView.php @@ -71,6 +71,14 @@ class PhabricatorBarePageView extends AphrontPageView { )); } + $referrer_tag = phutil_tag( + 'meta', + array( + 'name' => 'referrer', + 'content' => 'no-referrer', + )); + + $mask_icon = phutil_tag( 'link', array( @@ -80,47 +88,7 @@ class PhabricatorBarePageView extends AphrontPageView { '/rsrc/favicons/mask-icon.svg'), )); - $icon_tag_76 = phutil_tag( - 'link', - array( - 'rel' => 'apple-touch-icon', - 'href' => celerity_get_resource_uri( - '/rsrc/favicons/apple-touch-icon-76x76.png'), - )); - - $icon_tag_120 = phutil_tag( - 'link', - array( - 'rel' => 'apple-touch-icon', - 'sizes' => '120x120', - 'href' => celerity_get_resource_uri( - '/rsrc/favicons/apple-touch-icon-120x120.png'), - )); - - $icon_tag_152 = phutil_tag( - 'link', - array( - 'rel' => 'apple-touch-icon', - 'sizes' => '152x152', - 'href' => celerity_get_resource_uri( - '/rsrc/favicons/apple-touch-icon-152x152.png'), - )); - - $favicon_tag = phutil_tag( - 'link', - array( - 'id' => 'favicon', - 'rel' => 'shortcut icon', - 'href' => celerity_get_resource_uri( - '/rsrc/favicons/favicon.ico'), - )); - - $referrer_tag = phutil_tag( - 'meta', - array( - 'name' => 'referrer', - 'content' => 'no-referrer', - )); + $favicon_links = $this->newFavicons(); $response = CelerityAPI::getStaticResourceResponse(); @@ -136,13 +104,10 @@ class PhabricatorBarePageView extends AphrontPageView { } return hsprintf( - '%s%s%s%s%s%s%s%s', + '%s%s%s%s%s', $viewport_tag, $mask_icon, - $icon_tag_76, - $icon_tag_120, - $icon_tag_152, - $favicon_tag, + $favicon_links, $referrer_tag, $response->renderResourcesOfType('css')); } @@ -156,4 +121,61 @@ class PhabricatorBarePageView extends AphrontPageView { return $response->renderResourcesOfType('js'); } + private function newFavicons() { + $favicon_refs = array( + array( + 'rel' => 'apple-touch-icon', + 'sizes' => '76x76', + 'width' => 76, + 'height' => 76, + ), + array( + 'rel' => 'apple-touch-icon', + 'sizes' => '120x120', + 'width' => 120, + 'height' => 120, + ), + array( + 'rel' => 'apple-touch-icon', + 'sizes' => '152x152', + 'width' => 152, + 'height' => 152, + ), + array( + 'rel' => 'icon', + 'id' => 'favicon', + 'width' => 64, + 'height' => 64, + ), + ); + + $fetch_refs = array(); + foreach ($favicon_refs as $key => $spec) { + $ref = id(new PhabricatorFaviconRef()) + ->setWidth($spec['width']) + ->setHeight($spec['height']); + + $favicon_refs[$key]['ref'] = $ref; + $fetch_refs[] = $ref; + } + + id(new PhabricatorFaviconRefQuery()) + ->withRefs($fetch_refs) + ->execute(); + + $favicon_links = array(); + foreach ($favicon_refs as $spec) { + $favicon_links[] = phutil_tag( + 'link', + array( + 'rel' => $spec['rel'], + 'sizes' => idx($spec, 'sizes'), + 'id' => idx($spec, 'id'), + 'href' => $spec['ref']->getURI(), + )); + } + + return $favicon_links; + } + } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index aca98b7205..99143add5f 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -330,9 +330,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView Javelin::initBehavior( 'dark-console', $this->getConsoleConfig()); - - // Change this to initBehavior when there is some behavior to initialize - require_celerity_resource('javelin-behavior-error-log'); } if ($user) { diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index ad9510f348..583952b0a9 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -23,15 +23,29 @@ final class PhabricatorMainMenuView extends AphrontView { return $this->controller; } - private function getFaviconURI($type = null) { - switch ($type) { - case 'message': - return celerity_get_resource_uri('/rsrc/favicons/favicon-message.ico'); - case 'mention': - return celerity_get_resource_uri('/rsrc/favicons/favicon-mention.ico'); - default: - return celerity_get_resource_uri('/rsrc/favicons/favicon.ico'); - } + private static function getFavicons() { + $refs = array(); + + $refs['favicon'] = id(new PhabricatorFaviconRef()) + ->setWidth(64) + ->setHeight(64); + + $refs['message_favicon'] = id(new PhabricatorFaviconRef()) + ->setWidth(64) + ->setHeight(64) + ->setEmblems( + array( + 'dot-pink', + null, + null, + null, + )); + + id(new PhabricatorFaviconRefQuery()) + ->withRefs($refs) + ->execute(); + + return mpull($refs, 'getURI'); } public function render() { @@ -428,10 +442,7 @@ final class PhabricatorMainMenuView extends AphrontView { 'countType' => $conpherence_data['countType'], 'countNumber' => $message_count_number, 'unreadClass' => 'message-unread', - 'favicon' => $this->getFaviconURI('default'), - 'message_favicon' => $this->getFaviconURI('message'), - 'mention_favicon' => $this->getFaviconURI('mention'), - )); + ) + self::getFavicons()); $message_notification_dropdown = javelin_tag( 'div', @@ -509,10 +520,7 @@ final class PhabricatorMainMenuView extends AphrontView { 'countType' => $notification_data['countType'], 'countNumber' => $count_number, 'unreadClass' => 'alert-unread', - 'favicon' => $this->getFaviconURI('default'), - 'message_favicon' => $this->getFaviconURI('message'), - 'mention_favicon' => $this->getFaviconURI('mention'), - )); + ) + self::getFavicons()); $notification_dropdown = javelin_tag( 'div', @@ -594,10 +602,7 @@ final class PhabricatorMainMenuView extends AphrontView { 'countType' => null, 'countNumber' => null, 'unreadClass' => 'setup-unread', - 'favicon' => $this->getFaviconURI('default'), - 'message_favicon' => $this->getFaviconURI('message'), - 'mention_favicon' => $this->getFaviconURI('mention'), - )); + ) + self::getFavicons()); $setup_notification_dropdown = javelin_tag( 'div', diff --git a/src/view/phui/PHUITagView.php b/src/view/phui/PHUITagView.php index 482b3f4400..aa309a4bb6 100644 --- a/src/view/phui/PHUITagView.php +++ b/src/view/phui/PHUITagView.php @@ -117,8 +117,8 @@ final class PHUITagView extends AphrontTagView { return $this; } - public function setSlimShady($mm) { - $this->slimShady = $mm; + public function setSlimShady($is_eminem) { + $this->slimShady = $is_eminem; return $this; } diff --git a/webroot/favicon.ico b/webroot/favicon.ico deleted file mode 100644 index f07770aa8a..0000000000 Binary files a/webroot/favicon.ico and /dev/null differ diff --git a/webroot/rsrc/css/aphront/dark-console.css b/webroot/rsrc/css/aphront/dark-console.css index 81e888ba46..c3833e75bb 100644 --- a/webroot/rsrc/css/aphront/dark-console.css +++ b/webroot/rsrc/css/aphront/dark-console.css @@ -104,6 +104,7 @@ font-size: 11px; background: #333333; color: #ffffff; + border-bottom: 0px; } .dark-console .aphront-table-view td { @@ -210,10 +211,10 @@ } .dark-console-panel-request-log-separator { - background-color: #e8e8e8; - border-bottom: 1px solid #b7b7b7; - border-top: 1px solid #b7b7b7; - height: 2px; + background-color: #e8e8e8; + border-bottom: 1px solid #b7b7b7; + border-top: 1px solid #b7b7b7; + height: 2px; } .dark-console-panel-ErrorLog { diff --git a/webroot/rsrc/css/application/config/setup-issue.css b/webroot/rsrc/css/application/config/setup-issue.css index 7ba033d9c2..a80b02815c 100644 --- a/webroot/rsrc/css/application/config/setup-issue.css +++ b/webroot/rsrc/css/application/config/setup-issue.css @@ -84,7 +84,7 @@ } .setup-issue-body { - padding: 16px 16px 0 16px; + padding: 16px; } .setup-issue-status { diff --git a/webroot/rsrc/favicons/apple-touch-icon-114x114.png b/webroot/rsrc/favicons/apple-touch-icon-114x114.png deleted file mode 100644 index cb4d8cd937..0000000000 Binary files a/webroot/rsrc/favicons/apple-touch-icon-114x114.png and /dev/null differ diff --git a/webroot/rsrc/favicons/apple-touch-icon-144x144.png b/webroot/rsrc/favicons/apple-touch-icon-144x144.png deleted file mode 100644 index 92f2114b20..0000000000 Binary files a/webroot/rsrc/favicons/apple-touch-icon-144x144.png and /dev/null differ diff --git a/webroot/rsrc/favicons/apple-touch-icon-57x57.png b/webroot/rsrc/favicons/apple-touch-icon-57x57.png deleted file mode 100644 index 55df0a48e5..0000000000 Binary files a/webroot/rsrc/favicons/apple-touch-icon-57x57.png and /dev/null differ diff --git a/webroot/rsrc/favicons/apple-touch-icon-60x60.png b/webroot/rsrc/favicons/apple-touch-icon-60x60.png deleted file mode 100644 index 97dfdbc36e..0000000000 Binary files a/webroot/rsrc/favicons/apple-touch-icon-60x60.png and /dev/null differ diff --git a/webroot/rsrc/favicons/apple-touch-icon-72x72.png b/webroot/rsrc/favicons/apple-touch-icon-72x72.png deleted file mode 100644 index cb1f066ad0..0000000000 Binary files a/webroot/rsrc/favicons/apple-touch-icon-72x72.png and /dev/null differ diff --git a/webroot/rsrc/favicons/favicon-196x196.png b/webroot/rsrc/favicons/favicon-196x196.png deleted file mode 100644 index bf30eb1cf8..0000000000 Binary files a/webroot/rsrc/favicons/favicon-196x196.png and /dev/null differ diff --git a/webroot/rsrc/favicons/favicon-32x32.png b/webroot/rsrc/favicons/favicon-32x32.png deleted file mode 100644 index c6f2a75566..0000000000 Binary files a/webroot/rsrc/favicons/favicon-32x32.png and /dev/null differ diff --git a/webroot/rsrc/favicons/favicon-96x96.png b/webroot/rsrc/favicons/favicon-96x96.png deleted file mode 100644 index 5f0522be65..0000000000 Binary files a/webroot/rsrc/favicons/favicon-96x96.png and /dev/null differ diff --git a/webroot/rsrc/favicons/favicon-mention.ico b/webroot/rsrc/favicons/favicon-mention.ico deleted file mode 100644 index 6e27ce8339..0000000000 Binary files a/webroot/rsrc/favicons/favicon-mention.ico and /dev/null differ diff --git a/webroot/rsrc/favicons/favicon-message.ico b/webroot/rsrc/favicons/favicon-message.ico deleted file mode 100644 index 07408f3182..0000000000 Binary files a/webroot/rsrc/favicons/favicon-message.ico and /dev/null differ diff --git a/webroot/rsrc/favicons/favicon.ico b/webroot/rsrc/favicons/favicon.ico deleted file mode 100644 index f07770aa8a..0000000000 Binary files a/webroot/rsrc/favicons/favicon.ico and /dev/null differ diff --git a/webroot/rsrc/favicons/mstile-144x144.png b/webroot/rsrc/favicons/mstile-144x144.png deleted file mode 100644 index 92f2114b20..0000000000 Binary files a/webroot/rsrc/favicons/mstile-144x144.png and /dev/null differ diff --git a/webroot/rsrc/favicons/mstile-150x150.png b/webroot/rsrc/favicons/mstile-150x150.png deleted file mode 100644 index df1804953a..0000000000 Binary files a/webroot/rsrc/favicons/mstile-150x150.png and /dev/null differ diff --git a/webroot/rsrc/favicons/mstile-310x150.png b/webroot/rsrc/favicons/mstile-310x150.png deleted file mode 100644 index 56c3ed1985..0000000000 Binary files a/webroot/rsrc/favicons/mstile-310x150.png and /dev/null differ diff --git a/webroot/rsrc/favicons/mstile-310x310.png b/webroot/rsrc/favicons/mstile-310x310.png deleted file mode 100644 index ef911d803d..0000000000 Binary files a/webroot/rsrc/favicons/mstile-310x310.png and /dev/null differ diff --git a/webroot/rsrc/favicons/mstile-70x70.png b/webroot/rsrc/favicons/mstile-70x70.png deleted file mode 100644 index 3b51e6052b..0000000000 Binary files a/webroot/rsrc/favicons/mstile-70x70.png and /dev/null differ diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js index 24571b5981..ac058bde40 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js @@ -78,7 +78,7 @@ JX.behavior('aphlict-listen', function(config) { JX.Stratcom.invoke('notification-panel-update', null, {}); var response = e.getData(); - if (!response.showAnyNotification) { + if (!response.showAnyNotification && !response.showDesktopNotification) { return; } @@ -86,6 +86,7 @@ JX.behavior('aphlict-listen', function(config) { new JX.Notification() .setContent(JX.$H(response.content)) .setKey(response.primaryObjectPHID) + .setShowAsWebNotification(response.showAnyNotification) .setShowAsDesktopNotification(response.showDesktopNotification) .setTitle(response.title) .setBody(response.body) diff --git a/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js b/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js index c7b850f44f..f725ef6082 100644 --- a/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js +++ b/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js @@ -40,9 +40,19 @@ JX.behavior('doorkeeper-tag', function(config, statics) { }; for (var ii = 0; ii < tags.length; ii++) { - var tag_key = tags[ii].ref.join('@'); + var key_parts = []; + + key_parts = key_parts.concat(tags[ii].ref); + key_parts.push(tags[ii].view); + + var tag_key = key_parts.join(' '); + if (tag_key in statics.cache) { - have.push({id: tags[ii].id, markup: statics.cache[tag_key]}); + have.push( + { + id: tags[ii].id, + markup: statics.cache[tag_key] + }); } else { need.push(tags[ii]); keys[tags[ii].id] = tag_key; @@ -54,7 +64,11 @@ JX.behavior('doorkeeper-tag', function(config, statics) { } if (need.length) { - new JX.Workflow('/doorkeeper/tags/', {tags: JX.JSON.stringify(need)}) + var data = { + tags: JX.JSON.stringify(need) + }; + + new JX.Workflow('/doorkeeper/tags/', data) .setHandler(function(r) { draw(r.tags); }) .start(); } diff --git a/webroot/rsrc/js/core/Notification.js b/webroot/rsrc/js/core/Notification.js index 13e6cc47ae..67e1e5c910 100644 --- a/webroot/rsrc/js/core/Notification.js +++ b/webroot/rsrc/js/core/Notification.js @@ -27,6 +27,7 @@ JX.install('Notification', { _hideTimer : null, _duration : 12000, _asDesktop : false, + _asWeb : true, _key : null, _title : null, _body : null, @@ -88,6 +89,11 @@ JX.install('Notification', { return this; }, + setShowAsWebNotification: function(mode) { + this._asWeb = mode; + return this; + }, + setShowAsDesktopNotification : function(mode) { this._asDesktop = mode; return this; @@ -242,6 +248,13 @@ JX.install('Notification', { var notifications = []; for (var ii = 0; ii < self._active.length; ii++) { + + // Don't render this notification if it's not configured to show as + // a web notification. + if (!self._active[ii]._asWeb) { + continue; + } + notifications.push(self._active[ii]._getContainer()); if (!(--limit)) { break; diff --git a/webroot/rsrc/js/core/behavior-error-log.js b/webroot/rsrc/js/core/behavior-error-log.js deleted file mode 100644 index 24f3f61141..0000000000 --- a/webroot/rsrc/js/core/behavior-error-log.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @provides javelin-behavior-error-log - * @requires javelin-dom - */ - -/* exported show_details */ - -var current_details = null; - -function show_details(row) { - var node = JX.$('row-details-' + row); - - if (current_details !== null) { - JX.$('row-details-' + current_details).style.display = 'none'; - } - - node.style.display = 'block'; - current_details = row; -} diff --git a/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js index 13949432ff..a9f33d4594 100644 --- a/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js +++ b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js @@ -404,4 +404,24 @@ JX.behavior('dark-console', function(config, statics) { } + if (!statics.expand) { + statics.expand = true; + + var current_details = null; + JX.Stratcom.listen('click', 'darkconsole-expand', function(e) { + e.kill(); + + if (current_details) { + current_details.style.display = 'none'; + current_details = null; + } + + var id = e.getNodeData('darkconsole-expand').expandID; + var node = JX.$(id); + + node.style.display = 'block'; + current_details = node; + }); + } + });