<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\library;
use seekquarry\yioop\configs as C;
/**
* V10ResourceFolderMigration moves the folders of app/resources from the
* pre-version 10 layout to the version 10 layout ResourceFolders gives,
* and renames personal groups from Personal$<user id> to
* <user name>$Personal, which the version 10 layout names their folders
* from. Database upgrade 134, the first upgrade of version 10 to touch
* app/resources, runs it.
*
* In the pre-version 10 layout every page, post and user folder was
* app/resources/<prefix>/<hash>, with <hash> the crawlHash of the group
* id, the page id or "post" and a post id, and the site's AUTH_KEY, or
* of "user", a user id and AUTH_KEY, and <prefix> its first three
* letters; thumbnails were in t<hash> beside it, or, for the oldest
* folders, in the folder of the crawlHash of "thumb", the ids and
* AUTH_KEY. The feed pictures were in t followed by the hash of "group",
* the Public group's id and AUTH_KEY. The site's appearance files were
* directly in app/resources, and a host's in app/resources/<host>.
*
* run reads the rows of GROUP_PAGE, GROUP_ITEM and USERS once, keeping
* only the hashes it finds on disk, so its memory is about the number of
* folders, and its time is about the number of rows plus the number of
* folders, each moved by one rename. Every step is safe to repeat: what
* has been moved is not found again, so a migration stopped part way is
* finished by running it again. Database upgrade 134 runs it, and so does
* php ConfigureTool.php migrate-pre-v10-resources, which a site with many
* posts can run before upgrading so the upgrade, made during a web
* request, finds nothing left to do. All code that knows the pre-version
* 10 layout is in this class, so the classes used day to day know only
* the version 10 layout.
*/
class V10ResourceFolderMigration
{
/**
* UNMIGRATED_FOLDER is the folder directly in app/resources that the
* pre-version 10 folders no page, post or user names are moved to, for
* the site's administrator to look at, rather than being deleted.
*/
const UNMIGRATED_FOLDER = "unmigrated";
/**
* OLD_PERSONAL_GROUP_PREFIX began the name of a user's personal group
* before version 10, followed by the user's id, as in Personal$7.
* renamePersonalGroups renames such a group to the user's name followed
* by PERSONAL_GROUP_SUFFIX.
*/
const OLD_PERSONAL_GROUP_PREFIX = 'Personal$';
/**
* OLD_HASH_PATTERN matches the name of a folder of the pre-version 10
* layout: a crawlHash of 11 letters, digits, - or _, with a t before it
* for a thumbnail folder.
*/
const OLD_HASH_PATTERN = '/^t?[A-Za-z0-9_\-]{11}$/';
/**
* OLD_HASH_LEN is the length of the hash in a folder name of the
* pre-version 10 layout.
*/
const OLD_HASH_LEN = 11;
/**
* OLD_PREFIX_LEN is the length of the names of the prefix folders of
* the pre-version 10 layout.
*/
const OLD_PREFIX_LEN = 3;
/**
* KEPT_TOP_FILES are the files directly in app/resources that stay
* there: the lists the recommendation and description jobs read, which
* are never served.
*/
const KEPT_TOP_FILES = ["recommendation.txt", "needs_descriptions.txt"];
/**
* KEPT_TOP_FOLDERS are the folders directly in app/resources that the
* migration leaves where they are.
*/
const KEPT_TOP_FOLDERS = ["needs_thumbs", "site", "users", "domains",
self::UNMIGRATED_FOLDER];
/**
* OS_FOLDER_FILES are the files an operating system's file browser
* puts in a folder it has shown: .DS_Store by macOS's Finder,
* Thumbs.db and desktop.ini by Windows Explorer. A prefix folder
* holding only these once its hashed folders have moved counts as
* empty and is deleted.
*/
const OS_FOLDER_FILES = [".DS_Store", "Thumbs.db", "desktop.ini"];
/**
* REPORTED_NAMES is how many of the names in a prefix folder that is
* kept the report gives, so the reason it was kept can be seen without
* a line running on for a whole folder.
*/
const REPORTED_NAMES = 5;
/**
* $db is the database the rows are read from and the groups renamed
* in.
* @var object
*/
public $db;
/**
* $resource_path is the app/resources folder being migrated.
* @var string
*/
public $resource_path;
/**
* $auth_key is the AUTH_KEY the pre-version 10 folder names were made
* with.
* @var string
*/
public $auth_key;
/**
* $log_function is called with each line of the report as it is made,
* if not null.
* @var callable|null
*/
public $log_function;
/**
* $counts holds counts of what was done, by kind, for the report.
* @var array
*/
public $counts = [];
/**
* $old_folders maps each pre-version 10 hash found on disk to the
* paths of its folder and thumbnail folders.
* @var array
*/
public $old_folders = [];
/**
* $new_folders maps each pre-version 10 hash to its version 10 folder:
* the folder of the page or post it belongs to, which holds the page
* and thumbs folders; the feeds folder for the feed pictures; and the
* user's folder for a user.
* @var array
*/
public $new_folders = [];
/**
* $hash_kinds maps each pre-version 10 hash to PAGE_KIND, THUMBS_KIND
* or WHOLE_KIND, which says where in its version 10 folder the folder
* named by the hash goes.
* @var array
*/
public $hash_kinds = [];
/**
* PAGE_KIND marks a pre-version 10 hash of "group" and the ids: the
* folder named by it holds a page's or post's files and goes to the
* page folder, and the folder named by t and it goes to the thumbs
* folder.
*/
const PAGE_KIND = "page";
/**
* THUMBS_KIND marks a pre-version 10 hash that names only thumbnails,
* with or without a t before it: that of "thumb" and the ids, and that
* of the path a redirect.txt gives.
*/
const THUMBS_KIND = "thumbs";
/**
* WHOLE_KIND marks a pre-version 10 hash whose folder's contents
* become the version 10 folder itself: a user's folder, and the feed
* pictures.
*/
const WHOLE_KIND = "whole";
/**
* __construct sets up a migration of one app/resources folder.
*
* @param object $db the Yioop database
* @param string $resource_path the app/resources folder; "" for the
* work directory's
* @param string $auth_key the AUTH_KEY the pre-version 10 names were
* made with; "" for the site's own
* @param callable|null $log_function called with each report line
*/
public function __construct($db, $resource_path = "", $auth_key = "",
$log_function = null)
{
$this->db = $db;
$this->resource_path = ResourceFolders::basePath($resource_path);
$this->auth_key = ($auth_key === "") ? C\p('AUTH_KEY') : $auth_key;
$this->log_function = $log_function;
}
/**
* run does the whole migration, as the class docblock says, and gives
* what it did.
*
* @return array kind => count of what was done, and "seconds" => how
* long it took
*/
public function run()
{
$start = microtime(true);
$this->counts = ["personal groups renamed" => 0,
"groups renamed to free a personal group name" => 0,
"folders moved" => 0, "links remade" => 0,
"folders kept in unmigrated" => 0, "site files moved" => 0,
"host folders moved" => 0, "paths rewritten" => 0,
"prefix folders kept" => 0];
$this->renamePersonalGroups();
if (is_dir($this->resource_path)) {
$this->findOldFolders();
if (!empty($this->old_folders)) {
$this->mapOldFolders();
$moved = $this->moveOldFolders();
$this->rewriteStoredPaths($moved);
}
$this->removeEmptyPrefixFolders();
$this->moveSiteFiles();
}
$this->counts["seconds"] = round(microtime(true) - $start, 2);
foreach ($this->counts as $kind => $count) {
$this->log("$kind: $count");
}
return $this->counts;
}
/**
* renamePersonalGroups renames each group named
* OLD_PERSONAL_GROUP_PREFIX and a user id, owned by that user, to the
* user's name followed by PERSONAL_GROUP_SUFFIX. A group named in the
* pre-version 10 form but owned by someone else, which another user
* made before that user had a personal group, and any group whose name
* ends in PERSONAL_GROUP_SUFFIX but is not its owner's personal group,
* has every $ in its name replaced by _, so it is an ordinary group
* and cannot take a user's messages. This is done first, as the
* version 10 folder names of personal groups' pages and posts come
* from their version 10 names.
*/
public function renamePersonalGroups()
{
$db = $this->db;
$user_names = [];
$result = $db->execute("SELECT USER_ID, USER_NAME FROM USERS");
while ($result && ($row = $db->fetchArray($result))) {
$user_names[$row['USER_ID']] = mb_strtolower($row['USER_NAME']);
}
$groups = [];
$result = $db->execute("SELECT GROUP_ID, GROUP_NAME, OWNER_ID " .
"FROM SOCIAL_GROUPS");
while ($result && ($row = $db->fetchArray($result))) {
if (str_contains($row['GROUP_NAME'], '$')) {
$groups[] = $row;
}
}
$prefix_len = strlen(self::OLD_PERSONAL_GROUP_PREFIX);
foreach ($groups as $group) {
$name = $group['GROUP_NAME'];
$owner_name = $user_names[$group['OWNER_ID']] ?? false;
$is_old_personal = str_starts_with($name,
self::OLD_PERSONAL_GROUP_PREFIX) &&
ctype_digit(substr($name, $prefix_len));
if ($owner_name !== false &&
$name === $owner_name . C\PERSONAL_GROUP_SUFFIX) {
continue;
}
if ($is_old_personal && $owner_name !== false &&
substr($name, $prefix_len) == $group['OWNER_ID']) {
$this->freeGroupName($owner_name . C\PERSONAL_GROUP_SUFFIX,
$groups);
$this->renameGroup($group['GROUP_ID'],
$owner_name . C\PERSONAL_GROUP_SUFFIX);
$this->counts["personal groups renamed"]++;
} else if ($is_old_personal ||
str_ends_with($name, C\PERSONAL_GROUP_SUFFIX)) {
$this->renameGroup($group['GROUP_ID'],
$this->ordinaryGroupName($name, $group['GROUP_ID']));
$this->counts["groups renamed to free a personal group " .
"name"]++;
}
}
}
/**
* freeGroupName renames a group other than a personal group that
* holds a name a personal group is about to take, as
* ordinaryGroupName says.
*
* @param string $name the personal group name to free
* @param array &$groups the groups with a $ in their names, as
* renamePersonalGroups read them; the renamed one is updated
*/
public function freeGroupName($name, &$groups)
{
foreach ($groups as $index => $group) {
if ($group['GROUP_NAME'] === $name) {
$new_name = $this->ordinaryGroupName($name,
$group['GROUP_ID']);
$this->renameGroup($group['GROUP_ID'], $new_name);
$groups[$index]['GROUP_NAME'] = $new_name;
$this->counts["groups renamed to free a personal group " .
"name"]++;
}
}
}
/**
* ordinaryGroupName gives the name a group that must not keep a $ is
* renamed to: its name with each $ replaced by _ and, should that be
* taken, the group's id added.
*
* @param string $name the group's name
* @param int $group_id the group's id
* @return string the new name
*/
public function ordinaryGroupName($name, $group_id)
{
$new_name = str_replace('$', '_', $name);
$result = $this->db->execute("SELECT GROUP_ID FROM SOCIAL_GROUPS " .
"WHERE GROUP_NAME = ?", [$new_name]);
if ($result && $this->db->fetchArray($result)) {
$new_name .= "_" . $group_id;
}
return $new_name;
}
/**
* renameGroup changes a group's name in SOCIAL_GROUPS and reports it.
*
* @param int $group_id the group to rename
* @param string $new_name its new name
*/
public function renameGroup($group_id, $new_name)
{
$this->db->execute("UPDATE SOCIAL_GROUPS SET GROUP_NAME = ? " .
"WHERE GROUP_ID = ?", [$new_name, $group_id]);
$this->log("group $group_id renamed to $new_name");
}
/**
* findOldFolders lists, into old_folders, the folders and links of the
* pre-version 10 layout on disk, grouped by hash. It reads only the
* names in app/resources and in its three-letter prefix folders, never
* the names inside a page's folder.
*/
public function findOldFolders()
{
$this->old_folders = [];
foreach (scandir($this->resource_path) ?: [] as $prefix) {
$prefix_path = $this->resource_path . "/$prefix";
if (strlen($prefix) != self::OLD_PREFIX_LEN ||
!preg_match('/^[A-Za-z0-9_\-]+$/', $prefix) ||
is_link($prefix_path) || !is_dir($prefix_path)) {
continue;
}
foreach (scandir($prefix_path) ?: [] as $name) {
$path = "$prefix_path/$name";
$hash = (strlen($name) > self::OLD_HASH_LEN) ?
substr($name, 1) : $name;
if (preg_match(self::OLD_HASH_PATTERN, $name) &&
str_starts_with($hash, $prefix) &&
(is_link($path) || is_dir($path))) {
$this->old_folders[$hash][] = $path;
}
}
}
}
/**
* mapOldFolders works out, into new_folders, the version 10 folder of
* each pre-version 10 hash found on disk, from the rows of GROUP_PAGE,
* GROUP_ITEM and USERS and from the feed pictures' hash. Before
* version 10, a redirected page's thumbnails were kept under the hash
* of the path its redirect.txt names; they go to that page's thumbs
* folder.
*/
public function mapOldFolders()
{
$db = $this->db;
$key = $this->auth_key;
$group_names = [];
$result = $db->execute("SELECT GROUP_ID, GROUP_NAME FROM " .
"SOCIAL_GROUPS");
while ($result && ($row = $db->fetchArray($result))) {
$group_names[$row['GROUP_ID']] = $row['GROUP_NAME'];
}
$user_names = [];
$result = $db->execute("SELECT USER_ID, USER_NAME FROM USERS");
while ($result && ($row = $db->fetchArray($result))) {
$user_names[$row['USER_ID']] = $row['USER_NAME'];
$this->mapHash(crawlHash("user" . $row['USER_ID'] . $key),
ResourceFolders::userPath($row['USER_NAME'],
$this->resource_path), self::WHOLE_KIND);
}
$result = $db->execute("SELECT ID, GROUP_ID, TITLE, LOCALE_TAG " .
"FROM GROUP_PAGE");
while ($result && ($row = $db->fetchArray($result))) {
if (!isset($group_names[$row['GROUP_ID']])) {
continue;
}
$ids = $row['GROUP_ID'] . $row['ID'];
$new = ResourceFolders::wikiPagePath(
$group_names[$row['GROUP_ID']], $row['LOCALE_TAG'],
$row['TITLE'], $this->resource_path);
$this->mapHash(crawlHash("group" . $ids . $key), $new,
self::PAGE_KIND);
$this->mapHash(crawlHash("thumb" . $ids . $key), $new,
self::THUMBS_KIND);
}
$result = $db->execute("SELECT ID, GROUP_ID, USER_ID, PUBDATE " .
"FROM GROUP_ITEM");
while ($result && ($row = $db->fetchArray($result))) {
if (!isset($group_names[$row['GROUP_ID']])) {
continue;
}
$ids = $row['GROUP_ID'] . "post" . $row['ID'];
$new = ResourceFolders::postPath($group_names[$row['GROUP_ID']],
$user_names[$row['USER_ID']] ?? "", $row['PUBDATE'],
$this->resource_path);
$this->mapHash(crawlHash("group" . $ids . $key), $new,
self::PAGE_KIND);
$this->mapHash(crawlHash("thumb" . $ids . $key), $new,
self::THUMBS_KIND);
}
$this->mapHash(crawlHash("group" . C\PUBLIC_GROUP_ID . $key),
ResourceFolders::feedsPath($this->resource_path),
self::WHOLE_KIND);
foreach ($this->new_folders as $hash => $new) {
if ($this->hash_kinds[$hash] != self::PAGE_KIND) {
continue;
}
foreach ($this->old_folders[$hash] as $path) {
if (!is_link($path) && is_file("$path/redirect.txt")) {
$target = trim(file_get_contents("$path/redirect.txt"));
$this->mapHash(crawlHash($target), $new,
self::THUMBS_KIND);
}
}
}
}
/**
* mapHash records the version 10 folder of a pre-version 10 hash when
* that hash has a folder on disk and has not been given one already.
*
* @param string $hash the pre-version 10 hash
* @param string $new_folder its version 10 folder
* @param string $kind PAGE_KIND, THUMBS_KIND or WHOLE_KIND
*/
public function mapHash($hash, $new_folder, $kind)
{
if (isset($this->old_folders[$hash]) &&
!isset($this->new_folders[$hash])) {
$this->new_folders[$hash] = $new_folder;
$this->hash_kinds[$hash] = $kind;
}
}
/**
* moveOldFolders moves each folder of the pre-version 10 layout to its
* version 10 place: a page's or post's folder to <new>/page and its
* thumbnail folder to <new>/thumbs, a user's folder or the feed
* pictures to <new>. Each moves by one rename, which does not look
* inside it. Folders are moved before links, so a link can be made
* again pointing at its target's version 10 place, by a path relative
* to the link. A folder whose version 10 place is taken, or whose hash
* nothing names, is moved under UNMIGRATED_FOLDER.
*
* @return array pre-version 10 path => version 10 path of each folder
* moved, for rewriteStoredPaths
*/
public function moveOldFolders()
{
$moved = [];
$moved_by_name = [];
$links = [];
/* a page's own folders go before thumbnails reached through a
redirect.txt, which several pages can share, so a page keeps
its own thumbnails where both are found */
$kind_order = [self::PAGE_KIND => 0, self::WHOLE_KIND => 1,
self::THUMBS_KIND => 2];
$hashes = array_keys($this->old_folders);
usort($hashes, fn($first, $second) =>
($kind_order[$this->hash_kinds[$first] ?? ""] ?? 3) <=>
($kind_order[$this->hash_kinds[$second] ?? ""] ?? 3));
foreach ($hashes as $hash) {
$paths = $this->old_folders[$hash];
foreach ($paths as $path) {
if (is_link($path)) {
$links[$path] = $hash;
continue;
}
$new_path = $this->newPathOf($hash, $path);
if ($new_path !== "" && !file_exists($new_path) &&
$this->renamePath($path, $new_path)) {
$moved[$path] = $new_path;
$moved_by_name[basename(dirname($path)) . "/" .
basename($path)] = $new_path;
$this->counts["folders moved"]++;
} else {
$this->keepUnmigrated($path);
}
}
}
foreach ($links as $path => $hash) {
/* a link names its target by the target's prefix folder and
hash, which are the same however the work directory's path
is written */
$target = rtrim(strtr((string)readlink($path), "\\", "/"), "/");
$new_path = $this->newPathOf($hash, $path);
$new_target = $moved_by_name[basename(dirname($target)) . "/" .
basename($target)] ?? "";
if ($new_path !== "" && $new_target !== "" &&
!file_exists($new_path) && !is_link($new_path)) {
if (!file_exists(dirname($new_path))) {
makePath(dirname($new_path));
}
symlink(ResourceFolders::relativeLinkTarget($new_path,
$new_target), $new_path);
$moved[$path] = $new_path;
$this->counts["links remade"]++;
$this->removeLink($path);
} else {
$this->keepUnmigrated($path);
}
}
return $moved;
}
/**
* removeEmptyPrefixFolders deletes each three-letter prefix folder of
* the pre-version 10 layout that the moves left holding nothing but
* files named in OS_FOLDER_FILES. A prefix folder holding anything
* else is kept, and the report names it with the first REPORTED_NAMES
* names in it, so the site's administrator can see why it stayed: a
* folder whose name is no pre-version 10 hash, a hash that does not
* begin with the prefix, a plain file, or a folder whose move failed.
*/
public function removeEmptyPrefixFolders()
{
foreach (scandir($this->resource_path) ?: [] as $prefix) {
$prefix_path = $this->resource_path . "/$prefix";
if (strlen($prefix) != self::OLD_PREFIX_LEN ||
!preg_match('/^[A-Za-z0-9_\-]+$/', $prefix) ||
is_link($prefix_path) || !is_dir($prefix_path)) {
continue;
}
$left = [];
foreach (scandir($prefix_path) ?: [] as $name) {
$path = "$prefix_path/$name";
if ($name == "." || $name == ".." || (is_file($path) &&
!is_link($path) && in_array($name,
self::OS_FOLDER_FILES))) {
continue;
}
$left[] = $name;
}
if (empty($left)) {
foreach (self::OS_FOLDER_FILES as $os_file) {
if (is_file("$prefix_path/$os_file")) {
unlink("$prefix_path/$os_file");
}
}
rmdir($prefix_path);
continue;
}
$this->counts["prefix folders kept"]++;
$this->log("kept $prefix_path, which holds " . implode(", ",
array_slice($left, 0, self::REPORTED_NAMES)) .
((count($left) > self::REPORTED_NAMES) ? ", ..." : ""));
}
}
/**
* newPathOf gives where one folder of the pre-version 10 layout goes
* in the version 10 layout.
*
* @param string $hash its hash
* @param string $path its path, whose name begins with t for a
* thumbnail folder, or is the hash of "thumb" and the ids
* @return string the version 10 path, or "" when nothing names the
* hash
*/
public function newPathOf($hash, $path)
{
if (!isset($this->new_folders[$hash])) {
return "";
}
$new_folder = $this->new_folders[$hash];
$kind = $this->hash_kinds[$hash];
$is_thumb_name = strlen(basename($path)) > self::OLD_HASH_LEN;
if ($kind == self::WHOLE_KIND) {
/* before version 10 the feed pictures were in the t folder of
their hash, and a user's icon in the folder of its hash */
$is_feeds = basename($new_folder) == ResourceFolders::FEEDS_FOLDER;
return ($is_feeds == $is_thumb_name) ? $new_folder : "";
}
$sub_folder = ($kind == self::PAGE_KIND && !$is_thumb_name) ?
ResourceFolders::PAGE_FOLDER : ResourceFolders::THUMBS_FOLDER;
return "$new_folder/$sub_folder";
}
/**
* renamePath moves a folder, making the folders its new place sits in
* first.
*
* @param string $path the folder to move
* @param string $new_path where it goes
* @return bool whether it was moved
*/
public function renamePath($path, $new_path)
{
if (!file_exists(dirname($new_path)) &&
!makePath(dirname($new_path))) {
return false;
}
return @rename($path, $new_path);
}
/**
* keepUnmigrated moves a folder or link of the pre-version 10 layout
* that has no version 10 place to
* app/resources/UNMIGRATED_FOLDER/<prefix>/<name>, so the pre-version
* 10 layout is gone from app/resources but nothing is deleted.
*
* @param string $path the folder or link
*/
public function keepUnmigrated($path)
{
$prefix = basename(dirname($path));
$new_path = $this->resource_path . "/" . self::UNMIGRATED_FOLDER .
"/$prefix/" . basename($path);
if (!file_exists($new_path) && !is_link($new_path) &&
$this->renamePath($path, $new_path)) {
$this->counts["folders kept in unmigrated"]++;
}
}
/**
* removeLink removes a link to a folder, which Windows removes with
* rmdir and Linux and macOS with unlink.
*
* @param string $path the link
*/
public function removeLink($path)
{
if (!@unlink($path)) {
@rmdir($path);
}
}
/**
* rewriteStoredPaths changes the folder paths kept outside the folders
* themselves to their version 10 places: those in recommendation.txt
* and needs_descriptions.txt and the RESOURCE_PATH column of
* GROUP_RESOURCE_RECOMMENDATION. The lists in needs_thumbs name
* pre-version 10 folders and are deleted; a missing thumbnail is made
* when it is asked for.
*
* @param array $moved pre-version 10 path => version 10 path, as
* moveOldFolders gives
*/
public function rewriteStoredPaths($moved)
{
if (empty($moved)) {
return;
}
$replacements = [];
foreach ($moved as $old => $new) {
$replacements[$old . "/"] = $new . "/";
}
foreach (self::KEPT_TOP_FILES as $file_name) {
$file_path = $this->resource_path . "/$file_name";
if (is_file($file_path)) {
$text = file_get_contents($file_path) . "\n";
$lines = explode("\n", $text);
foreach ($lines as $index => $line) {
$lines[$index] = rtrim(strtr($line . "/",
$replacements), "/");
}
file_put_contents($file_path, rtrim(implode("\n", $lines),
"\n") . "\n");
$this->counts["paths rewritten"]++;
}
}
foreach (glob($this->resource_path . "/needs_thumbs/*.txt") ?: []
as $list) {
unlink($list);
}
$db = $this->db;
$rows = [];
$result = $db->execute("SELECT DISTINCT RESOURCE_PATH FROM " .
"GROUP_RESOURCE_RECOMMENDATION");
while ($result && ($row = $db->fetchArray($result))) {
$rows[] = $row['RESOURCE_PATH'];
}
foreach ($rows as $old_path) {
$new_path = strtr($old_path, $replacements);
if ($new_path !== $old_path) {
$db->execute("UPDATE GROUP_RESOURCE_RECOMMENDATION SET " .
"RESOURCE_PATH = ? WHERE RESOURCE_PATH = ?",
[$new_path, $old_path]);
$this->counts["paths rewritten"]++;
}
}
}
/**
* moveSiteFiles moves the site's appearance files, which before
* version 10 sat directly in app/resources, that is, every file there
* other than KEPT_TOP_FILES, into ResourceFolders::sitePath. It moves
* the pre-version 10 folder of each host with a row in
* DOMAIN_APPEARANCE, app/resources/<host>, into
* ResourceFolders::domainPath. A file or folder whose version 10 place
* is taken is left where it is.
*/
public function moveSiteFiles()
{
$site_path = ResourceFolders::sitePath($this->resource_path);
foreach (scandir($this->resource_path) ?: [] as $name) {
$path = $this->resource_path . "/$name";
if (!is_link($path) && is_file($path) &&
!in_array($name, self::KEPT_TOP_FILES) &&
!file_exists("$site_path/$name") &&
$this->renamePath($path, "$site_path/$name")) {
$this->counts["site files moved"]++;
}
}
$db = $this->db;
$hosts = [];
$result = $db->execute("SELECT DOMAIN FROM DOMAIN_APPEARANCE");
while ($result && ($row = $db->fetchArray($result))) {
$hosts[] = $row['DOMAIN'];
}
foreach ($hosts as $host) {
$old_path = $this->resource_path . "/" .
preg_replace("/[^A-Za-z0-9_\-\.]/", "", (string)$host);
$new_path = ResourceFolders::domainPath($host,
$this->resource_path);
if ($new_path !== "" && $old_path !== $this->resource_path . "/"
&& !in_array(basename($old_path), self::KEPT_TOP_FOLDERS) &&
!is_link($old_path) && is_dir($old_path) &&
!file_exists($new_path) &&
$this->renamePath($old_path, $new_path)) {
$this->counts["host folders moved"]++;
}
}
}
/**
* log passes one line of the report to log_function, if there is one.
*
* @param string $line the line
*/
public function log($line)
{
if (is_callable($this->log_function)) {
($this->log_function)($line);
}
}
}