2022-03-08 15:55:41 +01:00
< ? php namespace ProcessWire ;
/**
* ProcessWire Modules
*
* Loads and manages all runtime modules for ProcessWire
*
* Note that when iterating , find (), or calling any other method that returns module ( s ), excepting get (), a ModulePlaceholder may be
* returned rather than a real Module . ModulePlaceholders are used in instances when the module may or may not be needed at runtime
* in order to save resources . As a result , anything iterating through these Modules should check to make sure it ' s not a ModulePlaceholder
* before using it . If it ' s a ModulePlaceholder , then the real Module can be instantiated / retrieved by $modules -> get ( $className ) .
*
2023-03-10 19:41:40 +01:00
* ProcessWire 3. x , Copyright 2022 by Ryan Cramer
2022-03-08 15:55:41 +01:00
* https :// processwire . com
*
* #pw-summary Loads and manages all modules in ProcessWire.
* #pw-body =
* The `$modules` API variable is most commonly used for getting individual modules to use their API .
* ~~~~~
* // Getting a module by name
* $m = $modules -> get ( 'MarkupPagerNav' );
*
* // Getting a module by name (alternate)
* $m = $modules -> MarkupPagerNav ;
* ~~~~~
*
* #pw-body
*
* @ todo Move all module information methods to a ModulesInfo class
* @ todo Move all module loading methods to a ModulesLoad class
*
* @ method void refresh ( $showMessages = false ) Refresh the cache that stores module files by recreating it
* @ method null | Module install ( $class , $options = array ())
* @ method bool | int delete ( $class )
* @ method bool uninstall ( $class )
* @ method bool saveModuleConfigData ( $className , array $configData ) Alias of saveConfig () method #pw-internal
* @ method bool saveConfig ( $class , $data , $value = null )
* @ method InputfieldWrapper | null getModuleConfigInputfields ( $moduleName , InputfieldWrapper $form = null ) #pw-internal
* @ method void moduleVersionChanged ( Module $module , $fromVersion , $toVersion ) #pw-internal
* @ method bool | string isUninstallable ( $class , $returnReason = false ) hookable in 3.0 . 181 + #pw-internal
*
*/
class Modules extends WireArray {
/**
* Whether or not module debug mode is active
*
*/
protected $debug = false ;
/**
* Flag indicating the module may have only one instance at runtime .
*
*/
const flagsSingular = 1 ;
/**
* Flag indicating that the module should be instantiated at runtime , rather than when called upon .
*
*/
const flagsAutoload = 2 ;
/**
* Flag indicating the module has more than one copy of it on the file system .
*
*/
const flagsDuplicate = 4 ;
/**
* When combined with flagsAutoload , indicates that the autoload is conditional
*
*/
const flagsConditional = 8 ;
/**
* When combined with flagsAutoload , indicates that the module ' s autoload state is temporarily disabled
*
*/
const flagsDisabled = 16 ;
/**
* Indicates module that maintains a configurable interface but with no interactive Inputfields
*
*/
const flagsNoUserConfig = 32 ;
/**
* Module where no file could be located
*
*/
const flagsNoFile = 64 ;
/**
* Filename for module info cache file
*
*/
const moduleInfoCacheName = 'Modules.info' ;
/**
* Filename for verbose module info cache file
*
*/
const moduleInfoCacheVerboseName = 'ModulesVerbose.info' ;
/**
* Filename for uninstalled module info cache file
*
*/
const moduleInfoCacheUninstalledName = 'ModulesUninstalled.info' ;
/**
* Cache name for module version change cache
*
*/
const moduleLastVersionsCacheName = 'ModulesVersions.info' ;
/**
* Array of modules that are not currently installed , indexed by className => filename
*
*/
protected $installable = array ();
/**
* An array of module database IDs indexed by : class = > id
*
* Used internally for database operations
*
*/
protected $moduleIDs = array ();
/**
* Full system paths where modules are stored
*
* index 0 must be the core modules path ( / i . e . / wire / modules / )
*
*/
protected $paths = array ();
/**
* Cached module configuration data indexed by module ID
*
* Values are integer 1 for modules that have config data but data is not yet loaded .
* Values are an array for modules have have config data and has been loaded .
*
*/
protected $configData = array ();
/**
* Module created dates indexed by module ID
*
*/
protected $createdDates = array ();
/**
* Have the modules been init ' d () ?
*
*/
protected $initialized = false ;
/**
* Becomes an array if debug mode is on
*
*/
protected $debugLog = array ();
/**
* Array of moduleName => condition
*
* Condition can be either an anonymous function or a selector string to be evaluated at ready () .
*
*/
protected $conditionalAutoloadModules = array ();
/**
* Cache of module information
*
*/
protected $moduleInfoCache = array ();
/**
* Cache of module information ( verbose text ) including : summary , author , href , file , core
*
*/
protected $moduleInfoCacheVerbose = array ();
/**
* Cache of uninstalled module information ( verbose for uninstalled ) including : summary , author , href , file , core
*
* Note that this one is indexed by class name rather than by ID ( since uninstalled modules have no ID )
*
*/
protected $moduleInfoCacheUninstalled = array ();
/**
* Cache of module information from DB used across multiple calls temporarily by load () method
*
*/
protected $modulesTableCache = array ();
/**
* Cache of namespace => path for unique module namespaces
*
* @ var array | null Becomes an array once populated
*
*/
protected $moduleNamespaceCache = null ;
/**
* Last known versions of modules , for version change tracking
*
* @ var array of ModuleName ( string ) => last known version ( integer | string )
*
*/
protected $modulesLastVersions = array ();
/**
* Array of module ID => flags ( int )
*
* @ var array
*
*/
protected $moduleFlags = array ();
/**
* Array of moduleName => substituteModuleName to be used when moduleName doesn ' t exist
*
* Primarily for providing backwards compatiblity with modules assumed installed that
* may no longer be in core .
*
* see setSubstitutes () method
*
*/
protected $substitutes = array ();
/**
* Instance of ModulesDuplicates
*
* @ var ModulesDuplicates
*
*/
protected $duplicates ;
/**
* Module file extensions indexed by module name where value 1 =. module , and 2 =. module . php
*
* @ var array
*
*/
protected $moduleFileExts = array ();
/**
* Dir for core modules relative to root path , i . e . '/wire/modules/'
*
* @ var string
*
*/
protected $coreModulesDir = '' ;
/**
* Array of moduleName => order to indicate autoload order when necessary
*
* @ var array
*
*/
protected $autoloadOrders = array ();
/**
* Are we currently refreshing ?
*
* @ var bool
*
*/
protected $refreshing = false ;
/**
* Properties that only appear in 'verbose' moduleInfo
*
* @ var array
*
*/
protected $moduleInfoVerboseKeys = array (
'summary' ,
'author' ,
'href' ,
'file' ,
'core' ,
'versionStr' ,
'permissions' ,
'searchable' ,
'page' ,
// 'languages',
2023-03-10 19:41:40 +01:00
);
2022-03-08 15:55:41 +01:00
/**
* Core module types that are isolated by directory
*
* @ var array
*
*/
protected $coreTypes = array (
'AdminTheme' ,
'Fieldtype' ,
'Inputfield' ,
'Jquery' ,
'LanguageSupport' ,
'Markup' ,
'Process' ,
'Session' ,
'System' ,
'Textformatter' ,
2023-03-10 19:41:40 +01:00
);
2022-03-08 15:55:41 +01:00
/**
* Construct the Modules
*
* @ param string $path Core modules path ( you may add other paths with addPath method )
*
*/
public function __construct ( $path ) {
parent :: __construct ();
$this -> addPath ( $path );
}
2022-11-05 18:32:48 +01:00
/**
* Wired to API
*
* #pw-internal
*
*/
2022-03-08 15:55:41 +01:00
public function wired () {
2023-03-10 19:41:40 +01:00
$this -> coreModulesDir = '/' . $this -> wire () -> config -> urls -> data ( 'modules' );
2022-03-08 15:55:41 +01:00
parent :: wired ();
}
/**
* Get the ModulesDuplicates instance
*
* #pw-internal
*
* @ return ModulesDuplicates
*
*/
public function duplicates () {
if ( is_null ( $this -> duplicates )) $this -> duplicates = $this -> wire ( new ModulesDuplicates ());
return $this -> duplicates ;
}
/**
* Add another modules path , must be called before init ()
*
* #pw-internal
*
* @ param string $path
*
*/
public function addPath ( $path ) {
$this -> paths [] = $path ;
}
/**
* Return all assigned module root paths
*
* #pw-internal
*
* @ return array of modules paths , with index 0 always being the core modules path .
*
*/
public function getPaths () {
return $this -> paths ;
}
/**
* Initialize modules
*
* Must be called after construct before this class is ready to use
*
* #pw-internal
*
* @ see load ()
*
*/
public function init () {
$this -> setTrackChanges ( false );
$this -> loadModuleInfoCache ();
$this -> loadModulesTable ();
if ( ! empty ( $this -> autoloadOrders )) $this -> preloadModules ();
foreach ( $this -> paths as $path ) {
$this -> load ( $path );
}
$this -> modulesTableCache = array (); // clear out data no longer needed
}
/**
* Include site preload modules
*
* Preload modules load before all other modules , including core modules . In order
* for a module to be a preload module , it must meet the following conditions :
*
* - Module info `autoload` value is integer of 10000 or greater , i . e . `[ 'autoload' => 10000 ]`
* - Module info `singular` value must be non - empty , i . e . `[ 'singular' => true ]`
* - Module file is located in : / site / modules / ModuleName / ModuleName . module . php
* - Module cannot load any other modules at least until ready () method called .
* - Module cannot have any `requires` dependencies to any other modules .
*
* Please note the above is specifically stating that the module must be in its
* own “site / ModuleName / ” directory and have the “ . module . php” extension . Using
* just the “ . module” extension is not supported for preload modules .
*
* @ since 3.0 . 173
*
*/
protected function preloadModules () {
if ( ! isset ( $this -> paths [ 1 ])) return ;
arsort ( $this -> autoloadOrders );
foreach ( $this -> autoloadOrders as $moduleName => $order ) {
if ( $order < 10000 ) break ;
if ( ! isset ( $this -> moduleIDs [ $moduleName ])) continue ;
$moduleID = $this -> moduleIDs [ $moduleName ];
if ( ! isset ( $this -> moduleInfoCache [ $moduleID ])) continue ;
$info = $this -> moduleInfoCache [ $moduleID ];
if ( empty ( $info [ 'singular' ])) continue ;
$file = $this -> paths [ 1 ] . " $moduleName / $moduleName .module.php " ;
if ( ! file_exists ( $file ) || ! $this -> includeModuleFile ( $file , $moduleName )) continue ;
2022-11-05 18:32:48 +01:00
if ( ! isset ( $info [ 'namespace' ])) $info [ 'namespace' ] = '' ;
2022-03-08 15:55:41 +01:00
$className = $info [ 'namespace' ] . $moduleName ;
$module = $this -> newModule ( $className , $moduleName );
if ( $module ) parent :: set ( $moduleName , $module );
}
}
/**
* Modules class accepts only Module instances , per the WireArray interface
*
* #pw-internal
*
* @ param Wire $item
* @ return bool
*
*/
public function isValidItem ( $item ) {
return $item instanceof Module ;
}
/**
* The key / index used for each module in the array is it ' s class name , per the WireArray interface
*
* #pw-internal
*
* @ param Wire $item
2023-03-10 19:41:40 +01:00
* @ return string
2022-03-08 15:55:41 +01:00
*
*/
public function getItemKey ( $item ) {
return $this -> getModuleClass ( $item );
}
/**
* There is no blank / generic module type , so makeBlankItem returns null
*
* #pw-internal
*
*/
public function makeBlankItem () {
return null ;
}
/**
* Make a new / blank WireArray
*
* #pw-internal
*
*/
public function makeNew () {
// ensures that find(), etc. operations don't initalize a new Modules() class
return $this -> wire ( new WireArray ());
}
/**
* Make a new populated copy of a WireArray containing all the modules
*
* #pw-internal
*
* @ return WireArray
*
*/
public function makeCopy () {
// ensures that find(), etc. operations don't initalize a new Modules() class
$copy = $this -> makeNew ();
foreach ( $this -> data as $key => $value ) $copy [ $key ] = $value ;
$copy -> resetTrackChanges ( $this -> trackChanges ());
return $copy ;
}
/**
* Initialize all the modules that are loaded at boot
*
* #pw-internal
*
* @ param null | array | Modules $modules
* @ param array $completed
* @ param int $level
*
*/
public function triggerInit ( $modules = null , $completed = array (), $level = 0 ) {
$debugKey = null ;
$debugKey2 = null ;
if ( $this -> debug ) {
$debugKey = $this -> debugTimerStart ( " triggerInit $level " );
$this -> message ( " triggerInit(level= $level ) " );
}
$queue = array ();
if ( is_null ( $modules )) $modules = $this ;
foreach ( $modules as $class => $module ) {
if ( $module instanceof ModulePlaceholder ) {
// skip modules that aren't autoload and those that are conditional autoload
if ( ! $module -> autoload ) continue ;
if ( isset ( $this -> conditionalAutoloadModules [ $class ])) continue ;
}
if ( $this -> debug ) $debugKey2 = $this -> debugTimerStart ( " triggerInit $level ( $class ) " );
$info = $this -> getModuleInfo ( $module );
$skip = false ;
// module requires other modules
foreach ( $info [ 'requires' ] as $requiresClass ) {
if ( in_array ( $requiresClass , $completed )) continue ;
$dependencyInfo = $this -> getModuleInfo ( $requiresClass );
if ( empty ( $dependencyInfo [ 'autoload' ])) {
// if dependency isn't an autoload one, there's no point in waiting for it
if ( $this -> debug ) $this -> warning ( " Autoload module ' $module ' requires a non-autoload module ' $requiresClass ' " );
continue ;
} else if ( isset ( $this -> conditionalAutoloadModules [ $requiresClass ])) {
// autoload module requires another autoload module that may or may not load
if ( $this -> debug ) $this -> warning ( " Autoload module ' $module ' requires a conditionally autoloaded module ' $requiresClass ' " );
continue ;
}
// dependency is autoload and required by this module, so queue this module to init later
$queue [ $class ] = $module ;
$skip = true ;
break ;
}
if ( ! $skip ) {
if ( $info [ 'autoload' ] !== false ) {
if ( $info [ 'autoload' ] === true || $this -> isAutoload ( $module )) {
$this -> initModule ( $module );
}
}
$completed [] = $class ;
}
if ( $this -> debug ) $this -> debugTimerStop ( $debugKey2 );
}
// if there is a dependency queue, go recursive till the queue is completed
if ( count ( $queue ) && $level < 3 ) {
$this -> triggerInit ( $queue , $completed , $level + 1 );
}
$this -> initialized = true ;
if ( $this -> debug ) if ( $debugKey ) $this -> debugTimerStop ( $debugKey );
if ( ! $level && ( empty ( $this -> moduleInfoCache ))) { // || empty($this->moduleInfoCacheVerbose))) {
if ( $this -> debug ) $this -> message ( " saveModuleInfoCache from triggerInit " );
$this -> saveModuleInfoCache ();
}
}
/**
* Given a class name , return the constructed module
*
* @ param string $className Module class name
* @ param string $moduleName Optional module name only ( no namespace )
* @ return Module | null
*
*/
protected function newModule ( $className , $moduleName = '' ) {
if ( ! $moduleName ) {
$moduleName = wireClassName ( $className , false );
$className = wireClassName ( $className , true );
}
$debugKey = $this -> debug ? $this -> debugTimerStart ( " newModule( $moduleName ) " ) : null ;
if ( ! class_exists ( $className , false )) $this -> includeModule ( $moduleName );
if ( ! class_exists ( $className , false )) {
// attempt 2.x module in dedicated namespace or root namespace
$className = $this -> getModuleNamespace ( $moduleName ) . $moduleName ;
}
if ( ProcessWire :: getNumInstances () > 1 ) {
// in a multi-instance environment, ensures that anything happening during
// the module __construct is using the right instance. necessary because the
// construct method runs before the wire instance is set to the module
$wire1 = ProcessWire :: getCurrentInstance ();
$wire2 = $this -> wire ();
if ( $wire1 !== $wire2 ) {
ProcessWire :: setCurrentInstance ( $wire2 );
} else {
$wire1 = null ;
}
} else {
$wire1 = null ;
}
try {
$module = $this -> wire ( new $className ());
} catch ( \Exception $e ) {
$this -> error ( sprintf ( $this -> _ ( 'Failed to construct module: %s' ), $className ) . " - " . $e -> getMessage ());
$module = null ;
}
if ( $this -> debug ) $this -> debugTimerStop ( $debugKey );
if ( $wire1 ) ProcessWire :: setCurrentInstance ( $wire1 );
return $module ;
}
/**
* Return a new ModulePlaceholder for the given className
*
* @ param string $className Module class this placeholder will stand in for
* @ param string $ns Module namespace
* @ param string $file Full path and filename of $className
* @ param bool $singular Is the module a singular module ?
* @ param bool $autoload Is the module an autoload module ?
* @ return ModulePlaceholder
*
*/
protected function newModulePlaceholder ( $className , $ns , $file , $singular , $autoload ) {
2023-03-10 19:41:40 +01:00
/** @var ModulePlaceholder $module */
2022-03-08 15:55:41 +01:00
$module = $this -> wire ( new ModulePlaceholder ());
$module -> setClass ( $className );
$module -> setNamespace ( $ns );
$module -> singular = $singular ;
$module -> autoload = $autoload ;
$module -> file = $file ;
return $module ;
}
/**
* Initialize a single module
*
* @ param Module $module
* @ param array $options
* - `clearSettings` ( bool ) : When true , module settings will be cleared when appropriate to save space . ( default = true )
* - `configOnly` ( bool ) : When true , module init () method NOT called , but config data still set ( default = false ) 3.0 . 169 +
* - `configData` ( array ) : Extra config data merge with module’ s config data ( default = []) 3.0 . 169 +
* - `throw` ( bool ) : When true , exceptions will be allowed to pass through . ( default = false )
* @ return bool True on success , false on fail
* @ throws \Exception Only if the `throw` option is true .
*
*/
protected function initModule ( Module $module , array $options = array ()) {
$result = true ;
$debugKey = null ;
$clearSettings = isset ( $options [ 'clearSettings' ]) ? ( bool ) $options [ 'clearSettings' ] : true ;
$throw = isset ( $options [ 'throw' ]) ? ( bool ) $options [ 'throw' ] : false ;
if ( $this -> debug ) {
static $n = 0 ;
$this -> message ( " initModule ( " . ( ++ $n ) . " ): " . wireClassName ( $module ));
}
// if the module is configurable, then load its config data
// and set values for each before initializing the module
$extraConfigData = isset ( $options [ 'configData' ]) ? $options [ 'configData' ] : null ;
$this -> setModuleConfigData ( $module , null , $extraConfigData );
$moduleName = wireClassName ( $module , false );
$moduleID = isset ( $this -> moduleIDs [ $moduleName ]) ? $this -> moduleIDs [ $moduleName ] : 0 ;
if ( $moduleID && isset ( $this -> modulesLastVersions [ $moduleID ])) {
$this -> checkModuleVersion ( $module );
}
if ( method_exists ( $module , 'init' ) && empty ( $options [ 'configOnly' ])) {
if ( $this -> debug ) {
$debugKey = $this -> debugTimerStart ( " initModule( $moduleName ) " );
}
try {
$module -> init ();
} catch ( \Exception $e ) {
if ( $throw ) throw ( $e );
$this -> error ( sprintf ( $this -> _ ( 'Failed to init module: %s' ), $moduleName ) . " - " . $e -> getMessage ());
$result = false ;
}
if ( $this -> debug ) {
$this -> debugTimerStop ( $debugKey );
}
}
// if module is autoload (assumed here) and singular, then
// we no longer need the module's config data, so remove it
if ( $clearSettings && $this -> isSingular ( $module )) {
if ( ! $moduleID ) $moduleID = $this -> getModuleID ( $module );
if ( isset ( $this -> configData [ $moduleID ])) $this -> configData [ $moduleID ] = 1 ;
}
return $result ;
}
/**
* Call ready for a single module
*
* @ param Module $module
* @ return bool
*
*/
protected function readyModule ( Module $module ) {
$result = true ;
if ( method_exists ( $module , 'ready' )) {
$debugKey = $this -> debug ? $this -> debugTimerStart ( " readyModule( " . $module -> className () . " ) " ) : null ;
try {
$module -> ready ();
} catch ( \Exception $e ) {
$this -> error ( sprintf ( $this -> _ ( 'Failed to ready module: %s' ), $module -> className ()) . " - " . $e -> getMessage ());
$result = false ;
}
if ( $this -> debug ) {
$this -> debugTimerStop ( $debugKey );
static $n = 0 ;
$this -> message ( " readyModule ( " . ( ++ $n ) . " ): " . wireClassName ( $module ));
}
}
return $result ;
}
/**
* Init conditional autoload modules , if conditions allow
*
* @ return array of skipped module names
*
*/
protected function triggerConditionalAutoload () {
// conditional autoload modules that are skipped (className => 1)
$skipped = array ();
// init conditional autoload modules, now that $page is known
foreach ( $this -> conditionalAutoloadModules as $className => $func ) {
if ( $this -> debug ) {
$moduleID = $this -> getModuleID ( $className );
$flags = $this -> moduleFlags [ $moduleID ];
$this -> message ( " Conditional autoload: $className (flags= $flags , condition= " . ( is_string ( $func ) ? $func : 'func' ) . " ) " );
}
$load = true ;
if ( is_string ( $func )) {
// selector string
2023-03-10 19:41:40 +01:00
if ( ! $this -> wire () -> page -> is ( $func )) $load = false ;
2022-03-08 15:55:41 +01:00
} else {
// anonymous function
if ( ! is_callable ( $func )) $load = false ;
else if ( ! $func ()) $load = false ;
}
if ( $load ) {
$module = $this -> newModule ( $className );
if ( $module ) {
$this -> set ( $className , $module );
if ( $this -> initModule ( $module )) {
if ( $this -> debug ) $this -> message ( " Conditional autoload: $className LOADED " );
} else {
if ( $this -> debug ) $this -> warning ( " Failed conditional autoload: $className " );
}
}
} else {
$skipped [ $className ] = $className ;
if ( $this -> debug ) $this -> message ( " Conditional autoload: $className SKIPPED " );
}
}
// clear this out since we don't need it anymore
$this -> conditionalAutoloadModules = array ();
return $skipped ;
}
/**
* Trigger all modules 'ready' method , if they have it .
*
* This is to indicate to them that the API environment is fully ready and $page is in fuel .
*
* This is triggered by ProcessPageView :: ready
*
* #pw-internal
*
*/
public function triggerReady () {
$debugKey = $this -> debug ? $this -> debugTimerStart ( " triggerReady " ) : null ;
$skipped = $this -> triggerConditionalAutoload ();
// trigger ready method on all applicable modules
foreach ( $this as $module ) {
/** @var Module $module */
if ( $module instanceof ModulePlaceholder ) continue ;
// $info = $this->getModuleInfo($module);
// if($info['autoload'] === false) continue;
// if(!$this->isAutoload($module)) continue;
$class = $this -> getModuleClass ( $module );
if ( isset ( $skipped [ $class ])) continue ;
$id = $this -> moduleIDs [ $class ];
if ( ! ( $this -> moduleFlags [ $id ] & self :: flagsAutoload )) continue ;
if ( ! method_exists ( $module , 'ready' )) continue ;
$this -> readyModule ( $module );
}
if ( $this -> debug ) $this -> debugTimerStop ( $debugKey );
}
/**
* Retrieve the installed module info as stored in the database
*
*/
protected function loadModulesTable () {
$this -> autoloadOrders = array ();
$database = $this -> wire () -> database ;
// we use SELECT * so that this select won't be broken by future DB schema additions
// Currently: id, class, flags, data, with created added at sysupdate 7
$query = $database -> prepare ( " SELECT * FROM modules ORDER BY class " , " modules.loadModulesTable() " ); // QA
$query -> execute ();
/** @noinspection PhpAssignmentInConditionInspection */
while ( $row = $query -> fetch ( \PDO :: FETCH_ASSOC )) {
$moduleID = ( int ) $row [ 'id' ];
$flags = ( int ) $row [ 'flags' ];
$class = $row [ 'class' ];
$this -> moduleIDs [ $class ] = $moduleID ;
$this -> moduleFlags [ $moduleID ] = $flags ;
$autoload = $flags & self :: flagsAutoload ;
$loadSettings = $autoload || ( $flags & self :: flagsDuplicate ) || ( $class == 'SystemUpdater' );
if ( $loadSettings ) {
// preload config data for autoload modules since we'll need it again very soon
$data = strlen ( $row [ 'data' ]) ? wireDecodeJSON ( $row [ 'data' ]) : array ();
$this -> configData [ $moduleID ] = $data ;
// populate information about duplicates, if applicable
if ( $flags & self :: flagsDuplicate ) $this -> duplicates () -> addFromConfigData ( $class , $data );
} else if ( ! empty ( $row [ 'data' ])) {
// indicate that it has config data, but not yet loaded
$this -> configData [ $moduleID ] = 1 ;
}
if ( isset ( $row [ 'created' ]) && $row [ 'created' ] != '0000-00-00 00:00:00' ) {
$this -> createdDates [ $moduleID ] = $row [ 'created' ];
}
if ( $autoload && ! empty ( $this -> moduleInfoCache [ $moduleID ][ 'autoload' ])) {
$autoload = $this -> moduleInfoCache [ $moduleID ][ 'autoload' ];
$disabled = $flags & self :: flagsDisabled ;
if ( is_int ( $autoload ) && $autoload > 1 && ! $disabled ) {
// autoload specifies an order > 1, indicating it should load before others
$this -> autoloadOrders [ $class ] = $autoload ;
}
}
unset ( $row [ 'data' ], $row [ 'created' ]); // info we don't want stored in modulesTableCache
$this -> modulesTableCache [ $class ] = $row ;
}
$query -> closeCursor ();
}
/**
* Given a disk path to the modules , determine all installed modules and keep track of all uninstalled ( installable ) modules .
*
* @ param string $path
*
*/
protected function load ( $path ) {
2023-03-10 19:41:40 +01:00
$config = $this -> wire () -> config ;
2022-03-08 15:55:41 +01:00
$debugKey = $this -> debug ? $this -> debugTimerStart ( " load( $path ) " ) : null ;
$installed =& $this -> modulesTableCache ;
$modulesLoaded = array ();
$modulesDelayed = array ();
$modulesRequired = array ();
$rootPath = $config -> paths -> root ;
$basePath = substr ( $path , strlen ( $rootPath ));
foreach ( $this -> findModuleFiles ( $path , true ) as $pathname ) {
$pathname = trim ( $pathname );
if ( empty ( $pathname )) continue ;
$basename = basename ( $pathname );
list ( $moduleName , $ext ) = explode ( '.' , $basename , 2 ); // i.e. "module.php" or "module"
$this -> moduleFileExts [ $moduleName ] = $ext === 'module' ? 1 : 2 ;
// @todo next, remove the 'file' property from verbose module info since it is redundant
$requires = array ();
$name = $moduleName ;
$moduleName = $this -> loadModule ( $path , $pathname , $requires , $installed );
if ( ! $config -> paths -> get ( $name )) $this -> setConfigPaths ( $name , dirname ( $basePath . $pathname ));
if ( ! $moduleName ) continue ;
if ( count ( $requires )) {
// module not loaded because it required other module(s) not yet loaded
foreach ( $requires as $requiresModuleName ) {
if ( ! isset ( $modulesRequired [ $requiresModuleName ])) $modulesRequired [ $requiresModuleName ] = array ();
if ( ! isset ( $modulesDelayed [ $moduleName ])) $modulesDelayed [ $moduleName ] = array ();
// queue module for later load
$modulesRequired [ $requiresModuleName ][ $moduleName ] = $pathname ;
$modulesDelayed [ $moduleName ][] = $requiresModuleName ;
}
continue ;
}
// module was successfully loaded
$modulesLoaded [ $moduleName ] = 1 ;
$loadedNames = array ( $moduleName );
// now determine if this module had any other modules waiting on it as a dependency
/** @noinspection PhpAssignmentInConditionInspection */
while ( $moduleName = array_shift ( $loadedNames )) {
// iternate through delayed modules that require this one
if ( empty ( $modulesRequired [ $moduleName ])) continue ;
foreach ( $modulesRequired [ $moduleName ] as $delayedName => $delayedPathName ) {
$loadNow = true ;
if ( isset ( $modulesDelayed [ $delayedName ])) {
foreach ( $modulesDelayed [ $delayedName ] as $requiresModuleName ) {
if ( ! isset ( $modulesLoaded [ $requiresModuleName ])) {
$loadNow = false ;
}
}
}
if ( ! $loadNow ) continue ;
// all conditions satisified to load delayed module
unset ( $modulesDelayed [ $delayedName ], $modulesRequired [ $moduleName ][ $delayedName ]);
$unused = array ();
$loadedName = $this -> loadModule ( $path , $delayedPathName , $unused , $installed );
if ( ! $loadedName ) continue ;
$modulesLoaded [ $loadedName ] = 1 ;
$loadedNames [] = $loadedName ;
}
}
}
if ( count ( $modulesDelayed )) {
foreach ( $modulesDelayed as $moduleName => $requiredNames ) {
$this -> error ( " Module ' $moduleName ' dependency not fulfilled for: " . implode ( ', ' , $requiredNames ), Notice :: debug );
}
}
if ( $this -> debug ) $this -> debugTimerStop ( $debugKey );
}
/**
* Load a module into memory ( companion to load bootstrap method )
*
* @ param string $basepath Base path of modules being processed ( path provided to the load method )
* @ param string $pathname
* @ param array $requires This method will populate this array with required dependencies ( class names ) if present .
* @ param array $installed Array of installed modules info , indexed by module class name
* @ return string Returns module name ( classname )
*
*/
protected function loadModule ( $basepath , $pathname , array & $requires , array & $installed ) {
$pathname = $basepath . $pathname ;
$dirname = dirname ( $pathname );
$filename = basename ( $pathname );
$basename = basename ( $filename , '.php' );
$basename = basename ( $basename , '.module' );
$requires = array ();
$duplicates = $this -> duplicates ();
$moduleInfo = null ;
// check if module has duplicate files, where one to use has already been specified to use first
$currentFile = $duplicates -> getCurrent ( $basename ); // returns the current file in use, if more than one
if ( $currentFile ) {
// there is a duplicate file in use
2023-03-10 19:41:40 +01:00
$file = rtrim ( $this -> wire () -> config -> paths -> root , '/' ) . $currentFile ;
2022-03-08 15:55:41 +01:00
if ( file_exists ( $file ) && $pathname != $file ) {
// file in use is different from the file we are looking at
// check if this is a new/yet unknown duplicate
if ( ! $duplicates -> hasDuplicate ( $basename , $pathname )) {
// new duplicate
$duplicates -> recordDuplicate ( $basename , $pathname , $file , $installed );
}
return '' ;
}
}
// check if module has already been loaded, or maybe we've got duplicates
if ( wireClassExists ( $basename , false )) {
$module = parent :: get ( $basename );
2022-11-05 18:32:48 +01:00
$dir = rtrim (( string ) $this -> wire () -> config -> paths -> $basename , '/' );
2022-03-08 15:55:41 +01:00
if ( $module && $dir && $dirname != $dir ) {
$duplicates -> recordDuplicate ( $basename , $pathname , " $dir / $filename " , $installed );
return '' ;
}
if ( $module ) return $basename ;
}
// if the filename doesn't end with .module or .module.php, then stop and move onto the next
if ( ! strpos ( $filename , '.module' ) || ( substr ( $filename , - 7 ) !== '.module' && substr ( $filename , - 11 ) !== '.module.php' )) return false ;
// if the filename doesn't start with the requested path, then continue
if ( strpos ( $pathname , $basepath ) !== 0 ) return '' ;
// if the file isn't there, it was probably uninstalled, so ignore it
if ( ! file_exists ( $pathname )) return '' ;
// if the module isn't installed, then stop and move on to next
if ( ! array_key_exists ( $basename , $installed )) {
$this -> installable [ $basename ] = $pathname ;
return '' ;
}
$info = $installed [ $basename ];
$this -> setConfigPaths ( $basename , $dirname );
$module = null ;
$autoload = false ;
if ( $info [ 'flags' ] & self :: flagsAutoload ) {
// this is an Autoload module.
// include the module and instantiate it but don't init() it,
// because it will be done by Modules::init()
$moduleInfo = $this -> getModuleInfo ( $basename );
// determine if module has dependencies that are not yet met
if ( count ( $moduleInfo [ 'requires' ])) {
foreach ( $moduleInfo [ 'requires' ] as $requiresClass ) {
$nsRequiresClass = $this -> getModuleClass ( $requiresClass , true );
if ( ! wireClassExists ( $nsRequiresClass , false )) {
$requiresInfo = $this -> getModuleInfo ( $requiresClass );
if ( ! empty ( $requiresInfo [ 'error' ])
|| $requiresInfo [ 'autoload' ] === true
|| ! $this -> isInstalled ( $requiresClass )) {
// we only handle autoload===true since load() only instantiates other autoload===true modules
$requires [] = $requiresClass ;
}
}
}
if ( count ( $requires )) {
// module has unmet requirements
return $basename ;
}
}
// if not defined in getModuleInfo, then we'll accept the database flag as enough proof
// since the module may have defined it via an isAutoload() function
if ( ! isset ( $moduleInfo [ 'autoload' ])) $moduleInfo [ 'autoload' ] = true ;
/** @var bool|string|callable $autoload */
$autoload = $moduleInfo [ 'autoload' ];
if ( $autoload === 'function' ) {
// function is stored by the moduleInfo cache to indicate we need to call a dynamic function specified with the module itself
$i = $this -> getModuleInfoExternal ( $basename );
if ( empty ( $i )) {
$this -> includeModuleFile ( $pathname , $basename );
$className = $moduleInfo [ 'namespace' ] . $basename ;
if ( method_exists ( $className , 'getModuleInfo' )) {
$i = $className :: getModuleInfo ();
} else {
$i = array ();
}
}
$autoload = isset ( $i [ 'autoload' ]) ? $i [ 'autoload' ] : true ;
unset ( $i );
}
// check for conditional autoload
if ( ! is_bool ( $autoload ) && ( is_string ( $autoload ) || is_callable ( $autoload )) && ! ( $info [ 'flags' ] & self :: flagsDisabled )) {
// anonymous function or selector string
$this -> conditionalAutoloadModules [ $basename ] = $autoload ;
$this -> moduleIDs [ $basename ] = $info [ 'id' ];
$autoload = true ;
} else if ( $autoload ) {
$this -> includeModuleFile ( $pathname , $basename );
if ( ! ( $info [ 'flags' ] & self :: flagsDisabled )) {
if ( $this -> refreshing ) {
$module = parent :: get ( $basename );
} else if ( isset ( $this -> autoloadOrders [ $basename ]) && $this -> autoloadOrders [ $basename ] >= 10000 ) {
$module = parent :: get ( $basename ); // preloaded module
}
if ( ! $module ) $module = $this -> newModule ( $basename );
}
}
}
if ( is_null ( $module )) {
// placeholder for a module, which is not yet included and instantiated
if ( ! $moduleInfo ) $moduleInfo = $this -> getModuleInfo ( $basename );
$module = $this -> newModulePlaceholder ( $basename , $moduleInfo [ 'namespace' ], $pathname , $info [ 'flags' ] & self :: flagsSingular , $autoload );
}
$this -> moduleIDs [ $basename ] = $info [ 'id' ];
$this -> set ( $basename , $module );
return $basename ;
}
/**
* Find new module files in the given $path
*
* If $readCache is true , this will perform the find from the cache
*
* @ param string $path Path to the modules
* @ param bool $readCache Optional . If set to true , then this method will attempt to read modules from the cache .
* @ param int $level For internal recursive use .
* @ return array Array of module files
*
*/
protected function findModuleFiles ( $path , $readCache = false , $level = 0 ) {
static $startPath ;
static $callNum = 0 ;
static $prependFiles = array ();
$callNum ++ ;
2023-03-10 19:41:40 +01:00
$config = $this -> wire () -> config ;
$cache = $this -> wire () -> cache ;
2022-03-08 15:55:41 +01:00
$cacheName = '' ;
if ( $level == 0 ) {
$startPath = $path ;
$cacheName = " Modules. " . str_replace ( $config -> paths -> root , '' , $path );
if ( $readCache && $cache ) {
$cacheContents = $cache -> get ( $cacheName );
if ( $cacheContents !== null ) {
if ( empty ( $cacheContents ) && $callNum === 1 ) {
// don't accept empty cache for first path (/wire/modules/)
} else {
$cacheContents = explode ( " \n " , trim ( $cacheContents ));
return $cacheContents ;
}
}
}
}
$files = array ();
$autoloadOrders = null ;
if ( count ( $this -> autoloadOrders ) && $path !== $config -> paths -> modules ) {
$autoloadOrders = & $this -> autoloadOrders ;
}
try {
$dir = new \DirectoryIterator ( $path );
} catch ( \Exception $e ) {
$this -> trackException ( $e , false , true );
$dir = null ;
}
if ( $dir ) foreach ( $dir as $file ) {
if ( $file -> isDot ()) continue ;
$filename = $file -> getFilename ();
$pathname = $file -> getPathname ();
if ( DIRECTORY_SEPARATOR != '/' ) {
$pathname = str_replace ( DIRECTORY_SEPARATOR , '/' , $pathname );
}
if ( strpos ( $pathname , '/.' ) !== false ) {
$pos = strrpos ( rtrim ( $pathname , '/' ), '/' );
if ( $pathname [ $pos + 1 ] == '.' ) continue ; // skip hidden files and dirs
}
// if it's a directory with a .module file in it named the same as the dir, then descend into it
if ( $file -> isDir () && ( $level < 1 || ( is_file ( " $pathname / $filename .module " ) || is_file ( " $pathname / $filename .module.php " )))) {
$files = array_merge ( $files , $this -> findModuleFiles ( $pathname , false , $level + 1 ));
}
// if the filename doesn't end with .module or .module.php, then stop and move onto the next
$extension = $file -> getExtension ();
if ( $extension !== 'module' && $extension !== 'php' ) continue ;
list ( $moduleName , $extension ) = explode ( '.' , $filename , 2 );
if ( $extension !== 'module' && $extension !== 'module.php' ) continue ;
$pathname = str_replace ( $startPath , '' , $pathname );
if ( $autoloadOrders !== null && isset ( $autoloadOrders [ $moduleName ])) {
$prependFiles [ $pathname ] = $autoloadOrders [ $moduleName ];
} else {
$files [] = $pathname ;
}
}
if ( $level == 0 && $dir !== null ) {
if ( ! empty ( $prependFiles )) {
// one or more non-core modules must be loaded first in a specific order
arsort ( $prependFiles );
$files = array_merge ( array_keys ( $prependFiles ), $files );
$prependFiles = array ();
}
if ( $cache && $cacheName ) {
$cache -> save ( $cacheName , implode ( " \n " , $files ), WireCache :: expireReserved );
}
}
return $files ;
}
/**
* Setup entries in config -> urls and config -> paths for the given module
*
* @ param string $moduleName
* @ param string $path
*
*/
protected function setConfigPaths ( $moduleName , $path ) {
2023-03-10 19:41:40 +01:00
$config = $this -> wire () -> config ;
2022-03-08 15:55:41 +01:00
$rootPath = $config -> paths -> root ;
if ( strpos ( $path , $rootPath ) === 0 ) {
// if root path included, strip it out
$path = substr ( $path , strlen ( $config -> paths -> root ));
}
$path = rtrim ( $path , '/' ) . '/' ;
$config -> paths -> set ( $moduleName , $path );
$config -> urls -> set ( $moduleName , $path );
}
/**
* Get the requested Module
*
* - If the module is not installed , but is installable , it will be installed , instantiated , and initialized .
* If you don 't want that behavior, call `$modules->isInstalled(' ModuleName ' ) ` as a conditional first .
* - You can also get / load a module by accessing it directly , like `$modules->ModuleName` .
* - To get a module with additional options , use `$modules->getModule($name, $options)` instead .
*
* ~~~~~
* // Get the MarkupAdminDataTable module
* $table = $modules -> get ( 'MarkupAdminDataTable' );
*
* // You can also do this
* $table = $modules -> MarkupAdminDataTable ;
* ~~~~~
*
* @ param string | int $key Module name ( also accepts database ID )
* @ return Module | _Module | null Returns a Module or null if not found
* @ throws WirePermissionException If module requires a particular permission the user does not have
* @ see Modules :: getModule (), Modules :: isInstalled ()
*
*/
public function get ( $key ) {
// If the module is a ModulePlaceholder, then it will be converted to the real module (included, instantiated, initialized).
return $this -> getModule ( $key );
}
/**
* Attempt to find a substitute for moduleName and return module if found or null if not
*
* @ param $moduleName
* @ param array $options See getModule () options
* @ return Module | null
*
*/
protected function getSubstituteModule ( $moduleName , array $options = array ()) {
$module = null ;
$options [ 'noSubstitute' ] = true ; // prevent recursion
while ( isset ( $this -> substitutes [ $moduleName ]) && ! $module ) {
$substituteName = $this -> substitutes [ $moduleName ];
$module = $this -> getModule ( $substituteName , $options );
if ( ! $module ) $moduleName = $substituteName ;
}
return $module ;
}
/**
* Get the requested Module ( with options )
*
* This is the same as `$modules->get()` except that you can specify additional options to modify default behavior .
* These are the options you can specify in the `$options` array argument :
*
* - `noPermissionCheck` ( bool ) : Specify true to disable module permission checks ( and resulting exception ) . ( default = false )
* - `noInstall` ( bool ) : Specify true to prevent a non - installed module from installing from this request . ( default = false )
* - `noInit` ( bool ) : Specify true to prevent the module from being initialized or configured . ( default = false ) . See `configOnly` as alternative .
* - `noSubstitute` ( bool ) : Specify true to prevent inclusion of a substitute module . ( default = false )
* - `noCache` ( bool ) : Specify true to prevent module instance from being cached for later getModule () calls . ( default = false )
* - `noThrow` ( bool ) : Specify true to prevent exceptions from being thrown on permission or fatal error . ( default = false )
* - `returnError` ( bool ) : Return an error message ( string ) on error , rather than null . ( default = false )
* - `configOnly` ( bool ) : Populate module config data but do not call its init () method . ( default = false ) 3.0 . 169 +. Alternative to `noInit` .
* - `configData` ( array ) : Associative array of additional config data to populate to module . ( default = []) 3.0 . 169 +
*
* If the module is not installed , but is installable , it will be installed , instantiated , and initialized .
* If you don 't want that behavior, call `$modules->isInstalled(' ModuleName ' ) ` as a condition first , OR specify
* true for the `noInstall` option in the `$options` argument .
*
* @ param string | int $key Module name or database ID .
* @ param array $options Optional settings to change load behavior , see method description for details .
* @ return Module | _Module | null | string Returns ready - to - use module or NULL | string if not found ( string if `returnError` option used ) .
* @ throws WirePermissionException | \Exception If module requires a particular permission the user does not have
* @ see Modules :: get ()
*
*/
public function getModule ( $key , array $options = array ()) {
$needsInit = false ;
$noInit = ! empty ( $options [ 'noInit' ]); // force cancel of Module::init() call?
$initOptions = array (); // options for initModule() call
$find = false ; // try to find new location of module file?
$error = '' ;
if ( empty ( $key )) {
return empty ( $options [ 'returnError' ]) ? null : " No module specified " ;
}
// check for optional module ID and convert to classname if found
if ( ctype_digit ( " $key " )) {
$moduleID = ( int ) $key ;
if ( ! $key = array_search ( $key , $this -> moduleIDs )) {
return empty ( $options [ 'returnError' ]) ? null : " Unable to find module ID $moduleID " ;
}
} else {
$moduleID = 0 ;
$key = wireClassName ( $key , false );
}
$module = parent :: get ( $key );
if ( ! $module && ! $moduleID ) {
// make non case-sensitive for module name ($key)
$lowerKey = strtolower ( $key );
foreach ( array_keys ( $this -> moduleIDs ) as $className ) {
if ( strtolower ( $className ) !== $lowerKey ) continue ;
$module = parent :: get ( $className );
break ;
}
}
if ( ! $module ) {
if ( empty ( $options [ 'noSubstitute' ])) {
if ( $this -> isInstallable ( $key ) && empty ( $options [ 'noInstall' ])) {
// module is on file system and may be installed, no need to substitute
} else {
$module = $this -> getSubstituteModule ( $key , $options );
if ( $module ) return $module ; // returned module is ready to use
}
} else {
$error = " Module ' $key ' not found and substitute not allowed (noSubstitute=true) " ;
}
}
if ( $module ) {
// check if it's a placeholder, and if it is then include/instantiate/init the real module
// OR check if it's non-singular, so that a new instance is created
if ( $module instanceof ModulePlaceholder || ! $this -> isSingular ( $module )) {
$placeholder = $module ;
$class = $this -> getModuleClass ( $placeholder );
try {
if ( $module instanceof ModulePlaceholder ) $this -> includeModule ( $module );
$module = $this -> newModule ( $class );
} catch ( \Exception $e ) {
if ( empty ( $options [ 'noThrow' ])) throw $e ;
return empty ( $options [ 'returnError' ]) ? null : " Module ' $key ' - " . $e -> getMessage ();
}
// if singular, save the instance so it can be used in later calls
if ( $module && $this -> isSingular ( $module ) && empty ( $options [ 'noCache' ])) $this -> set ( $key , $module );
$needsInit = true ;
}
} else if ( empty ( $options [ 'noInstall' ])) {
// module was not available to get, see if we can install it
if ( array_key_exists ( $key , $this -> getInstallable ())) {
// check if the request is for an uninstalled module
// if so, install it and return it
try {
$module = $this -> install ( $key );
} catch ( \Exception $e ) {
if ( empty ( $options [ 'noThrow' ])) throw $e ;
if ( ! empty ( $options [ 'returnError' ])) return " Module ' $key ' install failed: " . $e -> getMessage ();
}
$needsInit = true ;
if ( ! $module ) $error = " Module ' $key ' not installed and install failed " ;
} else {
$error = " Module ' $key ' is not present or listed as installable " ;
$find = true ;
}
} else {
$error = " Module ' $key ' is not present and not installable (noInstall=true) " ;
$find = true ;
}
if ( ! $module && $find ) {
// This is reached if module has moved elsewhere in file system, like from:
// site/modules/ModuleName.module to site/modules/ModuleName/ModuleName.module
// Code below tries to find the file to keep it working, but modules need Refresh.
try {
if ( $this -> includeModule ( $key )) {
$module = $this -> newModule ( $key );
}
} catch ( \Exception $e ) {
if ( empty ( $options [ 'noThrow' ])) throw $e ;
$error .= ( $error ? " - " : " Module ' $key ' - " ) . $e -> getMessage ();
return empty ( $options [ 'returnError' ]) ? null : $error ;
}
}
if ( ! $module ) {
if ( ! $error ) $error = " Unable to get module ' $key ' " ;
return empty ( $options [ 'returnError' ]) ? null : $error ;
}
if ( empty ( $options [ 'noPermissionCheck' ])) {
// check that user has permission required to use module
2023-03-10 19:41:40 +01:00
$page = $this -> wire () -> page ;
$user = $this -> wire () -> user ;
if ( ! $this -> hasPermission ( $module , $user , $page )) {
$error = $this -> _ ( 'You do not have permission to execute this module' ) . ' - ' .
wireClassName ( $module ) . " (page= $page ) " ;
2022-03-08 15:55:41 +01:00
if ( empty ( $options [ 'noThrow' ])) throw new WirePermissionException ( $error );
return empty ( $options [ 'returnError' ]) ? null : $error ;
}
}
if ( $needsInit && $noInit ) {
// forced cancel of init() call
$needsInit = false ;
}
if ( ! $needsInit && ( ! empty ( $options [ 'configData' ]) || ! empty ( $options [ 'configOnly' ]))) {
// if config data was supplied in options then we have to init()
$needsInit = true ;
if ( ! empty ( $options [ 'configData' ])) $initOptions [ 'configData' ] = $options [ 'configData' ];
// if forced noInit then tell initModule() to only config and not call Module::init()
if ( $noInit || ! empty ( $options [ 'configOnly' ])) $initOptions [ 'configOnly' ] = true ;
}
// skip autoload modules because they have already been initialized in the load() method
// unless they were just installed, in which case we need do init now
if ( $needsInit ) {
// if the module is configurable, then load its config data
// and set values for each before initializing the module
$initOptions [ 'clearSettings' ] = false ;
$initOptions [ 'throw' ] = true ;
try {
if ( ! $this -> initModule ( $module , $initOptions )) {
return empty ( $options [ 'returnError' ]) ? null : " Module ' $module ' failed init " ;
}
} catch ( \Exception $e ) {
if ( empty ( $options [ 'noThrow' ])) throw $e ;
return empty ( $options [ 'returnError' ]) ? null : " Module ' $module ' throw Exception on init - " . $e -> getMessage ();
}
}
return $module ;
}
/**
* Check if user has permission for given module
*
* #pw-internal
*
* @ param string | object $moduleName Module instance or module name
* @ param User $user Optionally specify different user to consider than current .
* @ param Page $page Optionally specify different page to consider than current .
* @ param bool $strict If module specifies no permission settings , assume no permission .
* - Default ( false ) is to assume permission when module doesn ' t say anything about it .
* - Process modules ( for instance ) generally assume no permission when it isn ' t specifically defined
* ( though this method doesn ' t get involved in that , leaving you to specify $strict instead ) .
*
* @ return bool
*
*/
public function hasPermission ( $moduleName , User $user = null , Page $page = null , $strict = false ) {
if ( is_object ( $moduleName )) {
$module = $moduleName ;
$className = $module -> className ( true );
$moduleName = $module -> className ( false );
} else {
$module = null ;
// $className = wireClassName($moduleName, true);
$className = $this -> getModuleClass ( $moduleName , true ); // ???
$moduleName = wireClassName ( $moduleName , false );
}
$info = $this -> getModuleInfo ( $module ? $module : $moduleName );
2022-11-05 18:32:48 +01:00
if ( empty ( $info [ 'permission' ]) && empty ( $info [ 'permissionMethod' ])) return ( $strict ? false : true );
2022-03-08 15:55:41 +01:00
2023-03-10 19:41:40 +01:00
if ( is_null ( $user )) $user = $this -> wire () -> user ;
2022-03-08 15:55:41 +01:00
if ( $user && $user -> isSuperuser ()) return true ;
if ( ! empty ( $info [ 'permission' ])) {
if ( ! $user -> hasPermission ( $info [ 'permission' ])) return false ;
}
if ( ! empty ( $info [ 'permissionMethod' ])) {
// module specifies a static method to call for permission
2023-03-10 19:41:40 +01:00
if ( is_null ( $page )) $page = $this -> wire () -> page ;
2022-03-08 15:55:41 +01:00
$data = array (
'wire' => $this -> wire (),
'page' => $page ,
'user' => $user ,
'info' => $info ,
);
$method = $info [ 'permissionMethod' ];
$this -> includeModule ( $moduleName );
2022-11-05 18:32:48 +01:00
if ( method_exists ( $className , $method )) return $className :: $method ( $data );
return false ;
2022-03-08 15:55:41 +01:00
}
return true ;
}
/**
* Get the requested module and reset cache + install it if necessary .
*
* This is exactly the same as get () except that this one will rebuild the modules cache if
* it doesn ' t find the module at first . If the module is on the file system , this
* one will return it in some instances that a regular get () can ' t .
*
* #pw-internal
*
* @ param string | int $key Module className or database ID
* @ return Module | null
*
*/
public function getInstall ( $key ) {
$module = $this -> get ( $key );
if ( ! $module ) {
$this -> refresh ();
$module = $this -> getModule ( $key );
}
return $module ;
}
/**
* Include the file for a given module , but don ' t instantiate it
*
* #pw-internal
*
* @ param ModulePlaceholder | Module | string Expects a ModulePlaceholder or className
* @ param string $file Optionally specify the module filename if you already know it
* @ return bool true on success , false on fail or unknown
*
*/
public function includeModule ( $module , $file = '' ) {
$className = '' ;
$moduleName = '' ;
if ( is_string ( $module )) {
$moduleName = ctype_alnum ( $module ) ? $module : wireClassName ( $module );
$className = wireClassName ( $module , true );
} else if ( is_object ( $module )) {
if ( $module instanceof ModulePlaceholder ) {
$moduleName = $module -> className ();
$className = $module -> className ( true );
} else if ( $module instanceof Module ) {
return true ; // already included
}
} else {
$moduleName = $this -> getModuleClass ( $module , false );
$className = $this -> getModuleClass ( $module , true );
}
if ( ! $className ) return false ;
// already included
if ( class_exists ( $className , false )) return true ;
// attempt to retrieve module
$module = parent :: get ( $moduleName );
if ( $module ) {
// module found, check to make sure it actually points to a module
if ( ! $module instanceof Module ) $module = false ;
} else if ( $moduleName ) {
// This is reached for any of the following:
// 1. an uninstalled module
// 2. an installed module that has changed locations
// 3. a module outside the \ProcessWire\ namespace
// 4. a module that does not exist
$fast = true ;
if ( ! $file ) {
// determine module file, if not already provided to the method
$file = $this -> getModuleFile ( $moduleName , array ( 'fast' => true ));
if ( ! $file ) {
$fast = false ;
$file = $this -> getModuleFile ( $moduleName , array ( 'fast' => false ));
}
// still can't figure out what file is? fail
if ( ! $file ) return false ;
}
if ( ! $this -> includeModuleFile ( $file , $moduleName )) {
// module file failed to include(), try to identify and include file again
if ( $fast ) {
$filePrev = $file ;
$file = $this -> getModuleFile ( $moduleName , array ( 'fast' => false ));
if ( $file && $file !== $filePrev ) {
$this -> includeModuleFile ( $file , $moduleName );
}
} else {
// we already tried this earlier, no point in doing it again
}
}
// now check to see if included file resulted in presence of module class
if ( class_exists ( $className )) {
// module in ProcessWire namespace
$module = true ;
} else {
// module in root namespace or some other namespace
2023-03-10 19:41:40 +01:00
$namespace = ( string ) $this -> getModuleNamespace ( $moduleName , array ( 'file' => $file ));
2022-03-08 15:55:41 +01:00
$className = trim ( $namespace , " \\ " ) . " \\ $moduleName " ;
if ( class_exists ( $className , false )) {
// successful include module
$module = true ;
}
}
}
if ( $module === true ) {
// great
return true ;
} else if ( ! $module ) {
// darn
return false ;
} else if ( $module instanceof ModulePlaceholder ) {
$this -> includeModuleFile ( $module -> file , $moduleName );
return true ;
} else if ( $module instanceof Module ) {
// it's already been included, since we have a real module
return true ;
} else {
return false ;
}
}
/**
* Include the given filename
*
* @ param string $file
* @ param string $moduleName
* @ return bool
*
*/
protected function includeModuleFile ( $file , $moduleName ) {
$wire1 = ProcessWire :: getCurrentInstance ();
$wire2 = $this -> wire ();
// check if there is more than one PW instance active
if ( $wire1 !== $wire2 ) {
// multi-instance is active, don't autoload module if class already exists
// first do a fast check, which should catch any core modules
if ( class_exists ( __NAMESPACE__ . " \\ $moduleName " , false )) return true ;
// next do a slower check, figuring out namespace
$ns = $this -> getModuleNamespace ( $moduleName , array ( 'file' => $file ));
$className = trim ( $ns , " \\ " ) . " \\ $moduleName " ;
if ( class_exists ( $className , false )) return true ;
// if this point is reached, module is not yet in memory in either instance
// temporarily set the $wire instance to 2nd instance during include()
ProcessWire :: setCurrentInstance ( $wire2 );
}
// get compiled version (if it needs compilation)
$file = $this -> compile ( $moduleName , $file );
if ( $file ) {
/** @noinspection PhpIncludeInspection */
$success = @ include_once ( $file );
} else {
$success = false ;
}
// set instance back, if multi-instance
if ( $wire1 !== $wire2 ) ProcessWire :: setCurrentInstance ( $wire1 );
return ( bool ) $success ;
}
/**
* Find modules based on a selector string
*
* #pw-internal Almost always recommend using findByPrefix() instead
*
* @ param string $selector Selector string
* @ return WireArray of found modules , instantiated and ready - to - use
*
*/
public function find ( $selector ) {
// ensures any ModulePlaceholders are loaded in the returned result.
$a = parent :: find ( $selector );
if ( $a ) {
foreach ( $a as $key => $value ) {
$a [ $key ] = $this -> get ( $value -> className ());
}
}
return $a ;
}
/**
* Find modules matching the given prefix ( i . e . “Inputfield” )
*
* By default this method returns module class names matching the given prefix .
* To instead retrieve instantiated ( ready - to - use ) modules , specify boolean true
* for the second argument . Regardless of `$load` argument all returned arrays
* are indexed by module name .
*
* ~~~~~
* // Retrieve array of all Textformatter module names
* $items = $modules -> findByPrefix ( 'Textformatter' );
*
* // Retrieve array of all Textformatter modules (ready to use)
* $items = $modules -> findByPrefix ( 'Textformatter' , true );
* ~~~~~
*
* @ param string $prefix Specify prefix , i . e . " Process " , " Fieldtype " , " Inputfield " , etc .
* @ param bool | int $load Specify one of the following ( all indexed by module name ) :
* - Boolean true to return array of instantiated modules .
* - Boolean false to return array of module names ( default ) .
* - Integer 1 to return array of module info for each matching module .
* - Integer 2 to return array of verbose module info for each matching module .
* - Integer 3 to return array of Module or ModulePlaceholder objects ( whatever current state is ) . Added 3.0 . 146.
* @ return array Returns array of module class names , module info arrays , or Module objects . In all cases , array indexes are class names .
*
*/
public function findByPrefix ( $prefix , $load = false ) {
$results = array ();
foreach ( $this as $moduleName => $value ) {
if ( stripos ( $moduleName , $prefix ) !== 0 ) continue ;
if ( $load === false ) {
$results [ $moduleName ] = $moduleName ;
} else if ( $load === true ) {
$results [ $moduleName ] = $this -> getModule ( $moduleName );
} else if ( $load === 1 ) {
$results [ $moduleName ] = $this -> getModuleInfo ( $moduleName );
} else if ( $load === 2 ) {
$results [ $moduleName ] = $this -> getModuleInfoVerbose ( $moduleName );
} else if ( $load === 3 ) {
$results [ $moduleName ] = $value ;
} else {
$results [ $moduleName ] = $moduleName ;
}
}
ksort ( $results );
return $results ;
}
/**
* Find modules by matching a property or properties in their module info
*
* @ param string | array $selector Specify one of the following :
* - Selector string to match module info .
* - Array of [ 'property' => 'value' ] to match in module info ( this is not a selector array ) .
* - Name of property that will match module if not empty in module info .
* @ param bool | int $load Specify one of the following :
* - Boolean true to return array of instantiated modules .
* - Boolean false to return array of module names ( default ) .
* - Integer 1 to return array of module info for each matching module .
* - Integer 2 to return array of verbose module info for each matching module .
* @ return array Array of modules , module names or module info arrays , indexed by module name .
*
*/
public function findByInfo ( $selector , $load = false ) {
$selectors = null ;
$keys = null ;
$results = array ();
$verbose = $load === 2 ;
$properties = array ();
$has = '' ;
if ( is_array ( $selector )) {
// find matching all values in array
$keys = $selector ;
$properties = array_keys ( $keys );
} if ( ! ctype_alnum ( $selector ) && Selectors :: stringHasOperator ( $selector )) {
// find by selectors
$selectors = new Selectors ( $selector );
if ( ! $verbose ) foreach ( $selectors as $s ) {
$properties = array_merge ( $properties , $s -> fields ());
}
} else {
// find non-empty property
$has = $selector ;
$properties [] = $has ;
}
// check if any verbose properties are part of the find
if ( ! $verbose ) foreach ( $properties as $property ) {
if ( ! in_array ( $property , $this -> moduleInfoVerboseKeys )) continue ;
$verbose = true ;
break ;
}
$moduleInfoOptions = array (
'verbose' => $verbose ,
'minify' => false
);
foreach ( $this -> getModuleInfo ( '*' , $moduleInfoOptions ) as $info ) {
$isMatch = false ;
if ( $has ) {
// simply check if property is non-empty
if ( ! empty ( $info [ $has ])) $isMatch = true ;
} else if ( $selectors ) {
// match selector
$total = 0 ;
$n = 0 ;
foreach ( $selectors as $selector ) {
$total ++ ;
$values = array ();
foreach ( $selector -> fields () as $property ) {
if ( isset ( $info [ $property ])) $values [] = $info [ $property ];
}
if ( $selector -> matches ( $values )) $n ++ ;
}
if ( $n && $n === $total ) $isMatch = true ;
} else if ( $keys ) {
// match all values in $keys array
$n = 0 ;
foreach ( $keys as $key => $value ) {
if ( $value === '*' ) {
// match any non-empty value
if ( ! empty ( $info [ $key ])) $n ++ ;
} else {
// match exact value
if ( isset ( $info [ $key ]) && $value == $info [ $key ]) $n ++ ;
}
}
if ( $n && $n === count ( $keys )) $isMatch = true ;
}
if ( $isMatch ) {
$moduleName = $info [ 'name' ];
if ( is_int ( $load )) {
$results [ $moduleName ] = $info ;
} else if ( $load === true ) {
$results [ $moduleName ] = $this -> getModule ( $moduleName );
} else {
$results [ $moduleName ] = $moduleName ;
}
}
}
return $results ;
}
/**
* Get an associative array [ name => path ] for all modules that aren’ t currently installed .
*
* #pw-internal
*
* @ return array Array of elements with $moduleName => $pathName
*
*/
public function getInstallable () {
return $this -> installable ;
}
/**
* Is the given module name installed ?
*
* @ param string $class Just a module class name , or optionally : `ModuleClassName>=1.2.3` ( operator and version )
* @ return bool True if installed , false if not
*
*/
public function isInstalled ( $class ) {
if ( is_object ( $class )) $class = $this -> getModuleClass ( $class );
2023-03-10 19:41:40 +01:00
$class = ( string ) $class ;
if ( empty ( $class )) return false ;
2022-03-08 15:55:41 +01:00
$operator = null ;
$requiredVersion = null ;
$currentVersion = null ;
if ( ! ctype_alnum ( $class )) {
// class has something other than just a classname, likely operator + version
if ( preg_match ( '/^([a-zA-Z0-9_]+)\s*([<>=!]+)\s*([\d.]+)$/' , $class , $matches )) {
$class = $matches [ 1 ];
$operator = $matches [ 2 ];
$requiredVersion = $matches [ 3 ];
}
}
if ( $class === 'PHP' || $class === 'ProcessWire' ) {
$installed = true ;
if ( ! is_null ( $requiredVersion )) {
2023-03-10 19:41:40 +01:00
$currentVersion = $class === 'PHP' ? PHP_VERSION : $this -> wire () -> config -> version ;
2022-03-08 15:55:41 +01:00
}
} else {
$installed = parent :: get ( $class ) !== null ;
if ( $installed && ! is_null ( $requiredVersion )) {
$info = $this -> getModuleInfo ( $class );
$currentVersion = $info [ 'version' ];
}
}
if ( $installed && ! is_null ( $currentVersion )) {
$installed = $this -> versionCompare ( $currentVersion , $requiredVersion , $operator );
}
return $installed ;
}
/**
* Is the given module name installable ? ( i . e . not already installed )
*
* #pw-internal
*
* @ param string $class Module class name
* @ param bool $now Is module installable RIGHT NOW ? This makes it check that all dependencies are already fulfilled ( default = false )
* @ return bool True if module is installable , false if not
*
*/
public function isInstallable ( $class , $now = false ) {
2023-03-10 19:41:40 +01:00
if ( ! array_key_exists ( $class , $this -> installable )) return false ;
2022-03-08 15:55:41 +01:00
if ( ! wireInstanceOf ( $class , 'Module' )) {
$nsClass = $this -> getModuleClass ( $class , true );
if ( ! wireInstanceOf ( $nsClass , 'ProcessWire\\Module' )) return false ;
}
if ( $now ) {
$requires = $this -> getRequiresForInstall ( $class );
if ( count ( $requires )) return false ;
}
2023-03-10 19:41:40 +01:00
return true ;
2022-03-08 15:55:41 +01:00
}
/**
* Install the given module name
*
* #pw-group-manipulation
*
* @ param string $class Module name ( class name )
* @ param array | bool $options Optional associative array that can contain any of the following :
* - `dependencies` ( boolean ) : When true , dependencies will also be installed where possible . Specify false to prevent installation of uninstalled modules . ( default = true )
* - `resetCache` ( boolean ) : When true , module caches will be reset after installation . ( default = true )
* - `force` ( boolean ) : Force installation , even if dependencies can ' t be met .
* @ return null | Module Returns null if unable to install , or ready - to - use Module object if successfully installed .
* @ throws WireException
*
*/
public function ___install ( $class , $options = array ()) {
$defaults = array (
'dependencies' => true ,
'resetCache' => true ,
'force' => false ,
2023-03-10 19:41:40 +01:00
);
2022-03-08 15:55:41 +01:00
if ( is_bool ( $options )) {
// dependencies argument allowed instead of $options, for backwards compatibility
$dependencies = $options ;
$options = array ( 'dependencies' => $dependencies );
}
2023-03-10 19:41:40 +01:00
2022-03-08 15:55:41 +01:00
$options = array_merge ( $defaults , $options );
$dependencyOptions = $options ;
$dependencyOptions [ 'resetCache' ] = false ;
if ( ! $this -> isInstallable ( $class )) return null ;
$requires = $this -> getRequiresForInstall ( $class );
if ( count ( $requires )) {
$error = '' ;
$installable = false ;
if ( $options [ 'dependencies' ]) {
$installable = true ;
foreach ( $requires as $requiresModule ) {
if ( ! $this -> isInstallable ( $requiresModule )) $installable = false ;
}
if ( $installable ) {
foreach ( $requires as $requiresModule ) {
if ( ! $this -> install ( $requiresModule , $dependencyOptions )) {
$error = $this -> _ ( 'Unable to install required module' ) . " - $requiresModule . " ;
$installable = false ;
break ;
}
}
}
}
if ( ! $installable ) {
$error = sprintf ( $this -> _ ( 'Module %s requires: %s' ), $class , implode ( ', ' , $requires )) . ' ' . $error ;
if ( $options [ 'force' ]) {
$this -> warning ( $this -> _ ( 'Warning!' ) . ' ' . $error );
} else {
throw new WireException ( $error );
}
}
}
2023-03-10 19:41:40 +01:00
$database = $this -> wire () -> database ;
$languages = $this -> wire () -> languages ;
$config = $this -> wire () -> config ;
2022-03-08 15:55:41 +01:00
if ( $languages ) $languages -> setDefault ();
$pathname = $this -> installable [ $class ];
$this -> includeModuleFile ( $pathname , $class );
$this -> setConfigPaths ( $class , dirname ( $pathname ));
$module = $this -> newModule ( $class );
if ( ! $module ) return null ;
$flags = 0 ;
$moduleID = 0 ;
if ( $this -> isSingular ( $module )) $flags = $flags | self :: flagsSingular ;
if ( $this -> isAutoload ( $module )) $flags = $flags | self :: flagsAutoload ;
$sql = " INSERT INTO modules SET class=:class, flags=:flags, data='' " ;
2023-03-10 19:41:40 +01:00
if ( $config -> systemVersion >= 7 ) $sql .= " , created=NOW() " ;
2022-03-08 15:55:41 +01:00
$query = $database -> prepare ( $sql , " modules.install( $class ) " );
$query -> bindValue ( " :class " , $class , \PDO :: PARAM_STR );
$query -> bindValue ( " :flags " , $flags , \PDO :: PARAM_INT );
try {
if ( $query -> execute ()) $moduleID = ( int ) $database -> lastInsertId ();
} catch ( \Exception $e ) {
if ( $languages ) $languages -> unsetDefault ();
$this -> trackException ( $e , false , true );
return null ;
}
$this -> moduleIDs [ $class ] = $moduleID ;
$this -> add ( $module );
unset ( $this -> installable [ $class ]);
// note: the module's install is called here because it may need to know its module ID for installation of permissions, etc.
if ( method_exists ( $module , '___install' ) || method_exists ( $module , 'install' )) {
try {
/** @var _Module $module */
$module -> install ();
} catch ( \PDOException $e ) {
$error = $this -> _ ( 'Module reported error during install' ) . " ( $class ): " . $e -> getMessage ();
$this -> error ( $error );
$this -> trackException ( $e , false , $error );
} catch ( \Exception $e ) {
// remove the module from the modules table if the install failed
$moduleID = ( int ) $moduleID ;
$error = $this -> _ ( 'Unable to install module' ) . " ( $class ): " . $e -> getMessage ();
$ee = null ;
try {
$query = $database -> prepare ( 'DELETE FROM modules WHERE id=:id LIMIT 1' ); // QA
$query -> bindValue ( " :id " , $moduleID , \PDO :: PARAM_INT );
$query -> execute ();
} catch ( \Exception $ee ) {
$this -> trackException ( $e , false , $error ) -> trackException ( $ee , true );
}
if ( $languages ) $languages -> unsetDefault ();
if ( is_null ( $ee )) $this -> trackException ( $e , false , $error );
return null ;
}
}
$info = $this -> getModuleInfoVerbose ( $class , array ( 'noCache' => true ));
2023-03-10 19:41:40 +01:00
$sanitizer = $this -> wire () -> sanitizer ;
$permissions = $this -> wire () -> permissions ;
2022-03-08 15:55:41 +01:00
// if this module has custom permissions defined in its getModuleInfo()['permissions'] array, install them
foreach ( $info [ 'permissions' ] as $name => $title ) {
2023-03-10 19:41:40 +01:00
$name = $sanitizer -> pageName ( $name );
2022-03-08 15:55:41 +01:00
if ( ctype_digit ( " $name " ) || empty ( $name )) continue ; // permission name not valid
2023-03-10 19:41:40 +01:00
$permission = $permissions -> get ( $name );
2022-03-08 15:55:41 +01:00
if ( $permission -> id ) continue ; // permision already there
try {
2023-03-10 19:41:40 +01:00
$permission = $permissions -> add ( $name );
2022-03-08 15:55:41 +01:00
$permission -> title = $title ;
2023-03-10 19:41:40 +01:00
$permissions -> save ( $permission );
2022-03-08 15:55:41 +01:00
if ( $languages ) $languages -> unsetDefault ();
$this -> message ( sprintf ( $this -> _ ( 'Added Permission: %s' ), $permission -> name ));
} catch ( \Exception $e ) {
if ( $languages ) $languages -> unsetDefault ();
$error = sprintf ( $this -> _ ( 'Error adding permission: %s' ), $name );
$this -> trackException ( $e , false , $error );
}
}
// check if there are any modules in 'installs' that this module didn't handle installation of, and install them
$label = $this -> _ ( 'Module Auto Install' );
foreach ( $info [ 'installs' ] as $name ) {
if ( ! $this -> isInstalled ( $name )) {
try {
$this -> install ( $name , $dependencyOptions );
$this -> message ( " $label : $name " );
} catch ( \Exception $e ) {
$error = " $label : $name - " . $e -> getMessage ();
$this -> trackException ( $e , false , $error );
}
}
}
$this -> log ( " Installed module ' $module ' " );
if ( $languages ) $languages -> unsetDefault ();
if ( $options [ 'resetCache' ]) $this -> clearModuleInfoCache ();
return $module ;
}
/**
* Returns whether the module can be uninstalled
*
* #pw-internal
*
* @ param string | Module $class
* @ param bool $returnReason If true , the reason why it can ' t be uninstalled with be returned rather than boolean false .
* @ return bool | string
*
*/
public function ___isUninstallable ( $class , $returnReason = false ) {
$reason = '' ;
$reason1 = $this -> _ ( " Module is not already installed " );
$namespace = $this -> getModuleNamespace ( $class );
$class = $this -> getModuleClass ( $class );
if ( ! $this -> isInstalled ( $class )) {
$reason = $reason1 . ' (a)' ;
} else {
$this -> includeModule ( $class );
if ( ! wireClassExists ( $namespace . $class , false )) {
$reason = $reason1 . " (b: $namespace $class ) " ;
}
}
if ( ! $reason ) {
// if the moduleInfo contains a non-empty 'permanent' property, then it's not uninstallable
$info = $this -> getModuleInfo ( $class );
if ( ! empty ( $info [ 'permanent' ])) {
$reason = $this -> _ ( " Module is permanent " );
} else {
$dependents = $this -> getRequiresForUninstall ( $class );
if ( count ( $dependents )) $reason = $this -> _ ( " Module is required by other modules that must be removed first " );
}
if ( ! $reason && in_array ( 'Fieldtype' , wireClassParents ( $namespace . $class ))) {
2023-03-10 19:41:40 +01:00
foreach ( $this -> wire () -> fields as $field ) {
2022-03-08 15:55:41 +01:00
$fieldtype = wireClassName ( $field -> type , false );
if ( $fieldtype == $class ) {
$reason = $this -> _ ( " This module is a Fieldtype currently in use by one or more fields " );
break ;
}
}
}
}
if ( $returnReason && $reason ) return $reason ;
return $reason ? false : true ;
}
/**
* Returns whether the module can be deleted ( have it ' s files physically removed )
*
* #pw-internal
*
* @ param string | Module $class
* @ param bool $returnReason If true , the reason why it can ' t be removed will be returned rather than boolean false .
* @ return bool | string
*
*/
public function isDeleteable ( $class , $returnReason = false ) {
$reason = '' ;
$class = $this -> getModuleClass ( $class );
$filename = isset ( $this -> installable [ $class ]) ? $this -> installable [ $class ] : null ;
$dirname = dirname ( $filename );
if ( empty ( $filename ) || $this -> isInstalled ( $class )) {
$reason = " Module must be uninstalled before it can be deleted. " ;
} else if ( is_link ( $filename ) || is_link ( $dirname ) || is_link ( dirname ( $dirname ))) {
$reason = " Module is linked to another location " ;
} else if ( ! is_file ( $filename )) {
$reason = " Module file does not exist " ;
} else if ( strpos ( $filename , $this -> paths [ 0 ]) === 0 ) {
$reason = " Core modules may not be deleted. " ;
} else if ( ! is_writable ( $filename )) {
$reason = " We have no write access to the module file, it must be removed manually. " ;
}
if ( $returnReason && $reason ) return $reason ;
return $reason ? false : true ;
}
/**
* Delete the given module , physically removing its files
*
* #pw-group-manipulation
*
* @ param string $class Module name ( class name )
2023-03-10 19:41:40 +01:00
* @ return bool
2022-03-08 15:55:41 +01:00
* @ throws WireException If module can ' t be deleted , exception will be thrown containing reason .
*
*/
public function ___delete ( $class ) {
2023-03-10 19:41:40 +01:00
$config = $this -> wire () -> config ;
$fileTools = $this -> wire () -> files ;
2022-03-08 15:55:41 +01:00
$class = $this -> getModuleClass ( $class );
$success = true ;
$reason = $this -> isDeleteable ( $class , true );
if ( $reason !== true ) throw new WireException ( $reason );
2023-03-10 19:41:40 +01:00
$siteModulesPath = $config -> paths -> siteModules ;
2022-03-08 15:55:41 +01:00
$filename = $this -> installable [ $class ];
$basename = basename ( $filename );
// double check that $class is consistent with the actual $basename
if ( $basename === " $class .module " || $basename === " $class .module.php " ) {
// good, this is consistent with the format we require
} else {
throw new WireException ( " Unrecognized module filename format " );
}
// now determine if module is the owner of the directory it exists in
// this is the case if the module class name is the same as the directory name
$path = dirname ( $filename ); // full path to directory, i.e. .../site/modules/ProcessHello
$name = basename ( $path ); // just name of directory that module is, i.e. ProcessHello
$parentPath = dirname ( $path ); // full path to parent directory, i.e. ../site/modules
$backupPath = $parentPath . " /. $name " ; // backup path, in case module is backed up
// first check that we are still in the /site/modules/ (or another non core modules path)
$inPath = false ; // is module somewhere beneath /site/modules/ ?
$inRoot = false ; // is module in /site/modules/ root? i.e. /site/modules/ModuleName.module
foreach ( $this -> paths as $key => $modulesPath ) {
if ( $key === 0 ) continue ; // skip core modules path
if ( strpos ( " $parentPath / " , $modulesPath ) === 0 ) $inPath = true ;
if ( $modulesPath === $path ) $inRoot = true ;
}
$basename = basename ( $basename , '.php' );
$basename = basename ( $basename , '.module' );
$files = array (
" $basename .module " ,
" $basename .module.php " ,
" $basename .info.php " ,
" $basename .info.json " ,
" $basename .config.php " ,
" { $basename } Config.php " ,
2023-03-10 19:41:40 +01:00
);
2022-03-08 15:55:41 +01:00
if ( $inPath ) {
// module is in /site/modules/[ModuleName]/
$numOtherModules = 0 ; // num modules in dir other than this one
$numLinks = 0 ; // number of symbolic links
$dirs = array ( " $path / " );
do {
$dir = array_shift ( $dirs );
$this -> message ( " Scanning: $dir " , Notice :: debug );
foreach ( new \DirectoryIterator ( $dir ) as $file ) {
if ( $file -> isDot ()) continue ;
if ( $file -> isLink ()) {
$numLinks ++ ;
continue ;
}
if ( $file -> isDir ()) {
$dirs [] = $fileTools -> unixDirName ( $file -> getPathname ());
continue ;
}
if ( in_array ( $file -> getBasename (), $files )) continue ; // skip known files
if ( strpos ( $file -> getBasename (), '.module' ) && preg_match ( '{(\.module|\.module\.php)$}' , $file -> getBasename ())) {
// another module exists in this dir, so we don't want to delete that
$numOtherModules ++ ;
}
if ( preg_match ( '{^(' . $basename . '\.[-_.a-zA-Z0-9]+)$}' , $file -> getBasename (), $matches )) {
// keep track of potentially related files in case we have to delete them individually
$files [] = $matches [ 1 ];
}
}
} while ( count ( $dirs ));
if ( ! $inRoot && ! $numOtherModules && ! $numLinks ) {
// the modulePath had no other modules or directories in it, so we can delete it entirely
2023-03-10 19:41:40 +01:00
$success = ( bool ) $fileTools -> rmdir ( $path , true );
2022-03-08 15:55:41 +01:00
if ( $success ) {
$this -> message ( " Removed directory: $path " , Notice :: debug );
if ( is_dir ( $backupPath )) {
if ( $fileTools -> rmdir ( $backupPath , true )) $this -> message ( " Removed directory: $backupPath " , Notice :: debug );
}
$files = array ();
} else {
$this -> error ( " Failed to remove directory: $path " , Notice :: debug );
}
}
}
// remove module files individually
foreach ( $files as $file ) {
$file = " $path / $file " ;
if ( ! file_exists ( $file )) continue ;
if ( $fileTools -> unlink ( $file , $siteModulesPath )) {
$this -> message ( " Removed file: $file " , Notice :: debug );
} else {
$this -> error ( " Unable to remove file: $file " , Notice :: debug );
}
}
$this -> log ( " Deleted module ' $class ' " );
return $success ;
}
/**
* Uninstall the given module name
*
* #pw-group-manipulation
*
* @ param string $class Module name ( class name )
* @ return bool
* @ throws WireException
*
*/
public function ___uninstall ( $class ) {
$class = $this -> getModuleClass ( $class );
$reason = $this -> isUninstallable ( $class , true );
if ( $reason !== true ) {
// throw new WireException("$class - Can't Uninstall - $reason");
return false ;
}
// check if there are any modules still installed that this one says it is responsible for installing
foreach ( $this -> getUninstalls ( $class ) as $name ) {
// catch uninstall exceptions at this point since original module has already been uninstalled
$label = $this -> _ ( 'Module Auto Uninstall' );
try {
$this -> uninstall ( $name );
$this -> message ( " $label : $name " );
} catch ( \Exception $e ) {
$error = " $label : $name - " . $e -> getMessage ();
$this -> trackException ( $e , false , $error );
}
}
$info = $this -> getModuleInfoVerbose ( $class );
$module = $this -> getModule ( $class , array (
'noPermissionCheck' => true ,
'noInstall' => true ,
// 'noInit' => true
));
if ( ! $module ) return false ;
// remove all hooks attached to this module
$hooks = $module instanceof Wire ? $module -> getHooks () : array ();
foreach ( $hooks as $hook ) {
if ( $hook [ 'method' ] == 'uninstall' ) continue ;
$this -> message ( " Removed hook $class => " . $hook [ 'options' ][ 'fromClass' ] . " $hook[method] " , Notice :: debug );
$module -> removeHook ( $hook [ 'id' ]);
}
// remove all hooks attached to other ProcessWire objects
2023-03-10 19:41:40 +01:00
$hooks = array_merge ( $this -> getHooks ( '*' ), $this -> wire () -> hooks -> getAllLocalHooks ());
2022-03-08 15:55:41 +01:00
foreach ( $hooks as $hook ) {
/** @var Wire $toObject */
$toObject = $hook [ 'toObject' ];
$toClass = wireClassName ( $toObject , false );
$toMethod = $hook [ 'toMethod' ];
if ( $class === $toClass && $toMethod != 'uninstall' ) {
$toObject -> removeHook ( $hook [ 'id' ]);
$this -> message ( " Removed hook $class => " . $hook [ 'options' ][ 'fromClass' ] . " $hook[method] " , Notice :: debug );
}
}
if ( method_exists ( $module , '___uninstall' ) || method_exists ( $module , 'uninstall' )) {
// note module's uninstall method may throw an exception to abort the uninstall
/** @var _Module $module */
$module -> uninstall ();
}
2023-03-10 19:41:40 +01:00
$database = $this -> wire () -> database ;
2022-03-08 15:55:41 +01:00
$query = $database -> prepare ( 'DELETE FROM modules WHERE class=:class LIMIT 1' ); // QA
$query -> bindValue ( " :class " , $class , \PDO :: PARAM_STR );
$query -> execute ();
// add back to the installable list
if ( class_exists ( " ReflectionClass " )) {
$reflector = new \ReflectionClass ( $this -> getModuleClass ( $module , true ));
$this -> installable [ $class ] = $reflector -> getFileName ();
}
unset ( $this -> moduleIDs [ $class ]);
$this -> remove ( $module );
2023-03-10 19:41:40 +01:00
$sanitizer = $this -> wire () -> sanitizer ;
$permissions = $this -> wire () -> permissions ;
2022-03-08 15:55:41 +01:00
// delete permissions installed by this module
if ( isset ( $info [ 'permissions' ]) && is_array ( $info [ 'permissions' ])) {
foreach ( $info [ 'permissions' ] as $name => $title ) {
2023-03-10 19:41:40 +01:00
$name = $sanitizer -> pageName ( $name );
2022-03-08 15:55:41 +01:00
if ( ctype_digit ( " $name " ) || empty ( $name )) continue ;
2023-03-10 19:41:40 +01:00
$permission = $permissions -> get ( $name );
2022-03-08 15:55:41 +01:00
if ( ! $permission -> id ) continue ;
try {
2023-03-10 19:41:40 +01:00
$permissions -> delete ( $permission );
2022-03-08 15:55:41 +01:00
$this -> message ( sprintf ( $this -> _ ( 'Deleted Permission: %s' ), $name ));
} catch ( \Exception $e ) {
$error = sprintf ( $this -> _ ( 'Error deleting permission: %s' ), $name );
$this -> trackException ( $e , false , $error );
}
}
}
$this -> log ( " Uninstalled module ' $class ' " );
$this -> refresh ();
return true ;
}
/**
* Get flags for the given module
*
* #pw-internal
*
* @ param int | string | Module $class Module to add flag to
* @ return int | false Returns integer flags on success , or boolean false on fail
*
*/
public function getFlags ( $class ) {
$id = ctype_digit ( " $class " ) ? ( int ) $class : $this -> getModuleID ( $class );
if ( isset ( $this -> moduleFlags [ $id ])) return $this -> moduleFlags [ $id ];
if ( ! $id ) return false ;
2023-03-10 19:41:40 +01:00
$query = $this -> wire () -> database -> prepare ( 'SELECT flags FROM modules WHERE id=:id' );
2022-03-08 15:55:41 +01:00
$query -> bindValue ( ':id' , $id , \PDO :: PARAM_INT );
$query -> execute ();
if ( ! $query -> rowCount ()) return false ;
list ( $flags ) = $query -> fetch ( \PDO :: FETCH_NUM );
$flags = ( int ) $flags ;
$this -> moduleFlags [ $id ] = $flags ;
return $flags ;
}
/**
* Does module have flag ?
*
* #pw-internal
*
* @ param int | string | Module $class Module ID , class name or instance
* @ param int $flag
* @ return bool
* @ since 3.0 . 170
*
*/
public function hasFlag ( $class , $flag ) {
$flags = $this -> getFlags ( $class );
return $flags === false ? false : ( $flags & $flag );
}
/**
* Set module flags
*
* #pw-internal
*
* @ param $class
* @ param $flags
* @ return bool
*
*/
public function setFlags ( $class , $flags ) {
$flags = ( int ) $flags ;
$id = ctype_digit ( " $class " ) ? ( int ) $class : $this -> getModuleID ( $class );
if ( ! $id ) return false ;
if ( $this -> moduleFlags [ $id ] === $flags ) return true ;
2023-03-10 19:41:40 +01:00
$query = $this -> wire () -> database -> prepare ( 'UPDATE modules SET flags=:flags WHERE id=:id' );
2022-03-08 15:55:41 +01:00
$query -> bindValue ( ':flags' , $flags );
$query -> bindValue ( ':id' , $id );
if ( $this -> debug ) $this -> message ( " setFlags( " . $this -> getModuleClass ( $class ) . " , " . $this -> moduleFlags [ $id ] . " => $flags ) " );
$this -> moduleFlags [ $id ] = $flags ;
return $query -> execute ();
}
/**
* Add or remove a flag from a module
*
* #pw-internal
*
* @ param $class int | string | Module $class Module to add flag to
* @ param $flag int Flag to add ( see flags * constants )
* @ param $add bool $add Specify true to add the flag or false to remove it
* @ return bool True on success , false on fail
*
*/
public function setFlag ( $class , $flag , $add = true ) {
$id = ctype_digit ( " $class " ) ? ( int ) $class : $this -> getModuleID ( $class );
if ( ! $id ) return false ;
$flag = ( int ) $flag ;
if ( ! $flag ) return false ;
$flags = $this -> getFlags ( $id );
if ( $add ) {
if ( $flags & $flag ) return true ; // already has the flag
$flags = $flags | $flag ;
} else {
if ( ! ( $flags & $flag )) return true ; // doesn't already have the flag
$flags = $flags & ~ $flag ;
}
$this -> setFlags ( $id , $flags );
return true ;
}
/**
* Return an array of other module class names that are uninstalled when the given one is
*
* #pw-internal
*
* The opposite of this function is found in the getModuleInfo array property 'installs' .
* Note that 'installs' and uninstalls may be different , as only modules in the 'installs' list
* that indicate 'requires' for the installer module will be uninstalled .
*
* @ param $class
* @ return array
*
*/
public function getUninstalls ( $class ) {
$uninstalls = array ();
$class = $this -> getModuleClass ( $class );
if ( ! $class ) return $uninstalls ;
$info = $this -> getModuleInfoVerbose ( $class );
// check if there are any modules still installed that this one says it is responsible for installing
foreach ( $info [ 'installs' ] as $name ) {
// if module isn't installed, then great
if ( ! $this -> isInstalled ( $name )) continue ;
// if an 'installs' module doesn't indicate that it requires this one, then leave it installed
$i = $this -> getModuleInfo ( $name );
if ( ! in_array ( $class , $i [ 'requires' ])) continue ;
// add it to the uninstalls array
$uninstalls [] = $name ;
}
return $uninstalls ;
}
/**
* Returns the database ID of a given module class , or 0 if not found
*
* #pw-internal
*
* @ param string | Module $class Module or module name
* @ return int
*
*/
public function getModuleID ( $class ) {
$id = 0 ;
if ( is_object ( $class )) {
if ( $class instanceof Module ) {
$class = $this -> getModuleClass ( $class );
} else {
// Class is not a module
return $id ;
}
}
if ( isset ( $this -> moduleIDs [ $class ])) {
$id = ( int ) $this -> moduleIDs [ $class ];
} else foreach ( $this -> moduleInfoCache as $key => $info ) {
if ( $info [ 'name' ] == $class ) {
$id = ( int ) $key ;
break ;
}
}
return $id ;
}
/**
* Returns the module ' s class name .
*
* - Given a numeric database ID , returns the associated module class name or false if it doesn ' t exist
* - Given a Module or ModulePlaceholder instance , returns the Module ' s class name .
*
* If the module has a className () method then it uses that rather than PHP ' s get_class () .
* This is important because of placeholder modules . For example , get_class would return
* 'ModulePlaceholder' rather than the correct className for a Module .
*
* #pw-internal
*
* @ param string | int | Module
* @ param bool $withNamespace Specify true to include the namespace in the class
* @ return string | bool The Module ' s class name or false if not found .
* Note that 'false' is only possible if you give this method a non - Module , or an integer ID
* that doesn ' t correspond to a module ID .
*
*/
public function getModuleClass ( $module , $withNamespace = false ) {
$className = '' ;
$namespace = '' ;
if ( $module instanceof Module ) {
if ( wireMethodExists ( $module , 'className' )) {
if ( $withNamespace ) return $module -> className ( true );
return $module -> className ();
} else {
return wireClassName ( $module , $withNamespace );
}
} else if ( is_int ( $module ) || ctype_digit ( " $module " )) {
$className = array_search (( int ) $module , $this -> moduleIDs );
} else if ( is_string ( $module )) {
if ( strpos ( $module , " \\ " ) !== false ) {
$namespace = wireClassName ( $module , 1 );
$className = wireClassName ( $module , false );
}
// remove extensions if they were included in the module name
if ( strpos ( $module , '.' ) !== false ) {
$module = basename ( basename ( $module , '.php' ), '.module' );
}
if ( array_key_exists ( $module , $this -> moduleIDs )) {
$className = $module ;
} else if ( array_key_exists ( $module , $this -> installable )) {
$className = $module ;
}
}
if ( $className ) {
if ( $withNamespace ) {
if ( $namespace ) {
$className = " $namespace\\ $className " ;
} else {
$className = $this -> getModuleNamespace ( $className ) . $className ;
}
}
return $className ;
}
return false ;
}
/**
* Retrieve module info from ModuleName . info . json or ModuleName . info . php
*
* @ param string $moduleName
* @ return array
*
*/
protected function getModuleInfoExternal ( $moduleName ) {
// if($this->debug) $this->message("getModuleInfoExternal($moduleName)");
// ...attempt to load info by info file (Module.info.php or Module.info.json)
if ( ! empty ( $this -> installable [ $moduleName ])) {
$path = dirname ( $this -> installable [ $moduleName ]) . '/' ;
} else {
2023-03-10 19:41:40 +01:00
$path = $this -> wire () -> config -> paths ( $moduleName );
2022-03-08 15:55:41 +01:00
}
if ( empty ( $path )) return array ();
// module exists and has a dedicated path on the file system
// we will try to get info from a PHP or JSON info file
$filePHP = $path . " $moduleName .info.php " ;
$fileJSON = $path . " $moduleName .info.json " ;
$info = array ();
if ( file_exists ( $filePHP )) {
/** @noinspection PhpIncludeInspection */
include ( $filePHP ); // will populate $info automatically
if ( ! is_array ( $info ) || ! count ( $info )) $this -> error ( " Invalid PHP module info file for $moduleName " );
} else if ( file_exists ( $fileJSON )) {
$info = file_get_contents ( $fileJSON );
$info = json_decode ( $info , true );
if ( ! $info ) {
$info = array ();
$this -> error ( " Invalid JSON module info file for $moduleName " );
}
}
return $info ;
}
/**
* Retrieve module info from internal getModuleInfo function in the class
*
* @ param Module | string $module
* @ param string $namespace
* @ return array
*
*/
protected function getModuleInfoInternal ( $module , $namespace = '' ) {
// if($this->debug) $this->message("getModuleInfoInternal($module)");
$info = array ();
if ( $module instanceof ModulePlaceholder ) {
$this -> includeModule ( $module );
$module = $module -> className ();
}
if ( $module instanceof Module ) {
if ( method_exists ( $module , 'getModuleInfo' )) {
$info = $module :: getModuleInfo ();
}
} else if ( $module ) {
if ( empty ( $namespace )) $namespace = $this -> getModuleNamespace ( $module );
$className = wireClassName ( $namespace . $module , true );
if ( ! class_exists ( $className )) $this -> includeModule ( $module );
if ( is_callable ( " $className ::getModuleInfo " )) {
$info = call_user_func ( array ( $className , 'getModuleInfo' ));
}
}
return $info ;
}
/**
* Pull module info directly from the module file ' s getModuleInfo without letting PHP parse it
*
* Useful for getting module info from modules that extend another module not already on the file system .
*
* @ param $className
* @ return array Only includes module info specified in the module file itself .
*
protected function getModuleInfoInternalSafe ( $className ) {
// future addition
// load file, preg_split by /^\s*(public|private|protected)[^;{]+function\s*([^)]*)[^{]*{/
// isolate the one that starts has getModuleInfo in matches[1]
// parse data from matches[2]
return array ();
}
*/
/**
* Retrieve module info for system properties : PHP or ProcessWire
*
* @ param $moduleName
* @ return array
*
*/
protected function getModuleInfoSystem ( $moduleName ) {
$info = array ();
if ( $moduleName === 'PHP' ) {
$info [ 'id' ] = 0 ;
$info [ 'name' ] = $moduleName ;
$info [ 'title' ] = $moduleName ;
$info [ 'version' ] = PHP_VERSION ;
return $info ;
} else if ( $moduleName === 'ProcessWire' ) {
$info [ 'id' ] = 0 ;
$info [ 'name' ] = $moduleName ;
$info [ 'title' ] = $moduleName ;
2023-03-10 19:41:40 +01:00
$info [ 'version' ] = $this -> wire () -> config -> version ;
2022-03-08 15:55:41 +01:00
$info [ 'namespace' ] = strlen ( __NAMESPACE__ ) ? " \\ " . __NAMESPACE__ . " \\ " : " " ;
$info [ 'requiresVersions' ] = array (
'PHP' => array ( '>=' , '5.3.8' ),
'PHP_modules' => array ( '=' , 'PDO,mysqli' ),
'Apache_modules' => array ( '=' , 'mod_rewrite' ),
'MySQL' => array ( '>=' , '5.0.15' ),
);
$info [ 'requires' ] = array_keys ( $info [ 'requiresVersions' ]);
} else {
return array ();
}
$info [ 'versionStr' ] = $info [ 'version' ];
return $info ;
}
/**
* Returns an associative array of information for a Module
*
* The array returned by this method includes the following :
*
* - `id` ( int ) : module database ID .
* - `name` ( string ) : module class name .
* - `title` ( string ) : module title .
* - `version` ( int ) : module version .
* - `icon` ( string ) : Optional icon name ( excluding the " fa- " ) part .
* - `requires` ( array ) : module names required by this module .
* - `requiresVersions` ( array ) : required module versions– module name is key , value is array ( $operator , $version ) .
* - `installs` ( array ) : module names that this module installs .
* - `permission` ( string ) : permission name required to execute this module .
* - `autoload` ( bool ) : true if module is autoload , false if not .
* - `singular` ( bool ) : true if module is singular , false if not .
* - `created` ( int ) : unix - timestamp of date / time module added to system ( for uninstalled modules , it is the file date ) .
* - `installed` ( bool ) : is the module currently installed ? ( boolean , or null when not determined )
* - `configurable` ( bool | int ) : true or positive number when the module is configurable .
* - `namespace` ( string ) : PHP namespace that module lives in .
*
* The following properties are also included when " verbose " mode is requested . When not in verbose mode , these
* properties are present but blank :
*
* - `versionStr` ( string ) : formatted module version string .
* - `file` ( string ) : module filename from PW installation root , or false when it can ' t be found .
* - `core` ( bool ) : true when module is a core module , false when not .
* - `author` ( string ) : module author , when specified .
* - `summary` ( string ) : summary of what this module does .
* - `href` ( string ) : URL to module details ( when specified ) .
* - `permissions` ( array ) : permissions installed by this module , associative array ( 'permission-name' => 'Description' ) .
* - `page` ( array ) : definition of page to create for Process module ( see Process class )
*
* The following properties appear only for " Process " modules , and only if specified by module .
* See the Process class for more details :
*
* - `nav` ( array ) : navigation definition
* - `useNavJSON` ( bool ) : whether the Process module provides JSON navigation
* - `permissionMethod` ( string | callable ) : method to call to determine permission
* - `page` ( array ) : definition of page to create for Process module
*
* On error , an `error` index in returned array contains error message . You can also identify errors
* such as a non - existing module by the returned module info having an `id` index of `0`
*
* ~~~~~
* // example of getting module info
* $moduleInfo = $modules -> getModuleInfo ( 'InputfieldCKEditor' );
*
* // example of getting verbose module info
* $moduleInfo = $modules -> getModuleInfoVerbose ( 'MarkupAdminDataTable' );
* ~~~~~
*
* @ param string | Module | int $class Specify one of the following :
* - Module object instance
* - Module class name ( string )
* - Module ID ( int )
* - To get info for ALL modules , specify `*` or `all` .
* - To get system information , specify `ProcessWire` or `PHP` .
* - To get a blank module info template , specify `info` .
* @ param array $options Optional options to modify behavior of what gets returned
* - `verbose` ( bool ) : Makes the info also include verbose properties , which are otherwise blank . ( default = false )
* - `minify` ( bool ) : Remove non - applicable and properties that match defaults ? ( default = false , or true when getting `all` )
* - `noCache` ( bool ) : prevents use of cache to retrieve the module info . ( default = false )
* @ return array Associative array of module information .
* - On error , an `error` index is also populated with an error message .
* - When requesting a module that does not exist its `id` value will be `0` and its `name` will be blank .
* @ see Modules :: getModuleInfoVerbose ()
* @ todo move all getModuleInfo methods to their own ModuleInfo class and break this method down further .
*
*/
public function getModuleInfo ( $class , array $options = array ()) {
$getAll = $class === '*' || $class === 'all' ;
$getSystem = $class === 'ProcessWire' || $class === 'PHP' || $class === 'info' ;
$defaults = array (
'verbose' => false ,
'minify' => $getAll ,
'noCache' => false ,
'noInclude' => false ,
);
$options = array_merge ( $defaults , $options );
$info = array ();
$module = $class ;
$moduleName = '' ;
$moduleID = 0 ;
$fromCache = false ; // was the data loaded from cache?
if ( ! $getAll && ! $getSystem ) {
$moduleName = $this -> getModuleClass ( $module );
$moduleID = ( string ) $this -> getModuleID ( $module ); // typecast to string for cache
}
static $infoTemplate = array (
// module database ID
'id' => 0 ,
// module class name
'name' => '' ,
// module title
'title' => '' ,
// module version
'version' => 0 ,
// module version (always formatted string)
'versionStr' => '0.0.0' ,
// who authored the module? (included in 'verbose' mode only)
'author' => '' ,
// summary of what this module does (included in 'verbose' mode only)
'summary' => '' ,
// URL to module details (included in 'verbose' mode only)
'href' => '' ,
// Optional name of icon representing this module (currently font-awesome icon names, excluding the "fa-" portion)
'icon' => '' ,
// this method converts this to array of module names, regardless of how the module specifies it
'requires' => array (),
// module name is key, value is array($operator, $version). Note 'requiresVersions' index is created by this function.
'requiresVersions' => array (),
// array of module class names
'installs' => array (),
// permission required to execute this module
'permission' => '' ,
// permissions automatically installed/uninstalled with this module. array of ('permission-name' => 'Description')
'permissions' => array (),
// true if module is autoload, false if not. null=unknown
'autoload' => null ,
// true if module is singular, false if not. null=unknown
'singular' => null ,
// unix-timestamp date/time module added to system (for uninstalled modules, it is the file date)
'created' => 0 ,
// is the module currently installed? (boolean, or null when not determined)
'installed' => null ,
// this is set to true when the module is configurable, false when it's not, and null when it's not determined
'configurable' => null ,
// verbose mode only: true when module implements SearchableModule interface, or null|false when not
'searchable' => null ,
// namespace that module lives in (string)
'namespace' => null ,
// verbose mode only: this is set to the module filename (from PW installation root), false when it can't be found, null when it hasn't been determined
'file' => null ,
// verbose mode only: this is set to true when the module is a core module, false when it's not, and null when it's not determined
'core' => null ,
// verbose mode only: any translations supplied with the module
// 'languages' => null,
// other properties that may be present, but are optional, for Process modules:
// 'nav' => array(), // navigation definition: see Process.php
// 'useNavJSON' => bool, // whether the Process module provides JSON navigation
// 'page' => array(), // page to create for Process module: see Process.php
// 'permissionMethod' => string or callable // method to call to determine permission: see Process.php
);
if ( $getAll ) {
if ( empty ( $this -> moduleInfoCache )) $this -> loadModuleInfoCache ();
$modulesInfo = $this -> moduleInfoCache ;
if ( $options [ 'verbose' ]) {
if ( empty ( $this -> moduleInfoCacheVerbose )) $this -> loadModuleInfoCacheVerbose ();
foreach ( $this -> moduleInfoCacheVerbose as $moduleID => $moduleInfoVerbose ) {
$modulesInfo [ $moduleID ] = array_merge ( $modulesInfo [ $moduleID ], $moduleInfoVerbose );
}
}
if ( ! $options [ 'minify' ]) {
foreach ( $modulesInfo as $moduleID => $info ) {
$modulesInfo [ $moduleID ] = array_merge ( $infoTemplate , $info );
}
}
return $modulesInfo ;
} else if ( $getSystem ) {
// module is a system
if ( $class === 'info' ) return $infoTemplate ;
$info = $this -> getModuleInfoSystem ( $module );
return $options [ 'minify' ] ? $info : array_merge ( $infoTemplate , $info );
} else if ( $module instanceof Module ) {
// module is an instance
// $moduleName = method_exists($module, 'className') ? $module->className() : get_class($module);
// return from cache if available
if ( empty ( $options [ 'noCache' ]) && ! empty ( $this -> moduleInfoCache [ $moduleID ])) {
$info = $this -> moduleInfoCache [ $moduleID ];
$fromCache = true ;
} else {
$info = $this -> getModuleInfoExternal ( $moduleName );
if ( ! count ( $info )) $info = $this -> getModuleInfoInternal ( $module );
}
} else {
// module is a class name or ID
if ( ctype_digit ( " $module " )) $module = $moduleName ;
// return from cache if available (as it almost always should be)
if ( empty ( $options [ 'noCache' ]) && ! empty ( $this -> moduleInfoCache [ $moduleID ])) {
$info = $this -> moduleInfoCache [ $moduleID ];
$fromCache = true ;
} else if ( empty ( $options [ 'noCache' ]) && $moduleID == 0 ) {
// uninstalled module
if ( ! count ( $this -> moduleInfoCacheUninstalled )) $this -> loadModuleInfoCacheVerbose ( true );
if ( isset ( $this -> moduleInfoCacheUninstalled [ $moduleName ])) {
$info = $this -> moduleInfoCacheUninstalled [ $moduleName ];
$fromCache = true ;
}
}
if ( ! $fromCache ) {
$namespace = $this -> getModuleNamespace ( $moduleName );
if ( class_exists ( $namespace . $moduleName , false )) {
// module is already in memory, check external first, then internal
$info = $this -> getModuleInfoExternal ( $moduleName );
if ( ! count ( $info )) $info = $this -> getModuleInfoInternal ( $moduleName , $namespace );
} else {
// module is not in memory, check external first, then internal
$info = $this -> getModuleInfoExternal ( $moduleName );
if ( ! count ( $info )) {
if ( isset ( $this -> installable [ $moduleName ])) $this -> includeModuleFile ( $this -> installable [ $moduleName ], $moduleName );
// info not available externally, attempt to locate it interally
$info = $this -> getModuleInfoInternal ( $moduleName , $namespace );
}
}
}
}
if ( ! $fromCache && ! count ( $info )) {
$info = $infoTemplate ;
$info [ 'title' ] = $module ;
$info [ 'summary' ] = 'Inactive' ;
$info [ 'error' ] = 'Unable to locate module' ;
return $info ;
}
if ( ! $options [ 'minify' ]) $info = array_merge ( $infoTemplate , $info );
$info [ 'id' ] = ( int ) $moduleID ;
if ( $fromCache ) {
// since cache is loaded at init(), this is the most common scenario
if ( $options [ 'verbose' ]) {
if ( empty ( $this -> moduleInfoCacheVerbose )) $this -> loadModuleInfoCacheVerbose ();
if ( ! empty ( $this -> moduleInfoCacheVerbose [ $moduleID ])) {
$info = array_merge ( $info , $this -> moduleInfoCacheVerbose [ $moduleID ]);
}
}
// populate defaults for properties omitted from cache
if ( is_null ( $info [ 'autoload' ])) $info [ 'autoload' ] = false ;
if ( is_null ( $info [ 'singular' ])) $info [ 'singular' ] = false ;
if ( is_null ( $info [ 'configurable' ])) $info [ 'configurable' ] = false ;
if ( is_null ( $info [ 'core' ])) $info [ 'core' ] = false ;
if ( is_null ( $info [ 'installed' ])) $info [ 'installed' ] = true ;
if ( is_null ( $info [ 'namespace' ])) $info [ 'namespace' ] = strlen ( __NAMESPACE__ ) ? " \\ " . __NAMESPACE__ . " \\ " : " " ;
if ( ! empty ( $info [ 'requiresVersions' ])) $info [ 'requires' ] = array_keys ( $info [ 'requiresVersions' ]);
if ( $moduleName == 'SystemUpdater' ) $info [ 'configurable' ] = 1 ; // fallback, just in case
// we skip everything else when module comes from cache since we can safely assume the checks below
// are already accounted for in the cached module info
} else {
// not from cache, only likely to occur when refreshing modules info caches
// if $info[requires] isn't already an array, make it one
if ( ! is_array ( $info [ 'requires' ])) {
$info [ 'requires' ] = str_replace ( ' ' , '' , $info [ 'requires' ]); // remove whitespace
if ( strpos ( $info [ 'requires' ], ',' ) !== false ) {
$info [ 'requires' ] = explode ( ',' , $info [ 'requires' ]);
} else {
$info [ 'requires' ] = array ( $info [ 'requires' ]);
}
}
// populate requiresVersions
foreach ( $info [ 'requires' ] as $key => $class ) {
if ( ! ctype_alnum ( $class )) {
// has a version string
list ( $class , $operator , $version ) = $this -> extractModuleOperatorVersion ( $class );
$info [ 'requires' ][ $key ] = $class ; // convert to just class
} else {
// no version string
$operator = '>=' ;
$version = 0 ;
}
$info [ 'requiresVersions' ][ $class ] = array ( $operator , $version );
}
// what does it install?
// if $info[installs] isn't already an array, make it one
if ( ! is_array ( $info [ 'installs' ])) {
$info [ 'installs' ] = str_replace ( ' ' , '' , $info [ 'installs' ]); // remove whitespace
if ( strpos ( $info [ 'installs' ], ',' ) !== false ) {
$info [ 'installs' ] = explode ( ',' , $info [ 'installs' ]);
} else {
$info [ 'installs' ] = array ( $info [ 'installs' ]);
}
}
// misc
if ( $options [ 'verbose' ]) $info [ 'versionStr' ] = $this -> formatVersion ( $info [ 'version' ]); // versionStr
$info [ 'name' ] = $moduleName ; // module name
// module configurable?
$configurable = $this -> isConfigurable ( $moduleName , false );
if ( $configurable === true || is_int ( $configurable ) && $configurable > 1 ) {
// configurable via ConfigurableModule interface
// true=static, 2=non-static, 3=non-static $data, 4=non-static wrap,
// 19=non-static getModuleConfigArray, 20=static getModuleConfigArray
$info [ 'configurable' ] = $configurable ;
} else if ( $configurable ) {
// configurable via external file: ModuleName.config.php or ModuleNameConfig.php file
$info [ 'configurable' ] = basename ( $configurable );
} else {
// not configurable
$info [ 'configurable' ] = false ;
}
// created date
if ( isset ( $this -> createdDates [ $moduleID ])) $info [ 'created' ] = strtotime ( $this -> createdDates [ $moduleID ]);
$info [ 'installed' ] = isset ( $this -> installable [ $moduleName ]) ? false : true ;
if ( ! $info [ 'installed' ] && ! $info [ 'created' ] && isset ( $this -> installable [ $moduleName ])) {
// uninstalled modules get their created date from the file or dir that they are in (whichever is newer)
$pathname = $this -> installable [ $moduleName ];
$filemtime = ( int ) filemtime ( $pathname );
$dirname = dirname ( $pathname );
$dirmtime = substr ( $dirname , - 7 ) == 'modules' || strpos ( $dirname , $this -> paths [ 0 ]) !== false ? 0 : ( int ) filemtime ( $dirname );
$info [ 'created' ] = $dirmtime > $filemtime ? $dirmtime : $filemtime ;
}
// namespace
if ( $info [ 'core' ]) {
// default namespace, assumed since all core modules are in default namespace
$info [ 'namespace' ] = strlen ( __NAMESPACE__ ) ? " \\ " . __NAMESPACE__ . " \\ " : " " ;
} else {
$info [ 'namespace' ] = $this -> getModuleNamespace ( $moduleName , array (
'file' => $info [ 'file' ],
'noCache' => $options [ 'noCache' ]
));
}
if ( ! $options [ 'verbose' ]) foreach ( $this -> moduleInfoVerboseKeys as $key ) unset ( $info [ $key ]);
}
if ( is_null ( $info [ 'namespace' ])) {
$info [ 'namespace' ] = strlen ( __NAMESPACE__ ) ? " \\ " . __NAMESPACE__ . " \\ " : " " ;
}
if ( empty ( $info [ 'created' ]) && isset ( $this -> createdDates [ $moduleID ])) {
$info [ 'created' ] = strtotime ( $this -> createdDates [ $moduleID ]);
}
if ( $options [ 'verbose' ]) {
// the file property is not stored in the verbose cache, but provided as a verbose key
$info [ 'file' ] = $this -> getModuleFile ( $moduleName );
if ( $info [ 'file' ]) $info [ 'core' ] = strpos ( $info [ 'file' ], $this -> coreModulesDir ) !== false ; // is it core?
} else {
// module info may still contain verbose keys with undefined values
}
if ( $options [ 'minify' ]) {
// when minify, any values that match defaults from infoTemplate are removed
if ( ! $options [ 'verbose' ]) foreach ( $this -> moduleInfoVerboseKeys as $key ) unset ( $info [ $key ]);
foreach ( $info as $key => $value ) {
if ( ! array_key_exists ( $key , $infoTemplate )) continue ;
if ( $value !== $infoTemplate [ $key ]) continue ;
unset ( $info [ $key ]);
}
}
// if($this->debug) $this->message("getModuleInfo($moduleName) " . ($fromCache ? "CACHE" : "NO-CACHE"));
return $info ;
}
/**
* Returns a verbose array of information for a Module
*
* This is the same as what’ s returned by `Modules::getModuleInfo()` except that it has the following additional properties :
*
* - `versionStr` ( string ) : formatted module version string .
* - `file` ( string ) : module filename from PW installation root , or false when it can ' t be found .
* - `core` ( bool ) : true when module is a core module , false when not .
* - `author` ( string ) : module author , when specified .
* - `summary` ( string ) : summary of what this module does .
* - `href` ( string ) : URL to module details ( when specified ) .
* - `permissions` ( array ) : permissions installed by this module , associative array ( 'permission - name' => 'Description' ) .
* - `page` ( array ) : definition of page to create for Process module ( see Process class )
*
* @ param string | Module | int $class May be class name , module instance , or module ID
* @ param array $options Optional options to modify behavior of what gets returned :
* - `noCache` ( bool ) : prevents use of cache to retrieve the module info
* - `noInclude` ( bool ) : prevents include () of the module file , applicable only if it hasn ' t already been included
* @ return array Associative array of module information
* @ see Modules :: getModuleInfo ()
*
*/
public function getModuleInfoVerbose ( $class , array $options = array ()) {
$options [ 'verbose' ] = true ;
$info = $this -> getModuleInfo ( $class , $options );
return $info ;
}
/**
* Get just a single property of module info
*
* @ param Module | string $class Module instance or module name
* @ param string $property Name of property to get
* @ param array $options Additional options ( see getModuleInfo method for options )
* @ return mixed | null Returns value of property or null if not found
* @ since 3.0 . 107
*
*/
public function getModuleInfoProperty ( $class , $property , array $options = array ()) {
if ( in_array ( $property , $this -> moduleInfoVerboseKeys )) {
$info = $this -> getModuleInfoVerbose ( $class , $options );
$info [ 'verbose' ] = true ;
} else {
$info = $this -> getModuleInfo ( $class , $options );
}
if ( ! isset ( $info [ $property ]) && empty ( $info [ 'verbose' ])) {
// try again, just in case we can find it in verbose data
$info = $this -> getModuleInfoVerbose ( $class , $options );
}
return isset ( $info [ $property ]) ? $info [ $property ] : null ;
}
/**
* Get an array of all unique , non - default , non - root module namespaces mapped to directory names
*
* #pw-internal
*
* @ return array
*
*/
public function getNamespaces () {
2023-03-10 19:41:40 +01:00
$config = $this -> wire () -> config ;
2022-03-08 15:55:41 +01:00
if ( ! is_null ( $this -> moduleNamespaceCache )) return $this -> moduleNamespaceCache ;
$defaultNamespace = strlen ( __NAMESPACE__ ) ? " \\ " . __NAMESPACE__ . " \\ " : " " ;
$namespaces = array ();
2023-03-10 19:41:40 +01:00
foreach ( $this -> moduleInfoCache as /* $moduleID => */ $info ) {
2022-03-08 15:55:41 +01:00
if ( ! isset ( $info [ 'namespace' ]) || $info [ 'namespace' ] === $defaultNamespace || $info [ 'namespace' ] === " \\ " ) continue ;
$moduleName = $info [ 'name' ];
2023-03-10 19:41:40 +01:00
$namespaces [ $info [ 'namespace' ]] = $config -> paths ( $moduleName );
2022-03-08 15:55:41 +01:00
}
$this -> moduleNamespaceCache = $namespaces ;
return $namespaces ;
}
/**
* Get the namespace for the given module
*
* #pw-internal
*
* @ param string | Module $moduleName
* @ param array $options
* - `file` ( string ) : Known module path / file , as an optimization .
* - `noCache` ( bool ) : Specify true to force reload namespace info directly from module file . ( default = false )
* - `noLoad` ( bool ) : Specify true to prevent loading of file for namespace discovery . ( default = false ) Added 3.0 . 170
* @ return null | string Returns namespace , or NULL if unable to determine . Namespace is ready to use in a string ( i . e . has trailing slashes )
*
*/
public function getModuleNamespace ( $moduleName , $options = array ()) {
$defaults = array (
'file' => null ,
'noLoad' => false ,
'noCache' => false ,
);
$namespace = null ;
$options = array_merge ( $defaults , $options );
if ( is_object ( $moduleName ) || strpos ( $moduleName , " \\ " ) !== false ) {
$className = is_object ( $moduleName ) ? get_class ( $moduleName ) : $moduleName ;
$parts = explode ( " \\ " , $className );
array_pop ( $parts );
$namespace = count ( $parts ) ? implode ( " \\ " , $parts ) : " " ;
$namespace = $namespace == " " ? " \\ " : " \\ $namespace\\ " ;
return $namespace ;
}
if ( empty ( $options [ 'noCache' ])) {
$moduleID = $this -> getModuleID ( $moduleName );
$info = isset ( $this -> moduleInfoCache [ $moduleID ]) ? $this -> moduleInfoCache [ $moduleID ] : null ;
if ( $info && isset ( $info [ 'namespace' ])) {
return $info [ 'namespace' ];
}
}
if ( empty ( $options [ 'file' ])) {
$options [ 'file' ] = $this -> getModuleFile ( $moduleName );
}
if ( strpos ( $options [ 'file' ], $this -> coreModulesDir ) !== false ) {
// all core modules use \ProcessWire\ namespace
$namespace = strlen ( __NAMESPACE__ ) ? __NAMESPACE__ . " \\ " : " " ;
return $namespace ;
}
if ( ! $options [ 'file' ] || ! file_exists ( $options [ 'file' ])) {
return null ;
}
if ( empty ( $options [ 'noLoad' ])) {
$namespace = $this -> getFileNamespace ( $options [ 'file' ]);
}
return $namespace ;
}
/**
* Get the namespace used in the given . php or . module file
*
* #pw-internal
*
* @ param string $file
* @ return string Includes leading and trailing backslashes where applicable
*
*/
public function getFileNamespace ( $file ) {
$namespace = $this -> wire () -> files -> getNamespace ( $file );
if ( $namespace !== " \\ " ) $namespace = " \\ " . trim ( $namespace , " \\ " ) . " \\ " ;
return $namespace ;
}
/**
* Get the class defined in the file ( or optionally the 'extends' or 'implements' )
*
* #pw-internal
*
* @ param string $file
* @ return array Returns array with these indexes :
* 'class' => string ( class without namespace )
* 'className' => string ( class with namespace )
* 'extends' => string
* 'namespace' => string
* 'implements' => array
*
*/
public function getFileClassInfo ( $file ) {
$value = array (
'class' => '' ,
'className' => '' ,
'extends' => '' ,
'namespace' => '' ,
'implements' => array ()
);
if ( ! is_file ( $file )) return $value ;
$data = file_get_contents ( $file );
if ( ! strpos ( $data , 'class' )) return $value ;
if ( ! preg_match ( '/^\s*class\s+(.+)$/m' , $data , $matches )) return $value ;
if ( strpos ( $matches [ 1 ], " \t " ) !== false ) $matches [ 1 ] = str_replace ( " \t " , " " , $matches [ 1 ]);
$parts = explode ( ' ' , trim ( $matches [ 1 ]));
foreach ( $parts as $key => $part ) {
if ( empty ( $part )) unset ( $parts [ $key ]);
}
$className = array_shift ( $parts );
if ( strpos ( $className , '\\' ) !== false ) {
$className = trim ( $className , '\\' );
$a = explode ( '\\' , $className );
$value [ 'className' ] = " \\ $className\\ " ;
$value [ 'class' ] = array_pop ( $a );
$value [ 'namespace' ] = '\\' . implode ( '\\' , $a ) . '\\' ;
} else {
$value [ 'className' ] = '\\' . $className ;
$value [ 'class' ] = $className ;
$value [ 'namespace' ] = '\\' ;
}
while ( count ( $parts )) {
$next = array_shift ( $parts );
if ( $next == 'extends' ) {
$value [ 'extends' ] = array_shift ( $parts );
} else if ( $next == 'implements' ) {
$implements = array_shift ( $parts );
if ( strlen ( $implements )) {
$implements = str_replace ( ' ' , '' , $implements );
$value [ 'implements' ] = explode ( ',' , $implements );
}
}
}
return $value ;
}
/**
* Alias of getConfig () for backwards compatibility
*
* #pw-internal
*
* @ param string | Module $className
* @ return array
*
*/
public function getModuleConfigData ( $className ) {
return $this -> getConfig ( $className );
}
/**
* Return the URL where the module can be edited , configured or uninstalled
*
* If module is not installed , it just returns the URL to ProcessModule .
*
* #pw-group-configuration
*
* @ param string | Module $className
* @ param bool $collapseInfo
* @ return string
*
*/
public function getModuleEditUrl ( $className , $collapseInfo = true ) {
if ( ! is_string ( $className )) $className = $this -> getModuleClass ( $className );
2023-03-10 19:41:40 +01:00
$url = $this -> wire () -> config -> urls -> admin . 'module/' ;
2022-03-08 15:55:41 +01:00
if ( empty ( $className ) || ! $this -> isInstalled ( $className )) return $url ;
$url .= " edit/?name= $className " ;
if ( $collapseInfo ) $url .= " &collapse_info=1 " ;
return $url ;
}
/**
* Given a module name , return an associative array of configuration data for it
*
* - Applicable only for modules that support configuration .
* - Configuration data is stored encoded in the database " modules " table " data " field .
*
* ~~~~~~
* // Getting, modifying and saving module config data
* $data = $modules -> getConfig ( 'HelloWorld' );
* $data [ 'greeting' ] = 'Hello World! How are you today?' ;
* $modules -> saveConfig ( 'HelloWorld' , $data );
*
* // Getting just one property 'apiKey' from module config data
* @ apiKey = $modules -> getConfig ( 'HelloWorld' , 'apiKey' );
* ~~~~~~
*
* #pw-group-configuration
* #pw-changelog 3.0.16 Changed from more verbose name `getModuleConfigData()`, which can still be used.
*
* @ param string | Module $class
* @ param string $property Optionally just get value for a specific property ( omit to get all config )
* @ return array | string | int | float Module configuration data , returns array unless a specific $property was requested
* @ see Modules :: saveConfig ()
* @ since 3.0 . 16 Use method getModuleConfigData () with same arguments for prior versions ( can also be used on any version ) .
*
*/
public function getConfig ( $class , $property = '' ) {
$emptyReturn = $property ? null : array ();
$className = $class ;
if ( is_object ( $className )) $className = wireClassName ( $className -> className (), false );
if ( ! isset ( $this -> moduleIDs [ $className ])) return $emptyReturn ;
$id = $this -> moduleIDs [ $className ];
if ( ! $id ) return $emptyReturn ;
if ( ! isset ( $this -> configData [ $id ])) return $emptyReturn ; // module has no config data
if ( is_array ( $this -> configData [ $id ])) {
$data = $this -> configData [ $id ];
} else {
$configable = $this -> isConfigable ( $className );
if ( ! $configable ) return $emptyReturn ;
$database = $this -> wire () -> database ;
$query = $database -> prepare ( " SELECT data FROM modules WHERE id=:id " , " modules.getConfig( $className ) " ); // QA
$query -> bindValue ( " :id " , ( int ) $id , \PDO :: PARAM_INT );
$query -> execute ();
$data = $query -> fetchColumn ();
$query -> closeCursor ();
if ( strlen ( $data )) $data = wireDecodeJSON ( $data );
if ( empty ( $data )) $data = array ();
$this -> configData [ $id ] = $data ;
}
if ( $property ) return isset ( $data [ $property ]) ? $data [ $property ] : null ;
return $data ;
}
/**
* Get the path + filename ( or optionally URL ) for this module
*
* @ param string | Module $class Module class name or object instance
* @ param array | bool $options Options to modify default behavior :
* - `getURL` ( bool ) : Specify true if you want to get the URL rather than file path ( default = false ) .
* - `fast` ( bool ) : Specify true to omit file_exists () checks ( default = false ) .
* - `guess` ( bool ) : Manufacture / guess a module location if one cannot be found ( default = false ) 3.0 . 170 +
* - Note : If you specify a boolean for the $options argument , it is assumed to be the $getURL property .
* @ return bool | string Returns string of module file , or false on failure .
*
*/
public function getModuleFile ( $class , $options = array ()) {
$config = $this -> wire () -> config ;
$className = $class ;
if ( is_bool ( $options )) $options = array ( 'getURL' => $options );
if ( ! isset ( $options [ 'getURL' ])) $options [ 'getURL' ] = false ;
if ( ! isset ( $options [ 'fast' ])) $options [ 'fast' ] = false ;
$file = false ;
// first see it's an object, and if we can get the file from the object
if ( is_object ( $className )) {
$module = $className ;
if ( $module instanceof ModulePlaceholder ) $file = $module -> file ;
$moduleName = $module -> className ();
$className = $module -> className ( true );
} else {
$moduleName = wireClassName ( $className , false );
}
$hasDuplicate = $this -> duplicates () -> hasDuplicate ( $moduleName );
if ( ! $hasDuplicate ) {
// see if we can determine it from already stored paths
$path = $config -> paths -> $moduleName ;
if ( $path ) {
$file = $path . $moduleName . ( $this -> moduleFileExts [ $moduleName ] === 2 ? '.module.php' : '.module' );
if ( ! $options [ 'fast' ] && ! file_exists ( $file )) $file = false ;
}
}
// next see if we've already got the module filename cached locally
if ( ! $file && isset ( $this -> installable [ $moduleName ]) && ! $hasDuplicate ) {
$file = $this -> installable [ $moduleName ];
if ( ! $options [ 'fast' ] && ! file_exists ( $file )) $file = false ;
}
if ( ! $file ) {
$dupFile = $this -> duplicates () -> getCurrent ( $moduleName );
if ( $dupFile ) {
$rootPath = $config -> paths -> root ;
$file = rtrim ( $rootPath , '/' ) . $dupFile ;
if ( ! file_exists ( $file )) {
// module in use may have been deleted, find the next available one that exists
$file = '' ;
$dups = $this -> duplicates () -> getDuplicates ( $moduleName );
foreach ( $dups [ 'files' ] as $pathname ) {
$pathname = rtrim ( $rootPath , '/' ) . $pathname ;
if ( file_exists ( $pathname )) $file = $pathname ;
if ( $file ) break ;
}
}
}
}
if ( ! $file ) {
// see if it's a predefined core type that can be determined from the type
// this should only come into play if module has moved or had a load error
foreach ( $this -> coreTypes as $typeName ) {
if ( strpos ( $moduleName , $typeName ) !== 0 ) continue ;
$checkFiles = array (
" $typeName / $moduleName / $moduleName .module " ,
" $typeName / $moduleName / $moduleName .module.php " ,
" $typeName / $moduleName .module " ,
" $typeName / $moduleName .module.php " ,
);
$path1 = $config -> paths -> modules ;
foreach ( $checkFiles as $checkFile ) {
$file1 = $path1 . $checkFile ;
if ( file_exists ( $file1 )) $file = $file1 ;
if ( $file ) break ;
}
if ( $file ) break ;
}
if ( ! $file ) {
// check site modules
$checkFiles = array (
" $moduleName / $moduleName .module " ,
" $moduleName / $moduleName .module.php " ,
" $moduleName .module " ,
" $moduleName .module.php " ,
);
$path1 = $config -> paths -> siteModules ;
foreach ( $checkFiles as $checkFile ) {
$file1 = $path1 . $checkFile ;
if ( file_exists ( $file1 )) $file = $file1 ;
if ( $file ) break ;
}
}
}
if ( ! $file ) {
// if all the above failed, try to get it from Reflection
try {
// note we don't call getModuleClass() here because it may result in a circular reference
if ( strpos ( $className , " \\ " ) === false ) {
$moduleID = $this -> getModuleID ( $moduleName );
if ( ! empty ( $this -> moduleInfoCache [ $moduleID ][ 'namespace' ])) {
$className = rtrim ( $this -> moduleInfoCache [ $moduleID ][ 'namespace' ], " \\ " ) . " \\ $moduleName " ;
} else {
$className = strlen ( __NAMESPACE__ ) ? " \\ " . __NAMESPACE__ . " \\ $moduleName " : $moduleName ;
}
}
$reflector = new \ReflectionClass ( $className );
$file = $reflector -> getFileName ();
} catch ( \Exception $e ) {
$file = false ;
}
}
if ( ! $file && ! empty ( $options [ 'guess' ])) {
// make a guess about where module would be if we had been able to find it
$file = $config -> paths -> siteModules . " $moduleName / $moduleName .module " ;
}
if ( $file ) {
if ( DIRECTORY_SEPARATOR != '/' ) $file = str_replace ( DIRECTORY_SEPARATOR , '/' , $file );
if ( $options [ 'getURL' ]) $file = str_replace ( $config -> paths -> root , '/' , $file );
}
return $file ;
}
/**
* Is the given module interactively configurable ?
*
* This method can be used to simply determine if a module is configurable ( yes or no ), or more specifically
* how it is configurable .
*
* ~~~~~
* // Determine IF a module is configurable
* if ( $modules -> isConfigurable ( 'HelloWorld' )) {
* // Module is configurable
* } else {
* // Module is NOT configurable
* }
* ~~~~~
* ~~~~~
* // Determine HOW a module is configurable
* $configurable = $module -> isConfigurable ( 'HelloWorld' );
* if ( $configurable === true ) {
* // configurable in a way compatible with all past versions of ProcessWire
* } else if ( is_string ( $configurable )) {
* // configurable via an external configuration file
* // file is identifed in $configurable variable
* } else if ( is_int ( $configurable )) {
* // configurable via a method in the class
* // the $configurable variable contains a number with specifics
* } else {
* // module is NOT configurable
* }
* ~~~~~
*
* ### Return value details
*
* #### If module is configurable via external configuration file:
*
* - Returns string of full path / filename to `ModuleName.config.php` file
*
* #### If module is configurable because it implements a configurable module interface:
*
* - Returns boolean `true` if module is configurable via the static `getModuleConfigInputfields()` method .
* This particular method is compatible with all past versions of ProcessWire .
* - Returns integer `2` if module is configurable via the non - static `getModuleConfigInputfields()` and requires no arguments .
* - Returns integer `3` if module is configurable via the non - static `getModuleConfigInputfields()` and requires `$data` array .
* - Returns integer `4` if module is configurable via the non - static `getModuleConfigInputfields()` and requires `InputfieldWrapper` argument .
* - Returns integer `19` if module is configurable via non - static `getModuleConfigArray()` method .
* - Returns integer `20` if module is configurable via static `getModuleConfigArray()` method .
*
* #### If module is not configurable:
*
* - Returns boolean `false` if not configurable
*
* * This method is named isConfigurableModule () in ProcessWire versions prior to to 3.0 . 16. *
*
* #pw-group-configuration
*
* @ param Module | string $class Module name
* @ param bool $useCache Use caching ? This accepts a few options :
* - Specify boolean `true` to allow use of cache when available ( default behavior ) .
* - Specify boolean `false` to disable retrieval of this property from getModuleInfo ( forces a new check ) .
* - Specify string `interface` to check only if module implements ConfigurableModule interface .
* - Specify string `file` to check only if module has a separate configuration class / file .
* @ return bool | string | int See details about return values in method description .
* @ since 3.0 . 16
*
* @ todo all ConfigurableModule methods need to be split out into their own class ( ConfigurableModules ? )
* @ todo this method has two distinct parts ( file and interface ) that need to be split in two methods .
*
*/
public function isConfigurable ( $class , $useCache = true ) {
$className = $class ;
$moduleInstance = null ;
$namespace = $this -> getModuleNamespace ( $className );
if ( is_object ( $className )) {
$moduleInstance = $className ;
$className = $this -> getModuleClass ( $moduleInstance );
}
$nsClassName = $namespace . $className ;
if ( $useCache === true || $useCache === 1 || $useCache === " 1 " ) {
$info = $this -> getModuleInfo ( $className );
// if regular module info doesn't have configurable info, attempt it from verbose module info
// should only be necessary for transition period between the 'configurable' property being
// moved from verbose to non-verbose module info (i.e. this line can be deleted after PW 2.7)
if ( $info [ 'configurable' ] === null ) $info = $this -> getModuleInfoVerbose ( $className );
if ( ! $info [ 'configurable' ]) {
2023-03-10 19:41:40 +01:00
if ( $moduleInstance instanceof ConfigurableModule ) {
2022-03-08 15:55:41 +01:00
// re-try because moduleInfo may be temporarily incorrect for this request because of change in moduleInfo format
// this is due to reports of ProcessChangelogHooks not getting config data temporarily between 2.6.11 => 2.6.12
$this -> error (
" Configurable module check failed for $className . " .
" If this error persists, please do a Modules > Refresh. " ,
Notice :: debug
);
$useCache = false ;
} else {
return false ;
}
} else {
if ( $info [ 'configurable' ] === true ) return $info [ 'configurable' ];
if ( $info [ 'configurable' ] === 1 || $info [ 'configurable' ] === " 1 " ) return true ;
if ( is_int ( $info [ 'configurable' ]) || ctype_digit ( " $info[configurable] " )) return ( int ) $info [ 'configurable' ];
if ( strpos ( $info [ 'configurable' ], $className ) === 0 ) {
if ( empty ( $info [ 'file' ])) $info [ 'file' ] = $this -> getModuleFile ( $className );
if ( $info [ 'file' ]) {
return dirname ( $info [ 'file' ]) . " / $info[configurable] " ;
}
}
}
}
if ( $useCache !== " interface " ) {
// check for separate module configuration file
$dir = dirname ( $this -> getModuleFile ( $className ));
if ( $dir ) {
$files = array (
" $dir / { $className } Config.php " ,
" $dir / $className .config.php "
);
$found = false ;
foreach ( $files as $file ) {
if ( ! is_file ( $file )) continue ;
$config = null ; // include file may override
$this -> includeModuleFile ( $file , $className );
$classConfig = $nsClassName . 'Config' ;
if ( class_exists ( $classConfig , false )) {
$parents = wireClassParents ( $classConfig , false );
if ( is_array ( $parents ) && in_array ( 'ModuleConfig' , $parents )) {
$found = $file ;
break ;
}
} else {
// bypass include_once, because we need to read $config every time
if ( is_null ( $config )) {
$classInfo = $this -> getFileClassInfo ( $file );
if ( $classInfo [ 'class' ]) {
// not safe to include because this is not just a file with a $config array
} else {
$ns = $this -> getFileNamespace ( $file );
$file = $this -> compile ( $className , $file , $ns );
if ( $file ) {
/** @noinspection PhpIncludeInspection */
include ( $file );
}
}
}
if ( ! is_null ( $config )) {
// included file specified a $config array
$found = $file ;
break ;
}
}
}
if ( $found ) return $found ;
}
}
// if file-only check was requested and we reach this point, exit with false now
if ( $useCache === " file " ) return false ;
// ConfigurableModule interface checks
$result = false ;
foreach ( array ( 'getModuleConfigArray' , 'getModuleConfigInputfields' ) as $method ) {
$configurable = false ;
// if we have a module instance, use that for our check
2023-03-10 19:41:40 +01:00
if ( $moduleInstance instanceof ConfigurableModule ) {
2022-03-08 15:55:41 +01:00
if ( method_exists ( $moduleInstance , $method )) {
$configurable = $method ;
} else if ( method_exists ( $moduleInstance , " ___ $method " )) {
$configurable = " ___ $method " ;
}
}
// if we didn't have a module instance, load the file to find what we need to know
if ( ! $configurable ) {
if ( ! wireClassExists ( $nsClassName , false )) {
$this -> includeModule ( $className );
}
$interfaces = wireClassImplements ( $nsClassName , false );
if ( is_array ( $interfaces ) && in_array ( 'ConfigurableModule' , $interfaces )) {
if ( wireMethodExists ( $nsClassName , $method )) {
$configurable = $method ;
} else if ( wireMethodExists ( $nsClassName , " ___ $method " )) {
$configurable = " ___ $method " ;
}
}
}
// if still not determined to be configurable, move on to next method
if ( ! $configurable ) continue ;
// now determine if static or non-static
$ref = new \ReflectionMethod ( wireClassName ( $nsClassName , true ), $configurable );
if ( $ref -> isStatic ()) {
// config method is implemented as a static method
if ( $method == 'getModuleConfigInputfields' ) {
// static getModuleConfigInputfields
$result = true ;
} else {
// static getModuleConfigArray
$result = 20 ;
}
} else if ( $method == 'getModuleConfigInputfields' ) {
// non-static getModuleConfigInputfields
// we allow for different arguments, so determine what it needs
$parameters = $ref -> getParameters ();
if ( count ( $parameters )) {
$param0 = reset ( $parameters );
if ( strpos ( $param0 , 'array' ) !== false || strpos ( $param0 , '$data' ) !== false ) {
// method requires a $data array (for compatibility with non-static version)
$result = 3 ;
} else if ( strpos ( $param0 , 'InputfieldWrapper' ) !== false || strpos ( $param0 , 'inputfields' ) !== false ) {
// method requires an empty InputfieldWrapper (as a convenience)
$result = 4 ;
}
}
// method requires no arguments
if ( ! $result ) $result = 2 ;
} else {
// non-static getModuleConfigArray
$result = 19 ;
}
// if we make it here, we know we already have a result so can stop now
break ;
}
return $result ;
}
/**
* Indicates whether module accepts config settings , whether interactively or API only
*
* - Returns false if module does not accept config settings .
* - Returns integer `30` if module accepts config settings but is not interactively configurable .
* - Returns true , int or string if module is interactively configurable , see `Modules::isConfigurable()` return values .
*
* @ param string | Module $class
* @ param bool $useCache
* @ return bool | int | string
* @ since 3.0 . 179
*
*/
public function isConfigable ( $class , $useCache = true ) {
if ( is_object ( $class )) {
if ( $class instanceof ConfigModule ) {
$result = 30 ;
} else {
$result = $this -> isConfigurable ( $class , $useCache );
}
} else {
$result = $this -> isConfigurable ( $class , $useCache );
if ( ! $result && wireInstanceOf ( $class , 'ConfigModule' )) $result = 30 ;
}
return $result ;
}
/**
* Alias of isConfigurable () for backwards compatibility
*
* #pw-internal
*
* @ param $className
* @ param bool $useCache
2023-03-10 19:41:40 +01:00
* @ return bool | string | int
2022-03-08 15:55:41 +01:00
*
*/
public function isConfigurableModule ( $className , $useCache = true ) {
return $this -> isConfigurable ( $className , $useCache );
}
/**
* Populate configuration data to a ConfigurableModule
*
* If the Module has a 'setConfigData' method , it will send the array of data to that .
* Otherwise it will populate the properties individually .
*
* @ param Module $module
* @ param array | null $data Configuration data [ key = value ], or omit / null if you want it to retrieve the config data for you .
* @ param array | null $extraData Additional runtime configuration data to merge ( default = null ) 3.0 . 169 +
* @ return bool True if configured , false if not configurable
*
*/
protected function setModuleConfigData ( Module $module , $data = null , $extraData = null ) {
$configurable = $this -> isConfigable ( $module );
if ( ! $configurable ) return false ;
if ( ! is_array ( $data )) $data = $this -> getConfig ( $module );
2023-03-10 19:41:40 +01:00
if ( is_array ( $extraData )) $data = array_merge ( $data , $extraData );
2022-03-08 15:55:41 +01:00
$nsClassName = $module -> className ( true );
$moduleName = $module -> className ( false );
if ( is_string ( $configurable ) && is_file ( $configurable ) && strpos ( basename ( $configurable ), $moduleName ) === 0 ) {
// get defaults from ModuleConfig class if available
$className = $nsClassName . 'Config' ;
$config = null ; // may be overridden by included file
// $compile = strrpos($className, '\\') < 1 && $this->wire('config')->moduleCompile;
$configFile = '' ;
if ( ! class_exists ( $className , false )) {
$configFile = $this -> compile ( $className , $configurable );
// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;
if ( $configFile ) {
/** @noinspection PhpIncludeInspection */
include_once ( $configFile );
}
}
if ( wireClassExists ( $className )) {
$parents = wireClassParents ( $className , false );
if ( is_array ( $parents ) && in_array ( 'ModuleConfig' , $parents )) {
$moduleConfig = $this -> wire ( new $className ());
if ( $moduleConfig instanceof ModuleConfig ) {
$defaults = $moduleConfig -> getDefaults ();
$data = array_merge ( $defaults , $data );
}
}
} else {
// the file may have already been include_once before, so $config would not be set
// so we try a regular include() next.
if ( is_null ( $config )) {
if ( ! $configFile ) {
$configFile = $this -> compile ( $className , $configurable );
// $configFile = $compile ? $this->wire('files')->compile($configurable) : $configurable;
}
if ( $configFile ) {
/** @noinspection PhpIncludeInspection */
include ( $configFile );
}
}
if ( is_array ( $config )) {
// alternatively, file may just specify a $config array
2023-03-10 19:41:40 +01:00
/** @var ModuleConfig $moduleConfig */
2022-03-08 15:55:41 +01:00
$moduleConfig = $this -> wire ( new ModuleConfig ());
$moduleConfig -> add ( $config );
$defaults = $moduleConfig -> getDefaults ();
$data = array_merge ( $defaults , $data );
}
}
}
if ( method_exists ( $module , 'setConfigData' ) || method_exists ( $module , '___setConfigData' )) {
/** @var _Module $module */
$module -> setConfigData ( $data );
return true ;
}
foreach ( $data as $key => $value ) {
$module -> $key = $value ;
}
return true ;
}
/**
* Alias of saveConfig () for backwards compatibility
*
* #pw-internal
*
* @ param $className
* @ param array $configData
2023-03-10 19:41:40 +01:00
* @ return bool
2022-03-08 15:55:41 +01:00
*
*/
public function ___saveModuleConfigData ( $className , array $configData ) {
return $this -> saveConfig ( $className , $configData );
}
/**
* Save provided configuration data for the given module
*
* - Applicable only for modules that support configuration .
* - Configuration data is stored encoded in the database " modules " table " data " field .
*
* ~~~~~~
* // Getting, modifying and saving module config data
* $data = $modules -> getConfig ( 'HelloWorld' );
* $data [ 'greeting' ] = 'Hello World! How are you today?' ;
* $modules -> saveConfig ( 'HelloWorld' , $data );
* ~~~~~~
*
* #pw-group-configuration
* #pw-group-manipulation
* #pw-changelog 3.0.16 Changed name from the more verbose saveModuleConfigData(), which will still work.
*
* @ param string | Module $class Module or module name
* @ param array | string $data Associative array of configuration data , or name of property you want to save .
* @ param mixed | null $value If you specified a property in previous arg , the value for the property .
* @ return bool True on success , false on failure
* @ throws WireException
* @ see Modules :: getConfig ()
* @ since 3.0 . 16 Use method saveModuleConfigData () with same arguments for prior versions ( can also be used on any version ) .
*
*/
public function ___saveConfig ( $class , $data , $value = null ) {
$className = $class ;
if ( is_object ( $className )) $className = $className -> className ();
$moduleName = wireClassName ( $className , false );
if ( ! $id = $this -> moduleIDs [ $moduleName ]) throw new WireException ( " Unable to find ID for Module ' $moduleName ' " );
if ( is_string ( $data )) {
// a property and value have been provided
$property = $data ;
$data = $this -> getConfig ( $class );
if ( is_null ( $value )) {
// remove the property
unset ( $data [ $property ]);
} else {
// populate the value for the property
$data [ $property ] = $value ;
}
} else {
// data must be an associative array of configuration data
if ( ! is_array ( $data )) return false ;
}
// ensure original duplicates info is retained and validate that it is still current
$data = $this -> duplicates () -> getDuplicatesConfigData ( $moduleName , $data );
$this -> configData [ $id ] = $data ;
$json = count ( $data ) ? wireEncodeJSON ( $data , true ) : '' ;
2023-03-10 19:41:40 +01:00
$database = $this -> wire () -> database ;
2022-03-08 15:55:41 +01:00
$query = $database -> prepare ( " UPDATE modules SET data=:data WHERE id=:id " , " modules.saveConfig( $moduleName ) " ); // QA
$query -> bindValue ( " :data " , $json , \PDO :: PARAM_STR );
$query -> bindValue ( " :id " , ( int ) $id , \PDO :: PARAM_INT );
$result = $query -> execute ();
// $this->log("Saved module '$moduleName' config data");
return $result ;
}
/**
* Get the Inputfields that configure the given module or return null if not configurable
*
* #pw-internal
*
* @ param string | Module | int $moduleName
* @ param InputfieldWrapper | null $form Optionally specify the form you want Inputfields appended to .
* @ return InputfieldWrapper | null
*
*/
public function ___getModuleConfigInputfields ( $moduleName , InputfieldWrapper $form = null ) {
$moduleName = $this -> getModuleClass ( $moduleName );
$configurable = $this -> isConfigurable ( $moduleName );
if ( ! $configurable ) return null ;
2023-03-10 19:41:40 +01:00
/** @var InputfieldWrapper $form */
2022-03-08 15:55:41 +01:00
if ( is_null ( $form )) $form = $this -> wire ( new InputfieldWrapper ());
$data = $this -> getConfig ( $moduleName );
$fields = null ;
// check for configurable module interface
$configurableInterface = $this -> isConfigurable ( $moduleName , " interface " );
if ( $configurableInterface ) {
if ( is_int ( $configurableInterface ) && $configurableInterface > 1 && $configurableInterface < 20 ) {
// non-static
/** @var ConfigurableModule|Module|_Module $module */
if ( $configurableInterface === 2 ) {
// requires no arguments
$module = $this -> getModule ( $moduleName );
$fields = $module -> getModuleConfigInputfields ();
} else if ( $configurableInterface === 3 ) {
// requires $data array
$module = $this -> getModule ( $moduleName , array ( 'noInit' => true , 'noCache' => true ));
$this -> setModuleConfigData ( $module );
$fields = $module -> getModuleConfigInputfields ( $data );
} else if ( $configurableInterface === 4 ) {
// requires InputfieldWrapper
// we allow for option of no return statement in the method
$module = $this -> getModule ( $moduleName );
2023-03-10 19:41:40 +01:00
$fields = $this -> wire ( new InputfieldWrapper ()); /** @var InputfieldWrapper $fields */
2022-03-08 15:55:41 +01:00
$fields -> setParent ( $form );
$_fields = $module -> getModuleConfigInputfields ( $fields );
if ( $_fields instanceof InputfieldWrapper ) $fields = $_fields ;
unset ( $_fields );
} else if ( $configurableInterface === 19 ) {
// non-static getModuleConfigArray method
$module = $this -> getModule ( $moduleName );
2023-03-10 19:41:40 +01:00
$fields = $this -> wire ( new InputfieldWrapper ()); /** @var InputfieldWrapper $fields */
2022-03-08 15:55:41 +01:00
$fields -> importArray ( $module -> getModuleConfigArray ());
$fields -> populateValues ( $module );
}
} else if ( $configurableInterface === 20 ) {
// static getModuleConfigArray method
2023-03-10 19:41:40 +01:00
$fields = $this -> wire ( new InputfieldWrapper ()); /** @var InputfieldWrapper $fields */
2022-03-08 15:55:41 +01:00
$fields -> importArray ( call_user_func ( array ( wireClassName ( $moduleName , true ), 'getModuleConfigArray' )));
$fields -> populateValues ( $data );
2023-03-10 19:41:40 +01:00
} else {
2022-03-08 15:55:41 +01:00
// static getModuleConfigInputfields method
$nsClassName = $this -> getModuleNamespace ( $moduleName ) . $moduleName ;
$fields = call_user_func ( array ( $nsClassName , 'getModuleConfigInputfields' ), $data );
}
if ( $fields instanceof InputfieldWrapper ) {
foreach ( $fields as $field ) {
$form -> append ( $field );
}
} else if ( $fields instanceof Inputfield ) {
$form -> append ( $fields );
} else {
$this -> error ( " $moduleName .getModuleConfigInputfields() did not return InputfieldWrapper " );
}
}
// check for file-based config
$file = $this -> isConfigurable ( $moduleName , " file " );
if ( ! $file || ! is_string ( $file ) || ! is_file ( $file )) {
// config is not file-based
} else {
// file-based config
$config = null ;
$ns = $this -> getModuleNamespace ( $moduleName );
$configClass = $ns . $moduleName . " Config " ;
if ( ! class_exists ( $configClass )) {
$configFile = $this -> compile ( $moduleName , $file , $ns );
if ( $configFile ) {
/** @noinspection PhpIncludeInspection */
include_once ( $configFile );
}
}
$configModule = null ;
if ( wireClassExists ( $configClass )) {
// file contains a ModuleNameConfig class
$configModule = $this -> wire ( new $configClass ());
} else {
if ( is_null ( $config )) {
$configFile = $this -> compile ( $moduleName , $file , $ns );
// if(!$configFile) $configFile = $compile ? $this->wire('files')->compile($file) : $file;
if ( $configFile ) {
/** @noinspection PhpIncludeInspection */
include ( $configFile ); // in case of previous include_once
}
}
if ( is_array ( $config )) {
// file contains a $config array
$configModule = $this -> wire ( new ModuleConfig ());
$configModule -> add ( $config );
}
}
2023-03-10 19:41:40 +01:00
if ( $configModule instanceof ModuleConfig ) {
2022-03-08 15:55:41 +01:00
$defaults = $configModule -> getDefaults ();
$data = array_merge ( $defaults , $data );
$configModule -> setArray ( $data );
$fields = $configModule -> getInputfields ();
if ( $fields instanceof InputfieldWrapper ) {
foreach ( $fields as $field ) {
$form -> append ( $field );
}
foreach ( $data as $key => $value ) {
$f = $form -> getChildByName ( $key );
if ( ! $f ) continue ;
if ( $f instanceof InputfieldCheckbox && $value ) {
$f -> attr ( 'checked' , 'checked' );
} else {
$f -> attr ( 'value' , $value );
}
}
} else {
$this -> error ( " $configModule .getInputfields() did not return InputfieldWrapper " );
}
}
} // file-based config
if ( $form ) {
// determine how many visible Inputfields there are in the module configuration
// for assignment or removal of flagsNoUserConfig flag when applicable
$numVisible = 0 ;
foreach ( $form -> getAll () as $inputfield ) {
if ( $inputfield instanceof InputfieldHidden || $inputfield instanceof InputfieldWrapper ) continue ;
$numVisible ++ ;
}
$flags = $this -> getFlags ( $moduleName );
if ( $numVisible ) {
if ( $flags & self :: flagsNoUserConfig ) {
$info = $this -> getModuleInfoVerbose ( $moduleName );
if ( empty ( $info [ 'addFlag' ]) || ! ( $info [ 'addFlag' ] & self :: flagsNoUserConfig )) {
$this -> setFlag ( $moduleName , self :: flagsNoUserConfig , false ); // remove flag
}
}
} else {
if ( ! ( $flags & self :: flagsNoUserConfig )) {
if ( empty ( $info [ 'removeFlag' ]) || ! ( $info [ 'removeFlag' ] & self :: flagsNoUserConfig )) {
$this -> setFlag ( $moduleName , self :: flagsNoUserConfig , true ); // add flag
}
}
}
}
return $form ;
}
/**
* Is the given module Singular ( single instance ) ?
*
* isSingular and isAutoload Module methods have been deprecated . So this method , and isAutoload ()
* exist in part to enable singular and autoload properties to be set in getModuleInfo , rather than
* with methods .
*
* Note that isSingular () and isAutoload () are not deprecated for ModulePlaceholder , so the Modules
* class isn ' t going to stop looking for them .
*
* #pw-internal
*
* @ param Module | string $module Module instance or class name
* @ return bool
*
*/
public function isSingular ( $module ) {
$info = $this -> getModuleInfo ( $module );
2023-03-10 19:41:40 +01:00
if ( isset ( $info [ 'singular' ])) return $info [ 'singular' ];
2022-03-08 15:55:41 +01:00
if ( is_object ( $module )) {
if ( method_exists ( $module , 'isSingular' )) return $module -> isSingular ();
} else {
// singular status can't be determined if module not installed and not specified in moduleInfo
if ( isset ( $this -> installable [ $module ])) return null ;
$this -> includeModule ( $module );
$module = wireClassName ( $module , true );
if ( method_exists ( $module , 'isSingular' )) {
/** @var Module|_Module $moduleInstance */
$moduleInstance = $this -> wire ( new $module ());
return $moduleInstance -> isSingular ();
}
}
return false ;
}
/**
* Is the given module Autoload ( automatically loaded at runtime ) ?
*
* #pw-internal
*
* @ param Module | string $module Module instance or class name
* @ return bool | string | null Returns string " conditional " if conditional autoload , true if autoload , or false if not . Or null if unavailable .
*
*/
public function isAutoload ( $module ) {
$info = $this -> getModuleInfo ( $module );
$autoload = null ;
2023-03-10 19:41:40 +01:00
if ( isset ( $info [ 'autoload' ])) {
2022-03-08 15:55:41 +01:00
// if autoload is a string (selector) or callable, then we flag it as autoload
if ( is_string ( $info [ 'autoload' ]) || wireIsCallable ( $info [ 'autoload' ])) return " conditional " ;
$autoload = $info [ 'autoload' ];
} else if ( ! is_object ( $module )) {
if ( isset ( $this -> installable [ $module ])) {
// module is not installed
// we are not going to be able to determine if this is autoload or not
$flags = $this -> getFlags ( $module );
if ( $flags !== null ) {
$autoload = $flags & self :: flagsAutoload ;
} else {
// unable to determine
return null ;
}
} else {
// include for method exists call
$this -> includeModule ( $module );
$module = wireClassName ( $module , true );
$module = $this -> wire ( new $module ());
}
}
if ( $autoload === null && is_object ( $module ) && method_exists ( $module , 'isAutoload' )) {
2023-03-10 19:41:40 +01:00
/** @var Module $module */
2022-03-08 15:55:41 +01:00
$autoload = $module -> isAutoload ();
}
return $autoload ;
}
/**
* Returns whether the modules have been initialized yet
*
* #pw-internal
*
* @ return bool
*
*/
public function isInitialized () {
return $this -> initialized ;
}
/**
* Does the given module name resolve to a module in the system ( installed or uninstalled )
*
* If given module name also includes a namespace , then that namespace will be validated as well .
*
* #pw-internal
*
* @ param string | Module $moduleName With or without namespace
* @ return bool
*
*/
public function isModule ( $moduleName ) {
if ( ! is_string ( $moduleName )) {
if ( is_object ( $moduleName )) {
if ( $moduleName instanceof Module ) return true ;
return false ;
}
$moduleName = $this -> getModuleClass ( $moduleName );
}
/** @var string $moduleName */
if ( strpos ( $moduleName , " \\ " ) !== false ) {
$namespace = wireClassName ( $moduleName , 1 );
$moduleName = wireClassName ( $moduleName , false );
} else {
$namespace = false ;
}
if ( isset ( $this -> moduleIDs [ $moduleName ])) {
$isModule = true ;
} else if ( isset ( $this -> installable [ $moduleName ])) {
$isModule = true ;
} else {
$isModule = false ;
}
if ( $isModule && $namespace ) {
$actualNamespace = $this -> getModuleNamespace ( $moduleName );
2022-11-05 18:32:48 +01:00
if ( trim ( " $namespace " , '\\' ) != trim ( " $actualNamespace " , '\\' )) {
2022-03-08 15:55:41 +01:00
$isModule = false ;
}
}
return $isModule ;
}
/**
* Is the given namespace a unique recognized module namespace ? If yes , returns the path to it . If not , returns boolean false .
*
* #pw-internal
*
* @ param string $namespace
* @ return bool | string
*
*/
public function getNamespacePath ( $namespace ) {
if ( is_null ( $this -> moduleNamespaceCache )) $this -> getNamespaces ();
$namespace = " \\ " . trim ( $namespace , " \\ " ) . " \\ " ;
return isset ( $this -> moduleNamespaceCache [ $namespace ]) ? $this -> moduleNamespaceCache [ $namespace ] : false ;
}
/**
* Refresh the modules cache
*
* This forces the modules file and information cache to be re - created .
*
* #pw-group-manipulation
*
* @ param bool $showMessages Show notification messages about what was found ? ( default = false ) 3.0 . 172 +
*
*/
public function ___refresh ( $showMessages = false ) {
if ( $this -> wire () -> config -> systemVersion < 6 ) return ;
$this -> refreshing = true ;
$this -> clearModuleInfoCache ( $showMessages );
$this -> loadModulesTable ();
foreach ( $this -> paths as $path ) $this -> findModuleFiles ( $path , false );
foreach ( $this -> paths as $path ) $this -> load ( $path );
if ( $this -> duplicates () -> numNewDuplicates () > 0 ) $this -> duplicates () -> updateDuplicates (); // PR#1020
$this -> refreshing = false ;
}
/**
* Alias of refresh () method for backwards compatibility
*
* #pw-internal
*
*/
public function resetCache () {
$this -> refresh ();
}
/**
* Return an array of module class names that require the given one
*
* #pw-internal
*
* @ param string $class
* @ param bool $uninstalled Set to true to include modules dependent upon this one , even if they aren ' t installed .
* @ param bool $installs Set to true to exclude modules that indicate their install / uninstall is controlled by $class .
* @ return array ()
*
*/
public function getRequiredBy ( $class , $uninstalled = false , $installs = false ) {
$class = $this -> getModuleClass ( $class );
$info = $this -> getModuleInfo ( $class );
$dependents = array ();
foreach ( $this as $module ) {
$c = $this -> getModuleClass ( $module );
if ( ! $uninstalled && ! $this -> isInstalled ( $c )) continue ;
$i = $this -> getModuleInfo ( $c );
if ( ! count ( $i [ 'requires' ])) continue ;
if ( $installs && in_array ( $c , $info [ 'installs' ])) continue ;
if ( in_array ( $class , $i [ 'requires' ])) $dependents [] = $c ;
}
return $dependents ;
}
/**
* Return an array of module class names required by the given one
*
* Default behavior is to return all listed requirements , whether they are currently met by
* the environment or not . Specify TRUE for the 2 nd argument to return only requirements
* that are not currently met .
*
* #pw-internal
*
* @ param string $class
* @ param bool $onlyMissing Set to true to return only required modules / versions that aren ' t
* yet installed or don ' t have the right version . It excludes those that the class says it
* will install ( via 'installs' property of getModuleInfo )
* @ param null | bool $versions Set to true to always include versions in the returned requirements list .
* Set to null to always exclude versions in requirements list ( so only module class names will be there ) .
* Set to false ( which is the default ) to include versions only when version is the dependency issue .
* Note versions are already included when the installed version is not adequate .
* @ return array of strings each with ModuleName Operator Version , i . e . " ModuleName>=1.0.0 "
*
*/
public function getRequires ( $class , $onlyMissing = false , $versions = false ) {
$class = $this -> getModuleClass ( $class );
$info = $this -> getModuleInfo ( $class );
$requires = $info [ 'requires' ];
$currentVersion = 0 ;
// quick exit if arguments permit it
if ( ! $onlyMissing ) {
if ( $versions ) foreach ( $requires as $key => $value ) {
list ( $operator , $version ) = $info [ 'requiresVersions' ][ $value ];
if ( empty ( $version )) continue ;
if ( ctype_digit ( " $version " )) $version = $this -> formatVersion ( $version );
if ( ! empty ( $version )) $requires [ $key ] .= " $operator $version " ;
}
return $requires ;
}
foreach ( $requires as $key => $requiresClass ) {
if ( in_array ( $requiresClass , $info [ 'installs' ])) {
// if this module installs the required class, then we can stop now
// and we assume it's installing the version it wants
unset ( $requires [ $key ]);
}
list ( $operator , $requiresVersion ) = $info [ 'requiresVersions' ][ $requiresClass ];
$installed = true ;
if ( $requiresClass == 'PHP' ) {
$currentVersion = PHP_VERSION ;
} else if ( $requiresClass == 'ProcessWire' ) {
2023-03-10 19:41:40 +01:00
$currentVersion = $this -> wire () -> config -> version ;
2022-03-08 15:55:41 +01:00
} else if ( $this -> isInstalled ( $requiresClass )) {
if ( ! $requiresVersion ) {
// if no version is specified then requirement is already met
unset ( $requires [ $key ]);
continue ;
}
$i = $this -> getModuleInfo ( $requiresClass , array ( 'noCache' => true ));
$currentVersion = $i [ 'version' ];
} else {
// module is not installed
$installed = false ;
}
if ( $installed && $this -> versionCompare ( $currentVersion , $requiresVersion , $operator )) {
// required version is installed
unset ( $requires [ $key ]);
} else if ( empty ( $requiresVersion )) {
// just the class name is fine
continue ;
} else if ( is_null ( $versions )) {
// request is for no versions to be included (just class names)
$requires [ $key ] = $requiresClass ;
} else {
// update the requires string to clarify what version it requires
if ( ctype_digit ( " $requiresVersion " )) $requiresVersion = $this -> formatVersion ( $requiresVersion );
$requires [ $key ] = " $requiresClass $operator $requiresVersion " ;
}
}
return $requires ;
}
/**
* Compare one module version to another , returning TRUE if they match the $operator or FALSE otherwise
*
* #pw-internal
*
* @ param int | string $currentVersion May be a number like 123 or a formatted version like 1.2 . 3
* @ param int | string $requiredVersion May be a number like 123 or a formatted version like 1.2 . 3
* @ param string $operator
* @ return bool
*
*/
public function versionCompare ( $currentVersion , $requiredVersion , $operator ) {
if ( ctype_digit ( " $currentVersion " ) && ctype_digit ( " $requiredVersion " )) {
// integer comparison is ok
$currentVersion = ( int ) $currentVersion ;
$requiredVersion = ( int ) $requiredVersion ;
$result = false ;
switch ( $operator ) {
case '=' : $result = ( $currentVersion == $requiredVersion ); break ;
case '>' : $result = ( $currentVersion > $requiredVersion ); break ;
case '<' : $result = ( $currentVersion < $requiredVersion ); break ;
case '>=' : $result = ( $currentVersion >= $requiredVersion ); break ;
case '<=' : $result = ( $currentVersion <= $requiredVersion ); break ;
case '!=' : $result = ( $currentVersion != $requiredVersion ); break ;
}
return $result ;
}
// if either version has no periods or only one, like "1.2" then format it to stanard: "1.2.0"
if ( substr_count ( $currentVersion , '.' ) < 2 ) $currentVersion = $this -> formatVersion ( $currentVersion );
if ( substr_count ( $requiredVersion , '.' ) < 2 ) $requiredVersion = $this -> formatVersion ( $requiredVersion );
return version_compare ( $currentVersion , $requiredVersion , $operator );
}
/**
* Return array of ( $module , $operator , $requiredVersion )
*
* $version will be 0 and $operator blank if there are no requirements .
*
* @ param string $require Module class name with operator and version string
* @ return array of array ( $moduleClass , $operator , $version )
*
*/
protected function extractModuleOperatorVersion ( $require ) {
if ( ctype_alnum ( $require )) {
// no version is specified
return array ( $require , '' , 0 );
}
$operators = array ( '<=' , '>=' , '<' , '>' , '!=' , '=' );
$operator = '' ;
foreach ( $operators as $o ) {
if ( strpos ( $require , $o )) {
$operator = $o ;
break ;
}
}
// if no operator found, then no version is being specified
if ( ! $operator ) return array ( $require , '' , 0 );
// extract class and version
list ( $class , $version ) = explode ( $operator , $require );
// make version an integer if possible
if ( ctype_digit ( " $version " )) $version = ( int ) $version ;
return array ( $class , $operator , $version );
}
/**
* Return an array of module class names required by the given one to be installed before this one .
*
* Excludes modules that are required but already installed .
* Excludes uninstalled modules that $class indicates it handles via it 's ' installs ' getModuleInfo property .
*
* #pw-internal
*
* @ param string $class
* @ return array ()
*
*/
public function getRequiresForInstall ( $class ) {
return $this -> getRequires ( $class , true );
}
/**
* Return an array of module class names required by the given one to be uninstalled before this one .
*
* Excludes modules that the given one says it handles via it 's ' installs ' getModuleInfo property .
* Module class names in returned array include operator and version in the string .
*
* #pw-internal
*
* @ param string $class
* @ return array ()
*
*/
public function getRequiresForUninstall ( $class ) {
return $this -> getRequiredBy ( $class , false , true );
}
/**
* Return array of dependency errors for given module name
*
* #pw-internal
*
* @ param $moduleName
* @ return array If no errors , array will be blank . If errors , array will be of strings ( error messages )
*
*/
public function getDependencyErrors ( $moduleName ) {
$moduleName = $this -> getModuleClass ( $moduleName );
$info = $this -> getModuleInfo ( $moduleName );
$errors = array ();
if ( empty ( $info [ 'requires' ])) return $errors ;
foreach ( $info [ 'requires' ] as $requiresName ) {
$error = '' ;
if ( ! $this -> isInstalled ( $requiresName )) {
$error = $requiresName ;
} else if ( ! empty ( $info [ 'requiresVersions' ][ $requiresName ])) {
list ( $operator , $version ) = $info [ 'requiresVersions' ][ $requiresName ];
$info2 = $this -> getModuleInfo ( $requiresName );
$requiresVersion = $info2 [ 'version' ];
if ( ! empty ( $version ) && ! $this -> versionCompare ( $requiresVersion , $version , $operator )) {
$error = " $requiresName $operator $version " ;
}
}
if ( $error ) $errors [] = sprintf ( $this -> _ ( 'Failed module dependency: %s requires %s' ), $moduleName , $error );
}
return $errors ;
}
/**
* Find modules that are missing their module file on the file system
*
* Return value is array :
* ~~~~~
* [
* 'ModuleName' => [
* 'id' => 123 ,
* 'name' => 'ModuleName' ,
* 'file' => '/path/to/expected/file.module'
* ],
* 'ModuleName' => [
* ...
* ]
* ];
* ~~~~~
*
* #pw-internal
*
* @ return array
* @ since 3.0 . 170
*
*/
public function findMissingModules () {
$missing = array ();
$unflags = array ();
$sql = " SELECT id, class FROM modules WHERE flags & :flagsNoFile ORDER BY class " ;
$query = $this -> wire () -> database -> prepare ( $sql );
$query -> bindValue ( ':flagsNoFile' , self :: flagsNoFile , \PDO :: PARAM_INT );
$query -> execute ();
while ( $row = $query -> fetch ( \PDO :: FETCH_ASSOC )) {
$class = $row [ 'class' ];
$file = $this -> getModuleFile ( $class , array ( 'fast' => true ));
if ( $file && file_exists ( $file )) {
$unflags [] = $class ;
continue ;
}
$fileAlt = $this -> getModuleFile ( $class , array ( 'fast' => false ));
if ( $fileAlt ) {
$file = $fileAlt ;
if ( file_exists ( $file )) continue ;
}
if ( ! $file ) {
$file = $this -> getModuleFile ( $class , array ( 'fast' => true , 'guess' => true ));
}
$missing [ $class ] = array (
'id' => $row [ 'id' ],
'name' => $class ,
'file' => $file ,
);
}
foreach ( $unflags as $name ) {
$this -> setFlag ( $name , self :: flagsNoFile , false );
}
return $missing ;
}
/**
* Remove entry for module from modules table
*
* #pw-internal
*
* @ param string | int $class Module class or ID
* @ return bool
* @ since 3.0 . 170
*
*/
public function removeModuleEntry ( $class ) {
$database = $this -> wire () -> database ;
if ( ctype_digit ( " $class " )) {
$query = $database -> prepare ( 'DELETE FROM modules WHERE id=:id LIMIT 1' );
$query -> bindValue ( ':id' , ( int ) $class , \PDO :: PARAM_INT );
} else {
$query = $database -> prepare ( 'DELETE FROM modules WHERE class=:class LIMIT 1' );
$query -> bindValue ( ':class' , $class , \PDO :: PARAM_STR );
}
$result = $query -> execute () ? $query -> rowCount () > 0 : false ;
$query -> closeCursor ();
return $result ;
}
/**
* Given a module version number , format it in a consistent way as 3 parts : 1.2 . 3
*
* #pw-internal
*
* @ param $version int | string
* @ return string
*
*/
public function formatVersion ( $version ) {
$version = trim ( $version );
if ( ! ctype_digit ( str_replace ( '.' , '' , $version ))) {
// if version has some characters other than digits or periods, remove them
$version = preg_replace ( '/[^\d.]/' , '' , $version );
}
if ( ctype_digit ( " $version " )) {
// version contains only digits
// make sure version is at least 3 characters in length, left padded with 0s
$len = strlen ( $version );
if ( $len < 3 ) {
$version = str_pad ( $version , 3 , " 0 " , STR_PAD_LEFT );
} else if ( $len > 3 ) {
// they really need to use a string for this type of version,
// as we can't really guess, but we'll try, converting 1234 to 1.2.34
}
// $version = preg_replace('/(\d)(?=\d)/', '$1.', $version);
$version =
substr ( $version , 0 , 1 ) . '.' .
substr ( $version , 1 , 1 ) . '.' .
substr ( $version , 2 );
} else if ( strpos ( $version , '.' ) !== false ) {
// version is a formatted string
if ( strpos ( $version , '.' ) == strrpos ( $version , '.' )) {
// only 1 period, like: 2.0, convert that to 2.0.0
if ( preg_match ( '/^\d\.\d$/' , $version )) $version .= " .0 " ;
}
} else {
// invalid version?
}
if ( ! strlen ( $version )) $version = '0.0.0' ;
return $version ;
}
/**
* Load the module information cache
*
* @ return bool
*
*/
protected function loadModuleInfoCache () {
2023-03-10 19:41:40 +01:00
$cache = $this -> wire () -> cache ;
$data = $cache -> get ( self :: moduleInfoCacheName );
2022-03-08 15:55:41 +01:00
if ( $data ) {
// if module class name keys in use (i.e. ProcessModule) it's an older version of
// module info cache, so we skip over it to force its re-creation
if ( is_array ( $data ) && ! isset ( $data [ 'ProcessModule' ])) $this -> moduleInfoCache = $data ;
2023-03-10 19:41:40 +01:00
$data = $cache -> get ( self :: moduleLastVersionsCacheName );
2022-03-08 15:55:41 +01:00
if ( is_array ( $data )) $this -> modulesLastVersions = $data ;
return true ;
}
return false ;
}
/**
* Load the module information cache ( verbose info : summary , author , href , file , core )
*
* @ param bool $uninstalled If true , it will load the uninstalled verbose cache .
* @ return bool
*
*/
protected function loadModuleInfoCacheVerbose ( $uninstalled = false ) {
$name = $uninstalled ? self :: moduleInfoCacheUninstalledName : self :: moduleInfoCacheVerboseName ;
2023-03-10 19:41:40 +01:00
$data = $this -> wire () -> cache -> get ( $name );
2022-03-08 15:55:41 +01:00
if ( $data ) {
if ( is_array ( $data )) {
2023-03-10 19:41:40 +01:00
if ( $uninstalled ) {
$this -> moduleInfoCacheUninstalled = $data ;
} else {
$this -> moduleInfoCacheVerbose = $data ;
}
2022-03-08 15:55:41 +01:00
}
return true ;
}
return false ;
}
/**
* Clear the module information cache
*
* @ param bool | null $showMessages Specify true to show message notifications
*
*/
protected function clearModuleInfoCache ( $showMessages = false ) {
$cache = $this -> wire () -> cache ;
$versionChanges = array ();
$newModules = array ();
$moveModules = array ();
$missModules = array ();
// record current module versions currently in moduleInfo
$moduleVersions = array ();
foreach ( $this -> moduleInfoCache as $id => $moduleInfo ) {
if ( isset ( $this -> modulesLastVersions [ $id ])) {
$moduleVersions [ $id ] = $this -> modulesLastVersions [ $id ];
} else {
$moduleVersions [ $id ] = $moduleInfo [ 'version' ];
}
}
// delete the caches
$cache -> delete ( self :: moduleInfoCacheName );
$cache -> delete ( self :: moduleInfoCacheVerboseName );
$cache -> delete ( self :: moduleInfoCacheUninstalledName );
$this -> moduleInfoCache = array ();
$this -> moduleInfoCacheVerbose = array ();
$this -> moduleInfoCacheUninstalled = array ();
// save new moduleInfo cache
$this -> saveModuleInfoCache ();
// compare new moduleInfo versions with the previous ones, looking for changes
foreach ( $this -> moduleInfoCache as $id => $moduleInfo ) {
$moduleName = $moduleInfo [ 'name' ];
if ( ! isset ( $moduleVersions [ $id ])) {
if ( isset ( $this -> moduleIDs [ $moduleName ])) {
$moveModules [] = $moduleName ;
} else {
$newModules [] = $moduleName ;
}
continue ;
}
if ( $moduleVersions [ $id ] != $moduleInfo [ 'version' ]) {
$fromVersion = $this -> formatVersion ( $moduleVersions [ $id ]);
$toVersion = $this -> formatVersion ( $moduleInfo [ 'version' ]);
$versionChanges [] = " $fromVersion => $toVersion : $moduleName " ;
$this -> modulesLastVersions [ $id ] = $moduleVersions [ $id ];
if ( strpos ( $moduleName , 'Fieldtype' ) === 0 ) {
// apply update now, to Fieldtype modules only (since they are loaded differently)
$this -> getModule ( $moduleName );
}
}
}
foreach ( $this -> moduleIDs as $moduleName => $moduleID ) {
if ( isset ( $this -> moduleInfoCache [ $moduleID ])) {
// module is present in moduleInfo
if ( $this -> hasFlag ( $moduleID , self :: flagsNoFile )) {
$file = $this -> getModuleFile ( $moduleName , array ( 'fast' => false ));
if ( $file ) {
// remove flagsNoFile if file is found
$this -> setFlag ( $moduleID , self :: flagsNoFile , false );
}
}
} else {
// module is missing moduleInfo
$file = $this -> getModuleFile ( $moduleName , array ( 'fast' => false ));
if ( ! $file ) {
$file = $this -> getModuleFile ( $moduleName , array ( 'fast' => true , 'guess' => true ));
// add flagsNoFile if file cannot be located
$missModules [] = " $moduleName => $file " ;
$this -> setFlag ( $moduleID , self :: flagsNoFile , true );
}
}
}
$this -> updateModuleVersionsCache ();
// report detected changes
$sanitizer = $this -> wire () -> sanitizer ;
$reports = array (
array (
'label' => $this -> _ ( 'Found %d new module(s):' ),
'items' => $newModules ,
),
/*
array (
'label' => $this -> _ ( 'Found %d moved module(s):' ),
'items' => $moveModules ,
),
*/
array (
'label' => $this -> _ ( 'Found %d module(s) missing file:' ),
'items' => $missModules ,
),
array (
'label' => $this -> _ ( 'Found %d module version changes (applied when each module is loaded):' ),
'items' => $versionChanges ,
),
);
foreach ( $reports as $report ) {
if ( ! count ( $report [ 'items' ])) continue ;
if ( $showMessages ) $this -> message (
$sanitizer -> entities1 ( sprintf ( $report [ 'label' ], count ( $report [ 'items' ]))) .
'<pre>' . $sanitizer -> entities ( implode ( " \n " , $report [ 'items' ])) . '</pre>' ,
Notice :: allowMarkup | Notice :: noGroup
);
$this -> log (
sprintf ( $report [ 'label' ], count ( $report [ 'items' ])) . ' ' .
implode ( ', ' , $report [ 'items' ])
);
}
}
/**
* Update the cache of queued module version changes
*
*/
protected function updateModuleVersionsCache () {
foreach ( $this -> modulesLastVersions as $id => $version ) {
// clear out stale data, if present
if ( ! in_array ( $id , $this -> moduleIDs )) unset ( $this -> modulesLastVersions [ $id ]);
}
if ( count ( $this -> modulesLastVersions )) {
$this -> wire () -> cache -> save ( self :: moduleLastVersionsCacheName , $this -> modulesLastVersions , WireCache :: expireReserved );
} else {
$this -> wire () -> cache -> delete ( self :: moduleLastVersionsCacheName );
}
}
/**
* Check the module version to make sure it is consistent with our moduleInfo
*
* When not consistent , this triggers the moduleVersionChanged hook , which in turn
* triggers the $module -> ___upgrade ( $fromVersion , $toVersion ) method .
*
* @ param Module $module
*
*/
protected function checkModuleVersion ( Module $module ) {
$id = $this -> getModuleID ( $module );
$moduleInfo = $this -> getModuleInfo ( $module );
$lastVersion = isset ( $this -> modulesLastVersions [ $id ]) ? $this -> modulesLastVersions [ $id ] : null ;
if ( ! is_null ( $lastVersion )) {
if ( $lastVersion != $moduleInfo [ 'version' ]) {
$this -> moduleVersionChanged ( $module , $lastVersion , $moduleInfo [ 'version' ]);
unset ( $this -> modulesLastVersions [ $id ]);
}
$this -> updateModuleVersionsCache ();
}
}
/**
* Hook called when a module ' s version changes
*
* This calls the module ' s ___upgrade ( $fromVersion , $toVersion ) method .
*
* @ param Module | _Module $module
* @ param int | string $fromVersion
* @ param int | string $toVersion
*
*/
protected function ___moduleVersionChanged ( Module $module , $fromVersion , $toVersion ) {
$moduleName = wireClassName ( $module , false );
$moduleID = $this -> getModuleID ( $module );
$fromVersionStr = $this -> formatVersion ( $fromVersion );
$toVersionStr = $this -> formatVersion ( $toVersion );
$this -> message ( $this -> _ ( 'Upgrading module' ) . " ( $moduleName : $fromVersionStr => $toVersionStr ) " );
try {
2023-03-10 19:41:40 +01:00
if ( method_exists ( $module , '___upgrade' ) || method_exists ( $module , 'upgrade' )) {
2022-03-08 15:55:41 +01:00
$module -> upgrade ( $fromVersion , $toVersion );
}
unset ( $this -> modulesLastVersions [ $moduleID ]);
} catch ( \Exception $e ) {
$this -> error ( " Error upgrading module ( $moduleName ): " . $e -> getMessage ());
}
}
/**
* Update module flags if any happen to differ from what ' s in the given moduleInfo
*
* @ param $moduleID
* @ param array $info
*
*/
protected function updateModuleFlags ( $moduleID , array $info ) {
$flags = ( int ) $this -> getFlags ( $moduleID );
if ( $info [ 'autoload' ]) {
// module is autoload
if ( ! ( $flags & self :: flagsAutoload )) {
// add autoload flag
$this -> setFlag ( $moduleID , self :: flagsAutoload , true );
}
if ( is_string ( $info [ 'autoload' ])) {
// requires conditional flag
// value is either: "function", or the conditional string (like key=value)
if ( ! ( $flags & self :: flagsConditional )) $this -> setFlag ( $moduleID , self :: flagsConditional , true );
} else {
// should not have conditional flag
if ( $flags & self :: flagsConditional ) $this -> setFlag ( $moduleID , self :: flagsConditional , false );
}
} else if ( $info [ 'autoload' ] !== null ) {
// module is not autoload
if ( $flags & self :: flagsAutoload ) {
// remove autoload flag
$this -> setFlag ( $moduleID , self :: flagsAutoload , false );
}
if ( $flags & self :: flagsConditional ) {
// remove conditional flag
$this -> setFlag ( $moduleID , self :: flagsConditional , false );
}
}
if ( $info [ 'singular' ]) {
if ( ! ( $flags & self :: flagsSingular )) $this -> setFlag ( $moduleID , self :: flagsSingular , true );
} else {
if ( $flags & self :: flagsSingular ) $this -> setFlag ( $moduleID , self :: flagsSingular , false );
}
// handle addFlag and removeFlag moduleInfo properties
foreach ( array ( 0 => 'removeFlag' , 1 => 'addFlag' ) as $add => $flagsType ) {
if ( empty ( $info [ $flagsType ])) continue ;
if ( $flags & $info [ $flagsType ]) {
// already has the flags
if ( ! $add ) {
// remove the flag(s)
$this -> setFlag ( $moduleID , $info [ $flagsType ], false );
}
} else {
// does not have the flags
if ( $add ) {
// add the flag(s)
$this -> setFlag ( $moduleID , $info [ $flagsType ], true );
}
}
}
}
/**
* Save the module information cache
*
*/
protected function saveModuleInfoCache () {
if ( $this -> debug ) {
static $n = 0 ;
$this -> message ( " saveModuleInfoCache ( " . ( ++ $n ) . " ) " );
}
$this -> moduleInfoCache = array ();
$this -> moduleInfoCacheVerbose = array ();
$this -> moduleInfoCacheUninstalled = array ();
2023-03-10 19:41:40 +01:00
$user = $this -> wire () -> user ;
$languages = $this -> wire () -> languages ;
2022-03-08 15:55:41 +01:00
$language = null ;
if ( $languages ) {
// switch to default language to prevent caching of translated title/summary data
$language = $user -> language ;
try {
if ( $language && $language -> id && ! $language -> isDefault ()) $user -> language = $languages -> getDefault (); // save
} catch ( \Exception $e ) {
$this -> trackException ( $e , false , true );
}
}
foreach ( array ( true , false ) as $installed ) {
$items = $installed ? $this : array_keys ( $this -> installable );
foreach ( $items as $module ) {
$class = is_object ( $module ) ? $module -> className () : $module ;
$class = wireClassName ( $class , false );
$info = $this -> getModuleInfo ( $class , array ( 'noCache' => true , 'verbose' => true ));
$moduleID = ( int ) $info [ 'id' ]; // note ID is always 0 for uninstalled modules
if ( ! empty ( $info [ 'error' ])) {
if ( $this -> debug ) $this -> warning ( " $class reported error: $info[error] " );
continue ;
}
if ( ! $moduleID && $installed ) {
if ( $this -> debug ) $this -> warning ( " No module ID for $class " );
continue ;
}
if ( ! $this -> debug ) unset ( $info [ 'id' ]); // no need to double store this property since it is already the array key
if ( is_null ( $info [ 'autoload' ])) {
// module info does not indicate an autoload state
$info [ 'autoload' ] = $this -> isAutoload ( $module );
} else if ( ! is_bool ( $info [ 'autoload' ]) && ! is_string ( $info [ 'autoload' ]) && wireIsCallable ( $info [ 'autoload' ])) {
// runtime function, identify it only with 'function' so that it can be recognized later as one that
// needs to be dynamically loaded
$info [ 'autoload' ] = 'function' ;
}
if ( is_null ( $info [ 'singular' ])) {
$info [ 'singular' ] = $this -> isSingular ( $module );
}
if ( is_null ( $info [ 'configurable' ])) {
$info [ 'configurable' ] = $this -> isConfigurable ( $module , false );
}
if ( $moduleID ) $this -> updateModuleFlags ( $moduleID , $info );
if ( $installed ) {
$verboseKeys = $this -> moduleInfoVerboseKeys ;
$verboseInfo = array ();
foreach ( $verboseKeys as $key ) {
if ( ! empty ( $info [ $key ])) $verboseInfo [ $key ] = $info [ $key ];
unset ( $info [ $key ]); // remove from regular moduleInfo
}
$this -> moduleInfoCache [ $moduleID ] = $info ;
$this -> moduleInfoCacheVerbose [ $moduleID ] = $verboseInfo ;
} else {
$this -> moduleInfoCacheUninstalled [ $class ] = $info ;
}
}
}
$caches = array (
self :: moduleInfoCacheName => 'moduleInfoCache' ,
self :: moduleInfoCacheVerboseName => 'moduleInfoCacheVerbose' ,
self :: moduleInfoCacheUninstalledName => 'moduleInfoCacheUninstalled' ,
);
foreach ( $caches as $cacheName => $varName ) {
$data = $this -> $varName ;
foreach ( $data as $moduleID => $moduleInfo ) {
foreach ( $moduleInfo as $key => $value ) {
// remove unpopulated properties
if ( $key == 'installed' ) {
// no need to store an installed==true property
if ( $value ) unset ( $data [ $moduleID ][ $key ]);
} else if ( $key == 'requires' && ! empty ( $value ) && ! empty ( $data [ $moduleID ][ 'requiresVersions' ])) {
// requiresVersions has enough info to re-construct requires, so no need to store it
unset ( $data [ $moduleID ][ $key ]);
} else if (( $key == 'created' && empty ( $value ))
|| ( $value === 0 && ( $key == 'singular' || $key == 'autoload' || $key == 'configurable' ))
|| ( $value === null || $value === " " || $value === false )
|| ( is_array ( $value ) && ! count ( $value ))) {
// no need to store these false, null, 0, or blank array properties
unset ( $data [ $moduleID ][ $key ]);
} else if (( $key == 'namespace' && $value == " \\ " . __NAMESPACE__ . " \\ " ) || ( ! strlen ( __NAMESPACE__ ) && empty ( $value ))) {
// no need to cache default namespace in module info
unset ( $data [ $moduleID ][ $key ]);
} else if ( $key == 'file' ) {
// file property is cached elsewhere so doesn't need to be included in this cache
unset ( $data [ $moduleID ][ $key ]);
}
}
}
2023-03-10 19:41:40 +01:00
$this -> wire () -> cache -> save ( $cacheName , $data , WireCache :: expireReserved );
2022-03-08 15:55:41 +01:00
}
$this -> log ( 'Saved module info caches' );
if ( $languages && $language ) $user -> language = $language ; // restore
}
/**
* Start a debug timer , only works when module debug mode is on ( $this -> debug )
*
* @ param $note
* @ return int | null Returns a key for the debug timer
*
*/
protected function debugTimerStart ( $note ) {
if ( ! $this -> debug ) return null ;
$key = count ( $this -> debugLog );
while ( isset ( $this -> debugLog [ $key ])) $key ++ ;
$this -> debugLog [ $key ] = array (
0 => Debug :: timer ( " Modules $key " ),
1 => $note
);
return $key ;
}
/**
* Stop a debug timer , only works when module debug mode is on ( $this -> debug )
*
* @ param int $key The key returned by debugTimerStart
*
*/
protected function debugTimerStop ( $key ) {
if ( ! $this -> debug ) return ;
$log = $this -> debugLog [ $key ];
$timerKey = $log [ 0 ];
$log [ 0 ] = Debug :: timer ( $timerKey );
$this -> debugLog [ $key ] = $log ;
Debug :: removeTimer ( $timerKey );
}
/**
* Return a log of module construct , init and ready times , active only when debug mode is on ( $this -> debug )
*
* #pw-internal
*
* @ return array
*
*/
public function getDebugLog () {
return $this -> debugLog ;
}
/**
* Substitute one module for another , to be used only when $moduleName doesn ' t exist .
*
* #pw-internal
*
* @ param string $moduleName Module class name that may need a substitute
* @ param string $substituteName Module class name you want to substitute when $moduleName isn ' t found .
* Specify null to remove substitute .
*
*/
public function setSubstitute ( $moduleName , $substituteName = null ) {
if ( is_null ( $substituteName )) {
unset ( $this -> substitutes [ $moduleName ]);
} else {
$this -> substitutes [ $moduleName ] = $substituteName ;
}
}
/**
* Substitute modules for other modules , to be used only when $moduleName doesn ' t exist .
*
* This appends existing entries rather than replacing them .
*
* #pw-internal
*
* @ param array $substitutes Array of module name => substitute module name
*
*/
public function setSubstitutes ( array $substitutes ) {
$this -> substitutes = array_merge ( $this -> substitutes , $substitutes );
}
/**
* Load module related CSS and JS files ( where applicable )
*
* - Applies only to modules that carry class - named CSS and / or JS files , such as Process , Inputfield and ModuleJS modules .
* - Assets are populated to `$config->styles` and `$config->scripts` .
*
* #pw-internal
*
* @ param Module | int | string $module Module object or class name
* @ return int Returns number of files that were added
*
*/
public function loadModuleFileAssets ( $module ) {
$class = $this -> getModuleClass ( $module );
static $classes = array ();
if ( isset ( $classes [ $class ])) return 0 ; // already loaded
2023-03-10 19:41:40 +01:00
$config = $this -> wire () -> config ;
$path = $config -> paths ( $class );
$url = $config -> urls ( $class );
2022-03-08 15:55:41 +01:00
$debug = $config -> debug ;
$version = 0 ;
$cnt = 0 ;
foreach ( array ( 'styles' => 'css' , 'scripts' => 'js' ) as $type => $ext ) {
$fileURL = '' ;
$modified = 0 ;
$file = " $path $class . $ext " ;
$minFile = " $path $class .min. $ext " ;
if ( ! $debug && is_file ( $minFile )) {
$fileURL = " $url $class .min. $ext " ;
$modified = filemtime ( $minFile );
} else if ( is_file ( $file )) {
$fileURL = " $url $class . $ext " ;
$modified = filemtime ( $file );
}
if ( $fileURL ) {
if ( ! $version ) {
$info = $this -> getModuleInfo ( $module , array ( 'verbose' => false ));
$version = ( int ) isset ( $info [ 'version' ]) ? $info [ 'version' ] : 0 ;
}
$config -> $type -> add ( " $fileURL ?v= $version - $modified " );
$cnt ++ ;
}
}
$classes [ $class ] = true ;
return $cnt ;
}
/**
* Get module language translation files
*
* @ param Module | string $module
* @ return array Array of translation files including full path , indexed by basename without extension
* @ since 3.0 . 181
*
*/
public function getModuleLanguageFiles ( $module ) {
$module = $this -> getModuleClass ( $module );
if ( empty ( $module )) return array ();
$path = $this -> wire () -> config -> paths ( $module );
if ( empty ( $path )) return array ();
$pathHidden = $path . '.languages/' ;
$pathVisible = $path . 'languages/' ;
if ( is_dir ( $pathVisible )) {
$path = $pathVisible ;
} else if ( is_dir ( $pathHidden )) {
$path = $pathHidden ;
} else {
return array ();
}
$items = array ();
$options = array (
'extensions' => array ( 'csv' ),
'recursive' => false ,
'excludeHidden' => true ,
);
foreach ( $this -> wire () -> files -> find ( $path , $options ) as $file ) {
$basename = basename ( $file , '.csv' );
$items [ $basename ] = $file ;
}
return $items ;
}
/**
* Enables use of $modules ( 'ModuleName' )
*
* @ param string $key
2023-03-10 19:41:40 +01:00
* @ return Module | null
2022-03-08 15:55:41 +01:00
*
*/
public function __invoke ( $key ) {
return $this -> get ( $key );
}
/**
* Save to the modules log
*
* #pw-internal
*
* @ param string $str Message to log
* @ param string $moduleName
* @ return WireLog
*
*/
public function log ( $str , $moduleName = '' ) {
2023-03-10 19:41:40 +01:00
if ( ! in_array ( 'modules' , $this -> wire () -> config -> logs )) return $this -> ___log ();
2022-03-08 15:55:41 +01:00
if ( ! is_string ( $moduleName )) $moduleName = ( string ) $moduleName ;
if ( $moduleName && strpos ( $str , $moduleName ) === false ) $str .= " (Module: $moduleName ) " ;
return $this -> ___log ( $str , array ( 'name' => 'modules' ));
}
/**
* Record and log error message
*
* #pw-internal
*
* @ param array | Wire | string $text
* @ param int $flags
* @ return Modules | WireArray
*
*/
public function error ( $text , $flags = 0 ) {
$this -> log ( $text );
return parent :: error ( $text , $flags );
}
/**
* Compile and return the given file for module , if allowed to do so
*
* #pw-internal
*
* @ param Module | string $moduleName
* @ param string $file Optionally specify the module filename as an optimization
* @ param string | null $namespace Optionally specify namespace as an optimization
* @ return string | bool
*
*/
public function compile ( $moduleName , $file = '' , $namespace = null ) {
static $allowCompile = null ;
2023-03-10 19:41:40 +01:00
if ( $allowCompile === null ) $allowCompile = $this -> wire () -> config -> moduleCompile ;
2022-03-08 15:55:41 +01:00
// if not given a file, track it down
if ( empty ( $file )) $file = $this -> getModuleFile ( $moduleName );
// don't compile when module compilation is disabled
if ( ! $allowCompile ) return $file ;
// don't compile core modules
if ( strpos ( $file , $this -> coreModulesDir ) !== false ) return $file ;
// if namespace not provided, get it
if ( is_null ( $namespace )) {
if ( is_object ( $moduleName )) {
$className = $moduleName -> className ( true );
$namespace = wireClassName ( $className , 1 );
} else if ( is_string ( $moduleName ) && strpos ( $moduleName , " \\ " ) !== false ) {
$namespace = wireClassName ( $moduleName , 1 );
} else {
$namespace = $this -> getModuleNamespace ( $moduleName , array ( 'file' => $file ));
}
}
// determine if compiler should be used
if ( __NAMESPACE__ ) {
$compile = $namespace === '\\' || empty ( $namespace );
} else {
$compile = trim ( $namespace , '\\' ) === 'ProcessWire' ;
}
// compile if necessary
if ( $compile ) {
/** @var FileCompiler $compiler */
$compiler = $this -> wire ( new FileCompiler ( dirname ( $file )));
$compiledFile = $compiler -> compile ( basename ( $file ));
if ( $compiledFile ) $file = $compiledFile ;
}
return $file ;
}
}