372 lines
12 KiB
PHP
372 lines
12 KiB
PHP
|
<?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
|
||
|
);
|
||
|
}
|
||
|
}
|