artabro/wire/core/ModulesDuplicates.php

372 lines
12 KiB
PHP
Raw Permalink Normal View History

2024-08-27 11:35:37 +02:00
<?php namespace ProcessWire;
/**
* ProcessWire Modules Duplicates
*
* Provides functions for managing sitautions where more than one
* copy of the same module is intalled. This is a helper for the Modules class.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class ModulesDuplicates extends Wire {
/**
* Array of modules where more than one copy was found
*
* Associative array of 'ModuleName' => array(path1, path2, ...)
*
* @var array
*
*/
protected $duplicates = array();
/**
* Specifies which module file to use in cases where there is more than one
*
* Array of 'ModuleName' => '/path/to/file/from/pw/root/file.module'
*
* @var array
*
*/
protected $duplicatesUse = array();
/**
* Number of new duplicates found while loading modules
*
* @var int
*
*/
protected $numNewDuplicates = 0;
/**
* Return quantity of new duplicates found while loading modules
*
* @return int
*
*/
public function numNewDuplicates() {
return $this->numNewDuplicates;
}
/**
* Get the current duplicate in use (string) or null if not specified
*
* @param $className
* @return string|null Pathname or null
*
*/
public function getCurrent($className) {
return isset($this->duplicatesUse[$className]) ? $this->duplicatesUse[$className] : null;
}
/**
* Does the given module class have a duplicate?
*
* @param string $className
* @param string $pathname Optionally specify the duplicate to check
* @return bool
*
*/
public function hasDuplicate($className, $pathname = '') {
if(!isset($this->duplicates[$className])) return false;
if($pathname) {
$rootPath = $this->wire()->config->paths->root;
if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
return in_array($pathname, $this->duplicates[$className]);
}
return true;
}
/**
* Add a duplicate to the list
*
* @param $className
* @param $pathname
* @param bool $current Is this the current one in use?
*
*/
public function addDuplicate($className, $pathname, $current = false) {
if(!isset($this->duplicates[$className])) $this->duplicates[$className] = array();
$rootPath = $this->wire()->config->paths->root;
if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
if(!in_array($pathname, $this->duplicates[$className])) {
$this->duplicates[$className][] = $pathname;
}
if($current) {
$this->duplicatesUse[$className] = $pathname;
}
}
/**
* Add multiple duplicates
*
* @param $className
* @param array $files
*
*/
public function addDuplicates($className, array $files) {
foreach($files as $file) {
$this->addDuplicate($className, $file);
}
}
/**
* Add duplicates from module config data
*
* @param $className
* @param array $data
*
*/
public function addFromConfigData($className, array $data) {
$files = isset($data['-dups']) ? $data['-dups'] : array();
$using = isset($data['-dups-use']) ? $data['-dups-use'] : '';
if(count($files)) $this->addDuplicates($className, $files);
if($using) $this->addDuplicate($className, $using, true); // set current, in-use
}
/**
* Return a list of duplicate modules that were found
*
* If given a module className, the following is returned:
*
* Array(
* 'files' => array(file1, file2, ...)
* 'using' => '/path/to/file/from/pw/root/ModuleName.module' or blank if not defined
* )
*
* If no className is specivied, the following is returned:
*
* Array(
* 'ModuleName' => array(file1, file2, ...),
* 'ModuleName' => array(file1, file2, ...),
* ...and so on...
* )
*
* @param string|Module|int $className Optionally return only duplicates for given module name
*
* @return array
*
*/
public function getDuplicates($className = '') {
if(!$className) return $this->duplicates;
$modules = $this->wire()->modules;
$className = $modules->getModuleClass($className);
$files = isset($this->duplicates[$className]) ? $this->duplicates[$className] : array();
$using = isset($this->duplicatesUse[$className]) ? $this->duplicatesUse[$className] : '';
$rootPath = $this->wire()->config->paths->root;
foreach($files as $key => $file) {
$file = rtrim($rootPath, '/') . $file;
if(!file_exists($file)) {
unset($files[$key]);
}
}
if(count($files) > 1 && !$using) {
$using = $modules->getModuleFile($className);
$using = str_replace($rootPath, '/', $using);
}
if(count($files) < 2) {
// no need to store duplicate info if only 0 or 1
//unset($this->duplicates[$className], $this->duplicatesUse[$className]);
$files = array();
$using = '';
}
return array('files' => $files, 'using' => $using);
}
/**
* For a module that has duplicates, tell it which file to use
*
* @param string $className
* @param string $pathname Full path and filename to module file
*
* @throws WireException if given information that can't be resolved
*
*/
public function setUseDuplicate($className, $pathname) {
$modules = $this->wire()->modules;
$className = $modules->getModuleClass($className);
$rootPath = $this->wire()->config->paths->root;
if(!isset($this->duplicates[$className])) {
throw new WireException("Module $className does not have duplicates");
}
$pathname = str_replace($rootPath, '/', $pathname);
if(!in_array($pathname, $this->duplicates[$className])) {
throw new WireException("Duplicate module pathname must be one of: " . implode(" \n", $this->duplicates[$className]));
}
if(!file_exists($rootPath . ltrim($pathname, '/'))) {
throw new WireException("Duplicate module file does not exist: $pathname");
}
$this->duplicatesUse[$className] = $pathname;
$configData = $modules->getModuleConfigData($className);
$configData['-dups-use'] = $pathname;
$modules->saveModuleConfigData($className, $configData);
}
/**
* Update the database so that modules have information on their duplicates
*
*/
public function updateDuplicates() {
$modules = $this->wire()->modules;
$rootPath = $this->wire()->config->paths->root;
// store duplicate information in each module's data field
foreach($this->getDuplicates() as $moduleName => $files) {
$dup = $this->getDuplicates($moduleName); // so that we also have 'using' info
$files = $dup['files'];
$using = $dup['using'];
foreach($files as $key => $file) {
// make files relative to site root, for portability
$file = str_replace($rootPath, '/', $file);
$files[$key] = $file;
}
$files = array_unique($files);
$configData = $modules->getModuleConfigData($moduleName);
if((empty($configData['-dups']) && !empty($files))
|| (empty($configData['-dups-use']) || $configData['-dups-use'] != $using)
|| (isset($configData['-dups']) && implode(' ', $configData['-dups']) != implode(' ', $files))
) {
$this->duplicates[$moduleName] = $files;
$this->duplicatesUse[$moduleName] = $using;
$configData['-dups'] = $files;
$configData['-dups-use'] = $using;
$modules->saveModuleConfigData($moduleName, $configData);
}
}
// update any modules that no longer have duplicates
$removals = array();
$query = $this->wire()->database->prepare("SELECT `class`, `flags` FROM modules WHERE `flags` & :flag");
$query->bindValue(':flag', Modules::flagsDuplicate, \PDO::PARAM_INT);
$query->execute();
/** @noinspection PhpAssignmentInConditionInspection */
while($row = $query->fetch(\PDO::FETCH_NUM)) {
list($class, $flags) = $row;
if(empty($this->duplicates[$class])) {
$flags = $flags & ~Modules::flagsDuplicate;
$removals[$class] = $flags;
}
unset($this->duplicatesUse[$class]); // just in case
}
foreach($removals as $class => $flags) {
$modules->setFlags($class, $flags);
$configData = $modules->getModuleConfigData($class);
unset($configData['-dups'], $configData['-dups-use']);
$modules->saveModuleConfigData($class, $configData);
}
}
/**
* Record a duplicate at runtime
*
* @param string $basename Name of module
* @param string $pathname Path of module
* @param string $pathname2 Second path of module
* @param array $installed Installed module info array
*
*/
public function recordDuplicate($basename, $pathname, $pathname2, &$installed) {
$config = $this->wire()->config;
$modules = $this->wire()->modules;
$rootPath = $config->paths->root;
// ensure paths start from root of PW install
if(strpos($pathname, $rootPath) === 0) $pathname = str_replace($rootPath, '/', $pathname);
if(strpos($pathname2, $rootPath) === 0) $pathname2 = str_replace($rootPath, '/', $pathname2);
// there are two copies of the module on the file system (likely one in /site/modules/ and another in /wire/modules/)
if(!isset($this->duplicates[$basename])) {
$this->duplicates[$basename] = array($pathname, $pathname2); // array(str_replace($rootPath, '/', $this->getModuleFile($basename)));
$this->numNewDuplicates++;
}
if(!in_array($pathname, $this->duplicates[$basename])) {
$this->duplicates[$basename][] = $pathname;
$this->numNewDuplicates++;
}
if(!in_array($pathname2, $this->duplicates[$basename])) {
$this->duplicates[$basename][] = $pathname2;
$this->numNewDuplicates++;
}
if(isset($installed[$basename]['flags'])) {
$flags = $installed[$basename]['flags'];
} else {
$flags = $modules->getFlags($basename);
}
if($flags & Modules::flagsDuplicate) {
// flags already represent duplicate status
} else {
// make database aware this module has multiple files by adding the duplicate flag
$this->numNewDuplicates++; // trigger update needed
$flags = $flags | Modules::flagsDuplicate;
$modules->setFlags($basename, $flags);
}
$err = sprintf($this->_('There appear to be multiple copies of module "%s" on the file system.'), $basename) . ' ';
$this->wire()->log->save('modules', $err);
$user = $this->wire()->user;
if($user && $user->isSuperuser()) {
$err .= $this->_('Please edit the module settings to tell ProcessWire which one to use:') . ' ' .
"<a href='" . $config->urls->admin . 'module/edit?name=' . $basename . "'>$basename</a>";
$this->warning($err, Notice::allowMarkup);
}
//$this->message("recordDuplicate($basename, $pathname) $this->numNewDuplicates"); //DEBUG
//$this->message($this->duplicates[$basename]);//DEBUG
}
/**
* Populate duplicates info into config data, when applicable
*
* @param $className
* @param array $configData
*
* @return array Updated configData
*
*/
public function getDuplicatesConfigData($className, array $configData = array()) {
$config = $this->wire()->config;
// ensure original duplicates info is retained and validate that it is still current
if(isset($this->duplicates[$className])) {
foreach($this->duplicates[$className] as $key => $file) {
$pathname = rtrim($config->paths->root, '/') . $file;
if(!file_exists($pathname)) {
unset($this->duplicates[$className][$key]);
}
}
if(count($this->duplicates[$className]) < 2) {
// no need to store any info for this if there's only 0 or 1
unset($this->duplicates[$className], $this->duplicatesUse[$className], $configData['-dups'], $configData['-dups-use']);
} else {
$configData['-dups'] = $this->duplicates[$className];
if(isset($this->duplicatesUse[$className])) {
$pathname = rtrim($config->paths->root, '/') . $this->duplicatesUse[$className];
if(file_exists($pathname)) {
$configData['-dups-use'] = $this->duplicatesUse[$className];
} else {
unset($configData['-dups-use'], $this->duplicatesUse[$className]);
}
}
}
} else if(empty($this->duplicates[$className]) && isset($configData['-dups'])) {
unset($configData['-dups'], $configData['-dups-use']);
}
return $configData;
}
public function getDebugData() {
return array(
'duplicates' => $this->duplicates,
'duplicatesUse' => $this->duplicatesUse
);
}
}