From f583406ba93aba8d4e0d7da31554bf7ab2c43daa Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 26 Mar 2018 06:55:17 -0700 Subject: [PATCH 01/15] Drop uniqueness constraint on PushEvent request ID Summary: See . Mercurial may invoke hooks multiple times per push. Test Plan: Pushed to Mercurial, saw key constraint failure. Differential Revision: https://secure.phabricator.com/D19257 --- resources/sql/autopatches/20180326.lock.03.nonunique.sql | 2 ++ .../repository/storage/PhabricatorRepositoryPushEvent.php | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 resources/sql/autopatches/20180326.lock.03.nonunique.sql diff --git a/resources/sql/autopatches/20180326.lock.03.nonunique.sql b/resources/sql/autopatches/20180326.lock.03.nonunique.sql new file mode 100644 index 0000000000..9e12d7e864 --- /dev/null +++ b/resources/sql/autopatches/20180326.lock.03.nonunique.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_pushevent + DROP KEY `key_request`; diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php index 451f8acda5..e44f99df4d 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php @@ -46,9 +46,8 @@ final class PhabricatorRepositoryPushEvent 'key_repository' => array( 'columns' => array('repositoryPHID'), ), - 'key_request' => array( + 'key_identifier' => array( 'columns' => array('requestIdentifier'), - 'unique' => true, ), ), ) + parent::getConfiguration(); From 38999e25ac594c6a6fbb4083ccffdc8491c910f9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 26 Mar 2018 07:14:00 -0700 Subject: [PATCH 02/15] Support logged-out access to the document rendering endpoint Summary: Ref T13105. Currently, logged-out users can't render documents via the endpoint even if they otherwise have access to the file. Test Plan: Viewed a file as a logged-out user and re-rendered it via Ajax. Reviewers: mydeveloperday Reviewed By: mydeveloperday Maniphest Tasks: T13105 Differential Revision: https://secure.phabricator.com/D19258 --- .../files/controller/PhabricatorFileDocumentController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/files/controller/PhabricatorFileDocumentController.php b/src/applications/files/controller/PhabricatorFileDocumentController.php index b74d98f48e..61bf4427cf 100644 --- a/src/applications/files/controller/PhabricatorFileDocumentController.php +++ b/src/applications/files/controller/PhabricatorFileDocumentController.php @@ -7,6 +7,10 @@ final class PhabricatorFileDocumentController private $engine; private $ref; + public function shouldAllowPublic() { + return true; + } + public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); From b7d3101e7cbffb90c282f9ab9338d2f5cad67e3a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 26 Mar 2018 07:17:59 -0700 Subject: [PATCH 03/15] Minor document rendering fixes: dropdown for synchronous files, URI normalization for default renderers Summary: Depends on D19258. Ref T13105. - When the default renderer is an Ajax renderer, don't replace the URI. For example, when viewing a Jupyter notebook, the URI should remain `/F123`, not instantly change to `/view/123/jupyter/`. - Fix an issue where non-ajax renderers could fail to display the dropdown menu properly. Test Plan: - Viewed a Jupyter notebook, stayed on the same URI. - Changed rendering, got different URIs. - Viewed a JSON file and toggled renderers via dropdown. Reviewers: mydeveloperday Reviewed By: mydeveloperday Maniphest Tasks: T13105 Differential Revision: https://secure.phabricator.com/D19259 --- resources/celerity/map.php | 14 +++++++------- .../application/files/behavior-document-engine.js | 7 +++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index dabb6f1d5b..f7a1f391d0 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -392,7 +392,7 @@ return array( 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', - 'rsrc/js/application/files/behavior-document-engine.js' => 'd3f8623c', + 'rsrc/js/application/files/behavior-document-engine.js' => '194cbe53', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', @@ -607,7 +607,7 @@ return array( 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', - 'javelin-behavior-document-engine' => 'd3f8623c', + 'javelin-behavior-document-engine' => '194cbe53', 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', @@ -983,6 +983,11 @@ return array( '191b4909' => array( 'javelin-behavior', ), + '194cbe53' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -2002,11 +2007,6 @@ return array( 'd254d646' => array( 'javelin-util', ), - 'd3f8623c' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), 'd4505101' => array( 'javelin-stratcom', 'javelin-install', diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js index 4cb7d17723..3e49f35410 100644 --- a/webroot/rsrc/js/application/files/behavior-document-engine.js +++ b/webroot/rsrc/js/application/files/behavior-document-engine.js @@ -73,7 +73,6 @@ JX.behavior('document-engine', function(config, statics) { var handler = JX.bind(null, onrender, data, data.sequence); data.viewKey = spec.viewKey; - JX.History.replace(spec.viewURI); new JX.Request(spec.engineURI, handler) .send(); @@ -91,6 +90,10 @@ JX.behavior('document-engine', function(config, statics) { var load = JX.bind(null, onloading, data, spec); data.loadTimer = setTimeout(load, 333); + + // Replace the URI with the URI for the specific rendering the user + // has selected. + JX.History.replace(spec.viewURI); } } @@ -128,7 +131,7 @@ JX.behavior('document-engine', function(config, statics) { statics.initialized = true; } - if (config.renderControlID) { + if (config && config.renderControlID) { var control = JX.$(config.renderControlID); var data = JX.Stratcom.getData(control); From c5b244bfd04fe8a549f1066ecbad469f6ce82834 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 26 Mar 2018 07:21:24 -0700 Subject: [PATCH 04/15] Render directly embedded image data represented as a string in Jupyter notebooks Summary: Depends on D19259. Ref T13105. Some examples represent image data as `["da", "ta"]` while others represent it as `"data"`. Accept either. Test Plan: Rendered example notebooks with both kinds of images. Reviewers: mydeveloperday Reviewed By: mydeveloperday Maniphest Tasks: T13105 Differential Revision: https://secure.phabricator.com/D19260 --- .../files/document/PhabricatorJupyterDocumentEngine.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php index f960f5c8c0..93181921af 100644 --- a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php +++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php @@ -260,9 +260,8 @@ final class PhabricatorJupyterDocumentEngine $raw_data = $data[$image_format]; if (!is_array($raw_data)) { - continue; + $raw_data = array($raw_data); } - $raw_data = implode('', $raw_data); $content = phutil_tag( From b586ee065a7531fb936a4bf44f85178df895a1e3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 28 Mar 2018 15:21:33 -0700 Subject: [PATCH 05/15] Stop evaluating Herald rules when writing "someone mentioned this somewhere else." transactions Summary: Ref T13114. See PHI510. Firing Herald on mentioned objects tends to feel arbitrary and can substantially slow down edits which mention many objects. Test Plan: Mentioned tasks on other tasks; verified that the normal path is hit normally, the new Herald-free path is hit on the mentioned object, and both still work fine and show up in the timeline. Maniphest Tasks: T13114 Differential Revision: https://secure.phabricator.com/D19263 --- .../editor/PhabricatorApplicationTransactionEditor.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 8184eafb50..e4a13de54e 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1120,6 +1120,11 @@ abstract class PhabricatorApplicationTransactionEditor // We are the Herald editor, so stop work here and return the updated // transactions. return $xactions; + } else if ($this->getIsInverseEdgeEditor()) { + // Do not run Herald if we're just recording that this object was + // mentioned elsewhere. This tends to create Herald side effects which + // feel arbitrary, and can really slow down edits which mention a large + // number of other objects. See T13114. } else if ($this->shouldApplyHeraldRules($object, $xactions)) { // We are not the Herald editor, so try to apply Herald rules. $herald_xactions = $this->applyHeraldRules($object, $xactions); From 5cb683257249909ccb50e181b57db72cda020b25 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 28 Mar 2018 16:09:56 -0700 Subject: [PATCH 06/15] Fix usage of fprintf() in `bin/drydock command` Summary: See PHI513. `fprintf()` takes `(thing, pattern, args, ...)` but we aren't passing a `pattern`, so if the command returns a "%" in the output we get an error. Test Plan: - Installed `bytes`, a great useful program which prints all the bytes, on my HoaxOS(tm) system (see D19102). ``` epriestley@orbital ~/dev/phabricator $ ./bin/drydock command --lease 76287 -- bytes # Before patch. [2018-03-29 02:09:08] ERROR 2: fprintf(): Too few arguments at [/Users/epriestley/dev/core/lib/phabricator/src/applications/drydock/management/DrydockManagementCommandWorkflow.php:60] arcanist(head=experimental, ref.master=b8c9c385a7f5, ref.experimental=925c60e7b837), corgi(head=master, ref.master=6371578c9d32), instances(head=master, ref.master=d983b9517924), ledger(head=master, ref.master=4da4a24b8779), libcore(), phabricator(head=hoax1, ref.master=b586ee065a75, ref.hoax1=f8d7480bbdd1, custom=4), phutil(head=master, ref.master=1ad42491e44a), secure(head=master, ref.master=988cf9bd7958), services(head=master, ref.master=6b3fb8d8dd0a) #0 fprintf(resource, string) called at [/src/applications/drydock/management/DrydockManagementCommandWorkflow.php:60] #1 DrydockManagementCommandWorkflow::execute(PhutilArgumentParser) called at [/src/parser/argument/PhutilArgumentParser.php:441] #2 PhutilArgumentParser::parseWorkflowsFull(array) called at [/src/parser/argument/PhutilArgumentParser.php:333] #3 PhutilArgumentParser::parseWorkflows(array) called at [/scripts/drydock/drydock_control.php:21] epriestley@orbital ~/dev/phabricator $ ./bin/drydock command --lease 76287 -- bytes # After patch. !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? ``` Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D19264 --- .../drydock/management/DrydockManagementCommandWorkflow.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/drydock/management/DrydockManagementCommandWorkflow.php b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php index bc66966f8c..ae0bd711b2 100644 --- a/src/applications/drydock/management/DrydockManagementCommandWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php @@ -57,8 +57,8 @@ final class DrydockManagementCommandWorkflow array($interface, 'execx'), array('%Ls', $argv)); - fprintf(STDOUT, $stdout); - fprintf(STDERR, $stderr); + fwrite(STDOUT, $stdout); + fwrite(STDERR, $stderr); return 0; } From 74216ea8e008930ebd1167010f545b83c8948752 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 29 Mar 2018 07:17:37 -0700 Subject: [PATCH 07/15] Disable Herald and enormous change protection for repository initial imports Summary: See PHI514. Ref T13114. Ref T8951. When a push is an "initial import" (a push of at least 7 commits to an empty repository) don't run Herald or enormous change protection. Test Plan: Pushed some non-initial changes to a repository, and some initial changes. Maniphest Tasks: T13114, T8951 Differential Revision: https://secure.phabricator.com/D19265 --- .../engine/DiffusionCommitHookEngine.php | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index cc4526dbdc..dd5777b2b0 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -126,7 +126,6 @@ final class DiffusionCommitHookEngine extends Phobject { public function execute() { $ref_updates = $this->findRefUpdates(); - $all_updates = $ref_updates; $caught = null; try { @@ -140,21 +139,32 @@ final class DiffusionCommitHookEngine extends Phobject { throw $ex; } - $this->applyHeraldRefRules($ref_updates, $all_updates); - $content_updates = $this->findContentUpdates($ref_updates); + $all_updates = array_merge($ref_updates, $content_updates); + + // If this is an "initial import" (a sizable push to a previously empty + // repository) we'll allow enormous changes and disable Herald rules. + // These rulesets can consume a large amount of time and memory and are + // generally not relevant when importing repository history. + $is_initial_import = $this->isInitialImport($all_updates); + + if (!$is_initial_import) { + $this->applyHeraldRefRules($ref_updates); + } try { - $this->rejectEnormousChanges($content_updates); + if (!$is_initial_import) { + $this->rejectEnormousChanges($content_updates); + } } catch (DiffusionCommitHookRejectException $ex) { // If we're rejecting enormous changes, flag everything. $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS; throw $ex; } - $all_updates = array_merge($all_updates, $content_updates); - - $this->applyHeraldContentRules($content_updates, $all_updates); + if (!$is_initial_import) { + $this->applyHeraldContentRules($content_updates); + } // Run custom scripts in `hook.d/` directories. $this->applyCustomHooks($all_updates); @@ -186,12 +196,10 @@ final class DiffusionCommitHookEngine extends Phobject { throw $caught; } - // If this went through cleanly, detect pushes which are actually imports - // of an existing repository rather than an addition of new commits. If - // this push is importing a bunch of stuff, set the importing flag on - // the repository. It will be cleared once we fully process everything. + // If this went through cleanly and was an import, set the importing flag + // on the repository. It will be cleared once we fully process everything. - if ($this->isInitialImport($all_updates)) { + if ($is_initial_import) { $repository = $this->getRepository(); $repository->markImporting(); } @@ -281,28 +289,21 @@ final class DiffusionCommitHookEngine extends Phobject { /* -( Herald )------------------------------------------------------------- */ - private function applyHeraldRefRules( - array $ref_updates, - array $all_updates) { + private function applyHeraldRefRules(array $ref_updates) { $this->applyHeraldRules( $ref_updates, - new HeraldPreCommitRefAdapter(), - $all_updates); + new HeraldPreCommitRefAdapter()); } - private function applyHeraldContentRules( - array $content_updates, - array $all_updates) { + private function applyHeraldContentRules(array $content_updates) { $this->applyHeraldRules( $content_updates, - new HeraldPreCommitContentAdapter(), - $all_updates); + new HeraldPreCommitContentAdapter()); } private function applyHeraldRules( array $updates, - HeraldAdapter $adapter_template, - array $all_updates) { + HeraldAdapter $adapter_template) { if (!$updates) { return; From 93cb6e3bdeb07be7b9c6835bcc2d237e2abb708e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 29 Mar 2018 09:51:18 -0700 Subject: [PATCH 08/15] Make updating a revision with the same active diff a no-op Summary: Ref T13114. See PHI515. Updating a revision with the same, currently active diff became an error at some point (probably D19175). This is inconsistent; make it an allowable no-op instead. Test Plan: - Updated a revision's diff via Conduit. - Updated to the same diff, no-op. - Tried to update a different revision, error ("already attached elsewhere"). - Updated with a different diff. - Tried to update with the original diff, error ("previously attached version"). Maniphest Tasks: T13114 Differential Revision: https://secure.phabricator.com/D19267 --- .../DifferentialRevisionUpdateTransaction.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php index c89c3c79e1..2b74cbfab5 100644 --- a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php @@ -137,7 +137,32 @@ final class DifferentialRevisionUpdateTransaction continue; } - if ($diff->getRevisionID()) { + $is_attached = ($diff->getRevisionID() == $object->getID()); + if ($is_attached) { + $is_active = ($diff_phid == $object->getActiveDiffPHID()); + } else { + $is_active = false; + } + + if ($is_attached) { + if ($is_active) { + // This is a no-op: we're reattaching the current active diff to the + // revision it is already attached to. This is valid and will just + // be dropped later on in the process. + } else { + // At least for now, there's no support for "undoing" a diff and + // reverting to an older proposed change without just creating a + // new diff from whole cloth. + $errors[] = $this->newInvalidError( + pht( + 'You can not update this revision with the specified diff '. + '("%s") because this diff is already attached to the revision '. + 'as an older version of the change.', + $diff_phid), + $xaction); + continue; + } + } else if ($diff->getRevisionID()) { $errors[] = $this->newInvalidError( pht( 'You can not update this revision with the specified diff ("%s") '. From 79154455437be1210b36dab1939b24e0a06c8fb9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 29 Mar 2018 11:22:02 -0700 Subject: [PATCH 09/15] Fix two issues with Differential updates and Owners Summary: Ref T13114. - Followup fix for D19267, which didn't work correctly with //new// revision creation. - Followup fix for changes in T11015. Some of the querying logic was still handling "/x.y" and "/x.y/" differently. Instead, normalize consistently to "/x.y/" Test Plan: - Created a new revision cleanly. - Created a package owning only a `example.txt` file and saw Differential find it as an owning package in the table of contents. Maniphest Tasks: T13114 Differential Revision: https://secure.phabricator.com/D19268 --- .../DifferentialRevisionUpdateTransaction.php | 4 +++- .../storage/PhabricatorOwnersPackage.php | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php index 2b74cbfab5..189ed2f2ba 100644 --- a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php @@ -137,7 +137,9 @@ final class DifferentialRevisionUpdateTransaction continue; } - $is_attached = ($diff->getRevisionID() == $object->getID()); + $is_attached = + ($diff->getRevisionID()) && + ($diff->getRevisionID() == $object->getID()); if ($is_attached) { $is_active = ($diff_phid == $object->getActiveDiffPHID()); } else { diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 56dd51a3c6..c76864702c 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -393,19 +393,22 @@ final class PhabricatorOwnersPackage } public static function splitPath($path) { - $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; - $path = trim($path, '/'); + $result = array( + '/', + ); + $parts = explode('/', $path); + $buffer = '/'; + foreach ($parts as $part) { + if (!strlen($part)) { + continue; + } - $result = array(); - while (count($parts)) { - $result[] = '/'.implode('/', $parts).$trailing_slash; - $trailing_slash = '/'; - array_pop($parts); + $buffer = $buffer.$part.'/'; + $result[] = $buffer; } - $result[] = '/'; - return array_reverse($result); + return $result; } public function attachPaths(array $paths) { From 7f9a9bc800f76c10b2f8571f41815e15dfc0653f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 29 Mar 2018 11:20:38 -0700 Subject: [PATCH 10/15] Make Harbormaster objects destructible Summary: Ref T13114. See PHI511. Ref T13072. This makes Buildables, Builds, Targets and Artifacts destructible with `bin/remove destroy`. This might not be totally exhaustive. In particular: - File artifacts won't destroy the file. This is sort of okay because file artifacts are currently just a file reference, but probably shouldn't be how things work in the long term. - `BuildCommand` doesn't get cleaned up, but `BuildMessage` does on `Build`. See T13072 for more. Test Plan: Used `bin/remove destroy` to nuke a bunch of builds, buildables, etc. Loaded stuff in the web UI and it all looked like it got nuked properly. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13114, T13072 Differential Revision: https://secure.phabricator.com/D19269 --- src/__phutil_library_map__.php | 5 ++ .../storage/HarbormasterBuildMessage.php | 16 ++++- .../storage/HarbormasterBuildable.php | 34 +++++++++- .../storage/build/HarbormasterBuild.php | 32 +++++++++- .../build/HarbormasterBuildArtifact.php | 22 ++++++- .../storage/build/HarbormasterBuildTarget.php | 62 ++++++++++++++++++- 6 files changed, 162 insertions(+), 9 deletions(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b1afef3600..59d0e7eb99 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -6525,6 +6525,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildAbortedException' => 'Exception', 'HarbormasterBuildActionController' => 'HarbormasterController', @@ -6532,6 +6533,7 @@ phutil_register_library_map(array( 'HarbormasterBuildArtifact' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -6564,6 +6566,7 @@ phutil_register_library_map(array( 'HarbormasterBuildMessage' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType', @@ -6614,6 +6617,7 @@ phutil_register_library_map(array( 'HarbormasterBuildTarget' => array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', @@ -6628,6 +6632,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'HarbormasterBuildableInterface', + 'PhabricatorDestructibleInterface', ), 'HarbormasterBuildableActionController' => 'HarbormasterController', 'HarbormasterBuildableListController' => 'HarbormasterController', diff --git a/src/applications/harbormaster/storage/HarbormasterBuildMessage.php b/src/applications/harbormaster/storage/HarbormasterBuildMessage.php index 1066a93610..34aab3957e 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildMessage.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildMessage.php @@ -6,8 +6,11 @@ * conditions where we receive a message before a build plan is ready to * accept it. */ -final class HarbormasterBuildMessage extends HarbormasterDAO - implements PhabricatorPolicyInterface { +final class HarbormasterBuildMessage + extends HarbormasterDAO + implements + PhabricatorPolicyInterface, + PhabricatorDestructibleInterface { protected $authorPHID; protected $receiverPHID; @@ -74,4 +77,13 @@ final class HarbormasterBuildMessage extends HarbormasterDAO return pht('Build messages have the same policies as their receivers.'); } + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 5de2159e8d..3a092900eb 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,10 +1,12 @@ getViewer(); + + $this->openTransaction(); + $builds = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withBuildablePHIDs(array($this->getPHID())) + ->execute(); + foreach ($builds as $build) { + $engine->destroyObject($build); + } + + $messages = id(new HarbormasterBuildMessageQuery()) + ->setViewer($viewer) + ->withReceiverPHIDs(array($this->getPHID())) + ->execute(); + foreach ($messages as $message) { + $engine->destroyObject($message); + } + + $this->delete(); + $this->saveTransaction(); + } + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index d43b19d71f..ae0f6b13f3 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -4,7 +4,8 @@ final class HarbormasterBuild extends HarbormasterDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorDestructibleInterface { protected $buildablePHID; protected $buildPlanPHID; @@ -455,4 +456,33 @@ final class HarbormasterBuild extends HarbormasterDAO ); } + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $viewer = $engine->getViewer(); + + $this->openTransaction(); + $targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($viewer) + ->withBuildPHIDs(array($this->getPHID())) + ->execute(); + foreach ($targets as $target) { + $engine->destroyObject($target); + } + + $messages = id(new HarbormasterBuildMessageQuery()) + ->setViewer($viewer) + ->withReceiverPHIDs(array($this->getPHID())) + ->execute(); + foreach ($messages as $message) { + $engine->destroyObject($message); + } + + $this->delete(); + $this->saveTransaction(); + } + + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php index 461ef4b06f..7cd8d60b6a 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -1,7 +1,10 @@ getViewer(); + + $this->openTransaction(); + $this->releaseArtifact($viewer); + $this->delete(); + $this->saveTransaction(); + } + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index 8b47bdfc21..b559a66198 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -1,7 +1,10 @@ getViewer(); + + $this->openTransaction(); + + $lint_message = new HarbormasterBuildLintMessage(); + $conn = $lint_message->establishConnection('w'); + queryfx( + $conn, + 'DELETE FROM %T WHERE buildTargetPHID = %s', + $lint_message->getTableName(), + $this->getPHID()); + + $unit_message = new HarbormasterBuildUnitMessage(); + $conn = $unit_message->establishConnection('w'); + queryfx( + $conn, + 'DELETE FROM %T WHERE buildTargetPHID = %s', + $unit_message->getTableName(), + $this->getPHID()); + + $logs = id(new HarbormasterBuildLogQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs(array($this->getPHID())) + ->execute(); + foreach ($logs as $log) { + $engine->destroyObject($log); + } + + $artifacts = id(new HarbormasterBuildArtifactQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs(array($this->getPHID())) + ->execute(); + foreach ($artifacts as $artifact) { + $engine->destroyObject($artifact); + } + + $messages = id(new HarbormasterBuildMessageQuery()) + ->setViewer($viewer) + ->withReceiverPHIDs(array($this->getPHID())) + ->execute(); + foreach ($messages as $message) { + $engine->destroyObject($message); + } + + $this->delete(); + $this->saveTransaction(); + } + + } From 9fbf4ee58c0d506a8b684dcbbefd75997ae88353 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 29 Mar 2018 14:03:48 -0700 Subject: [PATCH 11/15] Provide comment actions for tokenizer custom fields Summary: Ref T13114. See PHI519. An install is interested in modifying a tokenizer custom field from the comment area. Provide this capability. This patch is fairly narrow but should solve the immediate need. Test Plan: Added, removed, and modified a tokenizer custom field using the comment action dropdown. Maniphest Tasks: T13114 Differential Revision: https://secure.phabricator.com/D19270 --- .../PhabricatorCustomFieldEditField.php | 34 +++++++++++++++++++ .../field/PhabricatorCustomField.php | 21 ++++++++++++ ...habricatorStandardCustomFieldTokenizer.php | 22 ++++++++++++ 3 files changed, 77 insertions(+) diff --git a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php index 1153f82a34..27b8276c85 100644 --- a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php +++ b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php @@ -7,6 +7,7 @@ final class PhabricatorCustomFieldEditField private $httpParameterType; private $conduitParameterType; private $bulkParameterType; + private $commentAction; public function setCustomField(PhabricatorCustomField $custom_field) { $this->customField = $custom_field; @@ -47,6 +48,16 @@ final class PhabricatorCustomFieldEditField return $this->bulkParameterType; } + public function setCustomFieldCommentAction( + PhabricatorEditEngineCommentAction $comment_action) { + $this->commentAction = $comment_action; + return $this; + } + + public function getCustomFieldCommentAction() { + return $this->commentAction; + } + protected function buildControl() { if ($this->getIsConduitOnly()) { return null; @@ -77,6 +88,19 @@ final class PhabricatorCustomFieldEditField return $clone->getNewValueForApplicationTransactions(); } + protected function getValueForCommentAction($value) { + $field = $this->getCustomField(); + $clone = clone $field; + $clone->setValueFromApplicationTransactions($value); + + // TODO: This is somewhat bogus because only StandardCustomFields + // implement a getFieldValue() method -- not all CustomFields. Today, + // only StandardCustomFields can ever actually generate a comment action + // so we never reach this method with other field types. + + return $clone->getFieldValue(); + } + protected function getValueExistsInSubmit(AphrontRequest $request, $key) { return true; } @@ -110,6 +134,16 @@ final class PhabricatorCustomFieldEditField return null; } + protected function newCommentAction() { + $action = $this->getCustomFieldCommentAction(); + + if ($action) { + return clone $action; + } + + return null; + } + protected function newConduitParameterType() { $type = $this->getCustomFieldConduitParameterType(); diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index 818bf119ff..36db8f239b 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1127,6 +1127,16 @@ abstract class PhabricatorCustomField extends Phobject { $field->setCustomFieldBulkParameterType($bulk_type); } + $comment_action = $this->getCommentAction(); + if ($comment_action) { + $field + ->setCustomFieldCommentAction($comment_action) + ->setCommentActionLabel( + pht( + 'Change %s', + $this->getFieldName())); + } + return $field; } @@ -1459,6 +1469,17 @@ abstract class PhabricatorCustomField extends Phobject { return null; } + public function getCommentAction() { + return $this->newCommentAction(); + } + + protected function newCommentAction() { + if ($this->proxy) { + return $this->proxy->newCommentAction(); + } + return null; + } + /* -( Herald )------------------------------------------------------------- */ diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index 9bf59d41f6..3c5268d65b 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -143,4 +143,26 @@ abstract class PhabricatorStandardCustomFieldTokenizer ->setDatasource($datasource); } + protected function newCommentAction() { + $viewer = $this->getViewer(); + + $datasource = $this->getDatasource() + ->setViewer($viewer); + + $action = id(new PhabricatorEditEngineTokenizerCommentAction()) + ->setDatasource($datasource); + + $limit = $this->getFieldConfigValue('limit'); + if ($limit) { + $action->setLimit($limit); + } + + $value = $this->getFieldValue(); + if ($value !== null) { + $action->setInitialValue($value); + } + + return $action; + } + } From 66392e5b8bb482fdbc526ecbede3deb585a5c476 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 30 Mar 2018 07:01:36 -0700 Subject: [PATCH 12/15] Add a rough "bin/repository unpublish" workflow to attempt to cleanup improperly published repositories Summary: Ref T13114. See PHI514. This makes some attempt to undo the damage caused by incorrectly publishing a repository. Don't run this. Test Plan: Yikes. Maniphest Tasks: T13114 Differential Revision: https://secure.phabricator.com/D19271 --- src/__phutil_library_map__.php | 7 +- .../feed/storage/PhabricatorFeedStoryData.php | 30 +- ...rRepositoryManagementUnpublishWorkflow.php | 273 ++++++++++++++++++ 3 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 59d0e7eb99..447d4a534e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4020,6 +4020,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php', 'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php', 'PhabricatorRepositoryManagementThawWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php', + 'PhabricatorRepositoryManagementUnpublishWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php', 'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php', @@ -8563,7 +8564,10 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorMarkupInterface', ), - 'PhabricatorFeedStoryData' => 'PhabricatorFeedDAO', + 'PhabricatorFeedStoryData' => array( + 'PhabricatorFeedDAO', + 'PhabricatorDestructibleInterface', + ), 'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryPublisher' => 'Phobject', 'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO', @@ -9814,6 +9818,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementThawWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementUnpublishWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', diff --git a/src/applications/feed/storage/PhabricatorFeedStoryData.php b/src/applications/feed/storage/PhabricatorFeedStoryData.php index 33d19fa616..0513590023 100644 --- a/src/applications/feed/storage/PhabricatorFeedStoryData.php +++ b/src/applications/feed/storage/PhabricatorFeedStoryData.php @@ -1,6 +1,8 @@ storyData, $key, $default); } + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + + $this->openTransaction(); + $conn = $this->establishConnection('w'); + + queryfx( + $conn, + 'DELETE FROM %T WHERE chronologicalKey = %s', + id(new PhabricatorFeedStoryNotification())->getTableName(), + $this->getChronologicalKey()); + + queryfx( + $conn, + 'DELETE FROM %T WHERE chronologicalKey = %s', + id(new PhabricatorFeedStoryReference())->getTableName(), + $this->getChronologicalKey()); + + $this->delete(); + $this->saveTransaction(); + } + } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php new file mode 100644 index 0000000000..5a6afb8c6e --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php @@ -0,0 +1,273 @@ +setName('unpublish') + ->setExamples( + '**unpublish** [__options__] __repository__') + ->setSynopsis( + pht( + 'Unpublish all feed stories and notifications that a repository '. + 'has generated. Keep expectations low; can not rewind time.')) + ->setArguments( + array( + array( + 'name' => 'force', + 'help' => pht('Do not prompt for confirmation.'), + ), + array( + 'name' => 'dry-run', + 'help' => pht('Do not perform any writes.'), + ), + array( + 'name' => 'repositories', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + $is_force = $args->getArg('force'); + $is_dry_run = $args->getArg('dry-run'); + + $repositories = $this->loadLocalRepositories($args, 'repositories'); + if (count($repositories) !== 1) { + throw new PhutilArgumentUsageException( + pht('Specify exactly one repository to unpublish.')); + } + $repository = head($repositories); + + if (!$is_force) { + echo tsprintf( + "%s\n", + pht( + 'This script will unpublish all feed stories and notifications '. + 'which a repository generated during import. This action can not '. + 'be undone.')); + + $prompt = pht( + 'Permanently unpublish "%s"?', + $repository->getDisplayName()); + if (!phutil_console_confirm($prompt)) { + throw new PhutilArgumentUsageException( + pht('User aborted workflow.')); + } + } + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepositoryPHIDs(array($repository->getPHID())) + ->execute(); + + echo pht("Will unpublish %s commits.\n", count($commits)); + + foreach ($commits as $commit) { + $this->unpublishCommit($commit, $is_dry_run); + } + + return 0; + } + + private function unpublishCommit( + PhabricatorRepositoryCommit $commit, + $is_dry_run) { + $viewer = $this->getViewer(); + + echo tsprintf( + "%s\n", + pht( + 'Unpublishing commit "%s".', + $commit->getMonogram())); + + $stories = id(new PhabricatorFeedQuery()) + ->setViewer($viewer) + ->withFilterPHIDs(array($commit->getPHID())) + ->execute(); + + if ($stories) { + echo tsprintf( + "%s\n", + pht( + 'Found %s feed storie(s).', + count($stories))); + + if (!$is_dry_run) { + $engine = new PhabricatorDestructionEngine(); + foreach ($stories as $story) { + $story_data = $story->getStoryData(); + $engine->destroyObject($story_data); + } + + echo tsprintf( + "%s\n", + pht( + 'Destroyed %s feed storie(s).', + count($stories))); + } + } + + $edge_types = array( + PhabricatorObjectMentionsObjectEdgeType::EDGECONST => true, + DiffusionCommitHasTaskEdgeType::EDGECONST => true, + DiffusionCommitHasRevisionEdgeType::EDGECONST => true, + DiffusionCommitRevertsCommitEdgeType::EDGECONST => true, + ); + + $query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($commit->getPHID())) + ->withEdgeTypes(array_keys($edge_types)); + $edges = $query->execute(); + + foreach ($edges[$commit->getPHID()] as $type => $edge_list) { + foreach ($edge_list as $edge) { + $dst = $edge['dst']; + + echo tsprintf( + "%s\n", + pht( + 'Commit "%s" has edge of type "%s" to object "%s".', + $commit->getMonogram(), + $type, + $dst)); + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($dst)) + ->executeOne(); + if ($object) { + if ($object instanceof PhabricatorApplicationTransactionInterface) { + $this->unpublishEdgeTransaction( + $commit, + $type, + $object, + $is_dry_run); + } + } + } + } + } + + private function unpublishEdgeTransaction( + $src, + $type, + PhabricatorApplicationTransactionInterface $dst, + $is_dry_run) { + $viewer = $this->getViewer(); + + $query = PhabricatorApplicationTransactionQuery::newQueryForObject($dst) + ->setViewer($viewer) + ->withObjectPHIDs(array($dst->getPHID())); + + $xactions = id(clone $query) + ->withTransactionTypes( + array( + PhabricatorTransactions::TYPE_EDGE, + )) + ->execute(); + + $type_obj = PhabricatorEdgeType::getByConstant($type); + $inverse_type = $type_obj->getInverseEdgeConstant(); + + $engine = new PhabricatorDestructionEngine(); + foreach ($xactions as $xaction) { + $edge_type = $xaction->getMetadataValue('edge:type'); + if ($edge_type != $inverse_type) { + // Some other type of edge was edited. + continue; + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + $changed = $record->getChangedPHIDs(); + if ($changed !== array($src->getPHID())) { + // Affected objects were not just the object we're unpublishing. + continue; + } + + echo tsprintf( + "%s\n", + pht( + 'Found edge transaction "%s" on object "%s" for type "%s".', + $xaction->getPHID(), + $dst->getPHID(), + $type)); + + if (!$is_dry_run) { + $engine->destroyObject($xaction); + + echo tsprintf( + "%s\n", + pht( + 'Destroyed transaction "%s" on object "%s".', + $xaction->getPHID(), + $dst->getPHID())); + } + } + + if ($type === DiffusionCommitHasTaskEdgeType::EDGECONST) { + $xactions = id(clone $query) + ->withTransactionTypes( + array( + ManiphestTaskStatusTransaction::TRANSACTIONTYPE, + )) + ->execute(); + + if ($xactions) { + foreach ($xactions as $xaction) { + $metadata = $xaction->getMetadata(); + if (idx($metadata, 'commitPHID') === $src->getPHID()) { + echo tsprintf( + "%s\n", + pht( + 'MANUAL Task "%s" was likely closed improperly by "%s".', + $dst->getMonogram(), + $src->getMonogram())); + } + } + } + } + + if ($type === DiffusionCommitHasRevisionEdgeType::EDGECONST) { + $xactions = id(clone $query) + ->withTransactionTypes( + array( + DifferentialRevisionCloseTransaction::TRANSACTIONTYPE, + )) + ->execute(); + + if ($xactions) { + foreach ($xactions as $xaction) { + $metadata = $xaction->getMetadata(); + if (idx($metadata, 'isCommitClose')) { + if (idx($metadata, 'commitPHID') === $src->getPHID()) { + echo tsprintf( + "%s\n", + pht( + 'MANUAL Revision "%s" was likely closed improperly by "%s".', + $dst->getMonogram(), + $src->getMonogram())); + } + } + } + } + } + + if (!$is_dry_run) { + id(new PhabricatorEdgeEditor()) + ->removeEdge($src->getPHID(), $type, $dst->getPHID()) + ->save(); + echo tsprintf( + "%s\n", + pht( + 'Destroyed edge of type "%s" between "%s" and "%s".', + $type, + $src->getPHID(), + $dst->getPHID())); + } + } + + +} From 7eaa27683e973e3b5e74e8736c387f293653cbe3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 30 Mar 2018 08:42:18 -0700 Subject: [PATCH 13/15] Make closed/disabled results in the remarkup autocomplete more visually clear Summary: Ref T13114. See PHI522. Although it looks like results are already ordered correctly, the override rendering isn't accommodating disabled results gracefully. Give closed results a distinctive look (grey + strikethru) so it's clear when you're autocompleting `@mention...` into a disabled user. Test Plan: {F5497621} Maniphest Tasks: T13114 Differential Revision: https://secure.phabricator.com/D19272 --- resources/celerity/map.php | 22 +++++++++++----------- webroot/rsrc/css/core/remarkup.css | 5 +++++ webroot/rsrc/js/phuix/PHUIXAutocomplete.js | 5 ++++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f7a1f391d0..c9768ce456 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => '3fd3b7b8', + 'core.pkg.css' => '1dd5fa4b', 'core.pkg.js' => 'b9b4a943', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', @@ -112,7 +112,7 @@ return array( 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '62fa3ace', - 'rsrc/css/core/remarkup.css' => 'b375546d', + 'rsrc/css/core/remarkup.css' => '1828e2ad', 'rsrc/css/core/syntax.css' => 'cae95e89', 'rsrc/css/core/z-index.css' => '9d8f7c4b', 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', @@ -509,7 +509,7 @@ return array( 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => 'ed18356a', - 'rsrc/js/phuix/PHUIXAutocomplete.js' => '7fa5c915', + 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'df1bbd34', 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca', @@ -780,7 +780,7 @@ return array( 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '77b0ae28', - 'phabricator-remarkup-css' => 'b375546d', + 'phabricator-remarkup-css' => '1828e2ad', 'phabricator-search-results-css' => '505dd8cf', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-slowvote-css' => 'a94b7230', @@ -865,7 +865,7 @@ return array( 'phui-workpanel-view-css' => 'a3a63478', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => 'ed18356a', - 'phuix-autocomplete' => '7fa5c915', + 'phuix-autocomplete' => 'df1bbd34', 'phuix-button-view' => '8a91e1ac', 'phuix-dropdown-menu' => '04b2ae03', 'phuix-form-control-view' => '210a16c1', @@ -1558,12 +1558,6 @@ return array( '7f243deb' => array( 'javelin-install', ), - '7fa5c915' => array( - 'javelin-install', - 'javelin-dom', - 'phuix-icon-view', - 'phabricator-prefab', - ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -2047,6 +2041,12 @@ return array( 'javelin-typeahead-ondemand-source', 'javelin-dom', ), + 'df1bbd34' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-icon-view', + 'phabricator-prefab', + ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 949deadbe4..990c6f0bab 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -724,6 +724,11 @@ var.remarkup-assist-textarea { color: {$darkgreytext}; } +.phuix-autocomplete-list a.jx-result .tokenizer-result-closed { + color: {$lightgreytext}; + text-decoration: line-through; +} + .phuix-autocomplete-list a.jx-result .phui-icon-view { margin-right: 4px; color: {$lightbluetext}; diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js index 8f062d900e..f46e7666e2 100644 --- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js +++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js @@ -185,7 +185,10 @@ JX.install('PHUIXAutocomplete', { .getNode(); } - map.display = [icon, map.displayName]; + var display = JX.$N('span', {}, [icon, map.displayName]); + JX.DOM.alterClass(display, 'tokenizer-result-closed', !!map.closed); + + map.display = display; return map; }, From ccbc8a430f62de470bfffbf4cb7c6b9ea3db8a26 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 30 Mar 2018 09:46:45 -0700 Subject: [PATCH 14/15] Make Jupyter notebooks use the fast builtin Python highlighter Summary: Ref T13105. This is silly, but "py" and "python" end up in different places today, and "py" is ~100x faster than "python". See also T3626 for longer-term plans on this. Test Plan: Reloaded a Jupyter notebook, saw it render almost instantly instead of taking a few seconds. Reviewers: mydeveloperday Reviewed By: mydeveloperday Maniphest Tasks: T13105 Differential Revision: https://secure.phabricator.com/D19273 --- .../files/document/PhabricatorJupyterDocumentEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php index 93181921af..90b9d33c0e 100644 --- a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php +++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php @@ -196,7 +196,7 @@ final class PhabricatorJupyterDocumentEngine $content = implode('', $content); $content = PhabricatorSyntaxHighlighter::highlightWithLanguage( - 'python', + 'py', $content); $outputs = array(); From 7189cb7ba816185f7da448aa51cfe9678333ee35 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 30 Mar 2018 11:02:01 -0700 Subject: [PATCH 15/15] Support text encoding and syntax highlighting options in document rendering Summary: Depends on D19273. Ref T13105. Adds "Change Text Encoding..." and "Highlight As..." options when rendering documents, and makes an effort to automatically detect and handle text encoding. Test Plan: - Uploaded a Shift-JIS file, saw it auto-detect as Shift-JIS. - Converted files between encodings. - Highlighted various things as "Rainbow", etc. Maniphest Tasks: T13105 Differential Revision: https://secure.phabricator.com/D19274 --- resources/celerity/map.php | 30 +++--- .../PhabricatorFileDocumentController.php | 10 ++ .../PhabricatorFileViewController.php | 24 +++++ .../document/PhabricatorDocumentEngine.php | 28 ++++++ .../PhabricatorSourceDocumentEngine.php | 17 +++- .../PhabricatorTextDocumentEngine.php | 60 ++++++++++- .../files/behavior-document-engine.js | 99 +++++++++++++++++-- webroot/rsrc/js/phuix/PHUIXActionView.js | 4 + 8 files changed, 243 insertions(+), 29 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c9768ce456..56ee3d5d45 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', 'core.pkg.css' => '1dd5fa4b', - 'core.pkg.js' => 'b9b4a943', + 'core.pkg.js' => '1ea38af8', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', 'diffusion.pkg.css' => 'a2d17c7d', @@ -392,7 +392,7 @@ return array( 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', - 'rsrc/js/application/files/behavior-document-engine.js' => '194cbe53', + 'rsrc/js/application/files/behavior-document-engine.js' => '9108ee1a', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', @@ -508,7 +508,7 @@ return array( 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', - 'rsrc/js/phuix/PHUIXActionView.js' => 'ed18356a', + 'rsrc/js/phuix/PHUIXActionView.js' => '8d4a8c72', 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'df1bbd34', 'rsrc/js/phuix/PHUIXButtonView.js' => '8a91e1ac', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03', @@ -607,7 +607,7 @@ return array( 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', - 'javelin-behavior-document-engine' => '194cbe53', + 'javelin-behavior-document-engine' => '9108ee1a', 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', @@ -864,7 +864,7 @@ return array( 'phui-workcard-view-css' => 'cca5fa92', 'phui-workpanel-view-css' => 'a3a63478', 'phuix-action-list-view' => 'b5c256b8', - 'phuix-action-view' => 'ed18356a', + 'phuix-action-view' => '8d4a8c72', 'phuix-autocomplete' => 'df1bbd34', 'phuix-button-view' => '8a91e1ac', 'phuix-dropdown-menu' => '04b2ae03', @@ -983,11 +983,6 @@ return array( '191b4909' => array( 'javelin-behavior', ), - '194cbe53' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -1619,6 +1614,11 @@ return array( 'javelin-stratcom', 'javelin-install', ), + '8d4a8c72' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + ), '8e1baf68' => array( 'phui-button-css', ), @@ -1644,6 +1644,11 @@ return array( 'javelin-stratcom', 'javelin-vector', ), + '9108ee1a' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), '92b9ec77' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2125,11 +2130,6 @@ return array( 'javelin-stratcom', 'javelin-vector', ), - 'ed18356a' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - ), 'edf8a145' => array( 'javelin-behavior', 'javelin-uri', diff --git a/src/applications/files/controller/PhabricatorFileDocumentController.php b/src/applications/files/controller/PhabricatorFileDocumentController.php index 61bf4427cf..15fadaa289 100644 --- a/src/applications/files/controller/PhabricatorFileDocumentController.php +++ b/src/applications/files/controller/PhabricatorFileDocumentController.php @@ -43,6 +43,16 @@ final class PhabricatorFileDocumentController $engine = $engines[$engine_key]; $this->engine = $engine; + $encode_setting = $request->getStr('encode'); + if (strlen($encode_setting)) { + $engine->setEncodingConfiguration($encode_setting); + } + + $highlight_setting = $request->getStr('highlight'); + if (strlen($highlight_setting)) { + $engine->setHighlightingConfiguration($highlight_setting); + } + try { $content = $engine->newDocument($ref); } catch (Exception $ex) { diff --git a/src/applications/files/controller/PhabricatorFileViewController.php b/src/applications/files/controller/PhabricatorFileViewController.php index 5251fdd8f1..9d32ff6fb1 100644 --- a/src/applications/files/controller/PhabricatorFileViewController.php +++ b/src/applications/files/controller/PhabricatorFileViewController.php @@ -422,6 +422,16 @@ final class PhabricatorFileViewController extends PhabricatorFileController { $engine->setHighlightedLines(range($lines[0], $lines[1])); } + $encode_setting = $request->getStr('encode'); + if (strlen($encode_setting)) { + $engine->setEncodingConfiguration($encode_setting); + } + + $highlight_setting = $request->getStr('highlight'); + if (strlen($highlight_setting)) { + $engine->setHighlightingConfiguration($highlight_setting); + } + $views = array(); foreach ($engines as $candidate_key => $candidate_engine) { $label = $candidate_engine->getViewAsLabel($ref); @@ -443,6 +453,8 @@ final class PhabricatorFileViewController extends PhabricatorFileController { 'engineURI' => $candidate_engine->getRenderURI($ref), 'viewURI' => $view_uri, 'loadingMarkup' => hsprintf('%s', $loading), + 'canEncode' => $candidate_engine->canConfigureEncoding($ref), + 'canHighlight' => $candidate_engine->CanConfigureHighlighting($ref), ); } @@ -474,6 +486,18 @@ final class PhabricatorFileViewController extends PhabricatorFileController { 'viewKey' => $engine->getDocumentEngineKey(), 'views' => $views, 'standaloneURI' => $engine->getRenderURI($ref), + 'encode' => array( + 'icon' => 'fa-font', + 'name' => pht('Change Text Encoding...'), + 'uri' => '/services/encoding/', + 'value' => $encode_setting, + ), + 'highlight' => array( + 'icon' => 'fa-lightbulb-o', + 'name' => pht('Highlight As...'), + 'uri' => '/services/highlight/', + 'value' => $highlight_setting, + ), ); $view_button = id(new PHUIButtonView()) diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php index a225d55ea9..c3a1a7317a 100644 --- a/src/applications/files/document/PhabricatorDocumentEngine.php +++ b/src/applications/files/document/PhabricatorDocumentEngine.php @@ -5,6 +5,8 @@ abstract class PhabricatorDocumentEngine private $viewer; private $highlightedLines = array(); + private $encodingConfiguration; + private $highlightingConfiguration; final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -28,6 +30,32 @@ abstract class PhabricatorDocumentEngine return $this->canRenderDocumentType($ref); } + public function canConfigureEncoding(PhabricatorDocumentRef $ref) { + return false; + } + + public function canConfigureHighlighting(PhabricatorDocumentRef $ref) { + return false; + } + + final public function setEncodingConfiguration($config) { + $this->encodingConfiguration = $config; + return $this; + } + + final public function getEncodingConfiguration() { + return $this->encodingConfiguration; + } + + final public function setHighlightingConfiguration($config) { + $this->highlightingConfiguration = $config; + return $this; + } + + final public function getHighlightingConfiguration() { + return $this->highlightingConfiguration; + } + public function shouldRenderAsync(PhabricatorDocumentRef $ref) { return false; } diff --git a/src/applications/files/document/PhabricatorSourceDocumentEngine.php b/src/applications/files/document/PhabricatorSourceDocumentEngine.php index 1c3e54575a..cd7c2af92b 100644 --- a/src/applications/files/document/PhabricatorSourceDocumentEngine.php +++ b/src/applications/files/document/PhabricatorSourceDocumentEngine.php @@ -9,6 +9,10 @@ final class PhabricatorSourceDocumentEngine return pht('View as Source'); } + public function canConfigureHighlighting(PhabricatorDocumentRef $ref) { + return true; + } + protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { return 'fa-code'; } @@ -20,9 +24,16 @@ final class PhabricatorSourceDocumentEngine protected function newDocumentContent(PhabricatorDocumentRef $ref) { $content = $this->loadTextData($ref); - $content = PhabricatorSyntaxHighlighter::highlightWithFilename( - $ref->getName(), - $content); + $highlighting = $this->getHighlightingConfiguration(); + if ($highlighting !== null) { + $content = PhabricatorSyntaxHighlighter::highlightWithLanguage( + $highlighting, + $content); + } else { + $content = PhabricatorSyntaxHighlighter::highlightWithFilename( + $ref->getName(), + $content); + } return $this->newTextDocumentContent($content); } diff --git a/src/applications/files/document/PhabricatorTextDocumentEngine.php b/src/applications/files/document/PhabricatorTextDocumentEngine.php index 4fb8d052e1..08a5cabe54 100644 --- a/src/applications/files/document/PhabricatorTextDocumentEngine.php +++ b/src/applications/files/document/PhabricatorTextDocumentEngine.php @@ -3,10 +3,16 @@ abstract class PhabricatorTextDocumentEngine extends PhabricatorDocumentEngine { + private $encodingMessage = null; + protected function canRenderDocumentType(PhabricatorDocumentRef $ref) { return $ref->isProbablyText(); } + public function canConfigureEncoding(PhabricatorDocumentRef $ref) { + return true; + } + protected function newTextDocumentContent($content) { $lines = phutil_split_lines($content); @@ -14,19 +20,69 @@ abstract class PhabricatorTextDocumentEngine ->setHighlights($this->getHighlightedLines()) ->setLines($lines); + $message = null; + if ($this->encodingMessage !== null) { + $message = $this->newMessage($this->encodingMessage); + } + $container = phutil_tag( 'div', array( 'class' => 'document-engine-text', ), - $view); + array( + $message, + $view, + )); return $container; } protected function loadTextData(PhabricatorDocumentRef $ref) { $content = $ref->loadData(); - $content = phutil_utf8ize($content); + + $encoding = $this->getEncodingConfiguration(); + if ($encoding !== null) { + if (function_exists('mb_convert_encoding')) { + $content = mb_convert_encoding($content, 'UTF-8', $encoding); + $this->encodingMessage = pht( + 'This document was converted from %s to UTF8 for display.', + $encoding); + } else { + $this->encodingMessage = pht( + 'Unable to perform text encoding conversion: mbstring extension '. + 'is not available.'); + } + } else { + if (!phutil_is_utf8($content)) { + if (function_exists('mb_detect_encoding')) { + $try_encodings = array( + 'JIS' => pht('JIS'), + 'EUC-JP' => pht('EUC-JP'), + 'SJIS' => pht('Shift JIS'), + 'ISO-8859-1' => pht('ISO-8859-1 (Latin 1)'), + ); + + $guess = mb_detect_encoding($content, array_keys($try_encodings)); + if ($guess) { + $content = mb_convert_encoding($content, 'UTF-8', $guess); + $this->encodingMessage = pht( + 'This document is not UTF8. It was detected as %s and '. + 'converted to UTF8 for display.', + idx($try_encodings, $guess, $guess)); + } + } + } + } + + if (!phutil_is_utf8($content)) { + $content = phutil_utf8ize($content); + $this->encodingMessage = pht( + 'This document is not UTF8 and its text encoding could not be '. + 'detected automatically. Use "Change Text Encoding..." to choose '. + 'an encoding.'); + } + return $content; } diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js index 3e49f35410..ccbd41ca92 100644 --- a/webroot/rsrc/js/application/files/behavior-document-engine.js +++ b/webroot/rsrc/js/application/files/behavior-document-engine.js @@ -52,6 +52,61 @@ JX.behavior('document-engine', function(config, statics) { }); } + list.addItem( + new JX.PHUIXActionView() + .setDivider(true)); + + var encode_item = new JX.PHUIXActionView() + .setName(data.encode.name) + .setIcon(data.encode.icon); + + var onencode = JX.bind(null, function(data, e) { + e.prevent(); + + if (encode_item.getDisabled()) { + return; + } + + new JX.Workflow(data.encode.uri, {encoding: data.encode.value}) + .setHandler(function(r) { + data.encode.value = r.encoding; + onview(data); + }) + .start(); + + menu.close(); + + }, data); + + encode_item.setHandler(onencode); + + list.addItem(encode_item); + + var highlight_item = new JX.PHUIXActionView() + .setName(data.highlight.name) + .setIcon(data.highlight.icon); + + var onhighlight = JX.bind(null, function(data, e) { + e.prevent(); + + if (highlight_item.getDisabled()) { + return; + } + + new JX.Workflow(data.highlight.uri, {highlight: data.highlight.value}) + .setHandler(function(r) { + data.highlight.value = r.highlight; + onview(data); + }) + .start(); + + menu.close(); + }, data); + + highlight_item.setHandler(onhighlight); + + list.addItem(highlight_item); + menu.setContent(list.getNode()); menu.listen('open', function() { @@ -61,6 +116,11 @@ JX.behavior('document-engine', function(config, statics) { // Highlight the current rendering engine. var is_selected = (engine.spec.viewKey == data.viewKey); engine.view.setSelected(is_selected); + + if (is_selected) { + encode_item.setDisabled(!engine.spec.canEncode); + highlight_item.setDisabled(!engine.spec.canHighlight); + } } }); @@ -68,13 +128,38 @@ JX.behavior('document-engine', function(config, statics) { menu.open(); } + function add_params(uri, data) { + uri = JX.$U(uri); + + if (data.highlight.value) { + uri.setQueryParam('highlight', data.highlight.value); + } + + if (data.encode.value) { + uri.setQueryParam('encode', data.encode.value); + } + + return uri.toString(); + } + function onview(data, spec, immediate) { + if (!spec) { + for (var ii = 0; ii < data.views.length; ii++) { + if (data.views[ii].viewKey == data.viewKey) { + spec = data.views[ii]; + break; + } + } + } + data.sequence = (data.sequence || 0) + 1; var handler = JX.bind(null, onrender, data, data.sequence); data.viewKey = spec.viewKey; - new JX.Request(spec.engineURI, handler) + var uri = add_params(spec.engineURI, data); + + new JX.Request(uri, handler) .send(); if (data.loadingView) { @@ -93,7 +178,9 @@ JX.behavior('document-engine', function(config, statics) { // Replace the URI with the URI for the specific rendering the user // has selected. - JX.History.replace(spec.viewURI); + + var view_uri = add_params(spec.viewURI, data); + JX.History.replace(view_uri); } } @@ -134,13 +221,7 @@ JX.behavior('document-engine', function(config, statics) { if (config && config.renderControlID) { var control = JX.$(config.renderControlID); var data = JX.Stratcom.getData(control); - - for (var ii = 0; ii < data.views.length; ii++) { - if (data.views[ii].viewKey == data.viewKey) { - onview(data, data.views[ii], true); - break; - } - } + onview(data, null, true); } }); diff --git a/webroot/rsrc/js/phuix/PHUIXActionView.js b/webroot/rsrc/js/phuix/PHUIXActionView.js index 7967fa7366..97e746c2c5 100644 --- a/webroot/rsrc/js/phuix/PHUIXActionView.js +++ b/webroot/rsrc/js/phuix/PHUIXActionView.js @@ -34,6 +34,10 @@ JX.install('PHUIXActionView', { return this; }, + getDisabled: function() { + return this._disabled; + }, + setLabel: function(label) { this._label = label; JX.DOM.alterClass(