From 2a6876baa3149923aa1318d9c91ae1e5ef1d3af3 Mon Sep 17 00:00:00 2001 From: Mathieu Lu Date: Thu, 24 Oct 2024 11:16:31 -0400 Subject: [PATCH] Fix CiviCRM 5.70+ and smarty3+ compatibility (#617) --- CRM/Volunteer/Form/IncludeProfile.php | 4 +- CRM/Volunteer/Form/Settings.php | 9 +- CRM/Volunteer/Form/VolunteerSignUp.php | 2 - CRM/Volunteer/Upgrader.php | 31 +- info.xml | 6 +- mixin/lib/civimix-schema@5.78.beta1.phar | Bin 0 -> 31692 bytes mixin/lib/pathload-0.php | 711 +++++++++++++++++++++++ mixin/smarty@1.0.3.mixin.php | 74 +++ volunteer.civix.php | 37 ++ 9 files changed, 841 insertions(+), 33 deletions(-) create mode 100644 mixin/lib/civimix-schema@5.78.beta1.phar create mode 100644 mixin/lib/pathload-0.php create mode 100644 mixin/smarty@1.0.3.mixin.php diff --git a/CRM/Volunteer/Form/IncludeProfile.php b/CRM/Volunteer/Form/IncludeProfile.php index 52966426..d06c988e 100644 --- a/CRM/Volunteer/Form/IncludeProfile.php +++ b/CRM/Volunteer/Form/IncludeProfile.php @@ -1,7 +1,5 @@ _elements as $element) { $groupName = $this->getGroupName($element); - $label = $element->getLabel(); - if (!empty($label)) { + if (!empty($label)) { if (!array_key_exists($groupName, $elementGroups)) { $elementGroups[$groupName] = array(); } @@ -403,8 +400,8 @@ private function getGroupName(HTML_QuickForm_element $element) { // otherwise fallback to settings metadata if (empty($groupName)) { - $setting = CRM_Utils_Array::value($element->getName(), $this->_settingsMetadata); - $groupName = $setting['group_name']; + $elName = $element->getName(); + $groupName = $this->_settingsMetadata[$elName]['group_name'] ?? NULL; } return $groupName; diff --git a/CRM/Volunteer/Form/VolunteerSignUp.php b/CRM/Volunteer/Form/VolunteerSignUp.php index c9b3b831..3b200e02 100644 --- a/CRM/Volunteer/Form/VolunteerSignUp.php +++ b/CRM/Volunteer/Form/VolunteerSignUp.php @@ -25,8 +25,6 @@ +--------------------------------------------------------------------+ */ -require_once 'CRM/Core/Form.php'; - /** * Form controller class * diff --git a/CRM/Volunteer/Upgrader.php b/CRM/Volunteer/Upgrader.php index 266bd06b..1ca368e5 100644 --- a/CRM/Volunteer/Upgrader.php +++ b/CRM/Volunteer/Upgrader.php @@ -35,7 +35,7 @@ class CRM_Volunteer_Upgrader extends CRM_Extension_Upgrader_Base { const customContactTypeName = 'Volunteer'; const skillLevelOptionGroupName = 'skill_level'; - public function install() { + public function postInstall() { $volActivityTypeId = $this->createActivityType(CRM_Volunteer_BAO_Assignment::CUSTOM_ACTIVITY_TYPE); $smarty = CRM_Core_Smarty::singleton(); $smarty->assign('volunteer_custom_activity_type_name', CRM_Volunteer_BAO_Assignment::CUSTOM_ACTIVITY_TYPE); @@ -62,6 +62,17 @@ public function install() { // uncomment the next line to insert sample data // $this->executeSqlFile('sql/volunteer_sample.mysql'); + + // See VOL-237. Avoid order of operation problems by assigning a value to the + // slider_widget_fields setting after the install, which is responsible for + // creating both the setting and the custom field whose ID is used in the + // initial value. + $customFieldId = civicrm_api3('customField', 'getvalue', array( + 'custom_group_id' => 'Volunteer_Information', + 'name' => 'camera_skill_level', + 'return' => 'id', + )); + _volunteer_update_slider_fields(array(CRM_Core_Action::ADD => $customFieldId)); } /** @@ -507,23 +518,6 @@ public function uninstall() { } } - /** - * Perform post-install tasks. - * - * See VOL-237. Avoid order of operation problems by assigning a value to the - * slider_widget_fields setting after the install, which is responsible for - * creating both the setting and the custom field whose ID is used in the - * initial value. - */ - public function postInstall() { - $customFieldId = civicrm_api3('customField', 'getvalue', array( - 'custom_group_id' => 'Volunteer_Information', - 'name' => 'camera_skill_level', - 'return' => 'id', - )); - _volunteer_update_slider_fields(array(CRM_Core_Action::ADD => $customFieldId)); - } - /** * Example: Run a simple query when a module is enabled * @@ -893,7 +887,6 @@ public function executeCustomDataTemplateFile($relativePath) { $xmlCode = $smarty->fetch($relativePath); $xml = simplexml_load_string($xmlCode); - require_once 'CRM/Utils/Migrate/Import.php'; $import = new CRM_Utils_Migrate_Import(); $import->runXmlElement($xml); return TRUE; diff --git a/info.xml b/info.xml index 1a7bd26b..fbf9eb0a 100644 --- a/info.xml +++ b/info.xml @@ -11,14 +11,14 @@ 2024-02-10 2.4.5 - 5.60 + 5.71 Developed by Ginkgo Street Labs and CiviCRM, LLC with contributions from the community. Special thanks to Friends of Georgia State Parks & Historic Sites for funding the initial release, and to The Manhattan Neighborhood Network for funding the 1.4 release. CRM/Volunteer - 23.02.1 + 24.09.1 https://docs.civicrm.org/volunteer/en/latest/ @@ -31,8 +31,8 @@ menu-xml@1.0.0 mgd-php@1.0.0 setting-php@1.0.0 - smarty-v2@1.0.0 entity-types-php@1.0.0 + smarty@1.0.3 diff --git a/mixin/lib/civimix-schema@5.78.beta1.phar b/mixin/lib/civimix-schema@5.78.beta1.phar new file mode 100644 index 0000000000000000000000000000000000000000..079ec3621bbf0b61249d15b6934ec3a0ac19dc65 GIT binary patch literal 31692 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 = $top; + } + public function offsetExists($version): bool { + return ($version === 'top' || $version <= $this->top->version); + } + public function offsetGet($version): ?\PathLoadInterface { + if ($version === 'top' || $version <= $this->top->version) { + return $this->top; + } + return NULL; + } + public function offsetSet($offset, $value): void { + error_log("Cannot overwrite PathLoad[$offset]"); + } + public function offsetUnset($offset): void { + error_log("Cannot remove PathLoad[$offset]"); + } + } + class Package { + /** + * Split a package identifier into its parts. + * + * @param string $package + * Ex: 'foobar@1.2.3' + * @return array + * Tuple: [$majorName, $name, $version] + * Ex: 'foobar@1', 'foobar', '1.2.3' + */ + public static function parseExpr(string $package): array { + if (strpos($package, '@') === FALSE) { + throw new \RuntimeException("Malformed package name: $package"); + } + [$prefix, $suffix] = explode('@', $package, 2); + $prefix = str_replace('/', '~', $prefix); + [$major] = explode('.', $suffix, 2); + return ["$prefix@$major", $prefix, $suffix]; + } + public static function parseFileType(string $file): array { + if (substr($file, -4) === '.php') { + return ['php', substr(basename($file), 0, -4)]; + } + elseif (substr($file, '-5') === '.phar') { + return ['phar', substr(basename($file), 0, -5)]; + } + elseif (is_dir($file)) { + return ['dir', basename($file)]; + } + else { + return [NULL, NULL]; + } + } + /** + * @param string $file + * Ex: '/var/www/app-1/lib/foobar@.1.2.3.phar' + * @return \PathLoad\Vn\Package|null + */ + public static function create(string $file): ?Package { + [$type, $base] = self::parseFileType($file); + if ($type === NULL) { + return NULL; + } + $self = new Package(); + [$self->majorName, $self->name, $self->version] = static::parseExpr($base); + $self->file = $file; + $self->type = $type; + return $self; + } + /** + * @var string + * Ex: '/var/www/app-1/lib/cloud-file-io@1.2.3.phar' + */ + public $file; + /** + * @var string + * Ex: 'cloud-file-io' + */ + public $name; + /** + * @var string + * Ex: 'cloud-file-io@1' + */ + public $majorName; + /** + * @var string + * Ex: '1.2.3' + */ + public $version; + /** + * @var string + * Ex: 'php' or 'phar' or 'dir' + */ + public $type; + public $reloadable = FALSE; + } + class Scanner { + /** + * @var array + * Array(string $id => [package => string, glob => string]) + * @internal + */ + public $allRules = []; + /** + * @var array + * Array(string $id => [package => string, glob => string]) + * @internal + */ + public $newRules = []; + /** + * @param array $rule + * Ex: ['package' => '*', 'glob' => '/var/www/lib/*@*'] + * Ex: ['package' => 'cloud-file-io@1', 'glob' => '/var/www/lib/cloud-io@1*.phar']) + * @return void + */ + public function addRule(array $rule): void { + $id = static::id($rule); + $this->newRules[$id] = $this->allRules[$id] = $rule; + } + public function reset(): void { + $this->newRules = $this->allRules; + } + /** + * Evaluate any rules that have a chance of finding $packageHint. + * + * @param string $packageHint + * Give a hint about what package we're looking for. + * The scanner will try to target packages based on this hint. + * Ex: '*' or 'cloud-file-io' + * @return \Generator + * A list of packages. These may not be the exact package you're looking for. + * You should assimilate knowledge of all outputs because you may not get them again. + */ + public function scan(string $packageHint): \Generator { + yield from []; + foreach (array_keys($this->newRules) as $id) { + $searchRule = $this->newRules[$id]; + if ($searchRule['package'] === '*' || $searchRule['package'] === $packageHint) { + unset($this->newRules[$id]); + if (isset($searchRule['glob'])) { + foreach ((array) glob($searchRule['glob']) as $file) { + if (($package = Package::create($file)) !== NULL) { + yield $package; + } + } + } + if (isset($searchRule['file'])) { + $package = new Package(); + $package->file = $searchRule['file']; + $package->name = $searchRule['package']; + $package->majorName = $searchRule['package'] . '@' . explode('.', $searchRule['version'])[0]; + $package->version = $searchRule['version']; + $package->type = $searchRule['type'] ?: Package::parseFileType($searchRule['file'])[0]; + yield $package; + } + } + } + } + protected static function id(array $rule): string { + if (isset($rule['glob'])) { + return $rule['glob']; + } + elseif (isset($rule['file'])) { + return md5(implode(' ', [$rule['file'], $rule['package'], $rule['version']])); + } + else { + throw new \RuntimeException("Cannot identify rule: " . json_encode($rule)); + } + } + } + class Psr0Loader { + /** + * @var array + * Ex: $paths['F']['Foo_'][0] = '/var/www/app/lib/foo@1.0.0/src/'; + * @internal + */ + public $paths = []; + /** + * @param string $dir + * @param array $config + * Ex: ['Foo_' => ['src/']] or ['Foo_' => ['Foo_']] + */ + public function addAll(string $dir, array $config) { + $dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + foreach ($config as $prefix => $relPaths) { + $bucket = $prefix[0]; + foreach ((array) $relPaths as $relPath) { + $this->paths[$bucket][$prefix][] = $dir . $relPath; + } + } + } + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * @return mixed The mapped file name on success, or boolean false on failure. + */ + public function loadClass(string $class) { + $bucket = $class[0]; + if (!isset($this->paths[$bucket])) { + return FALSE; + } + $file = DIRECTORY_SEPARATOR . str_replace(['_', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $class) . '.php'; + foreach ($this->paths[$bucket] as $prefix => $paths) { + if ($prefix === substr($class, 0, strlen($prefix))) { + foreach ($paths as $path) { + $fullFile = $path . $file; + if (file_exists($fullFile)) { + doRequire($fullFile); + return $fullFile; + } + } + } + } + return FALSE; + } + } + class Psr4Loader { + /** + * @var array + * Ex: $prefixes['Foo\\'][0] = '/var/www/app/lib/foo@1.0.0/src/'] + * @internal + */ + public $prefixes = []; + public function addAll(string $dir, array $config) { + foreach ($config as $prefix => $relPaths) { + foreach ($relPaths as $relPath) { + $this->addNamespace($prefix, $dir . '/' . $relPath); + } + } + } + /** + * Adds a base directory for a namespace prefix. + * + * @param string $prefix + * The namespace prefix. + * @param string $baseDir + * A base directory for class files in the namespace. + * @return void + */ + private function addNamespace($prefix, $baseDir) { + $prefix = trim($prefix, '\\') . '\\'; + $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/'; + if (isset($this->prefixes[$prefix]) === FALSE) { + $this->prefixes[$prefix] = []; + } + array_push($this->prefixes[$prefix], $baseDir); + } + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * @return mixed The mapped file name on success, or boolean false on failure. + */ + public function loadClass(string $class) { + $prefix = $class; + while (FALSE !== $pos = strrpos($prefix, '\\')) { + $prefix = substr($class, 0, $pos + 1); + $relativeClass = substr($class, $pos + 1); + if ($mappedFile = $this->findRelativeClass($prefix, $relativeClass)) { + doRequire($mappedFile); + return $mappedFile; + } + $prefix = rtrim($prefix, '\\'); + } + return FALSE; + } + /** + * Load the mapped file for a namespace prefix and relative class. + * + * @param string $prefix + * The namespace prefix. + * @param string $relativeClass + * The relative class name. + * @return string|FALSE + * Matched file name, or FALSE if none found. + */ + private function findRelativeClass($prefix, $relativeClass) { + if (isset($this->prefixes[$prefix]) === FALSE) { + return FALSE; + } + $relFile = str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; + foreach ($this->prefixes[$prefix] as $baseDir) { + $file = $baseDir . $relFile; + if (file_exists($file)) { + return $file; + } + } + return FALSE; + } + } + class PathLoad implements \PathLoadInterface { + /** + * @var null|int + */ + public $version; + /** + * @var Scanner + * @internal + */ + public $scanner; + /** + * List of best-known versions for each package. + * + * Packages are loaded lazily. Once loaded, the data is moved to $loadedPackages. + * + * @var Package[] + * Ex: ['cloud-file-io@1' => new Package('/usr/share/php-pathload/cloud-file-io@1.2.3.phar', + * ...)] + * @internal + */ + public $availablePackages = []; + /** + * List of packages that have already been resolved. + * + * @var Package[] + * Ex: ['cloud-file-io@1' => new Package('/usr/share/php-pathload/cloud-file-io@1.2.3.phar', + * ...)] Note: If PathLoad version is super-ceded, then the loadedPackages may be instances of + * an old `Package` class. Be mindful of duck-type compatibility. We don't strictly need to + * retain this data, but it feels it'd be handy for debugging. + * @internal + */ + public $loadedPackages = []; + /** + * Log of package activations. Used to re-initialize class-loader if we upgrade. + * + * @var array + * @internal + */ + public $activatedPackages = []; + /** + * List of hints for class-loading. If someone tries to use a matching class, then + * load the corresponding package. + * + * Namespace-rules are evaluated lazily. Once evaluated, the data is removed. + * + * @var array + * Array(string $prefix => [string $package => string $package]) + * Ex: ['Super\Cloud\IO\' => ['cloud-io@1' => 'cloud-io@1'] + * @internal + */ + public $availableNamespaces; + /** + * @var \PathLoad\Vn\Psr0Loader + * @internal + */ + public $psr0; + /** + * @var \PathLoad\Vn\Psr4Loader + * @internal + */ + public $psr4; + /** + * @param int $version + * Identify the version being instantiated. + * @param \PathLoadInterface|null $old + * If this instance is a replacement for an older instance, then it will be passed in. + * @return \ArrayAccess + * Versioned work-a-like array. + */ + public static function create(int $version, ?\PathLoadInterface $old = NULL) { + if ($old !== NULL) { + $old->unregister(); + } + $new = new static(); + $new->version = $version; + $new->scanner = new Scanner(); + $new->psr0 = new Psr0Loader(); + $new->psr4 = new Psr4Loader(); + $new->register(); + // The exact protocol for assimilating $old instances may need change. + // This seems like a fair guess as long as old properties are forward-compatible. + + if ($old === NULL) { + $baseDirs = getenv('PHP_PATHLOAD') ? explode(PATH_SEPARATOR, getenv('PHP_PATHLOAD')) : []; + foreach ($baseDirs as $baseDir) { + $new->addSearchDir($baseDir); + } + } + else { + // TIP: You might use $old->version to decide what to use. + foreach ($old->scanner->allRules as $rule) { + $new->scanner->addRule($rule); + } + $new->loadedPackages = $old->loadedPackages; + $new->availableNamespaces = $old->availableNamespaces; + foreach ($old->activatedPackages as $activatedPackage) { + $new->activatePackage($activatedPackage['name'], $activatedPackage['dir'], $activatedPackage['config']); + } + } + return new Versions($new); + } + public function register(): \PathLoadInterface { + spl_autoload_register([$this, 'loadClass']); + return $this; + } + public function unregister(): \PathLoadInterface { + spl_autoload_unregister([$this, 'loadClass']); + return $this; + } + public function reset(): \PathLoadInterface { + $this->scanner->reset(); + return $this; + } + /** + * Append a directory (with many packages) to the search-path. + * + * @param string $baseDir + * The path to a base directory (e.g. `/var/www/myapp/lib`) which contains many packages (e.g. + * `foo@1.2.3.phar` or `bar@4.5.6/autoload.php`). + */ + public function addSearchDir(string $baseDir): \PathLoadInterface { + $this->scanner->addRule(['package' => '*', 'glob' => "$baseDir/*@*"]); + return $this; + } + /** + * Append one specific item to the search list. + * + * @param string $name + * Ex: 'cloud-file-io' + * @param string $version + * Ex: '1.2.3' + * @param string $file + * Full path to the file or folder. + * @param string|null $type + * One of: 'php', 'phar', or 'dir'. NULL will auto-detect. + * + * @return \PathLoadInterface + */ + public function addSearchItem(string $name, string $version, string $file, ?string $type = NULL): \PathLoadInterface { + $this->scanner->addRule(['package' => $name, 'version' => $version, 'file' => $file, 'type' => $type]); + return $this; + } + /** + * Add auto-loading hints. If someone requests a class in $namespace, then we load $package. + * + * Consecutive/identical calls to addNamespace() are de-duplicated. + * + * @param string $package + * Ex: 'cloud-io@1' + * @param string|string[] $namespaces + * Ex: 'Super\Cloud\IO\' + */ + public function addNamespace(string $package, $namespaces): \PathLoadInterface { + foreach ((array) $namespaces as $namespace) { + $this->availableNamespaces[$namespace][$package] = $package; + } + return $this; + } + public function loadClass(string $class) { + if (strpos($class, '\\') !== FALSE) { + $this->loadPackagesByNamespace('\\', explode('\\', $class)); + } + elseif (strpos($class, '_') !== FALSE) { + $this->loadPackagesByNamespace('_', explode('_', $class)); + } + return $this->psr4->loadClass($class) || $this->psr0->loadClass($class); + } + /** + * If the application requests class "Foo\Bar\Whiz\Bang", then you should load + * any packages related to "Foo\*", "Foo\Bar\*", or "Foo\Bar\Whiz\*". + * + * @param string $delim + * Ex: '\\' or '_' + * @param string[] $classParts + * Ex: ['Symfony', 'Components', 'Filesystem', 'Filesystem'] + */ + private function loadPackagesByNamespace(string $delim, array $classParts): void { + array_pop($classParts); + do { + $foundPackages = FALSE; + $namespace = ''; + foreach ($classParts as $nsPart) { + $namespace .= $nsPart . $delim; + if (isset($this->availableNamespaces[$namespace])) { + $packages = $this->availableNamespaces[$namespace]; + foreach ($packages as $package) { + unset($this->availableNamespaces[$namespace][$package]); + if ($this->loadPackage($package)) { + $foundPackages = TRUE; + } + else { + trigger_error("PathLoad: Failed to locate package \"$package\" required for namespace \"$namespace\"", E_USER_WARNING); + $this->availableNamespaces[$namespace][$package] = $package; /* Maybe some other time */ + } + } + } + } + } while ($foundPackages); + // Loading a package could produce metadata about other packages. Assimilate those too. + } + /** + * Load the content of a package. + * + * @param string $majorName + * Ex: 'cloud-io@1' + * @param bool $reload + * @return string|NULL + * The version# of the loaded package. Otherwise, NULL + */ + public function loadPackage(string $majorName, bool $reload = FALSE): ?string { + if (isset($this->loadedPackages[$majorName])) { + if ($reload && $this->loadedPackages[$majorName]->reloadable) { + $this->scanner->reset(); + } + else { + if ($reload) { + trigger_error("PathLoad: Declined to reload \"$majorName\". Package is not reloadable.", E_USER_WARNING); + } + return $this->loadedPackages[$majorName]->version; + } + } + $this->scanAvailablePackages(explode('@', $majorName, 2)[0], $this->availablePackages); + if (!isset($this->availablePackages[$majorName])) { + return NULL; + } + $package = $this->loadedPackages[$majorName] = $this->availablePackages[$majorName]; + unset($this->availablePackages[$majorName]); + switch ($package->type ?? NULL) { + case 'php': + doRequire($package->file); + return $package->version; + case 'phar': + doRequire($package->file); + $this->useMetadataFiles($package, 'phar://' . $package->file); + return $package->version; + case 'dir': + $this->useMetadataFiles($package, $package->file); + return $package->version; + default: + \error_log("PathLoad: Package (\"$majorName\") appears malformed."); + return NULL; + } + } + private function scanAvailablePackages(string $hint, array &$avail): void { + foreach ($this->scanner->scan($hint) as $package) { + /** @var Package $package */ + if (!isset($avail[$package->majorName]) || \version_compare($package->version, $avail[$package->majorName]->version, '>')) { + $avail[$package->majorName] = $package; + } + } + } + /** + * When loading a package, execute metadata files like "pathload.main.php" or "pathload.json". + * + * @param Package $package + * @param string $dir + * Ex: '/var/www/lib/cloud-io@1.2.0' + * Ex: 'phar:///var/www/lib/cloud-io@1.2.0.phar' + */ + private function useMetadataFiles(Package $package, string $dir): void { + $phpFile = "$dir/pathload.main.php"; + $jsonFile = "$dir/pathload.json"; + if (file_exists($phpFile)) { + require $phpFile; + } + elseif (file_exists($jsonFile)) { + $jsonData = json_decode(file_get_contents($jsonFile), TRUE); + $id = $package->name . '@' . $package->version; + $this->activatePackage($id, $dir, $jsonData); + } + } + /** + * Given a configuration for the package, activate the correspond autoloader rules. + * + * @param string $majorName + * Ex: 'cloud-io@1' + * @param string|null $dir + * Used for applying the 'autoload' rules. + * Ex: '/var/www/lib/cloud-io@1.2.3' + * @param array $config + * Ex: ['autoload' => ['psr4' => ...], 'require-namespace' => [...], 'require-package' => [...]] + * @return \PathLoadInterface + */ + public function activatePackage(string $majorName, ?string $dir, array $config): \PathLoadInterface { + if (isset($config['reloadable'])) { + $this->loadedPackages[$majorName]->reloadable = $config['reloadable']; + } + if (!isset($config['autoload'])) { + return $this; + } + if ($dir === NULL) { + throw new \RuntimeException("Cannot activate package $majorName. The 'autoload' property requires a base-directory."); + } + $this->activatedPackages[] = ['name' => $majorName, 'dir' => $dir, 'config' => $config]; + if (!empty($config['autoload']['include'])) { + foreach ($config['autoload']['include'] as $file) { + doRequire($dir . DIRECTORY_SEPARATOR . $file); + } + } + if (isset($config['autoload']['psr-0'])) { + $this->psr0->addAll($dir, $config['autoload']['psr-0']); + } + if (isset($config['autoload']['psr-4'])) { + $this->psr4->addAll($dir, $config['autoload']['psr-4']); + } + foreach ($config['require-namespace'] ?? [] as $nsRule) { + foreach ((array) $nsRule['package'] as $package) { + foreach ((array) $nsRule['prefix'] as $prefix) { + $this->availableNamespaces[$prefix][$package] = $package; + } + } + } + foreach ($config['require-package'] ?? [] as $package) { + $this->loadPackage($package); + } + return $this; + } + } + } +} + +namespace { + // New or upgraded instance. + $GLOBALS['_PathLoad'] = \PathLoad\V0\PathLoad::create(0, $GLOBALS['_PathLoad']['top'] ?? NULL); + if (!function_exists('pathload')) { + /** + * Get a reference the PathLoad manager. + * + * @param int|string $version + * @return \PathLoadInterface + */ + function pathload($version = 'top') { + return $GLOBALS['_PathLoad'][$version]; + } + } + return pathload(); +} diff --git a/mixin/smarty@1.0.3.mixin.php b/mixin/smarty@1.0.3.mixin.php new file mode 100644 index 00000000..f6401978 --- /dev/null +++ b/mixin/smarty@1.0.3.mixin.php @@ -0,0 +1,74 @@ +getPath('templates'); + if (!file_exists($dir)) { + return; + } + + $register = function($newDirs) { + $smarty = CRM_Core_Smarty::singleton(); + $v2 = isset($smarty->_version) && version_compare($smarty->_version, 3, '<'); + $templateDirs = (array) ($v2 ? $smarty->template_dir : $smarty->getTemplateDir()); + $templateDirs = array_merge($newDirs, $templateDirs); + $templateDirs = array_unique(array_map(function($v) { + $v = str_replace(DIRECTORY_SEPARATOR, '/', $v); + $v = rtrim($v, '/') . '/'; + return $v; + }, $templateDirs)); + if ($v2) { + $smarty->template_dir = $templateDirs; + } + else { + $smarty->setTemplateDir($templateDirs); + } + }; + + // Let's figure out what environment we're in -- so that we know the best way to call $register(). + + if (!empty($GLOBALS['_CIVIX_MIXIN_POLYFILL'])) { + // Polyfill Loader (v<=5.45): We're already in the middle of firing `hook_config`. + if ($mixInfo->isActive()) { + $register([$dir]); + } + return; + } + + if (CRM_Extension_System::singleton()->getManager()->extensionIsBeingInstalledOrEnabled($mixInfo->longName)) { + // New Install, Standard Loader: The extension has just been enabled, and we're now setting it up. + // System has already booted. New templates may be needed for upcoming installation steps. + $register([$dir]); + return; + } + + // Typical Pageview, Standard Loader: Defer the actual registration for a moment -- to ensure that Smarty is online. + // We need to bundle-up all dirs -- Smarty 3/4/5 is inefficient with processing repeated calls to `getTemplateDir()`+`setTemplateDir()` + if (!isset(Civi::$statics[__FILE__]['event'])) { + Civi::$statics[__FILE__]['event'] = 'civi.smarty.addPaths.' . md5(__FILE__); + Civi::dispatcher()->addListener('hook_civicrm_config', function() use ($register) { + $dirs = []; + $event = \Civi\Core\Event\GenericHookEvent::create(['dirs' => &$dirs]); + Civi::dispatcher()->dispatch(Civi::$statics[__FILE__]['event'], $event); + $register($dirs); + }); + } + + Civi::dispatcher()->addListener(Civi::$statics[__FILE__]['event'], function($event) use ($mixInfo, $dir) { + if ($mixInfo->isActive()) { + array_unshift($event->dirs, $dir); + } + }); + +}; diff --git a/volunteer.civix.php b/volunteer.civix.php index 982c6332..283660c2 100644 --- a/volunteer.civix.php +++ b/volunteer.civix.php @@ -75,10 +75,47 @@ public static function findClass($suffix) { return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix); } + /** + * @return \CiviMix\Schema\SchemaHelperInterface + */ + public static function schema() { + if (!isset($GLOBALS['CiviMixSchema'])) { + pathload()->loadPackage('civimix-schema@5', TRUE); + } + return $GLOBALS['CiviMixSchema']->getHelper(static::LONG_NAME); + } + } use CRM_Volunteer_ExtensionUtil as E; +($GLOBALS['_PathLoad'][0] ?? require __DIR__ . '/mixin/lib/pathload-0.php'); +pathload()->addSearchDir(__DIR__ . '/mixin/lib'); +spl_autoload_register('_volunteer_civix_class_loader', TRUE, TRUE); + +function _volunteer_civix_class_loader($class) { + if ($class === 'CRM_Volunteer_DAO_Base') { + if (version_compare(CRM_Utils_System::version(), '5.74.beta', '>=')) { + class_alias('CRM_Core_DAO_Base', 'CRM_Volunteer_DAO_Base'); + // ^^ Materialize concrete names -- encourage IDE's to pick up on this association. + } + else { + $realClass = 'CiviMix\\Schema\\Volunteer\\DAO'; + class_alias($realClass, $class); + // ^^ Abstract names -- discourage IDE's from picking up on this association. + } + return; + } + + // This allows us to tap-in to the installation process (without incurring real file-reads on typical requests). + if (strpos($class, 'CiviMix\\Schema\\Volunteer\\') === 0) { + // civimix-schema@5 is designed for backported use in download/activation workflows, + // where new revisions may become dynamically available. + pathload()->loadPackage('civimix-schema@5', TRUE); + CiviMix\Schema\loadClass($class); + } +} + /** * (Delegated) Implements hook_civicrm_config(). *