moduleFileExts[$class] = (int) $setValue; return $setValue; } return isset($this->moduleFileExts[$class]) ? $this->moduleFileExts[$class] : 0; } /** * 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 * */ public function findModuleFiles($path, $readCache = false, $level = 0) { static $startPath; static $prependFiles = array(); $config = $this->wire()->config; $cacheName = ''; if($level == 0) { $startPath = $path; $cacheName = "Modules." . str_replace($config->paths->root, '', $path); if($readCache) { $cacheContents = $this->modules->getCache($cacheName); if($cacheContents) return explode("\n", trim($cacheContents)); } } $files = array(); $autoloadOrders = $this->modules->loader->getAutoloadOrders(); if(count($autoloadOrders) && $path !== $config->paths->modules) { // ok } else { $autoloadOrders = null; } 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($cacheName) { $this->modules->saveCache($cacheName, implode("\n", $files)); } } return $files; } /** * Get the path + filename (or optionally URL) for 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->modules->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->moduleFileExt($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) { $installableFile = $this->modules->installableFile($moduleName); if($installableFile && !$hasDuplicate) { $file = $installableFile; if(!$options['fast'] && !file_exists($file)) $file = false; } } if(!$file) { $dupFile = $this->modules->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->modules->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->moduleID($moduleName); $namespace = $this->modules->info->moduleInfoCache($moduleID, 'namespace'); if(!empty($namespace)) { $className = rtrim($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; } /** * Include the given filename * * @param string $file * @param string $moduleName * @return bool * */ public 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->modules->info->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; } if(!$success) { // handle case where module has moved from /modules/Foo.module to /modules/Foo/Foo.module // which can only occur during upgrades from much older versions. // examples are FieldtypeImage and FieldtypeText which moved to their own directories. $file2 = preg_replace('!([/\\\\])([^/\\\\]+)(\.module(?:\.php)?)$!', '$1$2$1$2$3', $file); if($file !== $file2) $success = @include_once($file2); } // set instance back, if multi-instance if($wire1 !== $wire2) ProcessWire::setCurrentInstance($wire1); return (bool) $success; } /** * Compile and return the given file for module, if allowed to do so * * @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; if($allowCompile === null) $allowCompile = $this->wire()->config->moduleCompile; // if not given a file, track it down if(empty($file)) $file = $this->modules->getModuleFile($moduleName); // don't compile when module compilation is disabled if(!$allowCompile) return $file; // don't compile core modules if(strpos($file, $this->modules->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->modules->info->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; } /** * 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', Modules::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->modules->flags->setFlag($name, Modules::flagsNoFile, false); } return $missing; } /** * 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->modules->getModuleClass($module); static $classes = array(); if(isset($classes[$class])) return 0; // already loaded $config = $this->wire()->config; $path = $config->paths($class); $url = $config->urls($class); $debug = $config->debug; $coreVersion = $config->version; $moduleVersion = 0; $cnt = 0; foreach(array('styles' => 'css', 'scripts' => 'js') as $type => $ext) { $fileURL = ''; $file = "$path$class.$ext"; $fileVersion = $coreVersion; $minFile = "$path$class.min.$ext"; if(!$debug && is_file($minFile)) { $fileURL = "$url$class.min.$ext"; } else if(is_file($file)) { $fileURL = "$url$class.$ext"; if($debug) $fileVersion = filemtime($file); } if($fileURL) { if(!$moduleVersion) { $info = $this->modules->info->getModuleInfo($module, array('verbose' => false)); $moduleVersion = (int) isset($info['version']) ? $info['version'] : 0; } $config->$type->add("$fileURL?v=$moduleVersion-$fileVersion"); $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->modules->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; } /** * Setup entries in config->urls and config->paths for the given module * * @param string $moduleName * @param string $path * */ public function setConfigPaths($moduleName, $path) { $config = $this->wire()->config; $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 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; } public function getDebugData() { return array( 'moduleFileExts' => $this->moduleFileExts ); } }