Short description of the issue
PagesEditor::_clone() leaves stale state from the source page on the in-memory copy it returns: $copy->published keeps the source's publication timestamp (while the copy's own DB row has published=NULL), and $copy->namePrevious is set to the source page's name as a side effect of _clone() assigning the new unique name.
A visible consequence: if API code holds the returned clone object past PagePathHistory's 120s minimumAge and then renames it, PagePathHistory records the source page's live path as history for the clone (e.g. /aaa/ → clone id), because the rename hook sees namePrevious='aaa'. This surfaced while working on #1024 (see discussion there from this comment onward), but the stale in-memory state is a core issue independent of that module.
Expected behavior
The Page object returned by $pages->clone() should describe the copy, not the source:
$copy->published should be 0 when the copy has not been published itself (matching its DB row, where published is NULL and is only set by published=NOW() when the page is actually published).
$copy->namePrevious should be empty — the copy has never had a previous name.
Actual behavior
$copy->published returns the source page's publication timestamp when the source was loaded from the DB.
$copy->namePrevious returns the source page's name, because _clone() sets $copy->name = $name while change tracking is active, and Page::trackChange() records the first name change into namePrevious.
- Downstream effect: renaming that clone object later records the source's live path into
page_path_history pointing at the clone.
Verified on a stock 3.0.271 install with a real DB (PagePathHistory installed, clone created unpublished the way ProcessPageClone::cloneAjax() does, object aged past minimumAge, then renamed):
clone: name=ptest-f-1 mem published=1788808049 namePrevious='ptest-f'
history: /ptest-f -> page 47137 (clone id=47137, source id=47136)
The relevant _clone() code initializes created/modified/id/numChildren on the copy but not published, and assigns the name with tracking active:
$copy->setQuietly('id', $options['forceID'] > 1 ? (int) $options['forceID'] : 0);
$copy->setQuietly('numChildren', 0);
$copy->setQuietly('created', time());
$copy->setQuietly('modified', time());
$copy->name = $name;
Optional: Suggestion for a possible fix
In wire/core/Pages/PagesEditor.php _clone():
$copy->setQuietly('created', time());
$copy->setQuietly('modified', time());
$copy->setQuietly('published', 0); // copy has not itself been published yet
I tested this line on 3.0.271: the in-memory value then matches the DB row, and the PagePathHistory symptom above goes away (in combination with the guard proposed in #1024; the published fix is what makes that guard reliable for held clone objects). Publishing the copy still works normally — published=NOW() is set by the save that publishes it.
For namePrevious, the same treatment would be $copy->setQuietly('name', $name) instead of $copy->name = $name (or clearing namePrevious after assignment), though I've only tested the published line; the name assignment may have other intentional side effects, so I'd defer to Ryan on that half.
Steps to reproduce the issue
- Install PagePathHistory. Have a published page, e.g.
/aaa/.
- In an API script (or Tracy console), load it fresh:
$src = $pages->get('/aaa/');
$copy = $pages->clone($src, null, false, ['set' => ['status' => $src->status | Page::statusUnpublished]]); (same as the PageList "copy" action does)
- Dump
$copy->published (source's timestamp, expected 0) and $copy->namePrevious (aaa, expected empty).
- To see the PagePathHistory consequence without waiting 2 minutes:
$copy->setQuietly('created', time() - 600); then $copy->of(false); $copy->name = 'bbb'; $pages->save($copy);
SELECT * FROM page_path_history now contains /aaa (the source's live path) pointing at the clone's ID.
Setup/Environment
- ProcessWire version: 3.0.271 (code unchanged on current dev)
- PHP version: 8.5.10
- MySQL version: MariaDB 12.3.2
Short description of the issue
PagesEditor::_clone()leaves stale state from the source page on the in-memory copy it returns:$copy->publishedkeeps the source's publication timestamp (while the copy's own DB row haspublished=NULL), and$copy->namePreviousis set to the source page's name as a side effect of_clone()assigning the new unique name.A visible consequence: if API code holds the returned clone object past PagePathHistory's 120s
minimumAgeand then renames it, PagePathHistory records the source page's live path as history for the clone (e.g./aaa/ → clone id), because the rename hook seesnamePrevious='aaa'. This surfaced while working on #1024 (see discussion there from this comment onward), but the stale in-memory state is a core issue independent of that module.Expected behavior
The Page object returned by
$pages->clone()should describe the copy, not the source:$copy->publishedshould be0when the copy has not been published itself (matching its DB row, wherepublishedisNULLand is only set bypublished=NOW()when the page is actually published).$copy->namePreviousshould be empty — the copy has never had a previous name.Actual behavior
$copy->publishedreturns the source page's publication timestamp when the source was loaded from the DB.$copy->namePreviousreturns the source page's name, because_clone()sets$copy->name = $namewhile change tracking is active, andPage::trackChange()records the first name change intonamePrevious.page_path_historypointing at the clone.Verified on a stock 3.0.271 install with a real DB (PagePathHistory installed, clone created unpublished the way
ProcessPageClone::cloneAjax()does, object aged pastminimumAge, then renamed):The relevant
_clone()code initializescreated/modified/id/numChildrenon the copy but notpublished, and assigns the name with tracking active:Optional: Suggestion for a possible fix
In
wire/core/Pages/PagesEditor.php_clone():I tested this line on 3.0.271: the in-memory value then matches the DB row, and the PagePathHistory symptom above goes away (in combination with the guard proposed in #1024; the
publishedfix is what makes that guard reliable for held clone objects). Publishing the copy still works normally —published=NOW()is set by the save that publishes it.For
namePrevious, the same treatment would be$copy->setQuietly('name', $name)instead of$copy->name = $name(or clearingnamePreviousafter assignment), though I've only tested thepublishedline; the name assignment may have other intentional side effects, so I'd defer to Ryan on that half.Steps to reproduce the issue
/aaa/.$src = $pages->get('/aaa/');$copy = $pages->clone($src, null, false, ['set' => ['status' => $src->status | Page::statusUnpublished]]);(same as the PageList "copy" action does)$copy->published(source's timestamp, expected 0) and$copy->namePrevious(aaa, expected empty).$copy->setQuietly('created', time() - 600);then$copy->of(false); $copy->name = 'bbb'; $pages->save($copy);SELECT * FROM page_path_historynow contains/aaa(the source's live path) pointing at the clone's ID.Setup/Environment