diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 43fd7c5ea3..90183aaf6e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '6913fe66', + 'core.pkg.css' => 'c7fc5aec', 'core.pkg.js' => '10275c16', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'b3eea3f5', @@ -81,7 +81,7 @@ return array( 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => '1898e534', 'rsrc/css/application/people/people-profile.css' => '2473d929', - 'rsrc/css/application/phame/phame.css' => '7448a969', + 'rsrc/css/application/phame/phame.css' => 'bf6a743f', 'rsrc/css/application/pholio/pholio-edit.css' => '07676f51', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => 'ca89d380', @@ -128,7 +128,7 @@ return array( 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-crumbs-view.css' => '6b813619', 'rsrc/css/phui/phui-curtain-view.css' => '7148ae25', - 'rsrc/css/phui/phui-document-pro.css' => '8419560b', + 'rsrc/css/phui/phui-document-pro.css' => 'a3730b94', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '715aedfb', 'rsrc/css/phui/phui-feed-story.css' => 'aa49845d', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => 'd5263e49', 'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2', - 'rsrc/css/phui/phui-timeline-view.css' => '6e342216', + 'rsrc/css/phui/phui-timeline-view.css' => '8ea41b25', 'rsrc/css/phui/phui-two-column-view.css' => '9fb86c85', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', 'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', @@ -808,7 +808,7 @@ return array( 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => '5b6fcf3f', - 'phame-css' => '7448a969', + 'phame-css' => 'bf6a743f', 'pholio-css' => 'ca89d380', 'pholio-edit-css' => '07676f51', 'pholio-inline-comments-css' => '8e545e49', @@ -831,7 +831,7 @@ return array( 'phui-curtain-view-css' => '7148ae25', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '715aedfb', - 'phui-document-view-pro-css' => '8419560b', + 'phui-document-view-pro-css' => 'a3730b94', 'phui-feed-story-css' => 'aa49845d', 'phui-font-icon-base-css' => '6449bce8', 'phui-fontkit-css' => '9cda225e', @@ -860,7 +860,7 @@ return array( 'phui-status-list-view-css' => 'd5263e49', 'phui-tag-view-css' => '6bbd83e2', 'phui-theme-css' => '027ba77e', - 'phui-timeline-view-css' => '6e342216', + 'phui-timeline-view-css' => '8ea41b25', 'phui-two-column-view-css' => '9fb86c85', 'phui-workboard-color-css' => 'ac6fe6a7', 'phui-workboard-view-css' => 'e6d89647', diff --git a/resources/sql/autopatches/20160616.phame.blog.header.1.sql b/resources/sql/autopatches/20160616.phame.blog.header.1.sql new file mode 100644 index 0000000000..d2764d964c --- /dev/null +++ b/resources/sql/autopatches/20160616.phame.blog.header.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_blog + ADD headerImagePHID VARBINARY(64); diff --git a/resources/sql/autopatches/20160616.repo.01.oldref.sql b/resources/sql/autopatches/20160616.repo.01.oldref.sql new file mode 100644 index 0000000000..63bced8aab --- /dev/null +++ b/resources/sql/autopatches/20160616.repo.01.oldref.sql @@ -0,0 +1,6 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_oldref ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + repositoryPHID VARBINARY(64) NOT NULL, + commitIdentifier VARCHAR(40) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_repository` (repositoryPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160617.harbormaster.01.arelease.sql b/resources/sql/autopatches/20160617.harbormaster.01.arelease.sql new file mode 100644 index 0000000000..6f067d1549 --- /dev/null +++ b/resources/sql/autopatches/20160617.harbormaster.01.arelease.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildartifact + ADD isReleased BOOL NOT NULL; diff --git a/resources/sql/autopatches/20160618.phame.blog.subtitle.sql b/resources/sql/autopatches/20160618.phame.blog.subtitle.sql new file mode 100644 index 0000000000..1ea6572bb0 --- /dev/null +++ b/resources/sql/autopatches/20160618.phame.blog.subtitle.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phame.phame_blog + ADD subtitle VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/patches/20130801.pastexactions.php b/resources/sql/patches/20130801.pastexactions.php index 75c2ece940..9d3d2c2853 100644 --- a/resources/sql/patches/20130801.pastexactions.php +++ b/resources/sql/patches/20130801.pastexactions.php @@ -1,47 +1,5 @@ establishConnection('w'); -$conn_w->openTransaction(); - -echo pht('Adding transactions for existing paste objects...')."\n"; - -$rows = new LiskRawMigrationIterator($conn_w, 'pastebin_paste'); -foreach ($rows as $row) { - - $id = $row['id']; - echo pht('Adding transactions for paste id %d...', $id)."\n"; - - $xaction_phid = PhabricatorPHID::generateNewPHID( - PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST); - - queryfx( - $conn_w, - 'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy, - transactionType, oldValue, newValue, - contentSource, metadata, dateCreated, dateModified, - commentVersion) - VALUES (%s, %s, %s, %s, %s, %s, %ns, %ns, %s, %s, %d, %d, %d)', - $x_table->getTableName(), - $xaction_phid, - $row['authorPHID'], - $row['phid'], - 'public', - $row['authorPHID'], - PhabricatorPasteTransaction::TYPE_CONTENT, - 'null', - $row['filePHID'], - PhabricatorContentSource::newForSource( - PhabricatorOldWorldContentSource::SOURCECONST)->serialize(), - '[]', - $row['dateCreated'], - $row['dateCreated'], - 0); - -} - -$conn_w->saveTransaction(); - -echo pht('Done.')."\n"; +// Long ago, this migration populated initial "create" transactions for old +// pastes from before transactions came into existence. It was removed after +// about three years. diff --git a/scripts/ssh/ssh-connect.php b/scripts/ssh/ssh-connect.php index fac8d19130..d42a542140 100755 --- a/scripts/ssh/ssh-connect.php +++ b/scripts/ssh/ssh-connect.php @@ -4,6 +4,11 @@ // This is a wrapper script for Git, Mercurial, and Subversion. It primarily // serves to inject "-o StrictHostKeyChecking=no" into the SSH arguments. +// In some cases, Subversion sends us SIGTERM. If we don't catch the signal and +// react to it, we won't run object destructors by default and thus won't clean +// up temporary files. Declare ticks so we can install a signal handler. +declare(ticks=1); + $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; @@ -21,6 +26,16 @@ $args->parsePartial( )); $unconsumed_argv = $args->getUnconsumedArgumentVector(); +if (function_exists('pcntl_signal')) { + pcntl_signal(SIGTERM, 'ssh_connect_signal'); +} + +function ssh_connect_signal($signo) { + // This is just letting destructors fire. In particular, we want to clean + // up any temporary files we wrote. See T10547. + exit(128 + $signo); +} + $pattern = array(); $arguments = array(); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bd8c8c1476..6a518c2244 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -489,6 +489,7 @@ phutil_register_library_map(array( 'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php', 'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php', 'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php', + 'DifferentialRepositoryDatasource' => 'applications/differential/typeahead/DifferentialRepositoryDatasource.php', 'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php', 'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php', 'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php', @@ -821,6 +822,7 @@ phutil_register_library_map(array( 'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php', 'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php', 'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php', + 'DiffusionTaggedRepositoriesFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php', 'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php', 'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php', 'DiffusionURIEditEngine' => 'applications/diffusion/editor/DiffusionURIEditEngine.php', @@ -2158,6 +2160,9 @@ phutil_register_library_map(array( 'PhabricatorController' => 'applications/base/controller/PhabricatorController.php', 'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php', 'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php', + 'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php', + 'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php', + 'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php', 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php', 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', @@ -2463,6 +2468,7 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php', 'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php', 'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php', + 'PhabricatorFileAES256StorageFormat' => 'applications/files/format/PhabricatorFileAES256StorageFormat.php', 'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php', 'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php', 'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php', @@ -2485,12 +2491,16 @@ phutil_register_library_map(array( 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', 'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php', + 'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php', + 'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php', 'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php', 'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php', 'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php', 'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php', 'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php', 'PhabricatorFileStorageEngineTestCase' => 'applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php', + 'PhabricatorFileStorageFormat' => 'applications/files/format/PhabricatorFileStorageFormat.php', + 'PhabricatorFileStorageFormatTestCase' => 'applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php', 'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php', 'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php', 'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php', @@ -2514,7 +2524,10 @@ phutil_register_library_map(array( 'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php', 'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php', 'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php', + 'PhabricatorFilesManagementCycleWorkflow' => 'applications/files/management/PhabricatorFilesManagementCycleWorkflow.php', + 'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php', 'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php', + 'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php', 'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php', 'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php', 'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php', @@ -2617,6 +2630,8 @@ phutil_register_library_map(array( 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', 'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php', + 'PhabricatorKeyring' => 'applications/files/keyring/PhabricatorKeyring.php', + 'PhabricatorKeyringConfigOptionType' => 'applications/files/keyring/PhabricatorKeyringConfigOptionType.php', 'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php', 'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php', 'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php', @@ -2755,6 +2770,8 @@ phutil_register_library_map(array( 'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php', 'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php', 'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php', + 'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php', + 'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php', 'PhabricatorMonospacedFontSetting' => 'applications/settings/setting/PhabricatorMonospacedFontSetting.php', 'PhabricatorMonospacedTextareasSetting' => 'applications/settings/setting/PhabricatorMonospacedTextareasSetting.php', 'PhabricatorMotivatorProfilePanel' => 'applications/search/profilepanel/PhabricatorMotivatorProfilePanel.php', @@ -2914,12 +2931,14 @@ phutil_register_library_map(array( 'PhabricatorPasteArchiveController' => 'applications/paste/controller/PhabricatorPasteArchiveController.php', 'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php', 'PhabricatorPasteContentSearchEngineAttachment' => 'applications/paste/engineextension/PhabricatorPasteContentSearchEngineAttachment.php', + 'PhabricatorPasteContentTransaction' => 'applications/paste/xaction/PhabricatorPasteContentTransaction.php', 'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php', 'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php', 'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php', 'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php', 'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php', 'PhabricatorPasteFilenameContextFreeGrammar' => 'applications/paste/lipsum/PhabricatorPasteFilenameContextFreeGrammar.php', + 'PhabricatorPasteLanguageTransaction' => 'applications/paste/xaction/PhabricatorPasteLanguageTransaction.php', 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php', 'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php', @@ -2928,10 +2947,13 @@ phutil_register_library_map(array( 'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php', 'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php', 'PhabricatorPasteSnippet' => 'applications/paste/snippet/PhabricatorPasteSnippet.php', + 'PhabricatorPasteStatusTransaction' => 'applications/paste/xaction/PhabricatorPasteStatusTransaction.php', 'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php', + 'PhabricatorPasteTitleTransaction' => 'applications/paste/xaction/PhabricatorPasteTitleTransaction.php', 'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php', 'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php', 'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php', + 'PhabricatorPasteTransactionType' => 'applications/paste/xaction/PhabricatorPasteTransactionType.php', 'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php', 'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php', 'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php', @@ -3241,6 +3263,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php', + 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php', 'PhabricatorRepositoryManagementMovePathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMovePathsWorkflow.php', 'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php', @@ -3254,6 +3277,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php', 'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php', 'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php', + 'PhabricatorRepositoryOldRef' => 'applications/repository/storage/PhabricatorRepositoryOldRef.php', 'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php', 'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php', 'PhabricatorRepositoryPullEvent' => 'applications/repository/storage/PhabricatorRepositoryPullEvent.php', @@ -3589,6 +3613,8 @@ phutil_register_library_map(array( 'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php', 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php', + 'PhabricatorTransactionChange' => 'applications/transactions/data/PhabricatorTransactionChange.php', + 'PhabricatorTransactionRemarkupChange' => 'applications/transactions/data/PhabricatorTransactionRemarkupChange.php', 'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php', 'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php', 'PhabricatorTransactionsDestructionEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsDestructionEngineExtension.php', @@ -3750,6 +3776,8 @@ phutil_register_library_map(array( 'PhameBlogEditEngine' => 'applications/phame/editor/PhameBlogEditEngine.php', 'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php', 'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php', + 'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php', + 'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php', 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', 'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php', @@ -3771,12 +3799,14 @@ phutil_register_library_map(array( 'PhameLiveController' => 'applications/phame/controller/PhameLiveController.php', 'PhameNextPostView' => 'applications/phame/view/PhameNextPostView.php', 'PhamePost' => 'applications/phame/storage/PhamePost.php', + 'PhamePostArchiveController' => 'applications/phame/controller/post/PhamePostArchiveController.php', 'PhamePostCommentController' => 'applications/phame/controller/post/PhamePostCommentController.php', 'PhamePostController' => 'applications/phame/controller/post/PhamePostController.php', 'PhamePostEditConduitAPIMethod' => 'applications/phame/conduit/PhamePostEditConduitAPIMethod.php', 'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php', 'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php', 'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php', + 'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php', 'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php', 'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php', 'PhamePostListView' => 'applications/phame/view/PhamePostListView.php', @@ -4787,6 +4817,7 @@ phutil_register_library_map(array( 'DifferentialReleephRequestFieldSpecification' => 'Phobject', 'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', + 'DifferentialRepositoryDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialRepositoryField' => 'DifferentialCoreCustomField', 'DifferentialRepositoryLookup' => 'Phobject', 'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField', @@ -5137,6 +5168,7 @@ phutil_register_library_map(array( 'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery', 'DiffusionTagListController' => 'DiffusionController', 'DiffusionTagListView' => 'DiffusionView', + 'DiffusionTaggedRepositoriesFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'DiffusionURIEditEngine' => 'PhabricatorEditEngine', @@ -6722,6 +6754,9 @@ phutil_register_library_map(array( 'PhabricatorController' => 'AphrontController', 'PhabricatorCookies' => 'Phobject', 'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', + 'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', 'PhabricatorCountdown' => array( 'PhabricatorCountdownDAO', 'PhabricatorPolicyInterface', @@ -7076,6 +7111,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), + 'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileBundleLoader' => 'Phobject', 'PhabricatorFileChunk' => array( 'PhabricatorFileDAO', @@ -7112,12 +7148,16 @@ phutil_register_library_map(array( 'PhabricatorFileLinkView' => 'AphrontView', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat', + 'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat', 'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileStorageConfigurationException' => 'Exception', 'PhabricatorFileStorageEngine' => 'Phobject', 'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase', + 'PhabricatorFileStorageFormat' => 'Phobject', + 'PhabricatorFileStorageFormatTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorFileTestCase' => 'PhabricatorTestCase', 'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator', @@ -7141,7 +7181,10 @@ phutil_register_library_map(array( 'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow', + 'PhabricatorFilesManagementCycleWorkflow' => 'PhabricatorFilesManagementWorkflow', + 'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow', + 'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow', @@ -7251,6 +7294,8 @@ phutil_register_library_map(array( 'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorJumpNavHandler' => 'Phobject', 'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache', + 'PhabricatorKeyring' => 'Phobject', + 'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorLegalpadApplication' => 'PhabricatorApplication', 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -7399,6 +7444,8 @@ phutil_register_library_map(array( 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', 'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', + 'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorModularTransactionType' => 'Phobject', 'PhabricatorMonospacedFontSetting' => 'PhabricatorStringSetting', 'PhabricatorMonospacedTextareasSetting' => 'PhabricatorSelectSetting', 'PhabricatorMotivatorProfilePanel' => 'PhabricatorProfilePanel', @@ -7594,12 +7641,14 @@ phutil_register_library_map(array( 'PhabricatorPasteArchiveController' => 'PhabricatorPasteController', 'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', + 'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteController' => 'PhabricatorController', 'PhabricatorPasteDAO' => 'PhabricatorLiskDAO', 'PhabricatorPasteEditController' => 'PhabricatorPasteController', 'PhabricatorPasteEditEngine' => 'PhabricatorEditEngine', 'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorPasteFilenameContextFreeGrammar' => 'PhutilContextFreeGrammar', + 'PhabricatorPasteLanguageTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType', 'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -7608,10 +7657,13 @@ phutil_register_library_map(array( 'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorPasteSnippet' => 'Phobject', + 'PhabricatorPasteStatusTransaction' => 'PhabricatorPasteTransactionType', 'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator', - 'PhabricatorPasteTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorPasteTitleTransaction' => 'PhabricatorPasteTransactionType', + 'PhabricatorPasteTransaction' => 'PhabricatorModularTransaction', 'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment', 'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorPasteTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorPasteViewController' => 'PhabricatorPasteController', 'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource', @@ -8003,6 +8055,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMovePathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', @@ -8016,6 +8069,10 @@ phutil_register_library_map(array( 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryMirror' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine', + 'PhabricatorRepositoryOldRef' => array( + 'PhabricatorRepositoryDAO', + 'PhabricatorPolicyInterface', + ), 'PhabricatorRepositoryParsedChange' => 'Phobject', 'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine', 'PhabricatorRepositoryPullEvent' => array( @@ -8392,6 +8449,8 @@ phutil_register_library_map(array( 'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorTooltipUIExample' => 'PhabricatorUIExample', + 'PhabricatorTransactionChange' => 'Phobject', + 'PhabricatorTransactionRemarkupChange' => 'PhabricatorTransactionChange', 'PhabricatorTransactions' => 'Phobject', 'PhabricatorTransactionsApplication' => 'PhabricatorApplication', 'PhabricatorTransactionsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', @@ -8585,6 +8644,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorFulltextInterface', ), 'PhameBlog404Controller' => 'PhameLiveController', 'PhameBlogArchiveController' => 'PhameBlogController', @@ -8595,6 +8655,8 @@ phutil_register_library_map(array( 'PhameBlogEditEngine' => 'PhabricatorEditEngine', 'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor', 'PhameBlogFeedController' => 'PhameBlogController', + 'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine', + 'PhameBlogHeaderPictureController' => 'PhameBlogController', 'PhameBlogListController' => 'PhameBlogController', 'PhameBlogListView' => 'AphrontTagView', 'PhameBlogManageController' => 'PhameBlogController', @@ -8626,13 +8688,16 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorTokenReceiverInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorFulltextInterface', ), + 'PhamePostArchiveController' => 'PhamePostController', 'PhamePostCommentController' => 'PhamePostController', 'PhamePostController' => 'PhameController', 'PhamePostEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhamePostEditController' => 'PhamePostController', 'PhamePostEditEngine' => 'PhabricatorEditEngine', 'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine', 'PhamePostHistoryController' => 'PhamePostController', 'PhamePostListController' => 'PhamePostController', 'PhamePostListView' => 'AphrontTagView', diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index d795d4b97d..f9524084a0 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -542,7 +542,7 @@ final class PhabricatorAuditEditor protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, - $blocks, + array $changes, PhutilMarkupEngine $engine) { // we are only really trying to find unmentionable phids here... @@ -563,7 +563,7 @@ final class PhabricatorAuditEditor return $result; } - $flat_blocks = array_mergev($blocks); + $flat_blocks = mpull($changes, 'getNewValue'); $huge_block = implode("\n\n", $flat_blocks); $phid_map = array(); $phid_map[] = $this->getUnmentionablePHIDMap(); diff --git a/src/applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php b/src/applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php index e91b66e647..bf79ea8743 100644 --- a/src/applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php +++ b/src/applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php @@ -30,9 +30,11 @@ final class PhabricatorAuthAuthProviderPHIDType extends PhabricatorPHIDType { array $objects) { foreach ($handles as $phid => $handle) { - $provider = $objects[$phid]; + $provider = $objects[$phid]->getProvider(); - $handle->setName($provider->getProviderName()); + if ($provider) { + $handle->setName($provider->getProviderName()); + } } } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 99d3f9962b..1e2eaa5680 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -190,6 +190,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'The Differential revision list view age UI elements have been removed '. 'to simplify the interface.'); + $global_settings_reason = pht( + 'The "Re: Prefix" and "Vary Subjects" settings are now configured '. + 'in global settings.'); + $ancient_config += array( 'phid.external-loaders' => pht( @@ -321,6 +325,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'differential.days-fresh' => $stale_reason, 'differential.days-stale' => $stale_reason, + + 'metamta.re-prefix' => $global_settings_reason, + 'metamta.vary-subjects' => $global_settings_reason, ); return $ancient_config; diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index 16bdb7e520..45604f6872 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -276,22 +276,6 @@ EODOC )) ->setSummary(pht('Show email preferences link in email.')) ->setDescription($email_preferences_description), - $this->newOption('metamta.re-prefix', 'bool', false) - ->setBoolOptions( - array( - pht('Force "Re:" Subject Prefix'), - pht('No "Re:" Subject Prefix'), - )) - ->setSummary(pht('Control "Re:" subject prefix, for Mail.app.')) - ->setDescription($re_prefix_description), - $this->newOption('metamta.vary-subjects', 'bool', true) - ->setBoolOptions( - array( - pht('Allow Varied Subjects'), - pht('Always Use the Same Thread Subject'), - )) - ->setSummary(pht('Control subject variance, for some mail clients.')) - ->setDescription($vary_subjects_description), $this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false) ->setBoolOptions( array( diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php index 311c2d30b6..dde1d0d7be 100644 --- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php +++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php @@ -43,6 +43,14 @@ final class PhabricatorSecurityConfigOptions '255.255.255.255/32', ); + $keyring_type = 'custom:PhabricatorKeyringConfigOptionType'; + $keyring_description = $this->deformat(pht(<<newOption('security.alternate-file-domain', 'string', null) ->setLocked(true) @@ -276,6 +284,10 @@ final class PhabricatorSecurityConfigOptions 'unsecured content over plain HTTP. It is very difficult to '. 'undo this change once users\' browsers have accepted the '. 'setting.')), + $this->newOption('keyring', $keyring_type, array()) + ->setHidden(true) + ->setSummary(pht('Configure master encryption keys.')) + ->setDescription($keyring_description), ); } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 2d2671227d..a87c12c926 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1303,10 +1303,10 @@ final class DifferentialTransactionEditor protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, - $blocks, + array $changes, PhutilMarkupEngine $engine) { - $flat_blocks = array_mergev($blocks); + $flat_blocks = mpull($changes, 'getNewValue'); $huge_block = implode("\n\n", $flat_blocks); $task_map = array(); diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index 5cf6f82198..5fac0708fc 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -75,7 +75,7 @@ final class DifferentialRevisionSearchEngine ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setAliases(array('repository', 'repositories', 'repositoryPHID')) - ->setDatasource(new DiffusionRepositoryDatasource()) + ->setDatasource(new DifferentialRepositoryDatasource()) ->setDescription( pht('Find revisions from specific repositories.')), id(new PhabricatorSearchSelectField()) diff --git a/src/applications/differential/typeahead/DifferentialRepositoryDatasource.php b/src/applications/differential/typeahead/DifferentialRepositoryDatasource.php new file mode 100644 index 0000000000..1decc9c1ca --- /dev/null +++ b/src/applications/differential/typeahead/DifferentialRepositoryDatasource.php @@ -0,0 +1,25 @@ +isUnreachable()) { + $this->commitErrors[] = pht( + 'This commit has been deleted in the repository: it is no longer '. + 'reachable from any branch, tag, or ref.'); + } + if ($this->getCommitErrors()) { $error_panel = id(new PHUIInfoView()) ->appendChild($this->getCommitErrors()) diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php index be21125e6c..62dfdd6ace 100644 --- a/src/applications/diffusion/data/DiffusionPathChange.php +++ b/src/applications/diffusion/data/DiffusionPathChange.php @@ -113,9 +113,7 @@ final class DiffusionPathChange extends Phobject { if (!$this->getCommitData()) { return null; } - $message = $this->getCommitData()->getCommitMessage(); - $first = idx(explode("\n", $message), 0); - return substr($first, 0, 80); + return $this->getCommitData()->getSummary(); } public static function convertToArcanistChanges(array $changes) { diff --git a/src/applications/diffusion/data/DiffusionRepositoryRef.php b/src/applications/diffusion/data/DiffusionRepositoryRef.php index 45c92c8c0a..de3451b25f 100644 --- a/src/applications/diffusion/data/DiffusionRepositoryRef.php +++ b/src/applications/diffusion/data/DiffusionRepositoryRef.php @@ -7,6 +7,7 @@ final class DiffusionRepositoryRef extends Phobject { private $shortName; private $commitIdentifier; + private $refType; private $rawFields = array(); public function setRawFields(array $raw_fields) { @@ -36,6 +37,25 @@ final class DiffusionRepositoryRef extends Phobject { return $this->shortName; } + public function setRefType($ref_type) { + $this->refType = $ref_type; + return $this; + } + + public function getRefType() { + return $this->refType; + } + + public function isBranch() { + $type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH; + return ($this->getRefType() === $type_branch); + } + + public function isTag() { + $type_tag = PhabricatorRepositoryRefCursor::TYPE_TAG; + return ($this->getRefType() === $type_tag); + } + /* -( Serialization )------------------------------------------------------ */ @@ -44,6 +64,7 @@ final class DiffusionRepositoryRef extends Phobject { return array( 'shortName' => $this->shortName, 'commitIdentifier' => $this->commitIdentifier, + 'refType' => $this->refType, 'rawFields' => $this->rawFields, ); } @@ -52,6 +73,7 @@ final class DiffusionRepositoryRef extends Phobject { return id(new DiffusionRepositoryRef()) ->setShortName($dict['shortName']) ->setCommitIdentifier($dict['commitIdentifier']) + ->setRefType($dict['refType']) ->setRawFields($dict['rawFields']); } diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php index 038d833670..3b9f88d232 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php @@ -14,96 +14,121 @@ final class DiffusionLowLevelGitRefQuery extends DiffusionLowLevelQuery { } protected function executeQuery() { + $type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH; + $type_tag = PhabricatorRepositoryRefCursor::TYPE_TAG; + $type_ref = PhabricatorRepositoryRefCursor::TYPE_REF; + $ref_types = $this->refTypes; - if ($ref_types) { - $type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH; - $type_tag = PhabricatorRepositoryRefCursor::TYPE_TAG; - - $ref_types = array_fuse($ref_types); - - $with_branches = isset($ref_types[$type_branch]); - $with_tags = isset($ref_types[$type_tag]); - } else { - $with_branches = true; - $with_tags = true; + if (!$ref_types) { + $ref_types = array($type_branch, $type_tag, $type_ref); } + $ref_types = array_fuse($ref_types); + + $with_branches = isset($ref_types[$type_branch]); + $with_tags = isset($ref_types[$type_tag]); + $with_refs = isset($refs_types[$type_ref]); + $repository = $this->getRepository(); $prefixes = array(); - if ($with_branches) { - if ($repository->isWorkingCopyBare()) { - $prefix = 'refs/heads/'; - } else { - $remote = DiffusionGitBranch::DEFAULT_GIT_REMOTE; - $prefix = 'refs/remotes/'.$remote.'/'; + if ($repository->isWorkingCopyBare()) { + $branch_prefix = 'refs/heads/'; + } else { + $remote = DiffusionGitBranch::DEFAULT_GIT_REMOTE; + $branch_prefix = 'refs/remotes/'.$remote.'/'; + } + + $tag_prefix = 'refs/tags/'; + + + if ($with_refs || count($ref_types) > 1) { + // If we're loading refs or more than one type of ref, just query + // everything. + $prefix = 'refs/'; + } else { + if ($with_branches) { + $prefix = $branch_prefix; + } + if ($with_tags) { + $prefix = $tag_prefix; } - $prefixes[] = $prefix; } - if ($with_tags) { - $prefixes[] = 'refs/tags/'; + $branch_len = strlen($branch_prefix); + $tag_len = strlen($tag_prefix); + + list($stdout) = $repository->execxLocalCommand( + 'for-each-ref --sort=%s --format=%s -- %s', + '-creatordate', + $this->getFormatString(), + $prefix); + + $stdout = rtrim($stdout); + if (!strlen($stdout)) { + return array(); } - $order = '-creatordate'; + $remote_prefix = 'refs/remotes/'; + $remote_len = strlen($remote_prefix); - $futures = array(); - foreach ($prefixes as $prefix) { - $futures[$prefix] = $repository->getLocalCommandFuture( - 'for-each-ref --sort=%s --format=%s %s', - $order, - $this->getFormatString(), - $prefix); - } - - // Resolve all the futures first. We want to iterate over them in prefix - // order, not resolution order. - foreach (new FutureIterator($futures) as $prefix => $future) { - $future->resolvex(); - } + // NOTE: Although git supports --count, we can't apply any offset or + // limit logic until the very end because we may encounter a HEAD which + // we want to discard. + $lines = explode("\n", $stdout); $results = array(); - foreach ($futures as $prefix => $future) { - list($stdout) = $future->resolvex(); + foreach ($lines as $line) { + $fields = $this->extractFields($line); - $stdout = rtrim($stdout); - if (!strlen($stdout)) { + $refname = $fields['refname']; + if (!strncmp($refname, $branch_prefix, $branch_len)) { + $short = substr($refname, $branch_len); + $type = $type_branch; + } else if (!strncmp($refname, $tag_prefix, $tag_len)) { + $short = substr($refname, $tag_len); + $type = $type_tag; + } else if (!strncmp($refname, $remote_prefix, $remote_len)) { + // If we've found a remote ref that we didn't recognize as naming a + // branch, just ignore it. This can happen if we're observing a remote, + // and that remote has its own remotes. We don't care about their + // state and they may be out of date, so ignore them. + continue; + } else { + $short = $refname; + $type = $type_ref; + } + + // If this isn't a type of ref we care about, skip it. + if (empty($ref_types[$type])) { continue; } - // NOTE: Although git supports --count, we can't apply any offset or - // limit logic until the very end because we may encounter a HEAD which - // we want to discard. - - $lines = explode("\n", $stdout); - foreach ($lines as $line) { - $fields = $this->extractFields($line); - - $creator = $fields['creator']; - $matches = null; - if (preg_match('/^(.*) ([0-9]+) ([0-9+-]+)$/', $creator, $matches)) { - $fields['author'] = $matches[1]; - $fields['epoch'] = (int)$matches[2]; - } else { - $fields['author'] = null; - $fields['epoch'] = null; - } - - $commit = nonempty($fields['*objectname'], $fields['objectname']); - - $short = substr($fields['refname'], strlen($prefix)); - if ($short == 'HEAD') { - continue; - } - - $ref = id(new DiffusionRepositoryRef()) - ->setShortName($short) - ->setCommitIdentifier($commit) - ->setRawFields($fields); - - $results[] = $ref; + // If this is the local HEAD, skip it. + if ($short == 'HEAD') { + continue; } + + $creator = $fields['creator']; + $matches = null; + if (preg_match('/^(.*) ([0-9]+) ([0-9+-]+)$/', $creator, $matches)) { + $fields['author'] = $matches[1]; + $fields['epoch'] = (int)$matches[2]; + } else { + $fields['author'] = null; + $fields['epoch'] = null; + } + + $commit = nonempty($fields['*objectname'], $fields['objectname']); + + $ref = id(new DiffusionRepositoryRef()) + ->setRefType($type) + ->setShortName($short) + ->setCommitIdentifier($commit) + ->setRawFields($fields); + + $results[] = $ref; } return $results; diff --git a/src/applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php b/src/applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php new file mode 100644 index 0000000000..bdf89bca0c --- /dev/null +++ b/src/applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php @@ -0,0 +1,100 @@ +)...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorProjectApplication'; + } + + public function getComponentDatasources() { + return array( + new PhabricatorProjectDatasource(), + ); + } + + public function getDatasourceFunctions() { + return array( + 'tagged' => array( + 'name' => pht('Repositories: ...'), + 'arguments' => pht('project'), + 'summary' => pht('Find results for repositories of a project.'), + 'description' => pht( + 'This function allows you to find results for any of the `. + `repositories of a project:'. + "\n\n". + '> tagged(engineering)'), + ), + ); + } + + protected function didLoadResults(array $results) { + foreach ($results as $result) { + $result + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setColor(null) + ->setPHID('tagged('.$result->getPHID().')') + ->setDisplayName(pht('Tagged: %s', $result->getDisplayName())) + ->setName('tagged '.$result->getName()); + } + + return $results; + } + + protected function evaluateFunction($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_OR, + $phids) + ->execute(); + + $results = array(); + + foreach ($repositories as $repository) { + $results[] = $repository->getPHID(); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $phids = array(); + foreach ($argv_list as $argv) { + $phids[] = head($argv); + } + + $tokens = $this->renderTokens($phids); + foreach ($tokens as $token) { + // Remove any project color on this token. + $token->setColor(null); + + if ($token->isInvalid()) { + $token + ->setValue(pht('Repositories: Invalid Project')); + } else { + $token + ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) + ->setKey('tagged('.$token->getKey().')') + ->setValue(pht('Tagged: %s', $token->getValue())); + } + } + + return $tokens; + } + +} diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php index 3b59edc37b..8d018c61b3 100644 --- a/src/applications/feed/PhabricatorFeedStoryPublisher.php +++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php @@ -215,6 +215,7 @@ final class PhabricatorFeedStoryPublisher extends Phobject { $all_prefs = id(new PhabricatorUserPreferencesQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUserPHIDs($phids) + ->needSyntheticPreferences(true) ->execute(); $all_prefs = mpull($all_prefs, null, 'getUserPHID'); } diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php index 063a6d9138..b16e07c148 100644 --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -44,6 +44,7 @@ final class PhabricatorFilesConfigOptions 'video/mp4' => 'video/mp4', 'video/ogg' => 'video/ogg', 'video/webm' => 'video/webm', + 'video/quicktime' => 'video/quicktime', ); $image_default = array( @@ -71,6 +72,7 @@ final class PhabricatorFilesConfigOptions // to set the mood for your task without distracting viewers.) 'video/mp4' => true, 'video/ogg' => true, + 'video/quicktime' => true, 'application/ogg' => true, ); @@ -78,6 +80,7 @@ final class PhabricatorFilesConfigOptions 'video/mp4' => true, 'video/ogg' => true, 'video/webm' => true, + 'video/quicktime' => true, 'application/ogg' => true, ); diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 5f60c5ec78..9633a3f956 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -256,8 +256,10 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $types[] = pht('Profile'); } - $types = implode(', ', $types); - $finfo->addProperty(pht('Attributes'), $types); + if ($types) { + $types = implode(', ', $types); + $finfo->addProperty(pht('Attributes'), $types); + } $storage_properties = new PHUIPropertyListView(); $box->addPropertyList($storage_properties, pht('Storage')); @@ -266,9 +268,14 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { pht('Engine'), $file->getStorageEngine()); - $storage_properties->addProperty( - pht('Format'), - $file->getStorageFormat()); + $format_key = $file->getStorageFormat(); + $format = PhabricatorFileStorageFormat::getFormat($format_key); + if ($format) { + $format_name = $format->getStorageFormatName(); + } else { + $format_name = pht('Unknown ("%s")', $format_key); + } + $storage_properties->addProperty(pht('Format'), $format_name); $storage_properties->addProperty( pht('Handle'), diff --git a/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php b/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php index eb6288b40f..8deb1f6465 100644 --- a/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php +++ b/src/applications/files/engine/PhabricatorChunkedFileStorageEngine.php @@ -174,7 +174,7 @@ final class PhabricatorChunkedFileStorageEngine return (4 * 1024 * 1024); } - public function getFileDataIterator(PhabricatorFile $file, $begin, $end) { + public function getRawFileDataIterator(PhabricatorFile $file, $begin, $end) { $chunks = id(new PhabricatorFileChunkQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withChunkHandles(array($file->getStorageHandle())) diff --git a/src/applications/files/engine/PhabricatorFileStorageEngine.php b/src/applications/files/engine/PhabricatorFileStorageEngine.php index 21f7401452..21f15eab50 100644 --- a/src/applications/files/engine/PhabricatorFileStorageEngine.php +++ b/src/applications/files/engine/PhabricatorFileStorageEngine.php @@ -325,10 +325,10 @@ abstract class PhabricatorFileStorageEngine extends Phobject { return $engine->getChunkSize(); } - public function getFileDataIterator(PhabricatorFile $file, $begin, $end) { + public function getRawFileDataIterator(PhabricatorFile $file, $begin, $end) { // The default implementation is trivial and just loads the entire file // upfront. - $data = $file->loadFileData(); + $data = $this->readFile($file->getStorageHandle()); if ($begin !== null && $end !== null) { $data = substr($data, $begin, ($end - $begin)); diff --git a/src/applications/files/format/PhabricatorFileAES256StorageFormat.php b/src/applications/files/format/PhabricatorFileAES256StorageFormat.php new file mode 100644 index 0000000000..6c8a0273bd --- /dev/null +++ b/src/applications/files/format/PhabricatorFileAES256StorageFormat.php @@ -0,0 +1,199 @@ +openEnvelope(); + return base64_encode($material); + } + + public function canCycleMasterKey() { + return true; + } + + public function cycleStorageProperties() { + $file = $this->getFile(); + list($key, $iv) = $this->extractKeyAndIV($file); + return $this->formatStorageProperties($key, $iv); + } + + public function newReadIterator($raw_iterator) { + $file = $this->getFile(); + $data = $file->loadDataFromIterator($raw_iterator); + + list($key, $iv) = $this->extractKeyAndIV($file); + + $data = $this->decryptData($data, $key, $iv); + + return array($data); + } + + public function newWriteIterator($raw_iterator) { + $file = $this->getFile(); + $data = $file->loadDataFromIterator($raw_iterator); + + list($key, $iv) = $this->extractKeyAndIV($file); + + $data = $this->encryptData($data, $key, $iv); + + return array($data); + } + + public function newStorageProperties() { + // Generate a unique key and IV for this block of data. + $key_envelope = self::newAES256Key(); + $iv_envelope = self::newAES256IV(); + + return $this->formatStorageProperties($key_envelope, $iv_envelope); + } + + private function formatStorageProperties( + PhutilOpaqueEnvelope $key_envelope, + PhutilOpaqueEnvelope $iv_envelope) { + + // Encode the raw binary data with base64 so we can wrap it in JSON. + $data = array( + 'iv.base64' => base64_encode($iv_envelope->openEnvelope()), + 'key.base64' => base64_encode($key_envelope->openEnvelope()), + ); + + // Encode the base64 data with JSON. + $data_clear = phutil_json_encode($data); + + // Encrypt the block key with the master key, using a unique IV. + $data_iv = self::newAES256IV(); + $key_name = $this->getMasterKeyName(); + $master_key = $this->getMasterKeyMaterial($key_name); + $data_cipher = $this->encryptData($data_clear, $master_key, $data_iv); + + return array( + 'key.name' => $key_name, + 'iv.base64' => base64_encode($data_iv->openEnvelope()), + 'payload.base64' => base64_encode($data_cipher), + ); + } + + private function extractKeyAndIV(PhabricatorFile $file) { + $outer_iv = $file->getStorageProperty('iv.base64'); + $outer_iv = base64_decode($outer_iv); + $outer_iv = new PhutilOpaqueEnvelope($outer_iv); + + $outer_payload = $file->getStorageProperty('payload.base64'); + $outer_payload = base64_decode($outer_payload); + + $outer_key_name = $file->getStorageProperty('key.name'); + $outer_key = $this->getMasterKeyMaterial($outer_key_name); + + $payload = $this->decryptData($outer_payload, $outer_key, $outer_iv); + $payload = phutil_json_decode($payload); + + $inner_iv = $payload['iv.base64']; + $inner_iv = base64_decode($inner_iv); + $inner_iv = new PhutilOpaqueEnvelope($inner_iv); + + $inner_key = $payload['key.base64']; + $inner_key = base64_decode($inner_key); + $inner_key = new PhutilOpaqueEnvelope($inner_key); + + return array($inner_key, $inner_iv); + } + + private function encryptData( + $data, + PhutilOpaqueEnvelope $key, + PhutilOpaqueEnvelope $iv) { + + $method = 'aes-256-cbc'; + $key = $key->openEnvelope(); + $iv = $iv->openEnvelope(); + + $result = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + if ($result === false) { + throw new Exception( + pht( + 'Failed to openssl_encrypt() data: %s', + openssl_error_string())); + } + + return $result; + } + + private function decryptData( + $data, + PhutilOpaqueEnvelope $key, + PhutilOpaqueEnvelope $iv) { + + $method = 'aes-256-cbc'; + $key = $key->openEnvelope(); + $iv = $iv->openEnvelope(); + + $result = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + if ($result === false) { + throw new Exception( + pht( + 'Failed to openssl_decrypt() data: %s', + openssl_error_string())); + } + + return $result; + } + + public static function newAES256Key() { + // Unsurprisingly, AES256 uses a 256 bit key. + $key = Filesystem::readRandomBytes(phutil_units('256 bits in bytes')); + return new PhutilOpaqueEnvelope($key); + } + + public static function newAES256IV() { + // AES256 uses a 256 bit key, but the initialization vector length is + // only 128 bits. + $iv = Filesystem::readRandomBytes(phutil_units('128 bits in bytes')); + return new PhutilOpaqueEnvelope($iv); + } + + public function selectMasterKey($key_name) { + // Require that the key exist on the key ring. + $this->getMasterKeyMaterial($key_name); + + $this->keyName = $key_name; + return $this; + } + + private function getMasterKeyName() { + if ($this->keyName !== null) { + return $this->keyName; + } + + $default = PhabricatorKeyring::getDefaultKeyName(self::FORMATKEY); + if ($default !== null) { + return $default; + } + + throw new Exception( + pht( + 'No AES256 key is specified in the keyring as a default encryption '. + 'key, and no encryption key has been explicitly selected.')); + } + + private function getMasterKeyMaterial($key_name) { + return PhabricatorKeyring::getKey($key_name, self::FORMATKEY); + } + +} diff --git a/src/applications/files/format/PhabricatorFileROT13StorageFormat.php b/src/applications/files/format/PhabricatorFileROT13StorageFormat.php new file mode 100644 index 0000000000..ab9bc541bc --- /dev/null +++ b/src/applications/files/format/PhabricatorFileROT13StorageFormat.php @@ -0,0 +1,44 @@ +getFile(); + $iterations = $file->getStorageProperty('iterations', 1); + + $value = $file->loadDataFromIterator($raw_iterator); + for ($ii = 0; $ii < $iterations; $ii++) { + $value = str_rot13($value); + } + + return array($value); + } + + public function newWriteIterator($raw_iterator) { + return $this->newReadIterator($raw_iterator); + } + + public function newStorageProperties() { + // For extreme security, repeatedly encode the data using a random (odd) + // number of iterations. + return array( + 'iterations' => (mt_rand(1, 3) * 2) - 1, + ); + } + +} diff --git a/src/applications/files/format/PhabricatorFileRawStorageFormat.php b/src/applications/files/format/PhabricatorFileRawStorageFormat.php new file mode 100644 index 0000000000..8a76b22d5b --- /dev/null +++ b/src/applications/files/format/PhabricatorFileRawStorageFormat.php @@ -0,0 +1,20 @@ +file = $file; + return $this; + } + + final public function getFile() { + if (!$this->file) { + throw new PhutilInvalidStateException('setFile'); + } + return $this->file; + } + + abstract public function getStorageFormatName(); + + abstract public function newReadIterator($raw_iterator); + abstract public function newWriteIterator($raw_iterator); + + public function newStorageProperties() { + return array(); + } + + public function canGenerateNewKeyMaterial() { + return false; + } + + public function generateNewKeyMaterial() { + throw new PhutilMethodNotImplementedException(); + } + + public function canCycleMasterKey() { + return false; + } + + public function cycleStorageProperties() { + throw new PhutilMethodNotImplementedException(); + } + + public function selectMasterKey($key_name) { + throw new Exception( + pht( + 'This storage format ("%s") does not support key selection.', + $this->getStorageFormatName())); + } + + final public function getStorageFormatKey() { + return $this->getPhobjectClassConstant('FORMATKEY'); + } + + final public static function getAllFormats() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getStorageFormatKey') + ->execute(); + } + + final public static function getFormat($key) { + $formats = self::getAllFormats(); + return idx($formats, $key); + } + + final public static function requireFormat($key) { + $format = self::getFormat($key); + + if (!$format) { + throw new Exception( + pht( + 'No file storage format with key "%s" exists.', + $key)); + } + + return $format; + } + +} diff --git a/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php b/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php new file mode 100644 index 0000000000..18364fd2a4 --- /dev/null +++ b/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php @@ -0,0 +1,77 @@ + true, + ); + } + + public function testRot13Storage() { + $engine = new PhabricatorTestStorageEngine(); + $rot13_format = PhabricatorFileROT13StorageFormat::FORMATKEY; + + $data = 'The cow jumped over the full moon.'; + $expect = 'Gur pbj whzcrq bire gur shyy zbba.'; + + $params = array( + 'name' => 'test.dat', + 'storageEngines' => array( + $engine, + ), + 'format' => $rot13_format, + ); + + $file = PhabricatorFile::newFromFileData($data, $params); + + // We should have a file stored as rot13, which reads back the input + // data correctly. + $this->assertEqual($rot13_format, $file->getStorageFormat()); + $this->assertEqual($data, $file->loadFileData()); + + // The actual raw data in the storage engine should be encoded. + $raw_data = $engine->readFile($file->getStorageHandle()); + $this->assertEqual($expect, $raw_data); + } + + public function testAES256Storage() { + $engine = new PhabricatorTestStorageEngine(); + + $key_name = 'test.abcd'; + $key_text = 'abcdefghijklmnopABCDEFGHIJKLMNOP'; + + PhabricatorKeyring::addKey( + array( + 'name' => $key_name, + 'type' => 'aes-256-cbc', + 'material.base64' => base64_encode($key_text), + )); + + $format = id(new PhabricatorFileAES256StorageFormat()) + ->selectMasterKey($key_name); + + $data = 'The cow jumped over the full moon.'; + + $params = array( + 'name' => 'test.dat', + 'storageEngines' => array( + $engine, + ), + 'format' => $format, + ); + + $file = PhabricatorFile::newFromFileData($data, $params); + + // We should have a file stored as AES256. + $format_key = $format->getStorageFormatKey(); + $this->assertEqual($format_key, $file->getStorageFormat()); + $this->assertEqual($data, $file->loadFileData()); + + // The actual raw data in the storage engine should be encrypted. We + // can't really test this, but we can make sure it's not the same as the + // input data. + $raw_data = $engine->readFile($file->getStorageHandle()); + $this->assertTrue($data !== $raw_data); + } +} diff --git a/src/applications/files/keyring/PhabricatorKeyring.php b/src/applications/files/keyring/PhabricatorKeyring.php new file mode 100644 index 0000000000..1772c145c2 --- /dev/null +++ b/src/applications/files/keyring/PhabricatorKeyring.php @@ -0,0 +1,52 @@ + $key) { + if (!empty($key['default'])) { + return $name; + } + } + + return null; + } + + private static function readConfiguration() { + if (self::$hasReadConfiguration) { + return true; + } + + self::$hasReadConfiguration = true; + + foreach (PhabricatorEnv::getEnvConfig('keyring') as $spec) { + self::addKey($spec); + } + } + +} diff --git a/src/applications/files/keyring/PhabricatorKeyringConfigOptionType.php b/src/applications/files/keyring/PhabricatorKeyringConfigOptionType.php new file mode 100644 index 0000000000..2550d17b44 --- /dev/null +++ b/src/applications/files/keyring/PhabricatorKeyringConfigOptionType.php @@ -0,0 +1,111 @@ + $spec) { + if (!is_array($spec)) { + throw new Exception( + pht( + 'Keyring configuration is not valid: each entry in the list must '. + 'be a dictionary describing an encryption key, but the value '. + 'with index "%s" is not a dictionary.', + $index)); + } + } + + + $map = array(); + $defaults = array(); + foreach ($value as $index => $spec) { + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'name' => 'string', + 'type' => 'string', + 'material.base64' => 'string', + 'default' => 'optional bool', + )); + } catch (Exception $ex) { + throw new Exception( + pht( + 'Keyring configuration has an invalid key specification (at '. + 'index "%s"): %s.', + $index, + $ex->getMessage())); + } + + $name = $spec['name']; + if (isset($map[$name])) { + throw new Exception( + pht( + 'Keyring configuration is invalid: it describes multiple keys '. + 'with the same name ("%s"). Each key must have a unique name.', + $name)); + } + $map[$name] = true; + + if (idx($spec, 'default')) { + $defaults[] = $name; + } + + $type = $spec['type']; + switch ($type) { + case 'aes-256-cbc': + if (!function_exists('openssl_encrypt')) { + throw new Exception( + pht( + 'Keyring is configured with a "%s" key, but the PHP OpenSSL '. + 'extension is not installed. Install the OpenSSL extension '. + 'to enable encryption.', + $type)); + } + + $material = $spec['material.base64']; + $material = base64_decode($material, true); + if ($material === false) { + throw new Exception( + pht( + 'Keyring specifies an invalid key ("%s"): key material '. + 'should be base64 encoded.', + $name)); + } + + if (strlen($material) != 32) { + throw new Exception( + pht( + 'Keyring specifies an invalid key ("%s"): key material '. + 'should be 32 bytes (256 bits) but has length %s.', + $name, + new PhutilNumber(strlen($material)))); + } + break; + default: + throw new Exception( + pht( + 'Keyring configuration is invalid: it describes a key with '. + 'type "%s", but this type is unknown.', + $type)); + } + } + + if (count($defaults) > 1) { + throw new Exception( + pht( + 'Keyring configuration is invalid: it describes multiple default '. + 'encryption keys. No more than one key may be the default key. '. + 'Keys currently configured as defaults: %s.', + implode(', ', $defaults))); + } + } + +} diff --git a/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php new file mode 100644 index 0000000000..6d574d633a --- /dev/null +++ b/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php @@ -0,0 +1,132 @@ +setName('cycle') + ->setSynopsis( + pht('Cycle master key for encrypted files.')) + ->setArguments( + array( + array( + 'name' => 'key', + 'param' => 'keyname', + 'help' => pht('Select a specific storage key to cycle to.'), + ), + array( + 'name' => 'all', + 'help' => pht('Change encoding for all files.'), + ), + array( + 'name' => 'names', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $iterator = $this->buildIterator($args); + if (!$iterator) { + throw new PhutilArgumentUsageException( + pht( + 'Either specify a list of files to cycle, or use --all to cycle '. + 'all files.')); + } + + $format_map = PhabricatorFileStorageFormat::getAllFormats(); + $engines = PhabricatorFileStorageEngine::loadAllEngines(); + + $key_name = $args->getArg('key'); + + $failed = array(); + foreach ($iterator as $file) { + $monogram = $file->getMonogram(); + + $engine_key = $file->getStorageEngine(); + $engine = idx($engines, $engine_key); + + if (!$engine) { + echo tsprintf( + "%s\n", + pht( + '%s: Uses unknown storage engine "%s".', + $monogram, + $engine_key)); + $failed[] = $file; + continue; + } + + if ($engine->isChunkEngine()) { + echo tsprintf( + "%s\n", + pht( + '%s: Stored as chunks, declining to cycle directly.', + $monogram)); + continue; + } + + $format_key = $file->getStorageFormat(); + if (empty($format_map[$format_key])) { + echo tsprintf( + "%s\n", + pht( + '%s: Uses unknown storage format "%s".', + $monogram, + $format_key)); + $failed[] = $file; + continue; + } + + $format = clone $format_map[$format_key]; + $format->setFile($file); + + if (!$format->canCycleMasterKey()) { + echo tsprintf( + "%s\n", + pht( + '%s: Storage format ("%s") does not support key cycling.', + $monogram, + $format->getStorageFormatName())); + continue; + } + + echo tsprintf( + "%s\n", + pht( + '%s: Cycling master key.', + $monogram)); + + try { + if ($key_name) { + $format->selectMasterKey($key_name); + } + + $file->cycleMasterStorageKey($format); + + echo tsprintf( + "%s\n", + pht('Done.')); + } catch (Exception $ex) { + echo tsprintf( + "%B\n", + pht('Failed! %s', (string)$ex)); + $failed[] = $file; + } + } + + if ($failed) { + $monograms = mpull($failed, 'getMonogram'); + + echo tsprintf( + "%s\n", + pht('Failures: %s.', implode(', ', $monograms))); + + return 1; + } + + return 0; + } + +} diff --git a/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php new file mode 100644 index 0000000000..1d972326da --- /dev/null +++ b/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php @@ -0,0 +1,151 @@ +setName('encode') + ->setSynopsis( + pht('Change the storage encoding of files.')) + ->setArguments( + array( + array( + 'name' => 'as', + 'param' => 'format', + 'help' => pht('Select the storage format to use.'), + ), + array( + 'name' => 'key', + 'param' => 'keyname', + 'help' => pht('Select a specific storage key.'), + ), + array( + 'name' => 'all', + 'help' => pht('Change encoding for all files.'), + ), + array( + 'name' => 'force', + 'help' => pht( + 'Re-encode files which are already stored in the target '. + 'encoding.'), + ), + array( + 'name' => 'names', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $iterator = $this->buildIterator($args); + if (!$iterator) { + throw new PhutilArgumentUsageException( + pht( + 'Either specify a list of files to encode, or use --all to '. + 'encode all files.')); + } + + $force = (bool)$args->getArg('force'); + + $format_list = PhabricatorFileStorageFormat::getAllFormats(); + $format_list = array_keys($format_list); + $format_list = implode(', ', $format_list); + + $format_key = $args->getArg('as'); + if (!strlen($format_key)) { + throw new PhutilArgumentUsageException( + pht( + 'Use --as to select a target encoding format. Available '. + 'formats are: %s.', + $format_list)); + } + + $format = PhabricatorFileStorageFormat::getFormat($format_key); + if (!$format) { + throw new PhutilArgumentUsageException( + pht( + 'Storage format "%s" is not valid. Available formats are: %s.', + $format_key, + $format_list)); + } + + $key_name = $args->getArg('key'); + if (strlen($key_name)) { + $format->selectMasterKey($key_name); + } + + $engines = PhabricatorFileStorageEngine::loadAllEngines(); + + $failed = array(); + foreach ($iterator as $file) { + $monogram = $file->getMonogram(); + + $engine_key = $file->getStorageEngine(); + $engine = idx($engines, $engine_key); + + if (!$engine) { + echo tsprintf( + "%s\n", + pht( + '%s: Uses unknown storage engine "%s".', + $monogram, + $engine_key)); + $failed[] = $file; + continue; + } + + if ($engine->isChunkEngine()) { + echo tsprintf( + "%s\n", + pht( + '%s: Stored as chunks, no data to encode directly.', + $monogram)); + continue; + } + + if (($file->getStorageFormat() == $format_key) && !$force) { + echo tsprintf( + "%s\n", + pht( + '%s: Already encoded in target format.', + $monogram)); + continue; + } + + echo tsprintf( + "%s\n", + pht( + '%s: Changing encoding from "%s" to "%s".', + $monogram, + $file->getStorageFormat(), + $format_key)); + + try { + $file->migrateToStorageFormat($format); + + echo tsprintf( + "%s\n", + pht('Done.')); + } catch (Exception $ex) { + echo tsprintf( + "%B\n", + pht('Failed! %s', (string)$ex)); + $failed[] = $file; + } + } + + if ($failed) { + $monograms = mpull($failed, 'getMonogram'); + + echo tsprintf( + "%s\n", + pht('Failures: %s.', implode(', ', $monograms))); + + return 1; + } + + return 0; + } + +} diff --git a/src/applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php new file mode 100644 index 0000000000..710c3586a6 --- /dev/null +++ b/src/applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php @@ -0,0 +1,63 @@ +setName('generate-key') + ->setSynopsis( + pht('Generate an encryption key.')) + ->setArguments( + array( + array( + 'name' => 'type', + 'param' => 'keytype', + 'help' => pht('Select the type of key to generate.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $type = $args->getArg('type'); + if (!strlen($type)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify the type of key to generate with --type.')); + } + + $format = PhabricatorFileStorageFormat::getFormat($type); + if (!$format) { + throw new PhutilArgumentUsageException( + pht( + 'No key type "%s" exists.', + $type)); + } + + if (!$format->canGenerateNewKeyMaterial()) { + throw new PhutilArgumentUsageException( + pht( + 'Storage format "%s" can not generate keys.', + $format->getStorageFormatName())); + } + + $material = $format->generateNewKeyMaterial(); + + $structure = array( + 'name' => 'generated-key-'.Filesystem::readRandomCharacters(12), + 'type' => $type, + 'material.base64' => $material, + ); + + $json = id(new PhutilJSON())->encodeFormatted($structure); + + echo tsprintf( + "%s: %s\n\n%B\n", + pht('Key Material'), + $format->getStorageFormatName(), + $json); + + return 0; + } + +} diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index b21b7dbe81..4e43e3dd18 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -26,14 +26,13 @@ final class PhabricatorFile extends PhabricatorFileDAO PhabricatorPolicyInterface, PhabricatorDestructibleInterface { - const STORAGE_FORMAT_RAW = 'raw'; - const METADATA_IMAGE_WIDTH = 'width'; const METADATA_IMAGE_HEIGHT = 'height'; const METADATA_CAN_CDN = 'canCDN'; const METADATA_BUILTIN = 'builtin'; const METADATA_PARTIAL = 'partial'; const METADATA_PROFILE = 'profile'; + const METADATA_STORAGE = 'storage'; protected $name; protected $mimeType; @@ -233,10 +232,10 @@ final class PhabricatorFile extends PhabricatorFileDAO $hash); if ($file) { - // copy storageEngine, storageHandle, storageFormat $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); + $copy_of_storage_properties = $file->getStorageProperties(); $copy_of_byte_size = $file->getByteSize(); $copy_of_mime_type = $file->getMimeType(); @@ -248,6 +247,7 @@ final class PhabricatorFile extends PhabricatorFileDAO $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); $new_file->setStorageFormat($copy_of_storage_format); + $new_file->setStorageProperties($copy_of_storage_properties); $new_file->setMimeType($copy_of_mime_type); $new_file->copyDimensions($file); @@ -290,7 +290,11 @@ final class PhabricatorFile extends PhabricatorFileDAO $file->setStorageEngine($engine->getEngineIdentifier()); $file->setStorageHandle(PhabricatorFileChunk::newChunkHandle()); - $file->setStorageFormat(self::STORAGE_FORMAT_RAW); + + // Chunked files are always stored raw because they do not actually store + // data. The chunks do, and can be individually formatted. + $file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY); + $file->setIsPartial(1); $file->readPropertiesFromParameters($params); @@ -322,6 +326,29 @@ final class PhabricatorFile extends PhabricatorFileDAO $file = self::initializeNewFile(); + $aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY; + $has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type); + if ($has_aes !== null) { + $default_key = PhabricatorFileAES256StorageFormat::FORMATKEY; + } else { + $default_key = PhabricatorFileRawStorageFormat::FORMATKEY; + } + $key = idx($params, 'format', $default_key); + + // Callers can pass in an object explicitly instead of a key. This is + // primarily useful for unit tests. + if ($key instanceof PhabricatorFileStorageFormat) { + $format = clone $key; + } else { + $format = clone PhabricatorFileStorageFormat::requireFormat($key); + } + + $format->setFile($file); + + $properties = $format->newStorageProperties(); + $file->setStorageFormat($format->getStorageFormatKey()); + $file->setStorageProperties($properties); + $data_handle = null; $engine_identifier = null; $exceptions = array(); @@ -361,10 +388,6 @@ final class PhabricatorFile extends PhabricatorFileDAO $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); - // TODO: This is probably YAGNI, but allows for us to do encryption or - // compression later if we want. - $file->setStorageFormat(self::STORAGE_FORMAT_RAW); - $file->readPropertiesFromParameters($params); if (!$file->getMimeType()) { @@ -427,6 +450,53 @@ final class PhabricatorFile extends PhabricatorFileDAO return $this; } + public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) { + if (!$this->getID() || !$this->getStorageHandle()) { + throw new Exception( + pht("You can not migrate a file which hasn't yet been saved.")); + } + + $data = $this->loadFileData(); + $params = array( + 'name' => $this->getName(), + ); + + $engine = $this->instantiateStorageEngine(); + $old_handle = $this->getStorageHandle(); + + $properties = $format->newStorageProperties(); + $this->setStorageFormat($format->getStorageFormatKey()); + $this->setStorageProperties($properties); + + list($identifier, $new_handle) = $this->writeToEngine( + $engine, + $data, + $params); + + $this->setStorageHandle($new_handle); + $this->save(); + + $this->deleteFileDataIfUnused( + $engine, + $identifier, + $old_handle); + + return $this; + } + + public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) { + if (!$this->getID() || !$this->getStorageHandle()) { + throw new Exception( + pht("You can not cycle keys for a file which hasn't yet been saved.")); + } + + $properties = $format->cycleStorageProperties(); + $this->setStorageProperties($properties); + $this->save(); + + return $this; + } + private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, @@ -434,7 +504,15 @@ final class PhabricatorFile extends PhabricatorFileDAO $engine_class = get_class($engine); - $data_handle = $engine->writeFile($data, $params); + $key = $this->getStorageFormat(); + $format = id(clone PhabricatorFileStorageFormat::requireFormat($key)) + ->setFile($this); + + $data_iterator = array($data); + $formatted_iterator = $format->newWriteIterator($data_iterator); + $formatted_data = $this->loadDataFromIterator($formatted_iterator); + + $data_handle = $engine->writeFile($formatted_data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. @@ -663,19 +741,8 @@ final class PhabricatorFile extends PhabricatorFileDAO } public function loadFileData() { - - $engine = $this->instantiateStorageEngine(); - $data = $engine->readFile($this->getStorageHandle()); - - switch ($this->getStorageFormat()) { - case self::STORAGE_FORMAT_RAW: - $data = $data; - break; - default: - throw new Exception(pht('Unknown storage format.')); - } - - return $data; + $iterator = $this->getFileDataIterator(); + return $this->loadDataFromIterator($iterator); } @@ -688,7 +755,14 @@ final class PhabricatorFile extends PhabricatorFileDAO */ public function getFileDataIterator($begin = null, $end = null) { $engine = $this->instantiateStorageEngine(); - return $engine->getFileDataIterator($this, $begin, $end); + $raw_iterator = $engine->getRawFileDataIterator($this, $begin, $end); + + $key = $this->getStorageFormat(); + + $format = id(clone PhabricatorFileStorageFormat::requireFormat($key)) + ->setFile($this); + + return $format->newReadIterator($raw_iterator); } @@ -917,6 +991,30 @@ final class PhabricatorFile extends PhabricatorFileDAO return Filesystem::readRandomCharacters(20); } + public function setStorageProperties(array $properties) { + $this->metadata[self::METADATA_STORAGE] = $properties; + return $this; + } + + public function getStorageProperties() { + return idx($this->metadata, self::METADATA_STORAGE, array()); + } + + public function getStorageProperty($key, $default = null) { + $properties = $this->getStorageProperties(); + return idx($properties, $key, $default); + } + + public function loadDataFromIterator($iterator) { + $result = ''; + + foreach ($iterator as $chunk) { + $result .= $chunk; + } + + return $result; + } + public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception(pht('This file is not a viewable image.')); diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 75a078a9d7..273f899f91 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -9,6 +9,7 @@ final class HarbormasterBuildEngine extends Phobject { private $build; private $viewer; private $newBuildTargets = array(); + private $artifactReleaseQueue = array(); private $forceBuildableUpdate; public function setForceBuildableUpdate($force_buildable_update) { @@ -94,6 +95,8 @@ final class HarbormasterBuildEngine extends Phobject { $this->updateBuildable($build->getBuildable()); } + $this->releaseQueuedArtifacts(); + // If we are no longer building for any reason, release all artifacts. if (!$build->isBuilding()) { $this->releaseAllArtifacts($build); @@ -149,20 +152,21 @@ final class HarbormasterBuildEngine extends Phobject { } private function updateBuildSteps(HarbormasterBuild $build) { - $targets = id(new HarbormasterBuildTargetQuery()) + $all_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($build->getBuildGeneration())) ->execute(); - $this->updateWaitingTargets($targets); + $this->updateWaitingTargets($all_targets); - $targets = mgroup($targets, 'getBuildStepPHID'); + $targets = mgroup($all_targets, 'getBuildStepPHID'); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($this->getViewer()) ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) ->execute(); + $steps = mpull($steps, null, 'getPHID'); // Identify steps which are in various states. @@ -252,6 +256,12 @@ final class HarbormasterBuildEngine extends Phobject { return; } + // Release any artifacts which are not inputs to any remaining build + // step. We're done with these, so something else is free to use them. + $ongoing_phids = array_keys($queued + $waiting + $underway); + $ongoing_steps = array_select_keys($steps, $ongoing_phids); + $this->releaseUnusedArtifacts($all_targets, $ongoing_steps); + // Identify all the steps which are ready to run (because all their // dependencies are complete). @@ -294,6 +304,59 @@ final class HarbormasterBuildEngine extends Phobject { } + /** + * Release any artifacts which aren't used by any running or waiting steps. + * + * This releases artifacts as soon as they're no longer used. This can be + * particularly relevant when a build uses multiple hosts since it returns + * hosts to the pool more quickly. + * + * @param list Targets in the build. + * @param list List of running and waiting steps. + * @return void + */ + private function releaseUnusedArtifacts(array $targets, array $steps) { + assert_instances_of($targets, 'HarbormasterBuildTarget'); + assert_instances_of($steps, 'HarbormasterBuildStep'); + + if (!$targets || !$steps) { + return; + } + + $target_phids = mpull($targets, 'getPHID'); + + $artifacts = id(new HarbormasterBuildArtifactQuery()) + ->setViewer($this->getViewer()) + ->withBuildTargetPHIDs($target_phids) + ->withIsReleased(false) + ->execute(); + if (!$artifacts) { + return; + } + + // Collect all the artifacts that remaining build steps accept as inputs. + $must_keep = array(); + foreach ($steps as $step) { + $inputs = $step->getStepImplementation()->getArtifactInputs(); + foreach ($inputs as $input) { + $artifact_key = $input['key']; + $must_keep[$artifact_key] = true; + } + } + + // Queue unreleased artifacts which no remaining step uses for immediate + // release. + foreach ($artifacts as $artifact) { + $key = $artifact->getArtifactKey(); + if (isset($must_keep[$key])) { + continue; + } + + $this->artifactReleaseQueue[] = $artifact; + } + } + + /** * Process messages which were sent to these targets, kicking applicable * targets out of "Waiting" and into either "Passed" or "Failed". @@ -488,12 +551,18 @@ final class HarbormasterBuildEngine extends Phobject { $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildTargetPHIDs($target_phids) + ->withIsReleased(false) ->execute(); - foreach ($artifacts as $artifact) { $artifact->releaseArtifact(); } + } + private function releaseQueuedArtifacts() { + foreach ($this->artifactReleaseQueue as $key => $artifact) { + $artifact->releaseArtifact(); + unset($this->artifactReleaseQueue[$key]); + } } } diff --git a/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php index 8f50aa1bb2..de35c05aa6 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php @@ -9,6 +9,7 @@ final class HarbormasterBuildArtifactQuery private $artifactIndexes; private $keyBuildPHID; private $keyBuildGeneration; + private $isReleased; public function withIDs(array $ids) { $this->ids = $ids; @@ -30,6 +31,11 @@ final class HarbormasterBuildArtifactQuery return $this; } + public function withIsReleased($released) { + $this->isReleased = $released; + return $this; + } + public function newResultObject() { return new HarbormasterBuildArtifact(); } @@ -94,6 +100,13 @@ final class HarbormasterBuildArtifactQuery $this->artifactIndexes); } + if ($this->isReleased !== null) { + $where[] = qsprintf( + $conn, + 'isReleased = %d', + (int)$this->isReleased); + } + return $where; } diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index 98da972a7d..4dac15f7a0 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -103,11 +103,6 @@ abstract class HarbormasterBuildStepImplementation extends Phobject { /** * Return the name of artifacts produced by this command. * - * Something like: - * - * return array( - * 'some_name_input_by_user' => 'host'); - * * Future steps will calculate all available artifact mappings * before them and filter on the type. * diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php index bb5f8382ba..6825bf6646 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -8,6 +8,7 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO protected $artifactIndex; protected $artifactKey; protected $artifactData = array(); + protected $isReleased = 0; private $buildTarget = self::ATTACHABLE; private $artifactImplementation; @@ -29,6 +30,7 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO 'artifactType' => 'text32', 'artifactIndex' => 'bytes12', 'artifactKey' => 'text255', + 'isReleased' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_artifact' => array( @@ -83,13 +85,18 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO } public function releaseArtifact() { - $impl = $this->getArtifactImplementation(); + if ($this->getIsReleased()) { + return $this; + } + $impl = $this->getArtifactImplementation(); if ($impl) { $impl->releaseArtifact(PhabricatorUser::getOmnipotentUser()); } - return null; + return $this + ->setIsReleased(1) + ->save(); } public function getArtifactImplementation() { diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 79901d7ae6..1caae54d47 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -54,16 +54,18 @@ final class ManiphestTransaction return parent::shouldGenerateOldValue(); } - public function getRemarkupBlocks() { - $blocks = parent::getRemarkupBlocks(); + protected function newRemarkupChanges() { + $changes = array(); switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: - $blocks[] = $this->getNewValue(); + $changes[] = $this->newRemarkupChange() + ->setOldValue($this->getOldValue()) + ->setNewValue($this->getNewValue()); break; } - return $blocks; + return $changes; } public function getRequiredHandlePHIDs() { diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index dcc14f0cc1..0c90e43832 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -877,6 +877,7 @@ final class PhabricatorMetaMTAMail $all_prefs = id(new PhabricatorUserPreferencesQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUserPHIDs($actor_phids) + ->needSyntheticPreferences(true) ->execute(); $all_prefs = mpull($all_prefs, null, 'getUserPHID'); @@ -1105,53 +1106,33 @@ final class PhabricatorMetaMTAMail private function loadPreferences($target_phid) { - if (!self::shouldMultiplexAllMail()) { - $target_phid = null; - } + $viewer = PhabricatorUser::getOmnipotentUser(); - if ($target_phid) { + if (self::shouldMultiplexAllMail()) { $preferences = id(new PhabricatorUserPreferencesQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($viewer) ->withUserPHIDs(array($target_phid)) + ->needSyntheticPreferences(true) ->executeOne(); - } else { - $preferences = null; + if ($preferences) { + return $preferences; + } } - // TODO: Here, we would load global preferences once they exist. - - if (!$preferences) { - // If we haven't found suitable preferences yet, return an empty object - // which implicitly has all the default values. - $preferences = id(new PhabricatorUserPreferences()) - ->attachUser(new PhabricatorUser()); - } - - return $preferences; + return PhabricatorUserPreferences::loadGlobalPreferences($viewer); } private function shouldAddRePrefix(PhabricatorUserPreferences $preferences) { - $default_value = PhabricatorEnv::getEnvConfig('metamta.re-prefix'); - - $value = $preferences->getPreference( + $value = $preferences->getSettingValue( PhabricatorEmailRePrefixSetting::SETTINGKEY); - if ($value === null) { - return $default_value; - } return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX); } private function shouldVarySubject(PhabricatorUserPreferences $preferences) { - $default_value = PhabricatorEnv::getEnvConfig('metamta.vary-subjects'); - - $value = $preferences->getPreference( + $value = $preferences->getSettingValue( PhabricatorEmailVarySubjectsSetting::SETTINGKEY); - if ($value === null) { - return $default_value; - } - return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS); } diff --git a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php index 5cdf6afdab..b6ccd151ba 100644 --- a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php +++ b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php @@ -47,15 +47,15 @@ final class PasteCreateConduitAPIMethod extends PasteConduitAPIMethod { $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) + ->setTransactionType(PhabricatorPasteContentTransaction::TRANSACTIONTYPE) ->setNewValue($content); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) + ->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) + ->setTransactionType(PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE) ->setNewValue($language); $editor = id(new PhabricatorPasteEditor()) diff --git a/src/applications/paste/controller/PhabricatorPasteArchiveController.php b/src/applications/paste/controller/PhabricatorPasteArchiveController.php index c51b522085..143584e426 100644 --- a/src/applications/paste/controller/PhabricatorPasteArchiveController.php +++ b/src/applications/paste/controller/PhabricatorPasteArchiveController.php @@ -20,7 +20,7 @@ final class PhabricatorPasteArchiveController return new Aphront404Response(); } - $view_uri = '/P'.$paste->getID(); + $view_uri = $paste->getURI(); if ($request->isFormPost()) { if ($paste->isArchived()) { @@ -32,7 +32,7 @@ final class PhabricatorPasteArchiveController $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS) + ->setTransactionType(PhabricatorPasteStatusTransaction::TRANSACTIONTYPE) ->setNewValue($new_status); id(new PhabricatorPasteEditor()) diff --git a/src/applications/paste/editor/PhabricatorPasteEditEngine.php b/src/applications/paste/editor/PhabricatorPasteEditEngine.php index f88ab76697..1bad86da15 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditEngine.php +++ b/src/applications/paste/editor/PhabricatorPasteEditEngine.php @@ -71,7 +71,7 @@ final class PhabricatorPasteEditEngine id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) + ->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE) ->setDescription(pht('The title of the paste.')) ->setConduitDescription(pht('Retitle the paste.')) ->setConduitTypeDescription(pht('New paste title.')) @@ -79,7 +79,8 @@ final class PhabricatorPasteEditEngine id(new PhabricatorSelectEditField()) ->setKey('language') ->setLabel(pht('Language')) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) + ->setTransactionType( + PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE) ->setAliases(array('lang')) ->setIsCopyable(true) ->setOptions($langs) @@ -94,7 +95,8 @@ final class PhabricatorPasteEditEngine id(new PhabricatorTextAreaEditField()) ->setKey('text') ->setLabel(pht('Text')) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) + ->setTransactionType( + PhabricatorPasteContentTransaction::TRANSACTIONTYPE) ->setMonospaced(true) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setDescription(pht('The main body text of the paste.')) @@ -104,7 +106,8 @@ final class PhabricatorPasteEditEngine id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS) + ->setTransactionType( + PhabricatorPasteStatusTransaction::TRANSACTIONTYPE) ->setIsConduitOnly(true) ->setOptions(PhabricatorPaste::getStatusNameMap()) ->setDescription(pht('Active or archived status.')) diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php index cd7a0f271a..063b72cfc0 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditor.php +++ b/src/applications/paste/editor/PhabricatorPasteEditor.php @@ -3,8 +3,6 @@ final class PhabricatorPasteEditor extends PhabricatorApplicationTransactionEditor { - private $fileName; - public function getEditorApplicationClass() { return 'PhabricatorPasteApplication'; } @@ -13,29 +11,17 @@ final class PhabricatorPasteEditor return pht('Pastes'); } - public static function initializeFileForPaste( - PhabricatorUser $actor, - $name, - $data) { + public function getCreateObjectTitle($author, $object) { + return pht('%s created this paste.', $author); + } - return PhabricatorFile::newFromFileData( - $data, - array( - 'name' => $name, - 'mime-type' => 'text/plain; charset=utf-8', - 'authorPHID' => $actor->getPHID(), - 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, - 'editPolicy' => PhabricatorPolicies::POLICY_NOONE, - )); + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = PhabricatorPasteTransaction::TYPE_CONTENT; - $types[] = PhabricatorPasteTransaction::TYPE_TITLE; - $types[] = PhabricatorPasteTransaction::TYPE_LANGUAGE; - $types[] = PhabricatorPasteTransaction::TYPE_STATUS; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_COMMENT; @@ -43,163 +29,14 @@ final class PhabricatorPasteEditor return $types; } - protected function shouldApplyInitialEffects( - PhabricatorLiskDAO $object, - array $xactions) { - return true; - } - - protected function applyInitialEffects( - PhabricatorLiskDAO $object, - array $xactions) { - - // Find the most user-friendly filename we can by examining the title of - // the paste and the pending transactions. We'll use this if we create a - // new file to store raw content later. - - $name = $object->getTitle(); - if (!strlen($name)) { - $name = 'paste.raw'; - } - - $type_title = PhabricatorPasteTransaction::TYPE_TITLE; - foreach ($xactions as $xaction) { - if ($xaction->getTransactionType() == $type_title) { - $name = $xaction->getNewValue(); - } - } - - $this->fileName = $name; - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - switch ($type) { - case PhabricatorPasteTransaction::TYPE_CONTENT: - if (!$object->getFilePHID() && !$xactions) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('You must provide content to create a paste.'), - null); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - } - - return $errors; - } - - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_CONTENT: - return $object->getFilePHID(); - case PhabricatorPasteTransaction::TYPE_TITLE: - return $object->getTitle(); - case PhabricatorPasteTransaction::TYPE_LANGUAGE: - return $object->getLanguage(); - case PhabricatorPasteTransaction::TYPE_STATUS: - return $object->getStatus(); - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_TITLE: - case PhabricatorPasteTransaction::TYPE_LANGUAGE: - case PhabricatorPasteTransaction::TYPE_STATUS: - return $xaction->getNewValue(); - case PhabricatorPasteTransaction::TYPE_CONTENT: - // If this transaction does not really change the paste content, return - // the current file PHID so this transaction no-ops. - $new_content = $xaction->getNewValue(); - $old_content = $object->getRawContent(); - $file_phid = $object->getFilePHID(); - if (($new_content === $old_content) && $file_phid) { - return $file_phid; - } - - $file = self::initializeFileForPaste( - $this->getActor(), - $this->fileName, - $xaction->getNewValue()); - - return $file->getPHID(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_CONTENT: - $object->setFilePHID($xaction->getNewValue()); - return; - case PhabricatorPasteTransaction::TYPE_TITLE: - $object->setTitle($xaction->getNewValue()); - return; - case PhabricatorPasteTransaction::TYPE_LANGUAGE: - $object->setLanguage($xaction->getNewValue()); - return; - case PhabricatorPasteTransaction::TYPE_STATUS: - $object->setStatus($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_CONTENT: - case PhabricatorPasteTransaction::TYPE_TITLE: - case PhabricatorPasteTransaction::TYPE_LANGUAGE: - case PhabricatorPasteTransaction::TYPE_STATUS: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function extractFilePHIDsFromCustomTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_CONTENT: - return array($xaction->getNewValue()); - } - - return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); - } - protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { - foreach ($xactions as $xaction) { - switch ($xaction->getTransactionType()) { - case PhabricatorPasteTransaction::TYPE_CONTENT: - return false; - default: - break; - } + + if ($this->getIsNewObject()) { + return false; } + return true; } @@ -258,8 +95,4 @@ final class PhabricatorPasteEditor return true; } - protected function supportsSearch() { - return false; - } - } diff --git a/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php b/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php index 2a4fa9da5d..43be2d0eba 100644 --- a/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php +++ b/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php @@ -17,15 +17,15 @@ final class PhabricatorPasteTestDataGenerator $xactions = array(); $xactions[] = $this->newTransaction( - PhabricatorPasteTransaction::TYPE_TITLE, + PhabricatorPasteTitleTransaction::TRANSACTIONTYPE, $name); $xactions[] = $this->newTransaction( - PhabricatorPasteTransaction::TYPE_LANGUAGE, + PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE, $language); $xactions[] = $this->newTransaction( - PhabricatorPasteTransaction::TYPE_CONTENT, + PhabricatorPasteContentTransaction::TRANSACTIONTYPE, $content); $editor = id(new PhabricatorPasteEditor()) diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php index 85acf7f45d..a8f7693d1c 100644 --- a/src/applications/paste/mail/PasteCreateMailReceiver.php +++ b/src/applications/paste/mail/PasteCreateMailReceiver.php @@ -24,17 +24,13 @@ final class PasteCreateMailReceiver extends PhabricatorMailReceiver { $xactions = array(); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) + ->setTransactionType(PhabricatorPasteContentTransaction::TRANSACTIONTYPE) ->setNewValue($mail->getCleanTextBody()); $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) + ->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title); - $xactions[] = id(new PhabricatorPasteTransaction()) - ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) - ->setNewValue(''); // auto-detect - $paste = PhabricatorPaste::initializeNewPaste($sender); $content_source = $mail->newContentSource(); diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php index 1402f4c14c..1cd77a7048 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransaction.php +++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php @@ -1,12 +1,7 @@ getTransactionType()) { - case self::TYPE_CONTENT: - $phids[] = $this->getObjectPHID(); - break; - } - - return $phids; - } - - public function shouldHide() { - $old = $this->getOldValue(); - switch ($this->getTransactionType()) { - case self::TYPE_TITLE: - case self::TYPE_LANGUAGE: - if ($old === null) { - return true; - } - break; - } - return parent::shouldHide(); - } - - public function getIcon() { - switch ($this->getTransactionType()) { - case self::TYPE_CONTENT: - return 'fa-plus'; - break; - case self::TYPE_TITLE: - case self::TYPE_LANGUAGE: - return 'fa-pencil'; - break; - case self::TYPE_STATUS: - $new = $this->getNewValue(); - switch ($new) { - case PhabricatorPaste::STATUS_ACTIVE: - return 'fa-check'; - case PhabricatorPaste::STATUS_ARCHIVED: - return 'fa-ban'; - } - break; - } - return parent::getIcon(); - } - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - $object_phid = $this->getObjectPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return pht( - '%s created this paste.', - $this->renderHandleLink($author_phid)); - case self::TYPE_CONTENT: - if ($old === null) { - return pht( - '%s created this paste.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s edited the content of this paste.', - $this->renderHandleLink($author_phid)); - } - break; - case self::TYPE_TITLE: - return pht( - '%s updated the paste\'s title to "%s".', - $this->renderHandleLink($author_phid), - $new); - break; - case self::TYPE_LANGUAGE: - return pht( - "%s updated the paste's language.", - $this->renderHandleLink($author_phid)); - break; - case self::TYPE_STATUS: - switch ($new) { - case PhabricatorPaste::STATUS_ACTIVE: - return pht( - '%s activated this paste.', - $this->renderHandleLink($author_phid)); - case PhabricatorPaste::STATUS_ARCHIVED: - return pht( - '%s archived this paste.', - $this->renderHandleLink($author_phid)); - } - break; - - } - - return parent::getTitle(); - } - - public function getTitleForFeed() { - $author_phid = $this->getAuthorPHID(); - $object_phid = $this->getObjectPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case self::TYPE_CONTENT: - if ($old === null) { - return pht( - '%s created %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); - } else { - return pht( - '%s edited %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); - } - break; - case self::TYPE_TITLE: - return pht( - '%s updated the title for %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); - break; - case self::TYPE_LANGUAGE: - return pht( - '%s updated the language for %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); - break; - case self::TYPE_STATUS: - switch ($new) { - case PhabricatorPaste::STATUS_ACTIVE: - return pht( - '%s activated %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); - case PhabricatorPaste::STATUS_ARCHIVED: - return pht( - '%s archived %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); - } - break; - } - - return parent::getTitleForFeed(); - } - - public function getColor() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_CONTENT: - return PhabricatorTransactions::COLOR_GREEN; - case self::TYPE_STATUS: - switch ($new) { - case PhabricatorPaste::STATUS_ACTIVE: - return 'green'; - case PhabricatorPaste::STATUS_ARCHIVED: - return 'indigo'; - } - break; - } - - return parent::getColor(); - } - - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_CONTENT: - return ($this->getOldValue() !== null); - } - - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - switch ($this->getTransactionType()) { - case self::TYPE_CONTENT: - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $files = id(new PhabricatorFileQuery()) - ->setViewer($viewer) - ->withPHIDs(array_filter(array($old, $new))) - ->execute(); - $files = mpull($files, null, 'getPHID'); - - $old_text = ''; - if (idx($files, $old)) { - $old_text = $files[$old]->loadFileData(); - } - - $new_text = ''; - if (idx($files, $new)) { - $new_text = $files[$new]->loadFileData(); - } - - return $this->renderTextCorpusChangeDetails( - $viewer, - $old_text, - $new_text); - } - - return parent::renderChangeDetails($viewer); + public function getBaseTransactionClass() { + return 'PhabricatorPasteTransactionType'; } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { - case self::TYPE_TITLE: - case self::TYPE_CONTENT: - case self::TYPE_LANGUAGE: + case PhabricatorPasteTitleTransaction::TRANSACTIONTYPE: + case PhabricatorPasteContentTransaction::TRANSACTIONTYPE: + case PhabricatorPasteLanguageTransaction::TRANSACTIONTYPE: $tags[] = self::MAILTAG_CONTENT; break; case PhabricatorTransactions::TYPE_COMMENT: diff --git a/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php b/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php new file mode 100644 index 0000000000..b04ed61642 --- /dev/null +++ b/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php @@ -0,0 +1,135 @@ +getFilePHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setFilePHID($value); + } + + public function extractFilePHIDs($object, $value) { + return array($value); + } + + public function validateTransactions($object, array $xactions) { + if ($object->getFilePHID() || $xactions) { + return array(); + } + + $error = $this->newError( + pht('Required'), + pht('You must provide content to create a paste.')); + $error->setIsMissingFieldError(true); + + return array($error); + } + + public function willApplyTransactions($object, array $xactions) { + // Find the most user-friendly filename we can by examining the title of + // the paste and the pending transactions. We'll use this if we create a + // new file to store raw content later. + + $name = $object->getTitle(); + if (!strlen($name)) { + $name = 'paste.raw'; + } + + $type_title = PhabricatorPasteTitleTransaction::TRANSACTIONTYPE; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() == $type_title) { + $name = $xaction->getNewValue(); + } + } + + $this->fileName = $name; + } + + public function generateNewValue($object, $value) { + // If this transaction does not really change the paste content, return + // the current file PHID so this transaction no-ops. + $old_content = $object->getRawContent(); + if ($value === $old_content) { + $file_phid = $object->getFilePHID(); + if ($file_phid) { + return $file_phid; + } + } + + $editor = $this->getEditor(); + $actor = $editor->getActor(); + + $file = $this->newFileForPaste($actor, $this->fileName, $value); + + return $file->getPHID(); + } + + private function newFileForPaste(PhabricatorUser $actor, $name, $data) { + return PhabricatorFile::newFromFileData( + $data, + array( + 'name' => $name, + 'mime-type' => 'text/plain; charset=utf-8', + 'authorPHID' => $actor->getPHID(), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + 'editPolicy' => PhabricatorPolicies::POLICY_NOONE, + )); + } + + public function getIcon() { + return 'fa-plus'; + } + + public function getTitle() { + return pht( + '%s edited the content of this paste.', + $this->renderAuthor()); + } + + public function getTitleForFeed() { + return pht( + '%s edited %s.', + $this->renderAuthor(), + $this->renderObject()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($old, $new)) + ->execute(); + $files = mpull($files, null, 'getPHID'); + + $old_text = ''; + if (idx($files, $old)) { + $old_text = $files[$old]->loadFileData(); + } + + $new_text = ''; + if (idx($files, $new)) { + $new_text = $files[$new]->loadFileData(); + } + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old_text) + ->setNewText($new_text); + } + +} diff --git a/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php b/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php new file mode 100644 index 0000000000..2dccc57385 --- /dev/null +++ b/src/applications/paste/xaction/PhabricatorPasteLanguageTransaction.php @@ -0,0 +1,29 @@ +getLanguage(); + } + + public function applyInternalEffects($object, $value) { + $object->setLanguage($value); + } + + public function getTitle() { + return pht( + "%s updated the paste's language.", + $this->renderAuthor()); + } + + public function getTitleForFeed() { + return pht( + '%s updated the language for %s.', + $this->renderAuthor(), + $this->renderObject()); + } + +} diff --git a/src/applications/paste/xaction/PhabricatorPasteStatusTransaction.php b/src/applications/paste/xaction/PhabricatorPasteStatusTransaction.php new file mode 100644 index 0000000000..a313e45eab --- /dev/null +++ b/src/applications/paste/xaction/PhabricatorPasteStatusTransaction.php @@ -0,0 +1,62 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + private function isActivate() { + return ($this->getNewValue() == PhabricatorPaste::STATUS_ARCHIVED); + } + + public function getIcon() { + if ($this->isActivate()) { + return 'fa-check'; + } else { + return 'fa-ban'; + } + } + + public function getColor() { + if ($this->isActivate()) { + return 'green'; + } else { + return 'indigo'; + } + } + + public function getTitle() { + if ($this->isActivate()) { + return pht( + '%s activated this paste.', + $this->renderAuthor()); + } else { + return pht( + '%s archived this paste.', + $this->renderAuthor()); + } + } + + public function getTitleForFeed() { + if ($this->isActivate()) { + return pht( + '%s activated %s.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s archived %s.', + $this->renderAuthor(), + $this->renderObject()); + } + } + +} diff --git a/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php b/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php new file mode 100644 index 0000000000..0fc86b3d96 --- /dev/null +++ b/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php @@ -0,0 +1,33 @@ +getTitle(); + } + + public function applyInternalEffects($object, $value) { + $object->setTitle($value); + } + + public function getTitle() { + return pht( + '%s updated the paste\'s title from "%s" to "%s".', + $this->renderAuthor(), + $this->getOldValue(), + $this->getNewValue()); + } + + public function getTitleForFeed() { + return pht( + '%s updated the title for %s from "%s" to "%s".', + $this->renderAuthor(), + $this->renderObject(), + $this->getOldValue(), + $this->getNewValue()); + } + +} diff --git a/src/applications/paste/xaction/PhabricatorPasteTransactionType.php b/src/applications/paste/xaction/PhabricatorPasteTransactionType.php new file mode 100644 index 0000000000..bed6af0a67 --- /dev/null +++ b/src/applications/paste/xaction/PhabricatorPasteTransactionType.php @@ -0,0 +1,4 @@ +setViewer($viewer) - ->withUserPHIDs($user_phids) + ->withUsers($users) + ->needSyntheticPreferences(true) ->execute(); + $preferences = mpull($preferences, null, 'getUserPHID'); $all_settings = PhabricatorSetting::getAllSettings(); $settings = array(); - foreach ($preferences as $preference) { - $user_phid = $preference->getUserPHID(); + foreach ($users as $user_phid => $user) { + $preference = idx($preferences, $user_phid); + + if (!$preference) { + continue; + } + foreach ($all_settings as $key => $setting) { $value = $preference->getSettingValue($key); // As an optimization, we omit the value from the cache if it is // exactly the same as the hardcoded default. $default_value = id(clone $setting) - ->setViewer($users[$user_phid]) + ->setViewer($user) ->getSettingDefaultValue(); if ($value === $default_value) { continue; diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index 9138961214..d385b65215 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -55,6 +55,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { 'preview/' => 'PhabricatorMarkupPreviewController', 'framed/(?P\d+)/' => 'PhamePostFramedController', 'move/(?P\d+)/' => 'PhamePostMoveController', + 'archive/(?P\d+)/' => 'PhamePostArchiveController', 'comment/(?P[1-9]\d*)/' => 'PhamePostCommentController', ), 'blog/' => array( @@ -66,6 +67,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { 'manage/(?P[^/]+)/' => 'PhameBlogManageController', 'feed/(?P[^/]+)/' => 'PhameBlogFeedController', 'picture/(?P[1-9]\d*)/' => 'PhameBlogProfilePictureController', + 'header/(?P[1-9]\d*)/' => 'PhameBlogHeaderPictureController', ), ) + $this->getResourceSubroutes(), ); diff --git a/src/applications/phame/constants/PhameConstants.php b/src/applications/phame/constants/PhameConstants.php index c9444411f0..39f35f71ce 100644 --- a/src/applications/phame/constants/PhameConstants.php +++ b/src/applications/phame/constants/PhameConstants.php @@ -2,13 +2,15 @@ final class PhameConstants extends Phobject { - const VISIBILITY_DRAFT = 0; + const VISIBILITY_DRAFT = 0; const VISIBILITY_PUBLISHED = 1; + const VISIBILITY_ARCHIVED = 2; public static function getPhamePostStatusMap() { return array( self::VISIBILITY_PUBLISHED => pht('Published'), self::VISIBILITY_DRAFT => pht('Draft'), + self::VISIBILITY_ARCHIVED => pht('Archived'), ); } @@ -16,6 +18,7 @@ final class PhameConstants extends Phobject { $map = array( self::VISIBILITY_PUBLISHED => pht('Published'), self::VISIBILITY_DRAFT => pht('Draft'), + self::VISIBILITY_ARCHIVED => pht('Archived'), ); return idx($map, $status, pht('Unknown')); } diff --git a/src/applications/phame/controller/PhameHomeController.php b/src/applications/phame/controller/PhameHomeController.php index 95d1544abe..037026fa80 100644 --- a/src/applications/phame/controller/PhameHomeController.php +++ b/src/applications/phame/controller/PhameHomeController.php @@ -35,7 +35,8 @@ final class PhameHomeController extends PhamePostController { $posts = id(new PhamePostQuery()) ->setViewer($viewer) ->withBlogPHIDs($blog_phids) - ->withVisibility(PhameConstants::VISIBILITY_PUBLISHED) + ->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)) + ->setOrder('datePublished') ->executeWithCursorPager($pager); if ($posts) { @@ -97,7 +98,7 @@ final class PhameHomeController extends PhamePostController { ->setViewer($viewer) ->withBloggerPHIDs(array($viewer->getPHID())) ->withBlogPHIDs(mpull($blogs, 'getPHID')) - ->withVisibility(PhameConstants::VISIBILITY_DRAFT) + ->withVisibility(array(PhameConstants::VISIBILITY_DRAFT)) ->setLimit(5) ->execute(); diff --git a/src/applications/phame/controller/PhameLiveController.php b/src/applications/phame/controller/PhameLiveController.php index 57ac875465..3fd9f64600 100644 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -58,6 +58,7 @@ abstract class PhameLiveController extends PhameController { $blog_query = id(new PhameBlogQuery()) ->setViewer($viewer) ->needProfileImage(true) + ->needHeaderImage(true) ->withIDs(array($blog_id)); // If this is a live view, only show active blogs. @@ -97,7 +98,8 @@ abstract class PhameLiveController extends PhameController { // Only show published posts on external domains. if ($is_external) { - $post_query->withVisibility(PhameConstants::VISIBILITY_PUBLISHED); + $post_query->withVisibility( + array(PhameConstants::VISIBILITY_PUBLISHED)); } $post = $post_query->executeOne(); @@ -210,6 +212,7 @@ abstract class PhameLiveController extends PhameController { if ($this->getIsLive()) { $page + ->addClass('phame-live-view') ->setShowChrome(false) ->setShowFooter(false); } diff --git a/src/applications/phame/controller/blog/PhameBlogFeedController.php b/src/applications/phame/controller/blog/PhameBlogFeedController.php index b8b47c019f..c0c6e60cc4 100644 --- a/src/applications/phame/controller/blog/PhameBlogFeedController.php +++ b/src/applications/phame/controller/blog/PhameBlogFeedController.php @@ -21,7 +21,7 @@ final class PhameBlogFeedController extends PhameBlogController { $posts = id(new PhamePostQuery()) ->setViewer($viewer) ->withBlogPHIDs(array($blog->getPHID())) - ->withVisibility(PhameConstants::VISIBILITY_PUBLISHED) + ->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)) ->execute(); $blog_uri = PhabricatorEnv::getProductionURI( diff --git a/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php b/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php new file mode 100644 index 0000000000..fbda0ab1ef --- /dev/null +++ b/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php @@ -0,0 +1,126 @@ +getViewer(); + $id = $request->getURIData('id'); + + $blog = id(new PhameBlogQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needHeaderImage(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$blog) { + return new Aphront404Response(); + } + + $blog_uri = '/phame/blog/manage/'.$id; + + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + $e_file = true; + $errors = array(); + $delete_header = ($request->getInt('delete') == 1); + + if ($request->isFormPost()) { + if ($request->getFileExists('header')) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['header'], + array( + 'authorPHID' => $viewer->getPHID(), + 'canCDN' => true, + )); + } else if (!$delete_header) { + $e_file = pht('Required'); + $errors[] = pht( + 'You must choose a file when uploading a new blog header.'); + } + + if (!$errors && !$delete_header) { + if (!$file->isTransformableImage()) { + $e_file = pht('Not Supported'); + $errors[] = pht( + 'This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } + } + + if (!$errors) { + if ($delete_header) { + $blog->setHeaderImagePHID(null); + } else { + $blog->setHeaderImagePHID($file->getPHID()); + $file->attachToObject($blog->getPHID()); + } + $blog->save(); + return id(new AphrontRedirectResponse())->setURI($blog_uri); + } + } + + $title = pht('Edit Blog Header'); + + $upload_form = id(new AphrontFormView()) + ->setUser($viewer) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('header') + ->setLabel(pht('Upload Header')) + ->setError($e_file) + ->setCaption( + pht('Supported formats: %s', implode(', ', $supported_formats)))) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setName('delete') + ->setLabel(pht('Delete Header')) + ->addCheckbox( + 'delete', + 1, + null, + null)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($blog_uri) + ->setValue(pht('Upload Header'))); + + $upload_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Upload New Header')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($upload_form); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Blogs'), + $this->getApplicationURI('blog/')); + $crumbs->addTextCrumb( + $blog->getName(), + $this->getApplicationURI('blog/view/'.$id)); + $crumbs->addTextCrumb(pht('Blog Header')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Edit Blog Header')) + ->setHeaderIcon('fa-camera'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( + $upload_box, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $view, + )); + + } +} diff --git a/src/applications/phame/controller/blog/PhameBlogManageController.php b/src/applications/phame/controller/blog/PhameBlogManageController.php index 30de8686c8..bcd9c9456b 100644 --- a/src/applications/phame/controller/blog/PhameBlogManageController.php +++ b/src/applications/phame/controller/blog/PhameBlogManageController.php @@ -14,6 +14,7 @@ final class PhameBlogManageController extends PhameBlogController { ->setViewer($viewer) ->withIDs(array($id)) ->needProfileImage(true) + ->needHeaderImage(true) ->executeOne(); if (!$blog) { return new Aphront404Response(); @@ -31,22 +32,33 @@ final class PhameBlogManageController extends PhameBlogController { $picture = $blog->getProfileImageURI(); + $view = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View Live')) + ->setIcon('fa-external-link') + ->setHref($blog->getLiveURI()); + $header = id(new PHUIHeaderView()) ->setHeader($blog->getName()) ->setUser($viewer) ->setPolicyObject($blog) ->setImage($picture) - ->setStatus($header_icon, $header_color, $header_name); + ->setStatus($header_icon, $header_color, $header_name) + ->addActionLink($view); - $actions = $this->renderActions($blog, $viewer); - $properties = $this->renderProperties($blog, $viewer, $actions); + $curtain = $this->buildCurtain($blog); + $properties = $this->buildPropertyView($blog); + $file = $this->buildFileView($blog); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Blogs'), $this->getApplicationURI('blog/')); $crumbs->addTextCrumb( - $blog->getName()); + $blog->getName(), + $this->getApplicationURI('blog/view/'.$id)); + $crumbs->addTextCrumb(pht('Manage Blog')); + $crumbs->setBorder(true); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) @@ -57,28 +69,34 @@ final class PhameBlogManageController extends PhameBlogController { new PhameBlogTransactionQuery()); $timeline->setShouldTerminate(true); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('Details'), $properties) + ->addPropertySection(pht('Header'), $file) + ->setMainColumn( + array( + $timeline, + )); + return $this->newPage() ->setTitle($blog->getName()) ->setCrumbs($crumbs) ->appendChild( array( - $object_box, - $timeline, + $view, )); } - private function renderProperties( - PhameBlog $blog, - PhabricatorUser $viewer, - PhabricatorActionListView $actions) { + private function buildPropertyView(PhameBlog $blog) { + $viewer = $this->getViewer(); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips'); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) - ->setObject($blog) - ->setActionList($actions); + ->setObject($blog); $domain = $blog->getDomain(); if (!$domain) { @@ -129,7 +147,11 @@ final class PhameBlogManageController extends PhameBlogController { return $properties; } - private function renderActions(PhameBlog $blog, PhabricatorUser $viewer) { + private function buildCurtain(PhameBlog $blog) { + $viewer = $this->getViewer(); + + $curtain = $this->newCurtainView($viewer); + $actions = id(new PhabricatorActionListView()) ->setObject($blog) ->setUser($viewer); @@ -139,7 +161,7 @@ final class PhameBlogManageController extends PhameBlogController { $blog, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/')) @@ -147,7 +169,15 @@ final class PhameBlogManageController extends PhameBlogController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $actions->addAction( + $curtain->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-camera') + ->setHref($this->getApplicationURI('blog/header/'.$blog->getID().'/')) + ->setName(pht('Edit Blog Header')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-picture-o') ->setHref($this->getApplicationURI('blog/picture/'.$blog->getID().'/')) @@ -156,7 +186,7 @@ final class PhameBlogManageController extends PhameBlogController { ->setWorkflow(!$can_edit)); if ($blog->isArchived()) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Blog')) ->setIcon('fa-check') @@ -165,7 +195,7 @@ final class PhameBlogManageController extends PhameBlogController { ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Blog')) ->setIcon('fa-ban') @@ -175,7 +205,27 @@ final class PhameBlogManageController extends PhameBlogController { ->setWorkflow(true)); } - return $actions; + return $curtain; + } + + private function buildFileView( + PhameBlog $blog) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + if ($blog->getHeaderImagePHID()) { + $view->addImageContent( + phutil_tag( + 'img', + array( + 'src' => $blog->getHeaderImageURI(), + 'class' => 'phabricator-image-macro-hero', + ))); + return $view; + } + return null; } } diff --git a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php index b41e7a5187..96d59c8ad4 100644 --- a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php +++ b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php @@ -3,10 +3,6 @@ final class PhameBlogProfilePictureController extends PhameBlogController { - public function shouldRequireAdmin() { - return false; - } - public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); @@ -175,6 +171,7 @@ final class PhameBlogProfilePictureController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $upload_form = id(new AphrontFormView()) @@ -194,6 +191,7 @@ final class PhameBlogProfilePictureController $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($upload_form); $crumbs = $this->buildApplicationCrumbs(); @@ -204,14 +202,25 @@ final class PhameBlogProfilePictureController $blog->getName(), $this->getApplicationURI('blog/view/'.$id)); $crumbs->addTextCrumb(pht('Blog Picture')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Edit Blog Picture')) + ->setHeaderIcon('fa-camera'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( + $form_box, + $upload_box, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( - $form_box, - $upload_box, + $view, )); } diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index da027d584b..1e751c905b 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -19,16 +19,23 @@ final class PhameBlogViewController extends PhameLiveController { $post_query = id(new PhamePostQuery()) ->setViewer($viewer) - ->withBlogPHIDs(array($blog->getPHID())); + ->withBlogPHIDs(array($blog->getPHID())) + ->setOrder('datePublished') + ->withVisibility(array( + PhameConstants::VISIBILITY_PUBLISHED, + PhameConstants::VISIBILITY_DRAFT, + )); if ($is_live) { - $post_query->withVisibility(PhameConstants::VISIBILITY_PUBLISHED); + $post_query->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)); } $posts = $post_query->executeWithCursorPager($pager); + $hero = $this->buildPhameHeader($blog); + $header = id(new PHUIHeaderView()) - ->setHeader($blog->getName()) + ->addClass('phame-header-bar') ->setUser($viewer); if (!$is_external) { @@ -104,6 +111,7 @@ final class PhameBlogViewController extends PhameLiveController { ->setCrumbs($crumbs) ->appendChild( array( + $hero, $page, $about, )); @@ -147,4 +155,33 @@ final class PhameBlogViewController extends PhameLiveController { return $actions; } + private function buildPhameHeader( + PhameBlog $blog) { + + $image = null; + if ($blog->getHeaderImagePHID()) { + $image = phutil_tag( + 'div', + array( + 'class' => 'phame-header-hero', + ), + phutil_tag( + 'img', + array( + 'src' => $blog->getHeaderImageURI(), + 'class' => 'phame-header-image', + ))); + } + + $title = phutil_tag_div('phame-header-title', $blog->getName()); + $subtitle = null; + if ($blog->getSubtitle()) { + $subtitle = phutil_tag_div('phame-header-subtitle', $blog->getSubtitle()); + } + + return phutil_tag_div( + 'phame-mega-header', array($image, $title, $subtitle)); + + } + } diff --git a/src/applications/phame/controller/post/PhamePostArchiveController.php b/src/applications/phame/controller/post/PhamePostArchiveController.php new file mode 100644 index 0000000000..5a3b32944a --- /dev/null +++ b/src/applications/phame/controller/post/PhamePostArchiveController.php @@ -0,0 +1,56 @@ +getViewer(); + + $id = $request->getURIData('id'); + $post = id(new PhamePostQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$post) { + return new Aphront404Response(); + } + + $cancel_uri = $post->getViewURI(); + + if ($request->isFormPost()) { + $xactions = array(); + + $new_value = PhameConstants::VISIBILITY_ARCHIVED; + $xactions[] = id(new PhamePostTransaction()) + ->setTransactionType(PhamePostTransaction::TYPE_VISIBILITY) + ->setNewValue($new_value); + + id(new PhamePostEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($post, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($cancel_uri); + } + + $title = pht('Archive Post'); + $body = pht( + 'This post will revert to archived status and no longer be visible '. + 'to other users or members of this blog.'); + $button = pht('Archive Post'); + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addSubmitButton($button) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index fdbaf139c2..d28f1b5150 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -48,6 +48,16 @@ final class PhamePostViewController 'Use "Publish" to publish this post.'))); } + if ($post->isArchived()) { + $document->appendChild( + id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_ERROR) + ->setTitle(pht('Archived Post')) + ->appendChild( + pht('Only you can see this archived post until you publish it. '. + 'Use "Publish" to publish this post.'))); + } + if (!$post->getBlog()) { $document->appendChild( id(new PHUIInfoView()) @@ -92,6 +102,8 @@ final class PhamePostViewController $date = phabricator_datetime($post->getDatePublished(), $viewer); if ($post->isDraft()) { $subtitle = pht('Unpublished draft by %s.', $author); + } else if ($post->isArchived()) { + $subtitle = pht('Archived post by %s.', $author); } else { $subtitle = pht('Written by %s on %s.', $author, $date); } @@ -207,6 +219,21 @@ final class PhamePostViewController ->setName(pht('Publish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-ban') + ->setHref($this->getApplicationURI('post/archive/'.$id.'/')) + ->setName(pht('Archive')) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } else if ($post->isArchived()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-eye') + ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) + ->setName(pht('Publish')) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) @@ -215,6 +242,13 @@ final class PhamePostViewController ->setName(pht('Unpublish')) ->setDisabled(!$can_edit) ->setWorkflow(true)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-ban') + ->setHref($this->getApplicationURI('post/archive/'.$id.'/')) + ->setName(pht('Archive')) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); } if ($post->isDraft()) { @@ -223,12 +257,14 @@ final class PhamePostViewController $live_name = pht('View Live'); } - $actions->addAction( - id(new PhabricatorActionView()) - ->setUser($viewer) - ->setIcon('fa-globe') - ->setHref($post->getLiveURI()) - ->setName($live_name)); + if (!$post->isArchived()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setUser($viewer) + ->setIcon('fa-globe') + ->setHref($post->getLiveURI()) + ->setName($live_name)); + } return $actions; } @@ -255,7 +291,7 @@ final class PhamePostViewController $query = id(new PhamePostQuery()) ->setViewer($viewer) - ->withVisibility(PhameConstants::VISIBILITY_PUBLISHED) + ->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)) ->withBlogPHIDs(array($post->getBlog()->getPHID())) ->setLimit(1); diff --git a/src/applications/phame/editor/PhameBlogEditEngine.php b/src/applications/phame/editor/PhameBlogEditEngine.php index b9ee23a442..9cc7217147 100644 --- a/src/applications/phame/editor/PhameBlogEditEngine.php +++ b/src/applications/phame/editor/PhameBlogEditEngine.php @@ -77,6 +77,14 @@ final class PhameBlogEditEngine ->setConduitTypeDescription(pht('New blog title.')) ->setTransactionType(PhameBlogTransaction::TYPE_NAME) ->setValue($object->getName()), + id(new PhabricatorTextEditField()) + ->setKey('subtitle') + ->setLabel(pht('Subtitle')) + ->setDescription(pht('Blog subtitle.')) + ->setConduitDescription(pht('Change the blog subtitle.')) + ->setConduitTypeDescription(pht('New blog subtitle.')) + ->setTransactionType(PhameBlogTransaction::TYPE_SUBTITLE) + ->setValue($object->getSubtitle()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) diff --git a/src/applications/phame/editor/PhameBlogEditor.php b/src/applications/phame/editor/PhameBlogEditor.php index a59ede32be..ec0cbe9e06 100644 --- a/src/applications/phame/editor/PhameBlogEditor.php +++ b/src/applications/phame/editor/PhameBlogEditor.php @@ -15,6 +15,7 @@ final class PhameBlogEditor $types = parent::getTransactionTypes(); $types[] = PhameBlogTransaction::TYPE_NAME; + $types[] = PhameBlogTransaction::TYPE_SUBTITLE; $types[] = PhameBlogTransaction::TYPE_DESCRIPTION; $types[] = PhameBlogTransaction::TYPE_DOMAIN; $types[] = PhameBlogTransaction::TYPE_STATUS; @@ -31,6 +32,8 @@ final class PhameBlogEditor switch ($xaction->getTransactionType()) { case PhameBlogTransaction::TYPE_NAME: return $object->getName(); + case PhameBlogTransaction::TYPE_SUBTITLE: + return $object->getSubtitle(); case PhameBlogTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case PhameBlogTransaction::TYPE_DOMAIN: @@ -46,6 +49,7 @@ final class PhameBlogEditor switch ($xaction->getTransactionType()) { case PhameBlogTransaction::TYPE_NAME: + case PhameBlogTransaction::TYPE_SUBTITLE: case PhameBlogTransaction::TYPE_DESCRIPTION: case PhameBlogTransaction::TYPE_STATUS: return $xaction->getNewValue(); @@ -65,6 +69,8 @@ final class PhameBlogEditor switch ($xaction->getTransactionType()) { case PhameBlogTransaction::TYPE_NAME: return $object->setName($xaction->getNewValue()); + case PhameBlogTransaction::TYPE_SUBTITLE: + return $object->setSubtitle($xaction->getNewValue()); case PhameBlogTransaction::TYPE_DESCRIPTION: return $object->setDescription($xaction->getNewValue()); case PhameBlogTransaction::TYPE_DOMAIN: @@ -82,6 +88,7 @@ final class PhameBlogEditor switch ($xaction->getTransactionType()) { case PhameBlogTransaction::TYPE_NAME: + case PhameBlogTransaction::TYPE_SUBTITLE: case PhameBlogTransaction::TYPE_DESCRIPTION: case PhameBlogTransaction::TYPE_DOMAIN: case PhameBlogTransaction::TYPE_STATUS: @@ -227,7 +234,7 @@ final class PhameBlogEditor protected function supportsSearch() { - return false; + return true; } protected function shouldApplyHeraldRules( diff --git a/src/applications/phame/editor/PhamePostEditor.php b/src/applications/phame/editor/PhamePostEditor.php index fa43a131ff..363f39fb46 100644 --- a/src/applications/phame/editor/PhamePostEditor.php +++ b/src/applications/phame/editor/PhamePostEditor.php @@ -66,6 +66,9 @@ final class PhamePostEditor case PhamePostTransaction::TYPE_VISIBILITY: if ($xaction->getNewValue() == PhameConstants::VISIBILITY_DRAFT) { $object->setDatePublished(0); + } else if ($xaction->getNewValue() == + PhameConstants::VISIBILITY_ARCHIVED) { + $object->setDatePublished(0); } else { $object->setDatePublished(PhabricatorTime::getNow()); } @@ -168,7 +171,7 @@ final class PhamePostEditor protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { - if ($object->isDraft()) { + if ($object->isDraft() || ($object->isArchived())) { return false; } return true; @@ -177,7 +180,7 @@ final class PhamePostEditor protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { - if ($object->isDraft()) { + if ($object->isDraft() || $object->isArchived()) { return false; } return true; @@ -228,7 +231,7 @@ final class PhamePostEditor foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhamePostTransaction::TYPE_VISIBILITY: - if (!$object->isDraft()) { + if (!$object->isDraft() && !$object->isArchived()) { $body->addRemarkupSection(null, $object->getBody()); } break; @@ -261,7 +264,7 @@ final class PhamePostEditor } protected function supportsSearch() { - return false; + return true; } protected function shouldApplyHeraldRules( diff --git a/src/applications/phame/phid/PhabricatorPhameBlogPHIDType.php b/src/applications/phame/phid/PhabricatorPhameBlogPHIDType.php index 18ea30aea8..905f4b893b 100644 --- a/src/applications/phame/phid/PhabricatorPhameBlogPHIDType.php +++ b/src/applications/phame/phid/PhabricatorPhameBlogPHIDType.php @@ -34,6 +34,11 @@ final class PhabricatorPhameBlogPHIDType extends PhabricatorPHIDType { $handle->setName($blog->getName()); $handle->setFullName($blog->getName()); $handle->setURI('/phame/blog/view/'.$blog->getID().'/'); + + if ($blog->isArchived()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } + } } diff --git a/src/applications/phame/phid/PhabricatorPhamePostPHIDType.php b/src/applications/phame/phid/PhabricatorPhamePostPHIDType.php index 5573f2db9f..4250c91949 100644 --- a/src/applications/phame/phid/PhabricatorPhamePostPHIDType.php +++ b/src/applications/phame/phid/PhabricatorPhamePostPHIDType.php @@ -34,7 +34,13 @@ final class PhabricatorPhamePostPHIDType extends PhabricatorPHIDType { $handle->setName($post->getTitle()); $handle->setFullName($post->getTitle()); $handle->setURI('/phame/post/view/'.$post->getID().'/'); + + if ($post->isArchived()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } + } + } } diff --git a/src/applications/phame/query/PhameBlogQuery.php b/src/applications/phame/query/PhameBlogQuery.php index 36ae306345..bd34255bd0 100644 --- a/src/applications/phame/query/PhameBlogQuery.php +++ b/src/applications/phame/query/PhameBlogQuery.php @@ -9,6 +9,7 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $needBloggers; private $needProfileImage; + private $needHeaderImage; public function withIDs(array $ids) { $this->ids = $ids; @@ -35,6 +36,11 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function needHeaderImage($need) { + $this->needHeaderImage = $need; + return $this; + } + public function newResultObject() { return new PhameBlog(); } @@ -107,6 +113,28 @@ final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery { $blog->attachProfileImageFile($file); } } + + if ($this->needHeaderImage) { + $file_phids = mpull($blogs, 'getHeaderImagePHID'); + $file_phids = array_filter($file_phids); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } else { + $files = array(); + } + + foreach ($blogs as $blog) { + $file = idx($files, $blog->getHeaderImagePHID()); + if ($file) { + $blog->attachHeaderImageFile($file); + } + } + } return $blogs; } diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php index d2a1feb2c1..1a29eda869 100644 --- a/src/applications/phame/query/PhamePostQuery.php +++ b/src/applications/phame/query/PhamePostQuery.php @@ -29,7 +29,7 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } - public function withVisibility($visibility) { + public function withVisibility(array $visibility) { $this->visibility = $visibility; return $this; } @@ -98,10 +98,10 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { $this->bloggerPHIDs); } - if ($this->visibility !== null) { + if ($this->visibility) { $where[] = qsprintf( $conn, - 'visibility = %d', + 'visibility IN (%Ld)', $this->visibility); } @@ -122,6 +122,36 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $where; } + public function getBuiltinOrders() { + return array( + 'datePublished' => array( + 'vector' => array('datePublished', 'id'), + 'name' => pht('Publish Date'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'datePublished' => array( + 'column' => 'datePublished', + 'type' => 'int', + 'reverse' => false, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $post = $this->loadCursorObject($cursor); + + $map = array( + 'datePublished' => $post->getDatePublished(), + 'id' => $post->getID(), + ); + + return $map; + } + public function getQueryApplicationClass() { // TODO: Does setting this break public blogs? return null; diff --git a/src/applications/phame/query/PhamePostSearchEngine.php b/src/applications/phame/query/PhamePostSearchEngine.php index a7a331fbdf..dbc248d8b2 100644 --- a/src/applications/phame/query/PhamePostSearchEngine.php +++ b/src/applications/phame/query/PhamePostSearchEngine.php @@ -19,7 +19,7 @@ final class PhamePostSearchEngine $query = $this->newQuery(); if (strlen($map['visibility'])) { - $query->withVisibility($map['visibility']); + $query->withVisibility(array($map['visibility'])); } return $query; @@ -35,6 +35,7 @@ final class PhamePostSearchEngine '' => pht('All'), PhameConstants::VISIBILITY_PUBLISHED => pht('Published'), PhameConstants::VISIBILITY_DRAFT => pht('Draft'), + PhameConstants::VISIBILITY_ARCHIVED => pht('Archived'), )), ); } @@ -48,6 +49,7 @@ final class PhamePostSearchEngine 'all' => pht('All Posts'), 'live' => pht('Published Posts'), 'draft' => pht('Draft Posts'), + 'archived' => pht('Archived Posts'), ); return $names; } @@ -65,6 +67,9 @@ final class PhamePostSearchEngine case 'draft': return $query->setParameter( 'visibility', PhameConstants::VISIBILITY_DRAFT); + case 'archived': + return $query->setParameter( + 'visibility', PhameConstants::VISIBILITY_ARCHIVED); } return parent::buildSavedQueryFromBuiltin($query_key); @@ -99,11 +104,19 @@ final class PhamePostSearchEngine if ($post->isDraft()) { $item->setStatusIcon('fa-star-o grey'); $item->setDisabled(true); - $item->addIcon('none', pht('Draft Post')); + $item->addIcon('fa-star-o', pht('Draft Post')); + } else if ($post->isArchived()) { + $item->setStatusIcon('fa-ban grey'); + $item->setDisabled(true); + $item->addIcon('fa-ban', pht('Archived Post')); } else { $date = $post->getDatePublished(); $item->setEpoch($date); } + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-pencil') + ->setHref($post->getEditURI())); $list->addItem($item); } diff --git a/src/applications/phame/search/PhameBlogFulltextEngine.php b/src/applications/phame/search/PhameBlogFulltextEngine.php new file mode 100644 index 0000000000..c25550c655 --- /dev/null +++ b/src/applications/phame/search/PhameBlogFulltextEngine.php @@ -0,0 +1,28 @@ +setDocumentTitle($blog->getName()); + + $document->addField( + PhabricatorSearchDocumentFieldType::FIELD_BODY, + $blog->getDescription()); + + $document->addRelationship( + $blog->isArchived() + ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED + : PhabricatorSearchRelationship::RELATIONSHIP_OPEN, + $blog->getPHID(), + PhabricatorPhameBlogPHIDType::TYPECONST, + PhabricatorTime::getNow()); + + } + +} diff --git a/src/applications/phame/search/PhamePostFulltextEngine.php b/src/applications/phame/search/PhamePostFulltextEngine.php new file mode 100644 index 0000000000..27a97ae4ba --- /dev/null +++ b/src/applications/phame/search/PhamePostFulltextEngine.php @@ -0,0 +1,34 @@ +setDocumentTitle($post->getTitle()); + + $document->addField( + PhabricatorSearchDocumentFieldType::FIELD_BODY, + $post->getBody()); + + $document->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, + $post->getBloggerPHID(), + PhabricatorPeopleUserPHIDType::TYPECONST, + $post->getDateCreated()); + + $document->addRelationship( + $post->isArchived() + ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED + : PhabricatorSearchRelationship::RELATIONSHIP_OPEN, + $post->getPHID(), + PhabricatorPhamePostPHIDType::TYPECONST, + PhabricatorTime::getNow()); + + } + +} diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index 861def127b..b956eaa1c6 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -9,11 +9,13 @@ final class PhameBlog extends PhameDAO PhabricatorProjectInterface, PhabricatorDestructibleInterface, PhabricatorApplicationTransactionInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorFulltextInterface { const MARKUP_FIELD_DESCRIPTION = 'markup:description'; protected $name; + protected $subtitle; protected $description; protected $domain; protected $configData; @@ -23,8 +25,10 @@ final class PhameBlog extends PhameDAO protected $status; protected $mailKey; protected $profileImagePHID; + protected $headerImagePHID; private $profileImageFile = self::ATTACHABLE; + private $headerImageFile = self::ATTACHABLE; const STATUS_ACTIVE = 'active'; const STATUS_ARCHIVED = 'archived'; @@ -37,11 +41,13 @@ final class PhameBlog extends PhameDAO ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', + 'subtitle' => 'text64', 'description' => 'text', 'domain' => 'text128?', 'status' => 'text32', 'mailKey' => 'bytes20', 'profileImagePHID' => 'phid?', + 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These policies should always be non-null. @@ -211,6 +217,19 @@ final class PhameBlog extends PhameDAO return $this->assertAttached($this->profileImageFile); } + public function getHeaderImageURI() { + return $this->getHeaderImageFile()->getBestURI(); + } + + public function attachHeaderImageFile(PhabricatorFile $file) { + $this->headerImageFile = $file; + return $this; + } + + public function getHeaderImageFile() { + return $this->assertAttached($this->headerImageFile); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ @@ -370,4 +389,10 @@ final class PhameBlog extends PhameDAO } +/* -( PhabricatorFulltextInterface )--------------------------------------- */ + + public function newFulltextEngine() { + return new PhameBlogFulltextEngine(); + } + } diff --git a/src/applications/phame/storage/PhameBlogTransaction.php b/src/applications/phame/storage/PhameBlogTransaction.php index 6a89b936f6..8d71fa0ee9 100644 --- a/src/applications/phame/storage/PhameBlogTransaction.php +++ b/src/applications/phame/storage/PhameBlogTransaction.php @@ -4,6 +4,7 @@ final class PhameBlogTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'phame.blog.name'; + const TYPE_SUBTITLE = 'phame.blog.subtitle'; const TYPE_DESCRIPTION = 'phame.blog.description'; const TYPE_DOMAIN = 'phame.blog.domain'; const TYPE_STATUS = 'phame.blog.status'; @@ -80,6 +81,7 @@ final class PhameBlogTransaction $tags[] = self::MAILTAG_SUBSCRIBERS; break; case self::TYPE_NAME: + case self::TYPE_SUBTITLE: case self::TYPE_DESCRIPTION: case self::TYPE_DOMAIN: $tags[] = self::MAILTAG_DETAILS; @@ -116,6 +118,19 @@ final class PhameBlogTransaction $new); } break; + case self::TYPE_SUBTITLE: + if ($old === null) { + return pht( + '%s set this blog\'s subtitle to "%s".', + $this->renderHandleLink($author_phid), + $new); + } else { + return pht( + '%s updated the blog\'s subtitle to "%s".', + $this->renderHandleLink($author_phid), + $new); + } + break; case self::TYPE_DESCRIPTION: return pht( '%s updated the blog\'s description.', @@ -166,6 +181,19 @@ final class PhameBlogTransaction $this->renderHandleLink($object_phid)); } break; + case self::TYPE_SUBTITLE: + if ($old === null) { + return pht( + '%s set the subtitle for %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } else { + return pht( + '%s updated the subtitle for %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } + break; case self::TYPE_DESCRIPTION: return pht( '%s updated the description for %s.', diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index fb2e8058dc..2d8fc887c5 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -10,7 +10,8 @@ final class PhamePost extends PhameDAO PhabricatorSubscribableInterface, PhabricatorDestructibleInterface, PhabricatorTokenReceiverInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorFulltextInterface { const MARKUP_FIELD_BODY = 'markup:body'; const MARKUP_FIELD_SUMMARY = 'markup:summary'; @@ -53,7 +54,8 @@ final class PhamePost extends PhameDAO public function getLiveURI() { $blog = $this->getBlog(); $is_draft = $this->isDraft(); - if (strlen($blog->getDomain()) && !$is_draft) { + $is_archived = $this->isArchived(); + if (strlen($blog->getDomain()) && !$is_draft && !$is_archived) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); @@ -92,6 +94,10 @@ final class PhamePost extends PhameDAO return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT); } + public function isArchived() { + return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED); + } + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -165,7 +171,7 @@ final class PhamePost extends PhameDAO switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: - if (!$this->isDraft() && $this->getBlog()) { + if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); @@ -319,6 +325,8 @@ final class PhamePost extends PhameDAO public function getFieldValuesForConduit() { if ($this->isDraft()) { $date_published = null; + } else if ($this->isArchived()) { + $date_published = null; } else { $date_published = (int)$this->getDatePublished(); } @@ -337,4 +345,11 @@ final class PhamePost extends PhameDAO return array(); } + +/* -( PhabricatorFulltextInterface )--------------------------------------- */ + + public function newFulltextEngine() { + return new PhamePostFulltextEngine(); + } + } diff --git a/src/applications/phame/storage/PhamePostTransaction.php b/src/applications/phame/storage/PhamePostTransaction.php index be5dbfd8b5..a1efb584b0 100644 --- a/src/applications/phame/storage/PhamePostTransaction.php +++ b/src/applications/phame/storage/PhamePostTransaction.php @@ -73,6 +73,8 @@ final class PhamePostTransaction case self::TYPE_VISIBILITY: if ($new == PhameConstants::VISIBILITY_PUBLISHED) { return 'fa-globe'; + } else if ($new == PhameConstants::VISIBILITY_ARCHIVED) { + return 'fa-ban'; } else { return 'fa-eye-slash'; } @@ -144,6 +146,10 @@ final class PhamePostTransaction return pht( '%s marked this post as a draft.', $this->renderHandleLink($author_phid)); + } else if ($new == PhameConstants::VISIBILITY_ARCHIVED) { + return pht( + '%s archived this post.', + $this->renderHandleLink($author_phid)); } else { return pht( '%s published this post.', @@ -201,6 +207,11 @@ final class PhamePostTransaction '%s marked %s as a draft.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); + } else if ($new == PhameConstants::VISIBILITY_ARCHIVED) { + return pht( + '%s marked %s as archived.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); } else { return pht( '%s published %s.', diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index e157211f7b..b6d3aeebcf 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -1099,6 +1099,18 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $column = $this->refreshColumn($user, $column); $this->assertTrue((bool)$column); + // This test has been failing randomly in a way that doesn't reproduce + // on any host, so add some extra assertions to try to nail it down. + $board = $this->refreshProject($board, $user, true); + $this->assertTrue((bool)$board); + $this->assertTrue($board->isUserMember($user->getPHID())); + + $can_view = PhabricatorPolicyFilter::hasCapability( + $user, + $column, + PhabricatorPolicyCapability::CAN_VIEW); + $this->assertTrue($can_view); + $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $column, diff --git a/src/applications/repository/daemon/PhabricatorGitGraphStream.php b/src/applications/repository/daemon/PhabricatorGitGraphStream.php index 334213048e..669cf9c105 100644 --- a/src/applications/repository/daemon/PhabricatorGitGraphStream.php +++ b/src/applications/repository/daemon/PhabricatorGitGraphStream.php @@ -11,14 +11,20 @@ final class PhabricatorGitGraphStream public function __construct( PhabricatorRepository $repository, - $start_commit) { + $start_commit = null) { $this->repository = $repository; - $future = $repository->getLocalCommandFuture( - 'log --format=%s %s --', - '%H%x01%P%x01%ct', - $start_commit); + if ($start_commit !== null) { + $future = $repository->getLocalCommandFuture( + 'log --format=%s %s --', + '%H%x01%P%x01%ct', + $start_commit); + } else { + $future = $repository->getLocalCommandFuture( + 'log --format=%s --all --', + '%H%x01%P%x01%ct'); + } $this->iterator = new LinesOfALargeExecFuture($future); $this->iterator->setDelimiter("\n"); diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index 9e0e13c720..cdb7e11d03 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -105,6 +105,8 @@ final class PhabricatorRepositoryDiscoveryEngine $this->commitCache[$ref->getIdentifier()] = true; } + $this->markUnreachableCommits($repository); + $version = $this->getObservedVersion($repository); if ($version !== null) { id(new DiffusionRepositoryClusterEngine()) @@ -130,39 +132,35 @@ final class PhabricatorRepositoryDiscoveryEngine $this->verifyGitOrigin($repository); } - // TODO: This should also import tags, but some of the logic is still - // branch-specific today. - - $branches = id(new DiffusionLowLevelGitRefQuery()) + $heads = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) - ->withRefTypes( - array( - PhabricatorRepositoryRefCursor::TYPE_BRANCH, - )) ->execute(); - if (!$branches) { - // This repository has no branches at all, so we don't need to do + if (!$heads) { + // This repository has no heads at all, so we don't need to do // anything. Generally, this means the repository is empty. return array(); } - $branches = $this->sortBranches($branches); - $branches = mpull($branches, 'getCommitIdentifier', 'getShortName'); + $heads = $this->sortRefs($heads); + $head_commits = mpull($heads, 'getCommitIdentifier'); $this->log( pht( 'Discovering commits in repository "%s".', $repository->getDisplayName())); - $this->fillCommitCache(array_values($branches)); + $this->fillCommitCache($head_commits); $refs = array(); - foreach ($branches as $name => $commit) { - $this->log(pht('Examining branch "%s", at "%s".', $name, $commit)); + foreach ($heads as $ref) { + $name = $ref->getShortName(); + $commit = $ref->getCommitIdentifier(); - if (!$repository->shouldTrackBranch($name)) { - $this->log(pht('Skipping, branch is untracked.')); + $this->log(pht('Examining ref "%s", at "%s".', $name, $commit)); + + if (!$repository->shouldTrackRef($ref)) { + $this->log(pht('Skipping, ref is untracked.')); continue; } @@ -173,14 +171,14 @@ final class PhabricatorRepositoryDiscoveryEngine $this->log(pht('Looking for new commits.')); - $branch_refs = $this->discoverStreamAncestry( + $head_refs = $this->discoverStreamAncestry( new PhabricatorGitGraphStream($repository, $commit), $commit, - $repository->shouldAutocloseBranch($name)); + $repository->shouldAutocloseRef($ref)); - $this->didDiscoverRefs($branch_refs); + $this->didDiscoverRefs($head_refs); - $refs[] = $branch_refs; + $refs[] = $head_refs; } return array_mergev($refs); @@ -448,10 +446,17 @@ final class PhabricatorRepositoryDiscoveryEngine return; } + // When filling the cache we ignore commits which have been marked as + // unreachable, treating them as though they do not exist. When recording + // commits later we'll revive commits that exist but are unreachable. + $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( - 'repositoryID = %d AND commitIdentifier IN (%Ls)', + 'repositoryID = %d AND commitIdentifier IN (%Ls) + AND (importStatus & %d) != %d', $this->getRepository()->getID(), - $identifiers); + $identifiers, + PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE, + PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE); foreach ($commits as $commit) { $this->commitCache[$commit->getCommitIdentifier()] = true; @@ -469,25 +474,23 @@ final class PhabricatorRepositoryDiscoveryEngine * * @task internal * - * @param list List of branch heads. - * @return list Sorted list of branch heads. + * @param list List of refs. + * @return list Sorted list of refs. */ - private function sortBranches(array $branches) { + private function sortRefs(array $refs) { $repository = $this->getRepository(); - $head_branches = array(); - $tail_branches = array(); - foreach ($branches as $branch) { - $name = $branch->getShortName(); - - if ($repository->shouldAutocloseBranch($name)) { - $head_branches[] = $branch; + $head_refs = array(); + $tail_refs = array(); + foreach ($refs as $ref) { + if ($repository->shouldAutocloseRef($ref)) { + $head_refs[] = $ref; } else { - $tail_branches[] = $branch; + $tail_refs[] = $ref; } } - return array_merge($head_branches, $tail_branches); + return array_merge($head_refs, $tail_refs); } @@ -499,6 +502,30 @@ final class PhabricatorRepositoryDiscoveryEngine array $parents) { $commit = new PhabricatorRepositoryCommit(); + $conn_w = $repository->establishConnection('w'); + + // First, try to revive an existing unreachable commit (if one exists) by + // removing the "unreachable" flag. If we succeed, we don't need to do + // anything else: we already discovered this commit some time ago. + queryfx( + $conn_w, + 'UPDATE %T SET importStatus = (importStatus & ~%d) + WHERE repositoryID = %d AND commitIdentifier = %s', + $commit->getTableName(), + PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE, + $repository->getID(), + $commit_identifier); + if ($conn_w->getAffectedRows()) { + $commit = $commit->loadOneWhere( + 'repositoryID = %d AND commitIdentifier = %s', + $repository->getID(), + $commit_identifier); + + // After reviving a commit, schedule new daemons for it. + $this->didDiscoverCommit($repository, $commit, $epoch); + return; + } + $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($commit_identifier); $commit->setEpoch($epoch); @@ -508,10 +535,7 @@ final class PhabricatorRepositoryDiscoveryEngine $data = new PhabricatorRepositoryCommitData(); - $conn_w = $repository->establishConnection('w'); - try { - // If this commit has parents, look up their IDs. The parent commits // should always exist already. @@ -559,21 +583,7 @@ final class PhabricatorRepositoryDiscoveryEngine } $commit->saveTransaction(); - $this->insertTask($repository, $commit); - - queryfx( - $conn_w, - 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) - VALUES (%d, 1, %d, %d) - ON DUPLICATE KEY UPDATE - size = size + 1, - lastCommitID = - IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), - epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', - PhabricatorRepository::TABLE_SUMMARY, - $repository->getID(), - $commit->getID(), - $epoch); + $this->didDiscoverCommit($repository, $commit, $epoch); if ($this->repairMode) { // Normally, the query should throw a duplicate key exception. If we @@ -589,8 +599,6 @@ final class PhabricatorRepositoryDiscoveryEngine 'commit' => $commit, ))); - - } catch (AphrontDuplicateKeyQueryException $ex) { $commit->killTransaction(); // Ignore. This can happen because we discover the same new commit @@ -600,6 +608,29 @@ final class PhabricatorRepositoryDiscoveryEngine } } + private function didDiscoverCommit( + PhabricatorRepository $repository, + PhabricatorRepositoryCommit $commit, + $epoch) { + + $this->insertTask($repository, $commit); + + // Update the repository summary table. + queryfx( + $commit->establishConnection('w'), + 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) + VALUES (%d, 1, %d, %d) + ON DUPLICATE KEY UPDATE + size = size + 1, + lastCommitID = + IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), + epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', + PhabricatorRepository::TABLE_SUMMARY, + $repository->getID(), + $commit->getID(), + $epoch); + } + private function didDiscoverRefs(array $refs) { foreach ($refs as $ref) { $this->workingSet[$ref->getIdentifier()] = true; @@ -702,4 +733,136 @@ final class PhabricatorRepositoryDiscoveryEngine return (int)$version['version']; } + private function markUnreachableCommits(PhabricatorRepository $repository) { + // For now, this is only supported for Git. + if (!$repository->isGit()) { + return; + } + + // Find older versions of refs which we haven't processed yet. We're going + // to make sure their commits are still reachable. + $old_refs = id(new PhabricatorRepositoryOldRef())->loadAllWhere( + 'repositoryPHID = %s', + $repository->getPHID()); + + // We can share a single graph stream across all the checks we need to do. + $stream = new PhabricatorGitGraphStream($repository); + + foreach ($old_refs as $old_ref) { + $identifier = $old_ref->getCommitIdentifier(); + $this->markUnreachableFrom($repository, $stream, $identifier); + + // If nothing threw an exception, we're all done with this ref. + $old_ref->delete(); + } + } + + private function markUnreachableFrom( + PhabricatorRepository $repository, + PhabricatorGitGraphStream $stream, + $identifier) { + + $unreachable = array(); + + $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( + 'repositoryID = %s AND commitIdentifier = %s', + $repository->getID(), + $identifier); + if (!$commit) { + return; + } + + $look = array($commit); + $seen = array(); + while ($look) { + $target = array_pop($look); + + // If we've already checked this commit (for example, because history + // branches and then merges) we don't need to check it again. + $target_identifier = $target->getCommitIdentifier(); + if (isset($seen[$target_identifier])) { + continue; + } + + $seen[$target_identifier] = true; + + try { + $stream->getCommitDate($target_identifier); + $reachable = true; + } catch (Exception $ex) { + $reachable = false; + } + + if ($reachable) { + // This commit is reachable, so we don't need to go any further + // down this road. + continue; + } + + $unreachable[] = $target; + + // Find the commit's parents and check them for reachability, too. We + // have to look in the database since we no may longer have the commit + // in the repository. + $rows = queryfx_all( + $commit->establishConnection('w'), + 'SELECT commit.* FROM %T commit + JOIN %T parents ON commit.id = parents.parentCommitID + WHERE parents.childCommitID = %d', + $commit->getTableName(), + PhabricatorRepository::TABLE_PARENTS, + $target->getID()); + if (!$rows) { + continue; + } + + $parents = id(new PhabricatorRepositoryCommit()) + ->loadAllFromArray($rows); + foreach ($parents as $parent) { + $look[] = $parent; + } + } + + $unreachable = array_reverse($unreachable); + + $flag = PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE; + foreach ($unreachable as $unreachable_commit) { + $unreachable_commit->writeImportStatusFlag($flag); + } + + // If anything was unreachable, just rebuild the whole summary table. + // We can't really update it incrementally when a commit becomes + // unreachable. + if ($unreachable) { + $this->rebuildSummaryTable($repository); + } + } + + private function rebuildSummaryTable(PhabricatorRepository $repository) { + $conn_w = $repository->establishConnection('w'); + + $data = queryfx_one( + $conn_w, + 'SELECT COUNT(*) N, MAX(id) id, MAX(epoch) epoch + FROM %T WHERE repositoryID = %d AND (importStatus & %d) != %d', + id(new PhabricatorRepositoryCommit())->getTableName(), + $repository->getID(), + PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE, + PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE); + + queryfx( + $conn_w, + 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) + VALUES (%d, %d, %d, %d) + ON DUPLICATE KEY UPDATE + size = VALUES(size), + lastCommitID = VALUES(lastCommitID), + epoch = VALUES(epoch)', + PhabricatorRepository::TABLE_SUMMARY, + $repository->getID(), + $data['N'], + $data['id'], + $data['epoch']); + } + } diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 3d7dce90fa..7e54bf4620 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -347,7 +347,7 @@ final class PhabricatorRepositoryPullEngine // For bare working copies, we need this magic incantation. $future = $repository->getRemoteCommandFuture( 'fetch origin %s --prune', - '+refs/heads/*:refs/heads/*'); + '+refs/*:refs/*'); } else { $future = $repository->getRemoteCommandFuture( 'fetch --all --prune'); diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php index da89db75e0..af07ea052a 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php @@ -25,29 +25,31 @@ final class PhabricatorRepositoryRefEngine switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // No meaningful refs of any type in Subversion. - $branches = array(); - $bookmarks = array(); - $tags = array(); + $maps = array(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $branches = $this->loadMercurialBranchPositions($repository); $bookmarks = $this->loadMercurialBookmarkPositions($repository); - $tags = array(); + $maps = array( + PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches, + PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks, + ); + $branches_may_close = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $branches = $this->loadGitBranchPositions($repository); - $bookmarks = array(); - $tags = $this->loadGitTagPositions($repository); + $maps = $this->loadGitRefPositions($repository); break; default: throw new Exception(pht('Unknown VCS "%s"!', $vcs)); } - $maps = array( - PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches, - PhabricatorRepositoryRefCursor::TYPE_TAG => $tags, - PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks, + // Fill in any missing types with empty lists. + $maps = $maps + array( + PhabricatorRepositoryRefCursor::TYPE_BRANCH => array(), + PhabricatorRepositoryRefCursor::TYPE_TAG => array(), + PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => array(), + PhabricatorRepositoryRefCursor::TYPE_REF => array(), ); $all_cursors = id(new PhabricatorRepositoryRefCursorQuery()) @@ -83,6 +85,13 @@ final class PhabricatorRepositoryRefEngine $ref->save(); } foreach ($this->deadRefs as $ref) { + // Shove this ref into the old refs table so the discovery engine + // can check if any commits have been rendered unreachable. + id(new PhabricatorRepositoryOldRef()) + ->setRepositoryPHID($repository->getPHID()) + ->setCommitIdentifier($ref->getCommitIdentifier()) + ->save(); + $ref->delete(); } $repository->saveTransaction(); @@ -91,6 +100,7 @@ final class PhabricatorRepositoryRefEngine $this->deadRefs = array(); } + $branches = $maps[PhabricatorRepositoryRefCursor::TYPE_BRANCH]; if ($branches && $branches_may_close) { $this->updateBranchStates($repository, $branches); } @@ -449,28 +459,12 @@ final class PhabricatorRepositoryRefEngine /** * @task git */ - private function loadGitBranchPositions(PhabricatorRepository $repository) { - return id(new DiffusionLowLevelGitRefQuery()) + private function loadGitRefPositions(PhabricatorRepository $repository) { + $refs = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) - ->withRefTypes( - array( - PhabricatorRepositoryRefCursor::TYPE_BRANCH, - )) ->execute(); - } - - /** - * @task git - */ - private function loadGitTagPositions(PhabricatorRepository $repository) { - return id(new DiffusionLowLevelGitRefQuery()) - ->setRepository($repository) - ->withRefTypes( - array( - PhabricatorRepositoryRefCursor::TYPE_TAG, - )) - ->execute(); + return mgroup($refs, 'getRefType'); } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php new file mode 100644 index 0000000000..84dea48948 --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php @@ -0,0 +1,103 @@ +setName('mark-reachable') + ->setExamples('**mark-reachable** [__options__] __repository__ ...') + ->setSynopsis( + pht( + 'Rebuild "unreachable" flags for commits in __repository__.')) + ->setArguments( + array( + array( + 'name' => 'repos', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $repos = $this->loadRepositories($args, 'repos'); + if (!$repos) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more repositories to correct reachability status '. + 'for.')); + } + + foreach ($repos as $repo) { + $this->markReachable($repo); + } + + echo tsprintf( + "%s\n", + pht( + 'Examined %s commits already in the correct state.', + new PhutilNumber($this->untouchedCount))); + + echo tsprintf( + "%s\n", + pht('Done.')); + + return 0; + } + + private function markReachable(PhabricatorRepository $repository) { + if (!$repository->isGit()) { + throw new PhutilArgumentUsageException( + pht( + 'Only Git repositories are supported, this repository ("%s") is '. + 'not a Git repository.', + $repository->getDisplayName())); + } + + $viewer = $this->getViewer(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepository($repository) + ->execute(); + + $flag = PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE; + + $graph = new PhabricatorGitGraphStream($repository); + foreach ($commits as $commit) { + $identifier = $commit->getCommitIdentifier(); + + try { + $graph->getCommitDate($identifier); + $unreachable = false; + } catch (Exception $ex) { + $unreachable = true; + } + + // The commit has proper reachability, so do nothing. + if ($commit->isUnreachable() === $unreachable) { + $this->untouchedCount++; + continue; + } + + if ($unreachable) { + echo tsprintf( + "%s: %s\n", + $commit->getMonogram(), + pht('Marking commit unreachable.')); + + $commit->writeImportStatusFlag($flag); + } else { + echo tsprintf( + "%s: %s\n", + $commit->getMonogram(), + pht('Marking commit reachable.')); + + $commit->clearImportStatusFlag($flag); + } + } + } + +} diff --git a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php index 0f62d523d2..5d3ce3c961 100644 --- a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php +++ b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php @@ -46,6 +46,10 @@ final class PhabricatorRepositoryCommitPHIDType extends PhabricatorPHIDType { $handle->setFullName($full_name); $handle->setURI($commit->getURI()); $handle->setTimestamp($commit->getEpoch()); + + if ($commit->isUnreachable()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } } } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index b771066ed6..d0f3f9f4d8 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -910,6 +910,21 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return null; } + public function shouldTrackRef(DiffusionRepositoryRef $ref) { + // At least for now, don't track the staging area tags. + if ($ref->isTag()) { + if (preg_match('(^phabricator/)', $ref->getShortName())) { + return false; + } + } + + if (!$ref->isBranch()) { + return true; + } + + return $this->shouldTrackBranch($ref->getShortName()); + } + public function shouldTrackBranch($branch) { return $this->isBranchInFilter($branch, 'branch-filter'); } @@ -1020,6 +1035,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO /* -( Autoclose )---------------------------------------------------------- */ + public function shouldAutocloseRef(DiffusionRepositoryRef $ref) { + if (!$ref->isBranch()) { + return false; + } + + return $this->shouldAutocloseBranch($ref->getShortName()); + } + /** * Determine if autoclose is active for a branch. * diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 6e56cc711a..ae43c67416 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -32,6 +32,7 @@ final class PhabricatorRepositoryCommit const IMPORTED_ALL = 15; const IMPORTED_CLOSEABLE = 1024; + const IMPORTED_UNREACHABLE = 2048; private $commitData = self::ATTACHABLE; private $audits = self::ATTACHABLE; @@ -58,14 +59,43 @@ final class PhabricatorRepositoryCommit return $this->isPartiallyImported(self::IMPORTED_ALL); } + public function isUnreachable() { + return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE); + } + public function writeImportStatusFlag($flag) { - queryfx( - $this->establishConnection('w'), - 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', - $this->getTableName(), - $flag, - $this->getID()); - $this->setImportStatus($this->getImportStatus() | $flag); + return $this->adjustImportStatusFlag($flag, true); + } + + public function clearImportStatusFlag($flag) { + return $this->adjustImportStatusFlag($flag, false); + } + + private function adjustImportStatusFlag($flag, $set) { + $conn_w = $this->establishConnection('w'); + $table_name = $this->getTableName(); + $id = $this->getID(); + + if ($set) { + queryfx( + $conn_w, + 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', + $table_name, + $flag, + $id); + + $this->setImportStatus($this->getImportStatus() | $flag); + } else { + queryfx( + $conn_w, + 'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d', + $table_name, + $flag, + $id); + + $this->setImportStatus($this->getImportStatus() & ~$flag); + } + return $this; } diff --git a/src/applications/repository/storage/PhabricatorRepositoryOldRef.php b/src/applications/repository/storage/PhabricatorRepositoryOldRef.php new file mode 100644 index 0000000000..65cba3c48b --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryOldRef.php @@ -0,0 +1,52 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'commitIdentifier' => 'text40', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_repository' => array( + 'columns' => array('repositoryPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php index d79aa9be70..febbed3ee6 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php +++ b/src/applications/repository/storage/PhabricatorRepositoryRefCursor.php @@ -12,6 +12,7 @@ final class PhabricatorRepositoryRefCursor const TYPE_BRANCH = 'branch'; const TYPE_TAG = 'tag'; const TYPE_BOOKMARK = 'bookmark'; + const TYPE_REF = 'ref'; protected $repositoryPHID; protected $refType; diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php index 9ad8ef2b66..4bd9344409 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -3,6 +3,10 @@ final class PhabricatorRepositoryCommitHeraldWorker extends PhabricatorRepositoryCommitParserWorker { + protected function getImportStepFlag() { + return PhabricatorRepositoryCommit::IMPORTED_HERALD; + } + public function getRequiredLeaseTime() { // Herald rules may take a long time to process. return phutil_units('4 hours in seconds'); @@ -12,6 +16,12 @@ final class PhabricatorRepositoryCommitHeraldWorker PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { + if ($this->shouldSkipImportStep()) { + // This worker has no followup tasks, so we can just bail out + // right away without queueing anything. + return; + } + // Reload the commit to pull commit data and audit requests. $commit = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 3f41226b9f..8f94df90be 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -3,14 +3,18 @@ final class PhabricatorRepositoryCommitOwnersWorker extends PhabricatorRepositoryCommitParserWorker { + protected function getImportStepFlag() { + return PhabricatorRepositoryCommit::IMPORTED_OWNERS; + } + protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { - $this->triggerOwnerAudits($repository, $commit); - - $commit->writeImportStatusFlag( - PhabricatorRepositoryCommit::IMPORTED_OWNERS); + if (!$this->shouldSkipImportStep()) { + $this->triggerOwnerAudits($repository, $commit); + $commit->writeImportStatusFlag($this->getImportStepFlag()); + } if ($this->shouldQueueFollowupTasks()) { $this->queueTask( diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php index aa2aaa270b..f6e2661b87 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php @@ -26,6 +26,14 @@ abstract class PhabricatorRepositoryCommitParserWorker pht('Commit "%s" does not exist.', $commit_id)); } + if ($commit->isUnreachable()) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Commit "%s" has been deleted: it is no longer reachable from '. + 'any ref.', + $commit_id)); + } + $this->commit = $commit; return $commit; @@ -44,6 +52,42 @@ abstract class PhabricatorRepositoryCommitParserWorker return !idx($this->getTaskData(), 'only'); } + protected function getImportStepFlag() { + return null; + } + + final protected function shouldSkipImportStep() { + // If this step has already been performed and this is a "natural" task + // which was queued by the normal daemons, decline to do the work again. + // This mitigates races if commits are rapidly deleted and revived. + $flag = $this->getImportStepFlag(); + if (!$flag) { + // This step doesn't have an associated flag. + return false; + } + + $commit = $this->commit; + if (!$commit->isPartiallyImported($flag)) { + // This commit doesn't have the flag set yet. + return false; + } + + + if (!$this->shouldQueueFollowupTasks()) { + // This task was queued by administrative tools, so do the work even + // if it duplicates existing work. + return false; + } + + $this->log( + "%s\n", + pht( + 'Skipping import step; this step was previously completed for '. + 'this commit.')); + + return true; + } + abstract protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit); diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php index 6a0161fd06..f58856d614 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php @@ -3,6 +3,10 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker extends PhabricatorRepositoryCommitParserWorker { + protected function getImportStepFlag() { + return PhabricatorRepositoryCommit::IMPORTED_CHANGE; + } + public function getRequiredLeaseTime() { // It can take a very long time to parse commits; some commits in the // Facebook repository affect many millions of paths. Acquire 24h leases. @@ -23,9 +27,15 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker return; } - $results = $this->parseCommitChanges($repository, $commit); - if ($results) { - $this->writeCommitChanges($repository, $commit, $results); + if (!$this->shouldSkipImportStep()) { + $results = $this->parseCommitChanges($repository, $commit); + if ($results) { + $this->writeCommitChanges($repository, $commit, $results); + } + + $commit->writeImportStatusFlag($this->getImportStepFlag()); + + PhabricatorSearchWorker::queueDocumentForIndexing($commit->getPHID()); } $this->finishParse(); @@ -85,12 +95,6 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker protected function finishParse() { $commit = $this->commit; - - $commit->writeImportStatusFlag( - PhabricatorRepositoryCommit::IMPORTED_CHANGE); - - PhabricatorSearchWorker::queueDocumentForIndexing($commit->getPHID()); - if ($this->shouldQueueFollowupTasks()) { $this->queueTask( 'PhabricatorRepositoryCommitOwnersWorker', diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index 4e92a0f32e..e28cd78cc7 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -3,42 +3,52 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker extends PhabricatorRepositoryCommitParserWorker { - abstract protected function parseCommitWithRef( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit, - DiffusionCommitRef $ref); + protected function getImportStepFlag() { + return PhabricatorRepositoryCommit::IMPORTED_MESSAGE; + } + + abstract protected function getFollowupTaskClass(); final protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { - $viewer = PhabricatorUser::getOmnipotentUser(); + if (!$this->shouldSkipImportStep()) { + $viewer = PhabricatorUser::getOmnipotentUser(); - $refs_raw = DiffusionQuery::callConduitWithDiffusionRequest( - $viewer, - DiffusionRequest::newFromDictionary( + $refs_raw = DiffusionQuery::callConduitWithDiffusionRequest( + $viewer, + DiffusionRequest::newFromDictionary( + array( + 'repository' => $repository, + 'user' => $viewer, + )), + 'diffusion.querycommits', array( - 'repository' => $repository, - 'user' => $viewer, - )), - 'diffusion.querycommits', - array( - 'repositoryPHID' => $repository->getPHID(), - 'phids' => array($commit->getPHID()), - 'bypassCache' => true, - 'needMessages' => true, - )); + 'repositoryPHID' => $repository->getPHID(), + 'phids' => array($commit->getPHID()), + 'bypassCache' => true, + 'needMessages' => true, + )); - if (empty($refs_raw['data'])) { - throw new Exception( - pht( - 'Unable to retrieve details for commit "%s"!', - $commit->getPHID())); + if (empty($refs_raw['data'])) { + throw new Exception( + pht( + 'Unable to retrieve details for commit "%s"!', + $commit->getPHID())); + } + + $ref = DiffusionCommitRef::newFromConduitResult(head($refs_raw['data'])); + $this->updateCommitData($ref); } - $ref = DiffusionCommitRef::newFromConduitResult(head($refs_raw['data'])); - - $this->parseCommitWithRef($repository, $commit, $ref); + if ($this->shouldQueueFollowupTasks()) { + $this->queueTask( + $this->getFollowupTaskClass(), + array( + 'commitID' => $commit->getID(), + )); + } } final protected function updateCommitData(DiffusionCommitRef $ref) { diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php index b41f4c421c..0c83a27fab 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php @@ -3,20 +3,8 @@ final class PhabricatorRepositoryGitCommitMessageParserWorker extends PhabricatorRepositoryCommitMessageParserWorker { - protected function parseCommitWithRef( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit, - DiffusionCommitRef $ref) { - - $this->updateCommitData($ref); - - if ($this->shouldQueueFollowupTasks()) { - $this->queueTask( - 'PhabricatorRepositoryGitCommitChangeParserWorker', - array( - 'commitID' => $commit->getID(), - )); - } + protected function getFollowupTaskClass() { + return 'PhabricatorRepositoryGitCommitChangeParserWorker'; } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php index 72d187d896..654083e736 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php @@ -3,20 +3,8 @@ final class PhabricatorRepositoryMercurialCommitMessageParserWorker extends PhabricatorRepositoryCommitMessageParserWorker { - protected function parseCommitWithRef( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit, - DiffusionCommitRef $ref) { - - $this->updateCommitData($ref); - - if ($this->shouldQueueFollowupTasks()) { - $this->queueTask( - 'PhabricatorRepositoryMercurialCommitChangeParserWorker', - array( - 'commitID' => $commit->getID(), - )); - } + protected function getFollowupTaskClass() { + return 'PhabricatorRepositoryMercurialCommitChangeParserWorker'; } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php index 648c8adaaf..7006957b50 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php @@ -3,20 +3,8 @@ final class PhabricatorRepositorySvnCommitMessageParserWorker extends PhabricatorRepositoryCommitMessageParserWorker { - protected function parseCommitWithRef( - PhabricatorRepository $repository, - PhabricatorRepositoryCommit $commit, - DiffusionCommitRef $ref) { - - $this->updateCommitData($ref); - - if ($this->shouldQueueFollowupTasks()) { - $this->queueTask( - 'PhabricatorRepositorySvnCommitChangeParserWorker', - array( - 'commitID' => $commit->getID(), - )); - } + protected function getFollowupTaskClass() { + return 'PhabricatorRepositorySvnCommitChangeParserWorker'; } } diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php index 2a368d8650..fada4a0937 100644 --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -152,6 +152,10 @@ final class PhabricatorSettingsMainController if (!$this->isSelf() && !$panel->isManagementPanel()) { continue; } + + if ($this->isSelf() && !$panel->isUserPanel()) { + continue; + } } if (!empty($result[$key])) { diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php index 5cab5dea41..107816f2eb 100644 --- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php @@ -13,7 +13,15 @@ final class PhabricatorEmailFormatSettingsPanel return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY; } + public function isUserPanel() { + return PhabricatorMetaMTAMail::shouldMultiplexAllMail(); + } + public function isManagementPanel() { + if (!$this->isUserPanel()) { + return false; + } + if ($this->getUser()->getIsMailingList()) { return true; } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php index b66b03f9c8..7d86eaf243 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php @@ -154,6 +154,18 @@ abstract class PhabricatorSettingsPanel extends Phobject { } + /** + * Return true if this panel is available to users while editing their own + * settings. + * + * @return bool True to enable management on behalf of a user. + * @task config + */ + public function isUserPanel() { + return true; + } + + /** * Return true if this panel is available to administrators while managing * bot and mailing list accounts. diff --git a/src/applications/settings/query/PhabricatorUserPreferencesQuery.php b/src/applications/settings/query/PhabricatorUserPreferencesQuery.php index 280ca3eea6..de4887cbb8 100644 --- a/src/applications/settings/query/PhabricatorUserPreferencesQuery.php +++ b/src/applications/settings/query/PhabricatorUserPreferencesQuery.php @@ -9,6 +9,7 @@ final class PhabricatorUserPreferencesQuery private $builtinKeys; private $hasUserPHID; private $users = array(); + private $synthetic; public function withIDs(array $ids) { $this->ids = $ids; @@ -42,12 +43,38 @@ final class PhabricatorUserPreferencesQuery return $this; } + /** + * Always return preferences for every queried user. + * + * If no settings exist for a user, a new empty settings object with + * appropriate defaults is returned. + * + * @param bool True to generat synthetic preferences for missing users. + */ + public function needSyntheticPreferences($synthetic) { + $this->synthetic = $synthetic; + return $this; + } + public function newResultObject() { return new PhabricatorUserPreferences(); } protected function loadPage() { - return $this->loadStandardPage($this->newResultObject()); + $preferences = $this->loadStandardPage($this->newResultObject()); + + if ($this->synthetic) { + $user_map = mpull($preferences, null, 'getUserPHID'); + foreach ($this->userPHIDs as $user_phid) { + if (isset($user_map[$user_phid])) { + continue; + } + $preferences[] = $this->newResultObject() + ->setUserPHID($user_phid); + } + } + + return $preferences; } protected function willFilterPage(array $prefs) { diff --git a/src/applications/settings/setting/PhabricatorEmailFormatSetting.php b/src/applications/settings/setting/PhabricatorEmailFormatSetting.php index 0cf0db5d74..333d85c6f4 100644 --- a/src/applications/settings/setting/PhabricatorEmailFormatSetting.php +++ b/src/applications/settings/setting/PhabricatorEmailFormatSetting.php @@ -20,10 +20,6 @@ final class PhabricatorEmailFormatSetting return 100; } - protected function isEnabledForViewer(PhabricatorUser $viewer) { - return PhabricatorMetaMTAMail::shouldMultiplexAllMail(); - } - protected function getControlInstructions() { return pht( 'You can opt to receive plain text email from Phabricator instead '. diff --git a/src/applications/settings/setting/PhabricatorEmailRePrefixSetting.php b/src/applications/settings/setting/PhabricatorEmailRePrefixSetting.php index 2a2f63b461..5e70b731cd 100644 --- a/src/applications/settings/setting/PhabricatorEmailRePrefixSetting.php +++ b/src/applications/settings/setting/PhabricatorEmailRePrefixSetting.php @@ -20,10 +20,6 @@ final class PhabricatorEmailRePrefixSetting return 200; } - protected function isEnabledForViewer(PhabricatorUser $viewer) { - return PhabricatorMetaMTAMail::shouldMultiplexAllMail(); - } - protected function getControlInstructions() { return pht( 'The **Add "Re:" Prefix** setting adds "Re:" in front of all messages, '. @@ -39,7 +35,7 @@ final class PhabricatorEmailRePrefixSetting } public function getSettingDefaultValue() { - return self::VALUE_RE_PREFIX; + return self::VALUE_NO_PREFIX; } protected function getSelectOptions() { diff --git a/src/applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php b/src/applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php index b8e6474155..1c088d9411 100644 --- a/src/applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php +++ b/src/applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php @@ -20,10 +20,6 @@ final class PhabricatorEmailVarySubjectsSetting return 300; } - protected function isEnabledForViewer(PhabricatorUser $viewer) { - return PhabricatorMetaMTAMail::shouldMultiplexAllMail(); - } - protected function getControlInstructions() { return pht( 'With **Vary Subjects** enabled, most mail subject lines will include '. @@ -48,8 +44,8 @@ final class PhabricatorEmailVarySubjectsSetting protected function getSelectOptions() { return array( - self::VALUE_VARY_SUBJECTS => pht('Enable "Re:" Prefix'), - self::VALUE_STATIC_SUBJECTS => pht('Disable "Re:" Prefix'), + self::VALUE_VARY_SUBJECTS => pht('Enable Vary Subjects'), + self::VALUE_STATIC_SUBJECTS => pht('Disable Vary Subjects'), ); } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 31719c8195..5ed360ca2c 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -122,17 +122,35 @@ final class PhabricatorUserPreferences * @param PhabricatorUser User to load or create preferences for. */ public static function loadUserPreferences(PhabricatorUser $user) { - $preferences = id(new PhabricatorUserPreferencesQuery()) + return id(new PhabricatorUserPreferencesQuery()) ->setViewer($user) ->withUsers(array($user)) + ->needSyntheticPreferences(true) ->executeOne(); - if ($preferences) { - return $preferences; + } + + /** + * Load or create a global preferences object. + * + * If no global preferences exist, an empty preferences object is returned. + * + * @param PhabricatorUser Viewing user. + */ + public static function loadGlobalPreferences(PhabricatorUser $viewer) { + $global = id(new PhabricatorUserPreferencesQuery()) + ->setViewer($viewer) + ->withBuiltinKeys( + array( + self::BUILTIN_GLOBAL_DEFAULT, + )) + ->executeOne(); + + if (!$global) { + $global = id(new self()) + ->attachUser(new PhabricatorUser()); } - return id(new self()) - ->setUserPHID($user->getPHID()) - ->attachUser($user); + return $global; } public function newTransaction($key, $value) { diff --git a/src/applications/transactions/data/PhabricatorTransactionChange.php b/src/applications/transactions/data/PhabricatorTransactionChange.php new file mode 100644 index 0000000000..2fc59ce5e5 --- /dev/null +++ b/src/applications/transactions/data/PhabricatorTransactionChange.php @@ -0,0 +1,37 @@ +transaction = $xaction; + return $this; + } + + final public function getTransaction() { + return $this->transaction; + } + + final public function setOldValue($old_value) { + $this->oldValue = $old_value; + return $this; + } + + final public function getOldValue() { + return $this->oldValue; + } + + final public function setNewValue($new_value) { + $this->newValue = $new_value; + return $this; + } + + final public function getNewValue() { + return $this->newValue; + } + +} diff --git a/src/applications/transactions/data/PhabricatorTransactionRemarkupChange.php b/src/applications/transactions/data/PhabricatorTransactionRemarkupChange.php new file mode 100644 index 0000000000..bf4a4a11b1 --- /dev/null +++ b/src/applications/transactions/data/PhabricatorTransactionRemarkupChange.php @@ -0,0 +1,4 @@ +setTransactionPHID($xaction->getPHID()); $comment->save(); + $old_comment = $xaction->getComment(); + $comment->attachOldComment($old_comment); + $xaction->setCommentVersion($new_version); $xaction->setCommentPHID($comment->getPHID()); $xaction->setViewPolicy($comment->getViewPolicy()); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 1a4afa2c9f..7443de7edc 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -68,6 +68,7 @@ abstract class PhabricatorApplicationTransactionEditor private $mailCCPHIDs = array(); private $feedNotifyPHIDs = array(); private $feedRelatedPHIDs = array(); + private $modularTypes; const STORAGE_ENCODING_BINARY = 'binary'; @@ -285,6 +286,14 @@ abstract class PhabricatorApplicationTransactionEditor $types[] = PhabricatorTransactions::TYPE_SPACE; } + $template = $this->object->getApplicationTransactionTemplate(); + if ($template instanceof PhabricatorModularTransaction) { + $xtypes = $template->newModularTransactionTypes(); + foreach ($xtypes as $xtype) { + $types[] = $xtype->getTransactionTypeConstant(); + } + } + return $types; } @@ -304,7 +313,15 @@ abstract class PhabricatorApplicationTransactionEditor private function getTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { + + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + return $xtype->generateOldValue($object); + } + + switch ($type) { case PhabricatorTransactions::TYPE_CREATE: return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: @@ -374,7 +391,15 @@ abstract class PhabricatorApplicationTransactionEditor private function getTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { + + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + return $xtype->generateNewValue($object, $xaction->getNewValue()); + } + + switch ($type) { case PhabricatorTransactions::TYPE_CREATE: return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: @@ -496,7 +521,14 @@ abstract class PhabricatorApplicationTransactionEditor PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + return $xtype->applyInternalEffects($object, $xaction->getNewValue()); + } + + switch ($type) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getCustomFieldForTransaction($object, $xaction); return $field->applyApplicationTransactionInternalEffects($xaction); @@ -520,7 +552,15 @@ abstract class PhabricatorApplicationTransactionEditor private function applyExternalEffects( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { + + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + return $xtype->applyExternalEffects($object, $xaction->getNewValue()); + } + + switch ($type) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($object) @@ -802,6 +842,8 @@ abstract class PhabricatorApplicationTransactionEditor throw new PhabricatorApplicationTransactionValidationException($errors); } + $this->willApplyTransactions($object, $xactions); + if ($object->getID()) { foreach ($xactions as $xaction) { @@ -1320,17 +1362,28 @@ abstract class PhabricatorApplicationTransactionEditor private function buildSubscribeTransaction( PhabricatorLiskDAO $object, array $xactions, - array $blocks) { + array $changes) { if (!($object instanceof PhabricatorSubscribableInterface)) { return null; } if ($this->shouldEnableMentions($object, $xactions)) { - $texts = array_mergev($blocks); - $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( + // Identify newly mentioned users. We ignore users who were previously + // mentioned so that we don't re-subscribe users after an edit of text + // which mentions them. + $old_texts = mpull($changes, 'getOldValue'); + $new_texts = mpull($changes, 'getNewValue'); + + $old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( $this->getActor(), - $texts); + $old_texts); + + $new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( + $this->getActor(), + $new_texts); + + $phids = array_diff($new_phids, $old_phids); } else { $phids = array(); } @@ -1381,11 +1434,6 @@ abstract class PhabricatorApplicationTransactionEditor return $xaction; } - protected function getRemarkupBlocksFromTransaction( - PhabricatorApplicationTransaction $transaction) { - return $transaction->getRemarkupBlocks(); - } - protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { @@ -1464,15 +1512,12 @@ abstract class PhabricatorApplicationTransactionEditor $xactions = $this->applyImplicitCC($object, $xactions); - $blocks = array(); - foreach ($xactions as $key => $xaction) { - $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction); - } + $changes = $this->getRemarkupChanges($xactions); $subscribe_xaction = $this->buildSubscribeTransaction( $object, $xactions, - $blocks); + $changes); if ($subscribe_xaction) { $xactions[] = $subscribe_xaction; } @@ -1484,7 +1529,7 @@ abstract class PhabricatorApplicationTransactionEditor $block_xactions = $this->expandRemarkupBlockTransactions( $object, $xactions, - $blocks, + $changes, $engine); foreach ($block_xactions as $xaction) { @@ -1494,27 +1539,46 @@ abstract class PhabricatorApplicationTransactionEditor return $xactions; } + private function getRemarkupChanges(array $xactions) { + $changes = array(); + + foreach ($xactions as $key => $xaction) { + foreach ($this->getRemarkupChangesFromTransaction($xaction) as $change) { + $changes[] = $change; + } + } + + return $changes; + } + + private function getRemarkupChangesFromTransaction( + PhabricatorApplicationTransaction $transaction) { + return $transaction->getRemarkupChanges(); + } + private function expandRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, - $blocks, + array $changes, PhutilMarkupEngine $engine) { $block_xactions = $this->expandCustomRemarkupBlockTransactions( $object, $xactions, - $blocks, + $changes, $engine); $mentioned_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { - foreach ($blocks as $key => $xaction_blocks) { - foreach ($xaction_blocks as $block) { - $engine->markupText($block); - $mentioned_phids += $engine->getTextMetadata( - PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS, - array()); - } + foreach ($changes as $change) { + // Here, we don't care about processing only new mentions after an edit + // because there is no way for an object to ever "unmention" itself on + // another object, so we can ignore the old value. + $engine->markupText($change->getNewValue()); + + $mentioned_phids += $engine->getTextMetadata( + PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS, + array()); } } @@ -1559,7 +1623,7 @@ abstract class PhabricatorApplicationTransactionEditor protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, - $blocks, + array $changes, PhutilMarkupEngine $engine) { return array(); } @@ -1984,6 +2048,12 @@ abstract class PhabricatorApplicationTransactionEditor array $xactions) { $errors = array(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + $errors[] = $xtype->validateTransactions($object, $xactions); + } + switch ($type) { case PhabricatorTransactions::TYPE_VIEW_POLICY: $errors[] = $this->validatePolicyTransaction( @@ -3096,11 +3166,8 @@ abstract class PhabricatorApplicationTransactionEditor PhabricatorLiskDAO $object, array $xactions) { - $blocks = array(); - foreach ($xactions as $xaction) { - $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction); - } - $blocks = array_mergev($blocks); + $changes = $this->getRemarkupChanges($xactions); + $blocks = mpull($changes, 'getNewValue'); $phids = array(); if ($blocks) { @@ -3110,9 +3177,16 @@ abstract class PhabricatorApplicationTransactionEditor } foreach ($xactions as $xaction) { - $phids[] = $this->extractFilePHIDsFromCustomTransaction( - $object, - $xaction); + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + $phids[] = $xtype->extractFilePHIDs($object, $xaction->getNewValue()); + } else { + $phids[] = $this->extractFilePHIDsFromCustomTransaction( + $object, + $xaction); + } } $phids = array_unique(array_filter(array_mergev($phids))); @@ -3673,5 +3747,50 @@ abstract class PhabricatorApplicationTransactionEditor $proxy_phids); } + private function getModularTransactionTypes() { + if ($this->modularTypes === null) { + $template = $this->object->getApplicationTransactionTemplate(); + if ($template instanceof PhabricatorModularTransaction) { + $xtypes = $template->newModularTransactionTypes(); + foreach ($xtypes as $key => $xtype) { + $xtype = clone $xtype; + $xtype->setEditor($this); + $xtypes[$key] = $xtype; + } + } else { + $xtypes = array(); + } + + $this->modularTypes = $xtypes; + } + + return $this->modularTypes; + } + + private function getModularTransactionType($type) { + $types = $this->getModularTransactionTypes(); + return idx($types, $type); + } + + private function willApplyTransactions($object, array $xactions) { + foreach ($xactions as $xaction) { + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if (!$xtype) { + continue; + } + + $xtype->willApplyTransactions($object, $xactions); + } + } + + public function getCreateObjectTitle($author, $object) { + return pht('%s created this object.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created an object: %s.', $author, $object); + } } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 3a7e3841a3..53259984b4 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -179,6 +179,50 @@ abstract class PhabricatorApplicationTransaction return $this->assertAttached($this->object); } + public function getRemarkupChanges() { + $changes = $this->newRemarkupChanges(); + assert_instances_of($changes, 'PhabricatorTransactionRemarkupChange'); + + // Convert older-style remarkup blocks into newer-style remarkup changes. + // This builds changes that do not have the correct "old value", so rules + // that operate differently against edits (like @user mentions) won't work + // properly. + foreach ($this->getRemarkupBlocks() as $block) { + $changes[] = $this->newRemarkupChange() + ->setOldValue(null) + ->setNewValue($block); + } + + $comment = $this->getComment(); + if ($comment) { + if ($comment->hasOldComment()) { + $old_value = $comment->getOldComment()->getContent(); + } else { + $old_value = null; + } + + $new_value = $comment->getContent(); + + $changes[] = $this->newRemarkupChange() + ->setOldValue($old_value) + ->setNewValue($new_value); + } + + return $changes; + } + + protected function newRemarkupChanges() { + return array(); + } + + protected function newRemarkupChange() { + return id(new PhabricatorTransactionRemarkupChange()) + ->setTransaction($this); + } + + /** + * @deprecated + */ public function getRemarkupBlocks() { $blocks = array(); @@ -195,10 +239,6 @@ abstract class PhabricatorApplicationTransaction break; } - if ($this->getComment()) { - $blocks[] = $this->getComment()->getContent(); - } - return $blocks; } @@ -240,6 +280,7 @@ abstract class PhabricatorApplicationTransaction $new = $this->getNewValue(); $phids[] = array($this->getAuthorPHID()); + $phids[] = array($this->getObjectPHID()); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php b/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php index 75964b218c..3239125249 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransactionComment.php @@ -18,6 +18,8 @@ abstract class PhabricatorApplicationTransactionComment protected $contentSource; protected $isDeleted = 0; + private $oldComment = self::ATTACHABLE; + abstract public function getApplicationTransactionObject(); public function generatePHID() { @@ -85,6 +87,20 @@ abstract class PhabricatorApplicationTransactionComment return $this; } + public function attachOldComment( + PhabricatorApplicationTransactionComment $old_comment) { + $this->oldComment = $old_comment; + return $this; + } + + public function getOldComment() { + return $this->assertAttached($this->oldComment); + } + + public function hasOldComment() { + return ($this->oldComment !== self::ATTACHABLE); + } + /* -( PhabricatorMarkupInterface )----------------------------------------- */ diff --git a/src/applications/transactions/storage/PhabricatorModularTransaction.php b/src/applications/transactions/storage/PhabricatorModularTransaction.php new file mode 100644 index 0000000000..5dc6229a77 --- /dev/null +++ b/src/applications/transactions/storage/PhabricatorModularTransaction.php @@ -0,0 +1,138 @@ +implementation) { + $this->implementation = $this->newTransactionImplementation(); + } + + return $this->implementation; + } + + public function newModularTransactionTypes() { + $base_class = $this->getBaseTransactionClass(); + + $types = id(new PhutilClassMapQuery()) + ->setAncestorClass($base_class) + ->setUniqueMethod('getTransactionTypeConstant') + ->execute(); + + // Add core transaction types. + $types += id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorCoreTransactionType') + ->setUniqueMethod('getTransactionTypeConstant') + ->execute(); + + return $types; + } + + private function newTransactionImplementation() { + $types = $this->newModularTransactionTypes(); + + $key = $this->getTransactionType(); + + if (empty($types[$key])) { + $type = new PhabricatorCoreVoidTransaction(); + } else { + $type = clone $types[$key]; + } + + $type->setStorage($this); + + return $type; + } + + final public function generateOldValue($object) { + return $this->getTransactionImplementation()->generateOldValue($object); + } + + final public function generateNewValue($object) { + return $this->getTransactionImplementation() + ->generateNewValue($object, $this->getNewValue()); + } + + final public function willApplyTransactions($object, array $xactions) { + return $this->getTransactionImplementation() + ->willApplyTransactions($object, $xactions); + } + + final public function applyInternalEffects($object) { + return $this->getTransactionImplementation() + ->applyInternalEffects($object); + } + + final public function applyExternalEffects($object) { + return $this->getTransactionImplementation() + ->applyExternalEffects($object); + } + + final public function shouldHide() { + if ($this->getTransactionImplementation()->shouldHide()) { + return true; + } + + return parent::shouldHide(); + } + + final public function getIcon() { + $icon = $this->getTransactionImplementation()->getIcon(); + if ($icon !== null) { + return $icon; + } + + return parent::getIcon(); + } + + final public function getTitle() { + $title = $this->getTransactionImplementation()->getTitle(); + if ($title !== null) { + return $title; + } + + return parent::getTitle(); + } + + final public function getTitleForFeed() { + $title = $this->getTransactionImplementation()->getTitleForFeed(); + if ($title !== null) { + return $title; + } + + return parent::getTitleForFeed(); + } + + final public function getColor() { + $color = $this->getTransactionImplementation()->getColor(); + if ($color !== null) { + return $color; + } + + return parent::getColor(); + } + + final public function hasChangeDetails() { + if ($this->getTransactionImplementation()->hasChangeDetailView()) { + return true; + } + + return parent::hasChangeDetails(); + } + + final public function renderChangeDetails(PhabricatorUser $viewer) { + $impl = $this->getTransactionImplementation(); + $impl->setViewer($viewer); + $view = $impl->newChangeDetailView(); + if ($view !== null) { + return $view; + } + + return parent::renderChangeDetails($viewer); + } + +} diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php new file mode 100644 index 0000000000..b37bf0d61d --- /dev/null +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -0,0 +1,140 @@ +getPhobjectClassConstant('TRANSACTIONTYPE'); + } + + public function generateOldValue($object) { + throw new PhutilMethodNotImplementedException(); + } + + public function generateNewValue($object, $value) { + return $value; + } + + public function validateTransactions($object, array $xactions) { + return array(); + } + + public function willApplyTransactions($object, array $xactions) { + return; + } + + public function applyInternalEffects($object, $value) { + return; + } + + public function applyExternalEffects($object, $value) { + return; + } + + public function extractFilePHIDs($object, $value) { + return array(); + } + + public function shouldHide() { + return false; + } + + public function getIcon() { + return null; + } + + public function getTitle() { + return null; + } + + public function getTitleForFeed() { + return null; + } + + public function getColor() { + return null; + } + + public function hasChangeDetailView() { + return false; + } + + public function newChangeDetailView() { + throw new PhutilMethodNotImplementedException(); + } + + final public function setStorage( + PhabricatorApplicationTransaction $xaction) { + $this->storage = $xaction; + return $this; + } + + private function getStorage() { + return $this->storage; + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final protected function getViewer() { + return $this->viewer; + } + + final public function setEditor( + PhabricatorApplicationTransactionEditor $editor) { + $this->editor = $editor; + return $this; + } + + final protected function getEditor() { + if (!$this->editor) { + throw new PhutilInvalidStateException('setEditor'); + } + return $this->editor; + } + + final protected function getAuthorPHID() { + return $this->getStorage()->getAuthorPHID(); + } + + final protected function getObjectPHID() { + return $this->getStorage()->getObjectPHID(); + } + + final protected function getObject() { + return $this->getStorage()->getObject(); + } + + final protected function getOldValue() { + return $this->getStorage()->getOldValue(); + } + + final protected function getNewValue() { + return $this->getStorage()->getNewValue(); + } + + final protected function renderAuthor() { + $author_phid = $this->getAuthorPHID(); + return $this->getStorage()->renderHandleLink($author_phid); + } + + final protected function renderObject() { + $object_phid = $this->getObjectPHID(); + return $this->getStorage()->renderHandleLink($object_phid); + } + + final protected function newError($title, $message, $xaction = null) { + return new PhabricatorApplicationTransactionValidationError( + $this->getTransactionTypeConstant(), + $title, + $message, + $xaction); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorCoreCreateTransaction.php b/src/applications/transactions/xaction/PhabricatorCoreCreateTransaction.php new file mode 100644 index 0000000000..0c8c337193 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorCoreCreateTransaction.php @@ -0,0 +1,30 @@ +getObject()->getApplicationTransactionEditor(); + + $author = $this->renderAuthor(); + $object = $this->renderObject(); + + return $editor->getCreateObjectTitle($author, $object); + } + + public function getTitleForFeed() { + $editor = $this->getObject()->getApplicationTransactionEditor(); + + $author = $this->renderAuthor(); + $object = $this->renderObject(); + + return $editor->getCreateObjectTitleForFeed($author, $object); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorCoreTransactionType.php b/src/applications/transactions/xaction/PhabricatorCoreTransactionType.php new file mode 100644 index 0000000000..f4020b1eb9 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorCoreTransactionType.php @@ -0,0 +1,4 @@ + F123 [--key ] +``` + +This will change the storage format of the sepcified file. + + +Verifying Storage Formats +========================= + +You can review the storage format of a file from the web UI, in the +{nav Storage} tab under "Format". You can also use the "Engine" and "Handle" +properties to identify where the underlying data is stored and verify that +it is encrypted or encoded in the way you expect. + +See @{article:Configuring File Storage} for more information on storage +engines. + + +Cycling Master Keys +=================== + +If you need to cycle your master key, some storage formats support key cycling. + +Cycling a file's encryption key decodes the local key for the data using the +old master key, then re-encodes it using the new master key. This is primarily +useful if you believe your master key may have been compromised. + +First, add a new key to the keyring and mark it as the default key. You need +to leave the old key in place for now so existing data can be decrypted. + +To cycle an individual file, run this command: + +``` +phabricator/ $ ./bin/files cycle F123 +``` + +Verify that cycling worked properly by examining the command output and +accessing the file to check that the data is present and decryptable. You +can cycle additional files to gain additional confidence. + +You can cycle all files with this command: + +``` +phabricator/ $ ./bin/files cycle --all +``` + +Once all files have been cycled, remove the old master key from the keyring. + +Not all storage formats support key cycling: cycling a file only has an effect +if the storage format is an encrypted format. For example, cycling a file that +uses the `raw` storage format has no effect. + + +Next Steps +========== + +Continue by: + + - understanding storage engines with @{article:Configuring File Storage}; or + - returning to the @{article:Configuration Guide}. diff --git a/src/docs/user/configuration/configuring_file_storage.diviner b/src/docs/user/configuration/configuring_file_storage.diviner index 8ea60080ac..64942b493e 100644 --- a/src/docs/user/configuration/configuring_file_storage.diviner +++ b/src/docs/user/configuration/configuring_file_storage.diviner @@ -197,4 +197,6 @@ Next Steps Continue by: + - reviewing at-rest encryption options with + @{article:Configuring Encryption}; or - returning to the @{article:Configuration Guide}. diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index 61c3ce4361..eca45b7741 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -84,7 +84,6 @@ .phame-blog-list-item { display: block; color: {$darkgreytext}; - height: 24px; position: relative; margin-bottom: 8px; padding-right: 20px; @@ -108,14 +107,12 @@ .phame-blog-list-title { margin-left: 30px; - margin-top: 4px; + margin-top: 2px; display: inline-block; font-weight: bold; color: {$bluetext}; width: 190px; overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } .phame-blog-list-new-post { @@ -253,3 +250,61 @@ color: {$darkgreytext}; background: {$greybackground}; } + +/* Blog Chrome */ +.phame-live-view .phui-crumbs-view { + background: #fff; + border: none; + padding-left: 8px; +} + +/* Hero Image */ +.phame-header-hero { + background-color: #fff; +} + +.phame-header-image { + max-height: 320px; + max-width: 100%; + margin: 0 auto; +} + +.phui-document-view.phui-document-view-pro .phui-header-shell.phame-header-bar { + border-top: 1px solid {$thinblueborder}; + border-bottom: none; + padding: 4px 0; +} + +.phame-header-bar .phui-header-subheader { + margin: 0; +} + +.phame-mega-header { + margin: 0 auto; + text-align: center; + background: #fff; + padding: 16px 0 24px; +} + +.device-phone .phame-mega-header { + padding: 24px 0; +} + +.phame-mega-header .phame-header-title { + color: #000; + font-size: 28px; + font-weight: bold; + font-family: 'Aleo', {$fontfamily}; + padding-top: 24px; +} + +.device-phone .phame-mega-header .phame-header-title { + padding-top: 0; +} + +.phame-mega-header .phame-header-subtitle { + color: {$greytext}; + font-size: 20px; + font-family: 'Aleo', {$fontfamily}; + padding-top: 8px; +} diff --git a/webroot/rsrc/css/phui/phui-document-pro.css b/webroot/rsrc/css/phui/phui-document-pro.css index 0df18815d9..1cedd133f9 100644 --- a/webroot/rsrc/css/phui/phui-document-pro.css +++ b/webroot/rsrc/css/phui/phui-document-pro.css @@ -121,8 +121,8 @@ a.button.phui-document-toc { } .device-phone .phui-document-view.phui-document-view-pro .phui-header-shell { - margin: 8px 0 0 0; - padding: 8px 0 20px; + margin: 0; + padding: 16px 0 20px; } .phui-document-view.phui-document-view-pro .phui-header-tall diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index 9778f1a58c..5394f75303 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -47,6 +47,7 @@ height: 9px; border-radius: 2px; margin-left: 76px; + margin-bottom: 20px; } .device-desktop .phui-timeline-wedge {