diff --git a/_build/test/Tests/Processors/Element/StaticFileChangedUpdateTest.php b/_build/test/Tests/Processors/Element/StaticFileChangedUpdateTest.php new file mode 100644 index 00000000000..e9b3353bbb5 --- /dev/null +++ b/_build/test/Tests/Processors/Element/StaticFileChangedUpdateTest.php @@ -0,0 +1,156 @@ +tmpDir = sys_get_temp_dir() . '/modx-static-file-changed-' . uniqid('', true); + mkdir($this->tmpDir, 0777, true); + } + + /** + * @after + */ + public function tearDownFixtures() + { + parent::tearDownFixtures(); + foreach ( + [ + modChunk::class, + modSnippet::class, + modPlugin::class, + modTemplate::class, + modTemplateVar::class, + ] as $class + ) { + $objects = $this->modx->getCollection($class, [ + 'name:LIKE' => 'UnitTestStaticFile%', + ]); + if ($class === modTemplate::class) { + $objects = $this->modx->getCollection($class, [ + 'templatename:LIKE' => 'UnitTestStaticFile%', + ]); + } + foreach ($objects as $object) { + $object->remove(); + } + } + if ($this->tmpDir && is_dir($this->tmpDir)) { + foreach (glob($this->tmpDir . '/*') ?: [] as $file) { + @unlink($file); + } + @rmdir($this->tmpDir); + } + $this->modx->error->reset(); + } + + /** + * @dataProvider providerElementTypes + */ + public function testStaticFileChangedFlagOnPathChange( + string $classKey, + string $processor, + string $nameField, + string $contentField, + string $contentValue + ) { + $fileA = $this->tmpDir . '/a.' . substr(md5($classKey), 0, 6) . '.txt'; + $fileB = $this->tmpDir . '/b.' . substr(md5($classKey), 0, 6) . '.txt'; + file_put_contents($fileA, $contentValue); + file_put_contents($fileB, $contentValue); + + /** @var \MODX\Revolution\modElement $element */ + $element = $this->modx->newObject($classKey); + $element->fromArray([ + $nameField => 'UnitTestStaticFile' . substr(md5($classKey), 0, 6), + 'description' => 'static file changed test', + 'static' => true, + 'static_file' => $fileA, + $contentField => $contentValue, + ]); + $this->assertTrue((bool)$element->save(), 'Failed to create static ' . $classKey); + + $payload = [ + 'id' => $element->get('id'), + $nameField => $element->get($nameField), + 'description' => $element->get('description'), + 'category' => 0, + 'locked' => false, + 'static' => 1, + 'static_file' => $fileB, + $contentField => $contentValue, + 'clearCache' => false, + ]; + if ($classKey === modPlugin::class) { + $payload['disabled'] = false; + } + + $result = $this->modx->runProcessor($processor, $payload); + $this->assertTrue($this->checkForSuccess($result), $result->getMessage()); + $object = $result->getObject(); + $this->assertArrayHasKey('static_file_changed', $object); + $this->assertTrue( + (bool)$object['static_file_changed'], + 'Expected static_file_changed=true after path change for ' . $classKey + ); + + $payload['static_file'] = $fileB; + $result = $this->modx->runProcessor($processor, $payload); + $this->assertTrue($this->checkForSuccess($result), $result->getMessage()); + $object = $result->getObject(); + $this->assertArrayHasKey('static_file_changed', $object); + $this->assertFalse( + (bool)$object['static_file_changed'], + 'Expected static_file_changed=false when path unchanged for ' . $classKey + ); + } + + public function providerElementTypes(): array + { + return [ + 'chunk' => [modChunk::class, ChunkUpdate::class, 'name', 'snippet', '

static

'], + 'snippet' => [modSnippet::class, SnippetUpdate::class, 'name', 'snippet', 'return "static";'], + 'plugin' => [modPlugin::class, PluginUpdate::class, 'name', 'plugincode', 'return true;'], + 'template' => [modTemplate::class, TemplateUpdate::class, 'templatename', 'content', 'static'], + 'tv' => [modTemplateVar::class, TemplateVarUpdate::class, 'name', 'default_text', 'static'], + ]; + } +} diff --git a/core/src/Revolution/Processors/Element/Chunk/Update.php b/core/src/Revolution/Processors/Element/Chunk/Update.php index bcec19933b4..f9df90f3a42 100644 --- a/core/src/Revolution/Processors/Element/Chunk/Update.php +++ b/core/src/Revolution/Processors/Element/Chunk/Update.php @@ -53,8 +53,6 @@ public function beforeSave() public function cleanup() { - return $this->success('', - array_merge($this->object->get(['id', 'name', 'description', 'locked', 'category', 'snippet']), - ['previous_category' => $this->previousCategory])); + return $this->success('', $this->getCleanupData(['id', 'name', 'description', 'locked', 'category', 'snippet'])); } } diff --git a/core/src/Revolution/Processors/Element/Plugin/Update.php b/core/src/Revolution/Processors/Element/Plugin/Update.php index c1fa021ed6e..d4ca37c5260 100644 --- a/core/src/Revolution/Processors/Element/Plugin/Update.php +++ b/core/src/Revolution/Processors/Element/Plugin/Update.php @@ -91,9 +91,8 @@ public function setSystemEvents() public function cleanup() { - return $this->success('', array_merge( - $this->object->get(['id', 'name', 'description', 'locked', 'category', 'disabled', 'plugincode']), - ['previous_category' => $this->previousCategory] - )); + return $this->success('', $this->getCleanupData([ + 'id', 'name', 'description', 'locked', 'category', 'disabled', 'plugincode', + ])); } } diff --git a/core/src/Revolution/Processors/Element/Snippet/Update.php b/core/src/Revolution/Processors/Element/Snippet/Update.php index 74741026c91..42b1a92901e 100644 --- a/core/src/Revolution/Processors/Element/Snippet/Update.php +++ b/core/src/Revolution/Processors/Element/Snippet/Update.php @@ -54,8 +54,6 @@ public function beforeSave() public function cleanup() { - return $this->success('', - array_merge($this->object->get(['id', 'name', 'description', 'locked', 'category', 'snippet']), - ['previous_category' => $this->previousCategory])); + return $this->success('', $this->getCleanupData(['id', 'name', 'description', 'locked', 'category', 'snippet'])); } } diff --git a/core/src/Revolution/Processors/Element/TemplateVar/Update.php b/core/src/Revolution/Processors/Element/TemplateVar/Update.php index deea3f12674..5dda1da58bb 100644 --- a/core/src/Revolution/Processors/Element/TemplateVar/Update.php +++ b/core/src/Revolution/Processors/Element/TemplateVar/Update.php @@ -247,9 +247,9 @@ public function setMediaSources() public function cleanup() { - return $this->success('', - array_merge($this->object->get(['id', 'name', 'description', 'locked', 'category', 'default_text']), - ['previous_category' => $this->previousCategory])); + return $this->success('', $this->getCleanupData([ + 'id', 'name', 'description', 'locked', 'category', 'default_text', + ])); } } diff --git a/core/src/Revolution/Processors/Element/Update.php b/core/src/Revolution/Processors/Element/Update.php index 81b424c5589..806a6823359 100644 --- a/core/src/Revolution/Processors/Element/Update.php +++ b/core/src/Revolution/Processors/Element/Update.php @@ -28,6 +28,8 @@ abstract class Update extends UpdateProcessor public $previousCategory; /** @var modElement $object */ public $object; + /** @var bool Whether static source identity changed (path/source/static); triggers manager reload */ + protected bool $staticFileChanged = false; public function beforeSet() { @@ -79,6 +81,9 @@ public function beforeSave() } } + /* Reload manager form when static source identity changes (path, media source, or static flag). */ + $this->staticFileChanged = $this->object->staticSourceChanged(); + return !$this->hasErrors(); } @@ -99,13 +104,26 @@ public function afterSave() } } + /** + * Build success payload shared by all element Update processors. + * Subclasses must use this so static_file_changed is never dropped. + * + * @param array $fields Object fields to include + * @return array + */ + protected function getCleanupData(array $fields): array + { + return array_merge($this->object->get($fields), [ + 'previous_category' => $this->previousCategory, + 'static_file_changed' => $this->staticFileChanged, + ]); + } + public function cleanup() { - $fields = array('id', 'description', 'locked', 'category', 'content'); - array_push($fields, ($this->classKey == modTemplate::class ? 'templatename' : 'name')); - return $this->success( - '', - array_merge($this->object->get($fields), ['previous_category' => $this->previousCategory]) - ); + $fields = ['id', 'description', 'locked', 'category', 'content']; + $fields[] = ($this->classKey == modTemplate::class ? 'templatename' : 'name'); + + return $this->success('', $this->getCleanupData($fields)); } } diff --git a/manager/assets/modext/core/modx.layout.js b/manager/assets/modext/core/modx.layout.js index f74246198e6..20345e8425d 100644 --- a/manager/assets/modext/core/modx.layout.js +++ b/manager/assets/modext/core/modx.layout.js @@ -905,3 +905,16 @@ MODx.loadPage = MODx.LayoutMgr.loadPage; MODx.showDashboard = MODx.LayoutMgr.showDashboard; MODx.hideDashboard = MODx.LayoutMgr.hideDashboard; MODx.changeMenu = MODx.LayoutMgr.changeMenu; + +/** + * If the element update response indicates static_file path changed, reload the page and return true. + * @param {Object} resultObject - response.result.object from element update processor + * @returns {boolean} true if page reload was triggered + */ +MODx.reloadIfStaticFileChanged = function(resultObject) { + if (resultObject && resultObject.static_file_changed) { + MODx.loadPage(MODx.request.a, `id=${resultObject.id}`); + return true; + } + return false; +}; diff --git a/manager/assets/modext/widgets/element/modx.panel.chunk.js b/manager/assets/modext/widgets/element/modx.panel.chunk.js index b354fc5b416..744993e6de6 100644 --- a/manager/assets/modext/widgets/element/modx.panel.chunk.js +++ b/manager/assets/modext/widgets/element/modx.panel.chunk.js @@ -440,8 +440,10 @@ Ext.extend(MODx.panel.Chunk, MODx.FormPanel, { }, success: function(r) { + const data = r.result.object; + if (MODx.reloadIfStaticFileChanged(data)) { return; } if (MODx.request.id) { Ext.getCmp('modx-grid-element-properties').save(); } - this.getForm().setValues(r.result.object); + this.getForm().setValues(data); const c = Ext.getCmp('modx-chunk-category').getValue(), @@ -449,7 +451,7 @@ Ext.extend(MODx.panel.Chunk, MODx.FormPanel, { t = Ext.getCmp('modx-tree-element') ; if (t) { - const node = t.getNodeById(`n_chunk_element_${Ext.getCmp('modx-chunk-id').getValue()}_${r.result.object.previous_category}`); + const node = t.getNodeById(`n_chunk_element_${Ext.getCmp('modx-chunk-id').getValue()}_${data.previous_category}`); if (node) { node.destroy(); } t.refreshNode(n, true); } diff --git a/manager/assets/modext/widgets/element/modx.panel.plugin.js b/manager/assets/modext/widgets/element/modx.panel.plugin.js index cd0ffa6432a..ea9dc4551be 100644 --- a/manager/assets/modext/widgets/element/modx.panel.plugin.js +++ b/manager/assets/modext/widgets/element/modx.panel.plugin.js @@ -461,16 +461,18 @@ Ext.extend(MODx.panel.Plugin, MODx.FormPanel, { }, success: function(o) { + const data = o.result.object; + if (MODx.reloadIfStaticFileChanged(data)) { return; } if (MODx.request.id) { Ext.getCmp('modx-grid-element-properties').save(); } Ext.getCmp('modx-grid-plugin-event').getStore().commitChanges(); - this.getForm().setValues(o.result.object); + this.getForm().setValues(data); const t = Ext.getCmp('modx-tree-element'); if (t) { const c = Ext.getCmp('modx-plugin-category').getValue(), u = c !== '' && c != null && c !== 0 ? `n_plugin_category_${c}` : 'n_type_plugin', - node = t.getNodeById(`n_plugin_element_${Ext.getCmp('modx-plugin-id').getValue()}_${o.result.object.previous_category}`) + node = t.getNodeById(`n_plugin_element_${Ext.getCmp('modx-plugin-id').getValue()}_${data.previous_category}`) ; if (node) { node.destroy(); } t.refreshNode(u, true); diff --git a/manager/assets/modext/widgets/element/modx.panel.snippet.js b/manager/assets/modext/widgets/element/modx.panel.snippet.js index f5fe3f257dc..601ce07a1fe 100644 --- a/manager/assets/modext/widgets/element/modx.panel.snippet.js +++ b/manager/assets/modext/widgets/element/modx.panel.snippet.js @@ -444,17 +444,19 @@ Ext.extend(MODx.panel.Snippet, MODx.FormPanel, { }, success: function(r) { + const data = r.result.object; + if (MODx.reloadIfStaticFileChanged(data)) { return; } if (MODx.request.id) { Ext.getCmp('modx-grid-element-properties').save(); } - this.getForm().setValues(r.result.object); + this.getForm().setValues(data); const t = Ext.getCmp('modx-tree-element'); if (t) { const c = Ext.getCmp('modx-snippet-category').getValue(), u = c !== '' && c != null && c !== 0 ? `n_snippet_category_${c}` : 'n_type_snippet', - node = t.getNodeById(`n_snippet_element_${Ext.getCmp('modx-snippet-id').getValue()}_${r.result.object.previous_category}`) + node = t.getNodeById(`n_snippet_element_${Ext.getCmp('modx-snippet-id').getValue()}_${data.previous_category}`) ; if (node) { node.destroy(); diff --git a/manager/assets/modext/widgets/element/modx.panel.template.js b/manager/assets/modext/widgets/element/modx.panel.template.js index 44d9766f4bb..9ff6471496c 100644 --- a/manager/assets/modext/widgets/element/modx.panel.template.js +++ b/manager/assets/modext/widgets/element/modx.panel.template.js @@ -568,10 +568,9 @@ Ext.extend(MODx.panel.Template, MODx.FormPanel, { }); }, success: function(response) { - const - data = response.result.object, - tree = Ext.getCmp('modx-tree-element') - ; + const data = response.result.object; + if (MODx.reloadIfStaticFileChanged(data)) { return; } + const tree = Ext.getCmp('modx-tree-element'); if (MODx.request.id) { Ext.getCmp('modx-grid-element-properties').save(); } diff --git a/manager/assets/modext/widgets/element/modx.panel.tv.js b/manager/assets/modext/widgets/element/modx.panel.tv.js index b02049f199d..57da317509c 100644 --- a/manager/assets/modext/widgets/element/modx.panel.tv.js +++ b/manager/assets/modext/widgets/element/modx.panel.tv.js @@ -752,18 +752,20 @@ Ext.extend(MODx.panel.TV, MODx.FormPanel, { }, success: function(r) { + const data = r.result.object; + if (MODx.reloadIfStaticFileChanged(data)) { return; } Ext.getCmp('modx-grid-tv-template').getStore().commitChanges(); Ext.getCmp('modx-grid-tv-security').getStore().commitChanges(); Ext.getCmp('modx-grid-element-sources').getStore().commitChanges(); if (MODx.request.id) { Ext.getCmp('modx-grid-element-properties').save(); } - this.getForm().setValues(r.result.object); + this.getForm().setValues(data); const t = Ext.getCmp('modx-tree-element'); if (t) { const c = Ext.getCmp('modx-tv-category').getValue(), u = c !== '' && c != null && c !== 0 ? `n_tv_category_${c}` : 'n_type_tv', - node = t.getNodeById(`n_tv_element_${Ext.getCmp('modx-tv-id').getValue()}_${r.result.object.previous_category}`); + node = t.getNodeById(`n_tv_element_${Ext.getCmp('modx-tv-id').getValue()}_${data.previous_category}`); if (node) { node.destroy(); } t.refreshNode(u, true); }