From 2b135039c58b0309de530054b2f96fdf9fab2e70 Mon Sep 17 00:00:00 2001 From: Olaf Gleba Date: Sun, 25 Jan 2026 16:20:06 +0100 Subject: [PATCH 1/4] Add script to clean up orphaned page folders --- cleanup-orphaned-page-folders.md | 171 +++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 cleanup-orphaned-page-folders.md diff --git a/cleanup-orphaned-page-folders.md b/cleanup-orphaned-page-folders.md new file mode 100644 index 0000000..848bea0 --- /dev/null +++ b/cleanup-orphaned-page-folders.md @@ -0,0 +1,171 @@ +--- + +title: "Clean up orphaned page folders and their content" + +--- + +version: 1.0.0 + +--- + +authors: Olaf Gleba + +--- + +tags: pages, files + +--- + +date: 2026-01-25 + +--- + + + +## Problem + +For your new project you duplicate a old installation so you don't have to start from scratch. While building the new website your `assets/files` folder gets messy,- there are a growing number of orphaned page id folders with obsolete content you want to get rid of. + +## Solution + +Create a new document, copy&paste the code below, save it to the root of your PW installation and open it in a browser. Initially it perfoms a dry run, so your are save before actual deletion take place. + +Because we are dealing with actual files, make a backup of you `files` folder and double check the output results before changing the mode. + +```php +Orphaned page folder cleanup

"; +echo "

Mode: " . ($dryRun ? "DRY RUN - NO DELETION" : "LIVE - ACTUAL DELETION") . "

"; + +/** + * Load existing Page-Ids from database + */ +$existingPageIds = []; + +$sql = $database->query("SELECT id FROM pages"); +while ($row = $sql->fetch(\PDO::FETCH_ASSOC)) { + $existingPageIds[(int)$row['id']] = true; +} + +// Print to browser +echo "

Existing pages loaded: " . count($existingPageIds) . "

"; + +/** + * Iterate over the `files` folder + */ +// Get files root +$filesRoot = realpath($config->paths->files); + +$removedFolders = 0; + +$dirs = scandir($filesRoot); + +foreach ($dirs as $dir) { + + // Only nummeric folders (Page-IDs) + if (!ctype_digit($dir)) { + continue; + } + + $pageId = (int)$dir; + $pageDir = $filesRoot . DIRECTORY_SEPARATOR . $dir; + + if (!is_dir($pageDir)) { + continue; + } + + // Skip if page exists + if (isset($existingPageIds[$pageId])) { + continue; + } + + // Print to browser + echo "

Orphaned page folder: {$pageDir}

"; + + // List folder files + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $pageDir, + \FilesystemIterator::SKIP_DOTS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $item) { + if ($item->isFile()) { + echo " File: " . $item->getRealPath() . "
"; + } + } + + // Delete folder (if args `--delete` is present, s.above) + if (!$dryRun) { + deleteDirectory($pageDir); + echo " FOLDER DELETED
"; + } else { + echo " (DRY RUN - NO DELETION)
"; + } + + echo "
"; + $removedFolders++; +} + +/** + * Print summary to browser + */ +echo "

"; +echo "---------------------------------
"; +echo "Done
"; +echo "Orphaned page folders: {$removedFolders}
"; +echo "---------------------------------
"; +echo "

"; + +/** + * Recursive deletion + */ +function deleteDirectory(string $dir): void +{ + if (!is_dir($dir)) { + return; + } + + $items = scandir($dir); + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir . DIRECTORY_SEPARATOR . $item; + + if (is_dir($path)) { + deleteDirectory($path); + } else { + unlink($path); + } + } + + rmdir($dir); +} +``` \ No newline at end of file From 0ddfaf0cb2b8f98275223629b1d2015da830006b Mon Sep 17 00:00:00 2001 From: olafgleba Date: Sun, 25 Jan 2026 17:05:38 +0100 Subject: [PATCH 2/4] Correct some spelling --- cleanup-orphaned-page-folders.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cleanup-orphaned-page-folders.md b/cleanup-orphaned-page-folders.md index 848bea0..27117f9 100644 --- a/cleanup-orphaned-page-folders.md +++ b/cleanup-orphaned-page-folders.md @@ -28,7 +28,7 @@ For your new project you duplicate a old installation so you don't have to start ## Solution -Create a new document, copy&paste the code below, save it to the root of your PW installation and open it in a browser. Initially it perfoms a dry run, so your are save before actual deletion take place. +Create a new document, copy&paste the code below, save it to the root of your PW installation and open it in a browser. Initially it performs a dry run, so your are save before actual deletion take place. Because we are dealing with actual files, make a backup of you `files` folder and double check the output results before changing the mode. From b596191fac083654a352796cdd7fcca7705a3780 Mon Sep 17 00:00:00 2001 From: olafgleba Date: Sun, 25 Jan 2026 23:48:39 +0100 Subject: [PATCH 3/4] Again some spelling --- cleanup-orphaned-page-folders.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cleanup-orphaned-page-folders.md b/cleanup-orphaned-page-folders.md index 27117f9..99bc02c 100644 --- a/cleanup-orphaned-page-folders.md +++ b/cleanup-orphaned-page-folders.md @@ -42,8 +42,10 @@ Because we are dealing with actual files, make a backup of you `files` folder an * - Implements a dry run mode * - Tested with ProcessWire 3.0.x, PHP 8.4.x * + * Usage: + * * Place the script file in the root of your PW installation (or - * adapt the pw bootstrap path), than open the file within the browser. + * adapt the pw bootstrap path), than open the file in the browser. * * @author Olaf Gleba * @version 1.0.0 From ce1375e9ebeaab60fa5241513d8f18471a1f8947 Mon Sep 17 00:00:00 2001 From: olafgleba Date: Sun, 25 Jan 2026 23:50:16 +0100 Subject: [PATCH 4/4] Add new script to swap multilangauge content --- swap-multilanguage-content.md | 220 ++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 swap-multilanguage-content.md diff --git a/swap-multilanguage-content.md b/swap-multilanguage-content.md new file mode 100644 index 0000000..62cec9e --- /dev/null +++ b/swap-multilanguage-content.md @@ -0,0 +1,220 @@ +--- + +title: "Swap the content of multilingual fields between two languages" + +--- + +version: 1.0.0 + +--- + +authors: Olaf Gleba + +--- + +tags: pages, languages, multilingual, multilanguages + +--- + +date: 2026-01-25 + +--- + + + +## Problem + +On Launch the defined default language of a website is e.g. `german` (`/`). Beside e.g. `english` as a second language (`/en/`). After a while eventually the client wants to have `english` as the default language and `german` as second. + +Usually this gives the editorial staff a really hard time, because every bit of multilingual content has to be replaced by hand. Beside approaches that imposes redirects. + + +## Solution + +The script iterates recursively over multilingual fields of different types and swap the content between two languages. + +Create a new document, copy&paste the code below, save it to the root of your PW installation, adapt params (s. instructions within the script file) and open it in a browser. + +```php +` with the name of the language that should + * become default. + * 2. Adapt `` with the required template names + * (assuming that multiple templates are to consider). Execute the script + * file (e.g. reload the browser) ``each time`` you adapt the template name. + * + * Code example: + * + * $_pages = wire('pages')->find("template="); + * swapMultiLanguageContent($_pages, 'default', ''); + * + */ + +/** + * MANDATORY STEPS AFTER FINAL EXECUTION + * + * After you run all considered templates you need to do: + * + * 1. Assuming `german` was your default language so far (name `default`) + * and you want to define a second language, for example `english` + * (name `english`), as the new default, adapt the name (and surely the + * title too) on both related languages (e.g. english => default, + * german => german). + * + * 2. Go to your Homepage ("/") and in the settings tab, adapt the url of your + * new default language and the url of your swapped language (e.g. english + * => /, german => /de/). + */ + + +// bootstrap processwire +require_once './index.php'; + + +/** + * Function to swap the content of multilingual fields between two languages + * + * 1. Page name swap + * 2. Execute recursive function `swapFieldValuesRecursive` + * 3. Save pages array + */ + +function swapMultiLanguageContent($pages, $langA = 'default', $langB = 'deutsch') { + + $languages = wire('languages'); + $lA = $languages->get($langA); + $lB = $languages->get($langB); + + if(!$lA || !$lB) throw new WireException("Language not found"); + + foreach($pages as $page) { + + if(!$page instanceof Page || !$page->id) continue; + + $page->of(false); + + /* 1 */ + try { + $nameA = $page->localName($lA); + $nameB = $page->localName($lB); + + if($nameA !== $nameB) { + + $page->set("status$langB", 1); + + $page->set("name$langA", $nameB); + $page->set("name$langB", $nameA); + } + } catch(Exception $e) { + wire('log')->save('swap-lang', "Page {$page->id}: Name swap error: " . $e->getMessage()); + } + + /* 2 */ + swapFieldValuesRecursive($page, $lA, $lB); + + /* 3 */ + try { + $page->save(); + } catch(Exception $e) { + wire('log')->save('swap-lang', "Page {$page->id}: Save error: " . $e->getMessage()); + } + + $page->of(true); + } + + return "Done."; +} + + +/** + * Internal function to swap field values recursively + * + * NOT intended to use as stand-alone, s. func `swapMultiLanguageContent` + * + * 1. Ordinary field type + * 2. Repeater field type + * 3. RepeaterMatrix field type + */ + +function swapFieldValuesRecursive(Page $page, Language $lA, Language $lB) { + + foreach($page->template->fieldgroup as $field) { + + $fieldname = $field->name; + if(!$page->hasField($fieldname)) continue; + + $value = $page->$fieldname; + + if($value === null) continue; + + /* 1 */ + $type = $field->type; + + if($type instanceof FieldtypeLanguageInterface) { + + try { + $valueA = $value->getLanguageValue($lA); + $valueB = $value->getLanguageValue($lB); + + if($valueA != $valueB) { + $value->setLanguageValue($lA, $valueB); + $value->setLanguageValue($lB, $valueA); + } + + } catch(Exception $e) { + wire('log')->save('swap-lang', "Field {$fieldname} error: " . $e->getMessage()); + } + + continue; + } + + /* 2 */ + if($type instanceof FieldtypeRepeater && $value->count()) { + + foreach($value as $repItem) { + $repItem->of(false); + swapFieldValuesRecursive($repItem, $lA, $lB); + $repItem->save(); + $repItem->of(true); + } + + continue; + } + + /* 3 */ + if($type instanceof FieldtypeRepeaterMatrix && $value->count()) { + + foreach($value as $matrixItem) { + $matrixItem->of(false); + swapFieldValuesRecursive($matrixItem, $lA, $lB); + $matrixItem->save(); + $matrixItem->of(true); + } + + continue; + } + + // Ignore all other field types... + } +} +``` \ No newline at end of file