diff --git a/CRM/Emailamender/Upgrader.php b/CRM/Emailamender/Upgrader.php
index 8255348..8874d7b 100644
--- a/CRM/Emailamender/Upgrader.php
+++ b/CRM/Emailamender/Upgrader.php
@@ -3,7 +3,7 @@
/**
* Collection of upgrade steps
*/
-class CRM_Emailamender_Upgrader extends CRM_Emailamender_Upgrader_Base {
+class CRM_Emailamender_Upgrader extends CRM_Extension_Upgrader_Base {
// By convention, functions that look like "function upgrade_NNNN()" are
// upgrade tasks. They are executed in order (like Drupal's hook_update_N).
diff --git a/CRM/Emailamender/Upgrader/Base.php b/CRM/Emailamender/Upgrader/Base.php
deleted file mode 100644
index aeb6dd3..0000000
--- a/CRM/Emailamender/Upgrader/Base.php
+++ /dev/null
@@ -1,298 +0,0 @@
-ctx = array_shift($args);
- $instance->queue = $instance->ctx->queue;
- $method = array_shift($args);
- return call_user_func_array(array($instance, $method), $args);
- }
-
- public function __construct($extensionName, $extensionDir) {
- $this->extensionName = $extensionName;
- $this->extensionDir = $extensionDir;
- }
-
- // ******** Task helpers ********
-
- /**
- * Run a CustomData file
- *
- * @param string $relativePath the CustomData XML file path (relative to this extension's dir)
- * @return bool
- */
- public function executeCustomDataFile($relativePath) {
- $xml_file = $this->extensionDir . '/' . $relativePath;
- return $this->executeCustomDataFileByAbsPath($xml_file);
- }
-
- /**
- * Run a CustomData file
- *
- * @param string $xml_file the CustomData XML file path (absolute path)
- * @return bool
- */
- protected static function executeCustomDataFileByAbsPath($xml_file) {
- require_once 'CRM/Utils/Migrate/Import.php';
- $import = new CRM_Utils_Migrate_Import();
- $import->run($xml_file);
- return TRUE;
- }
-
- /**
- * Run a SQL file
- *
- * @param string $relativePath the SQL file path (relative to this extension's dir)
- * @return bool
- */
- public function executeSqlFile($relativePath) {
- CRM_Utils_File::sourceSQLFile(
- CIVICRM_DSN,
- $this->extensionDir . '/' . $relativePath
- );
- return TRUE;
- }
-
- /**
- * Run one SQL query
- *
- * This is just a wrapper for CRM_Core_DAO::executeSql, but it
- * provides syntatic sugar for queueing several tasks that
- * run different queries
- */
- public function executeSql($query, $params = array()) {
- // FIXME verify that we raise an exception on error
- CRM_Core_DAO::executeSql($query, $params);
- return TRUE;
- }
-
- /**
- * Syntatic sugar for enqueuing a task which calls a function
- * in this class. The task is weighted so that it is processed
- * as part of the currently-pending revision.
- *
- * After passing the $funcName, you can also pass parameters that will go to
- * the function. Note that all params must be serializable.
- */
- public function addTask($title) {
- $args = func_get_args();
- $title = array_shift($args);
- $task = new CRM_Queue_Task(
- array(get_class($this), '_queueAdapter'),
- $args,
- $title
- );
- return $this->queue->createItem($task, array('weight' => -1));
- }
-
- // ******** Revision-tracking helpers ********
-
- /**
- * Determine if there are any pending revisions
- *
- * @return bool
- */
- public function hasPendingRevisions() {
- $revisions = $this->getRevisions();
- $currentRevision = $this->getCurrentRevision();
-
- if (empty($revisions)) {
- return FALSE;
- }
- if (empty($currentRevision)) {
- return TRUE;
- }
-
- return ($currentRevision < max($revisions));
- }
-
- /**
- * Add any pending revisions to the queue
- */
- public function enqueuePendingRevisions(CRM_Queue_Queue $queue) {
- $this->queue = $queue;
-
- $currentRevision = $this->getCurrentRevision();
- foreach ($this->getRevisions() as $revision) {
- if ($revision > $currentRevision) {
- $title = ts('Upgrade %1 to revision %2', array(
- 1 => $this->extensionName,
- 2 => $revision,
- ));
-
- // note: don't use addTask() because it sets weight=-1
-
- $task = new CRM_Queue_Task(
- array(get_class($this), '_queueAdapter'),
- array('upgrade_' . $revision),
- $title
- );
- $this->queue->createItem($task);
-
- $task = new CRM_Queue_Task(
- array(get_class($this), '_queueAdapter'),
- array('setCurrentRevision', $revision),
- $title
- );
- $this->queue->createItem($task);
- }
- }
- }
-
- /**
- * Get a list of revisions
- *
- * @return array(revisionNumbers) sorted numerically
- */
- public function getRevisions() {
- if (! is_array($this->revisions)) {
- $this->revisions = array();
-
- $clazz = new ReflectionClass(get_class($this));
- $methods = $clazz->getMethods();
- foreach ($methods as $method) {
- if (preg_match('/^upgrade_(.*)/', $method->name, $matches)) {
- $this->revisions[] = $matches[1];
- }
- }
- sort($this->revisions, SORT_NUMERIC);
- }
-
- return $this->revisions;
- }
-
- public function getCurrentRevision() {
- // return CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName);
- $key = $this->extensionName . ':version';
- return Civi::settings()->get($key);
- }
-
- public function setCurrentRevision($revision) {
- // We call this during hook_civicrm_install, but the underlying SQL
- // UPDATE fails because the extension record hasn't been INSERTed yet.
- // Instead, track revisions in our own namespace.
- // CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision);
-
- $key = $this->extensionName . ':version';
- Civi::settings()->set($key, $revision);
- return TRUE;
- }
-
- // ******** Hook delegates ********
-
- public function onInstall() {
- $files = glob($this->extensionDir . '/sql/*_install.sql');
- if (is_array($files)) {
- foreach ($files as $file) {
- CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file);
- }
- }
- $files = glob($this->extensionDir . '/xml/*_install.xml');
- if (is_array($files)) {
- foreach ($files as $file) {
- $this->executeCustomDataFileByAbsPath($file);
- }
- }
- if (is_callable(array($this, 'install'))) {
- $this->install();
- }
- $revisions = $this->getRevisions();
- if (!empty($revisions)) {
- $this->setCurrentRevision(max($revisions));
- }
- }
-
- public function onUninstall() {
- if (is_callable(array($this, 'uninstall'))) {
- $this->uninstall();
- }
- $files = glob($this->extensionDir . '/sql/*_uninstall.sql');
- if (is_array($files)) {
- foreach ($files as $file) {
- CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file);
- }
- }
- $this->setCurrentRevision(NULL);
- }
-
- public function onEnable() {
- // stub for possible future use
- if (is_callable(array($this, 'enable'))) {
- $this->enable();
- }
- }
-
- public function onDisable() {
- // stub for possible future use
- if (is_callable(array($this, 'disable'))) {
- $this->disable();
- }
- }
-
- public function onUpgrade($op, CRM_Queue_Queue $queue = NULL) {
- switch($op) {
- case 'check':
- return array($this->hasPendingRevisions());
- case 'enqueue':
- return $this->enqueuePendingRevisions($queue);
- default:
- }
- }
-}
diff --git a/emailamender.civix.php b/emailamender.civix.php
index fa004e6..c0b66cb 100755
--- a/emailamender.civix.php
+++ b/emailamender.civix.php
@@ -24,7 +24,7 @@ class CRM_Emailamender_ExtensionUtil {
* Translated text.
* @see ts
*/
- public static function ts($text, $params = []) {
+ public static function ts($text, $params = []): string {
if (!array_key_exists('domain', $params)) {
$params['domain'] = [self::LONG_NAME, NULL];
}
@@ -41,7 +41,7 @@ public static function ts($text, $params = []) {
* Ex: 'http://example.org/sites/default/ext/org.example.foo'.
* Ex: 'http://example.org/sites/default/ext/org.example.foo/css/foo.css'.
*/
- public static function url($file = NULL) {
+ public static function url($file = NULL): string {
if ($file === NULL) {
return rtrim(CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME), '/');
}
@@ -75,49 +75,70 @@ 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_Emailamender_ExtensionUtil as E;
+($GLOBALS['_PathLoad'][0] ?? require __DIR__ . '/mixin/lib/pathload-0.php');
+pathload()->addSearchDir(__DIR__ . '/mixin/lib');
+spl_autoload_register('_emailamender_civix_class_loader', TRUE, TRUE);
+
+function _emailamender_civix_class_loader($class) {
+ if ($class === 'CRM_Emailamender_DAO_Base') {
+ if (version_compare(CRM_Utils_System::version(), '5.74.beta', '>=')) {
+ class_alias('CRM_Core_DAO_Base', 'CRM_Emailamender_DAO_Base');
+ // ^^ Materialize concrete names -- encourage IDE's to pick up on this association.
+ }
+ else {
+ $realClass = 'CiviMix\\Schema\\Emailamender\\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\\Emailamender\\') === 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);
+ }
+}
+
+function _emailamender_civix_mixin_polyfill() {
+ if (!class_exists('CRM_Extension_MixInfo')) {
+ $polyfill = __DIR__ . '/mixin/polyfill.php';
+ (require $polyfill)(E::LONG_NAME, E::SHORT_NAME, E::path());
+ }
+}
+
/**
* (Delegated) Implements hook_civicrm_config().
*
* @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config
*/
-function _emailamender_civix_civicrm_config(&$config = NULL) {
+function _emailamender_civix_civicrm_config($config = NULL) {
static $configured = FALSE;
if ($configured) {
return;
}
$configured = TRUE;
- $template =& CRM_Core_Smarty::singleton();
-
- $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR;
- $extDir = $extRoot . 'templates';
-
- if (is_array($template->template_dir)) {
- array_unshift($template->template_dir, $extDir);
- }
- else {
- $template->template_dir = [$extDir, $template->template_dir];
- }
-
+ $extRoot = __DIR__ . DIRECTORY_SEPARATOR;
$include_path = $extRoot . PATH_SEPARATOR . get_include_path();
set_include_path($include_path);
-}
-
-/**
- * (Delegated) Implements hook_civicrm_xmlMenu().
- *
- * @param $files array(string)
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu
- */
-function _emailamender_civix_civicrm_xmlMenu(&$files) {
- foreach (_emailamender_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) {
- $files[] = $file;
- }
+ _emailamender_civix_mixin_polyfill();
}
/**
@@ -127,35 +148,7 @@ function _emailamender_civix_civicrm_xmlMenu(&$files) {
*/
function _emailamender_civix_civicrm_install() {
_emailamender_civix_civicrm_config();
- if ($upgrader = _emailamender_civix_upgrader()) {
- $upgrader->onInstall();
- }
-}
-
-/**
- * Implements hook_civicrm_postInstall().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall
- */
-function _emailamender_civix_civicrm_postInstall() {
- _emailamender_civix_civicrm_config();
- if ($upgrader = _emailamender_civix_upgrader()) {
- if (is_callable([$upgrader, 'onPostInstall'])) {
- $upgrader->onPostInstall();
- }
- }
-}
-
-/**
- * Implements hook_civicrm_uninstall().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall
- */
-function _emailamender_civix_civicrm_uninstall() {
- _emailamender_civix_civicrm_config();
- if ($upgrader = _emailamender_civix_upgrader()) {
- $upgrader->onUninstall();
- }
+ _emailamender_civix_mixin_polyfill();
}
/**
@@ -163,212 +156,9 @@ function _emailamender_civix_civicrm_uninstall() {
*
* @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
*/
-function _emailamender_civix_civicrm_enable() {
+function _emailamender_civix_civicrm_enable(): void {
_emailamender_civix_civicrm_config();
- if ($upgrader = _emailamender_civix_upgrader()) {
- if (is_callable([$upgrader, 'onEnable'])) {
- $upgrader->onEnable();
- }
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_disable().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable
- * @return mixed
- */
-function _emailamender_civix_civicrm_disable() {
- _emailamender_civix_civicrm_config();
- if ($upgrader = _emailamender_civix_upgrader()) {
- if (is_callable([$upgrader, 'onDisable'])) {
- $upgrader->onDisable();
- }
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_upgrade().
- *
- * @param $op string, the type of operation being performed; 'check' or 'enqueue'
- * @param $queue CRM_Queue_Queue, (for 'enqueue') the modifiable list of pending up upgrade tasks
- *
- * @return mixed
- * based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending)
- * for 'enqueue', returns void
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade
- */
-function _emailamender_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
- if ($upgrader = _emailamender_civix_upgrader()) {
- return $upgrader->onUpgrade($op, $queue);
- }
-}
-
-/**
- * @return CRM_Emailamender_Upgrader
- */
-function _emailamender_civix_upgrader() {
- if (!file_exists(__DIR__ . '/CRM/Emailamender/Upgrader.php')) {
- return NULL;
- }
- else {
- return CRM_Emailamender_Upgrader_Base::instance();
- }
-}
-
-/**
- * Search directory tree for files which match a glob pattern.
- *
- * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored.
- * Note: In Civi 4.3+, delegate to CRM_Utils_File::findFiles()
- *
- * @param string $dir base dir
- * @param string $pattern , glob pattern, eg "*.txt"
- *
- * @return array
- */
-function _emailamender_civix_find_files($dir, $pattern) {
- if (is_callable(['CRM_Utils_File', 'findFiles'])) {
- return CRM_Utils_File::findFiles($dir, $pattern);
- }
-
- $todos = [$dir];
- $result = [];
- while (!empty($todos)) {
- $subdir = array_shift($todos);
- foreach (_emailamender_civix_glob("$subdir/$pattern") as $match) {
- if (!is_dir($match)) {
- $result[] = $match;
- }
- }
- if ($dh = opendir($subdir)) {
- while (FALSE !== ($entry = readdir($dh))) {
- $path = $subdir . DIRECTORY_SEPARATOR . $entry;
- if ($entry[0] == '.') {
- }
- elseif (is_dir($path)) {
- $todos[] = $path;
- }
- }
- closedir($dh);
- }
- }
- return $result;
-}
-
-/**
- * (Delegated) Implements hook_civicrm_managed().
- *
- * Find any *.mgd.php files, merge their content, and return.
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed
- */
-function _emailamender_civix_civicrm_managed(&$entities) {
- $mgdFiles = _emailamender_civix_find_files(__DIR__, '*.mgd.php');
- sort($mgdFiles);
- foreach ($mgdFiles as $file) {
- $es = include $file;
- foreach ($es as $e) {
- if (empty($e['module'])) {
- $e['module'] = E::LONG_NAME;
- }
- if (empty($e['params']['version'])) {
- $e['params']['version'] = '3';
- }
- $entities[] = $e;
- }
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_caseTypes().
- *
- * Find any and return any files matching "xml/case/*.xml"
- *
- * Note: This hook only runs in CiviCRM 4.4+.
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes
- */
-function _emailamender_civix_civicrm_caseTypes(&$caseTypes) {
- if (!is_dir(__DIR__ . '/xml/case')) {
- return;
- }
-
- foreach (_emailamender_civix_glob(__DIR__ . '/xml/case/*.xml') as $file) {
- $name = preg_replace('/\.xml$/', '', basename($file));
- if ($name != CRM_Case_XMLProcessor::mungeCaseType($name)) {
- $errorMessage = sprintf("Case-type file name is malformed (%s vs %s)", $name, CRM_Case_XMLProcessor::mungeCaseType($name));
- throw new CRM_Core_Exception($errorMessage);
- }
- $caseTypes[$name] = [
- 'module' => E::LONG_NAME,
- 'name' => $name,
- 'file' => $file,
- ];
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_angularModules().
- *
- * Find any and return any files matching "ang/*.ang.php"
- *
- * Note: This hook only runs in CiviCRM 4.5+.
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules
- */
-function _emailamender_civix_civicrm_angularModules(&$angularModules) {
- if (!is_dir(__DIR__ . '/ang')) {
- return;
- }
-
- $files = _emailamender_civix_glob(__DIR__ . '/ang/*.ang.php');
- foreach ($files as $file) {
- $name = preg_replace(':\.ang\.php$:', '', basename($file));
- $module = include $file;
- if (empty($module['ext'])) {
- $module['ext'] = E::LONG_NAME;
- }
- $angularModules[$name] = $module;
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_themes().
- *
- * Find any and return any files matching "*.theme.php"
- */
-function _emailamender_civix_civicrm_themes(&$themes) {
- $files = _emailamender_civix_glob(__DIR__ . '/*.theme.php');
- foreach ($files as $file) {
- $themeMeta = include $file;
- if (empty($themeMeta['name'])) {
- $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file));
- }
- if (empty($themeMeta['ext'])) {
- $themeMeta['ext'] = E::LONG_NAME;
- }
- $themes[$themeMeta['name']] = $themeMeta;
- }
-}
-
-/**
- * Glob wrapper which is guaranteed to return an array.
- *
- * The documentation for glob() says, "On some systems it is impossible to
- * distinguish between empty match and an error." Anecdotally, the return
- * result for an empty match is sometimes array() and sometimes FALSE.
- * This wrapper provides consistency.
- *
- * @link http://php.net/glob
- * @param string $pattern
- *
- * @return array
- */
-function _emailamender_civix_glob($pattern) {
- $result = glob($pattern);
- return is_array($result) ? $result : [];
+ _emailamender_civix_mixin_polyfill();
}
/**
@@ -387,8 +177,8 @@ function _emailamender_civix_insert_navigation_menu(&$menu, $path, $item) {
if (empty($path)) {
$menu[] = [
'attributes' => array_merge([
- 'label' => CRM_Utils_Array::value('name', $item),
- 'active' => 1,
+ 'label' => $item['name'] ?? NULL,
+ 'active' => 1,
], $item),
];
return TRUE;
@@ -452,26 +242,3 @@ function _emailamender_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parent
}
}
}
-
-/**
- * (Delegated) Implements hook_civicrm_alterSettingsFolders().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders
- */
-function _emailamender_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
- $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings';
- if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) {
- $metaDataFolders[] = $settingsDir;
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_entityTypes().
- *
- * Find any *.entityType.php files, merge their content, and return.
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
- */
-function _emailamender_civix_civicrm_entityTypes(&$entityTypes) {
- $entityTypes = array_merge($entityTypes, []);
-}
diff --git a/emailamender.php b/emailamender.php
index 0dd200c..d732bcf 100755
--- a/emailamender.php
+++ b/emailamender.php
@@ -9,13 +9,6 @@ function emailamender_civicrm_config(&$config) {
_emailamender_civix_civicrm_config($config);
}
-/**
- * Implements hook_civicrm_xmlMenu().
- */
-function emailamender_civicrm_xmlMenu(&$files) {
- _emailamender_civix_civicrm_xmlMenu($files);
-}
-
/**
* Implements hook_civicrm_install().
*/
@@ -37,7 +30,7 @@ function emailamender_civicrm_uninstall() {
CRM_Core_DAO::executeQuery("DELETE FROM civicrm_setting WHERE name LIKE 'emailamender%'");
- return _emailamender_civix_civicrm_uninstall();
+ return;
}
/**
@@ -47,30 +40,6 @@ function emailamender_civicrm_enable() {
return _emailamender_civix_civicrm_enable();
}
-/**
- * Implements hook_civicrm_disable().
- */
-function emailamender_civicrm_disable() {
- return _emailamender_civix_civicrm_disable();
-}
-
-/**
- * Implements hook_civicrm_upgrade().
- */
-function emailamender_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
- return _emailamender_civix_civicrm_upgrade($op, $queue);
-}
-
-/**
- * Implements hook_civicrm_managed().
- *
- * Generate a list of entities to create/deactivate/delete when this module
- * is installed, disabled, uninstalled.
- */
-function emailamender_civicrm_managed(&$entities) {
- return _emailamender_civix_civicrm_managed($entities);
-}
-
/**
* Implements hook_civicrm_post().
*
@@ -93,15 +62,6 @@ function emailamender_civicrm_emailProcessorContact($email, $contactID, &$result
CRM_Emailamender_Equivalentmatcher::processHook($email, $contactID, $result);
}
-/**
- * Implements hook_civicrm_alterSettingsFolders().
- *
- * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
- */
-function emailamender_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
- _emailamender_civix_civicrm_alterSettingsFolders($metaDataFolders);
-}
-
/**
* civicrm_civicrm_navigationMenu
*
@@ -142,18 +102,3 @@ function emailamender_civicrm_searchTasks($objectType, &$tasks) {
);
}
}
-
-/**
- * Implements hook_civicrm_angularModules().
- *
- * Generate a list of Angular modules.
- * Generate a list of Angular modules.
- *
- * Note: This hook only runs in CiviCRM 4.5+. It may
- * use features only available in v4.6+.
- *
- * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules
- */
-function emailamender_civicrm_angularModules(&$angularModules) {
- _emailamender_civix_civicrm_angularModules($angularModules);
-}
diff --git a/info.xml b/info.xml
index 38755d6..e961363 100755
--- a/info.xml
+++ b/info.xml
@@ -4,7 +4,7 @@
Email Address Corrector
This extension allows automatic and manual correction of email addresses that are added to your database. i.e. john@hotmai.cpm is corrected to john@hotmail.com.
- Settings under Administrator -> System Settings -> Email Address Corrector. Automatic correction is disabled by default. Also supports domain equivalents to reduce duplicates. See comments.
+ Settings under Administrator -> System Settings -> Email Address Corrector. Automatic correction is disabled by default. Also supports domain equivalents to reduce duplicates. See comments.
AGPL 3.0
@@ -15,9 +15,7 @@
3.0.1
stable
- 4.7
- 5.18
- 5.22
+ 5.38
Data Cleaning
@@ -28,6 +26,17 @@
CRM/Emailamender
+ 25.01.1
https://github.com/JohnFF/Email-Amender/wiki
+
+ menu-xml@1.0.0
+ setting-php@1.0.0
+ smarty-v2@1.0.3
+
+
+
+
+
+ CiviMix\Schema\Emailamender\AutomaticUpgrader
diff --git a/mixin/lib/civimix-schema@5.85.beta1/pathload.main.php b/mixin/lib/civimix-schema@5.85.beta1/pathload.main.php
new file mode 100644
index 0000000..f0442e8
--- /dev/null
+++ b/mixin/lib/civimix-schema@5.85.beta1/pathload.main.php
@@ -0,0 +1,28 @@
+activatePackage('civimix-schema@5', __DIR__, [
+ 'reloadable' => TRUE,
+ // The civimix-schema library specifically supports installation processes. From a
+ // bootstrap/service-availability POV, this is a rough environment which leads to
+ // the "Multi-Activation Issue" and "Multi-Download Issue". To adapt to them,
+ // civimix-schema follows "Reloadable Library" patterns.
+ // More information: https://github.com/totten/pathload-poc/blob/master/doc/issues.md
+]);
+
+// When reloading, we make newer instance of the Facade object.
+$GLOBALS['CiviMixSchema'] = require __DIR__ . '/src/CiviMixSchema.php';
+
+if (!interface_exists(__NAMESPACE__ . '\SchemaHelperInterface')) {
+ require __DIR__ . '/src/SchemaHelperInterface.php';
+}
+
+// \CiviMix\Schema\loadClass() is a facade. The facade should remain identical across versions.
+if (!function_exists(__NAMESPACE__ . '\loadClass')) {
+
+ function loadClass(string $class) {
+ return $GLOBALS['CiviMixSchema']->loadClass($class);
+ }
+
+ spl_autoload_register(__NAMESPACE__ . '\loadClass');
+}
diff --git a/mixin/lib/civimix-schema@5.85.beta1/src/AutomaticUpgrader.php b/mixin/lib/civimix-schema@5.85.beta1/src/AutomaticUpgrader.php
new file mode 100644
index 0000000..db6140b
--- /dev/null
+++ b/mixin/lib/civimix-schema@5.85.beta1/src/AutomaticUpgrader.php
@@ -0,0 +1,181 @@
+initIdentity($params);
+ if ($info = $this->getInfo()) {
+ if ($class = $this->getDelegateUpgraderClass($info)) {
+ $this->customUpgrader = new $class();
+ $this->customUpgrader->init($params);
+ if ($errors = $this->checkDelegateCompatibility($this->customUpgrader)) {
+ throw new \CRM_Core_Exception("AutomaticUpgrader is not compatible with $class:\n" . implode("\n", $errors));
+ }
+ }
+ }
+ }
+
+ public function notify(string $event, array $params = []) {
+ $info = $this->getInfo();
+ if (!$info) {
+ return;
+ }
+
+ if ($event === 'install') {
+ $GLOBALS['CiviMixSchema']->getHelper($this->getExtensionKey())->install();
+ }
+
+ if ($this->customUpgrader) {
+ $result = $this->customUpgrader->notify($event, $params);
+ // for upgrade checks, we need to pass check results up to the caller
+ // (for now - could definitely be more elegant!)
+ if ($event === 'upgrade') {
+ return $result;
+ }
+ }
+
+ if ($event === 'uninstall') {
+ $GLOBALS['CiviMixSchema']->getHelper($this->getExtensionKey())->uninstall();
+ }
+ }
+
+ /**
+ * Civix-based extensions have a conventional name for their upgrader class ("CRM_Myext_Upgrader"
+ * or "Civi\Myext\Upgrader"). Figure out if this class exists.
+ *
+ * @param \CRM_Extension_Info $info
+ * @return string|null
+ * Ex: 'CRM_Myext_Upgrader' or 'Civi\Myext\Upgrader'
+ */
+ public function getDelegateUpgraderClass(\CRM_Extension_Info $info): ?string {
+ $candidates = [];
+
+ if (!empty($info->civix['namespace'])) {
+ $namespace = $info->civix['namespace'];
+ $candidates[] = sprintf('%s_Upgrader', str_replace('/', '_', $namespace));
+ $candidates[] = sprintf('%s\\Upgrader', str_replace('/', '\\', $namespace));
+ }
+
+ foreach ($candidates as $candidate) {
+ if (class_exists($candidate)) {
+ return $candidate;
+ }
+ }
+
+ return NULL;
+ }
+
+ public function getInfo(): ?\CRM_Extension_Info {
+ try {
+ return \CRM_Extension_System::singleton()->getMapper()->keyToInfo($this->extensionName);
+ }
+ catch (\CRM_Extension_Exception_ParseException $e) {
+ \Civi::log()->error("Parse error in extension " . $this->extensionName . ": " . $e->getMessage());
+ return NULL;
+ }
+ }
+
+ /**
+ * @param \CRM_Extension_Upgrader_Interface $upgrader
+ * @return array
+ * List of error messages.
+ */
+ public function checkDelegateCompatibility($upgrader): array {
+ $class = get_class($upgrader);
+
+ $errors = [];
+
+ if (!($upgrader instanceof \CRM_Extension_Upgrader_Base)) {
+ $errors[] = "$class is not based on CRM_Extension_Upgrader_Base.";
+ return $errors;
+ }
+
+ // In the future, we will probably modify AutomaticUpgrader to build its own
+ // sequence of revisions (based on other sources of data). AutomaticUpgrader
+ // is only regarded as compatible with classes that strictly follow the standard revision-model.
+ $methodNames = [
+ 'appendTask',
+ 'onUpgrade',
+ 'getRevisions',
+ 'getCurrentRevision',
+ 'setCurrentRevision',
+ 'enqueuePendingRevisions',
+ 'hasPendingRevisions',
+ ];
+ foreach ($methodNames as $methodName) {
+ $method = new \ReflectionMethod($upgrader, $methodName);
+ if ($method->getDeclaringClass()->getName() !== 'CRM_Extension_Upgrader_Base') {
+ $errors[] = "To ensure future interoperability, AutomaticUpgrader only supports {$class}::{$methodName}() if it's inherited from CRM_Extension_Upgrader_Base";
+ }
+ }
+
+ return $errors;
+ }
+
+ public function __set($property, $value) {
+ switch ($property) {
+ // _queueAdapter() needs these properties.
+ case 'ctx':
+ case 'queue':
+ if (!$this->customUpgrader) {
+ throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot assign delegated property: $property (No custom-upgrader found)");
+ }
+ // "Invasive": unlike QueueTrait, we are not in the same class as the recipient. And we can't replace previously-published QueueTraits.
+ Invasive::set([$this->customUpgrader, $property], $value);
+ return;
+ }
+
+ throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot assign unknown property: $property");
+ }
+
+ public function __get($property) {
+ switch ($property) {
+ // _queueAdapter() needs these properties.
+ case 'ctx':
+ case 'queue':
+ if (!$this->customUpgrader) {
+ throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot read delegated property: $property (No custom-upgrader found)");
+ }
+ // "Invasive": Unlike QueueTrait, we are not in the same class as the recipient. And we can't replace previously-published QueueTraits.
+ return Invasive::get([$this->customUpgrader, $property]);
+ }
+ throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot read unknown property: $property");
+ }
+
+ public function __call($name, $arguments) {
+ if ($this->customUpgrader) {
+ return call_user_func_array([$this->customUpgrader, $name], $arguments);
+ }
+ else {
+ throw new \RuntimeException("AutomaticUpgrader($this->extensionName): Cannot delegate method $name (No custom-upgrader found)");
+ }
+ }
+
+};
diff --git a/mixin/lib/civimix-schema@5.85.beta1/src/CiviMixSchema.php b/mixin/lib/civimix-schema@5.85.beta1/src/CiviMixSchema.php
new file mode 100644
index 0000000..6d0a350
--- /dev/null
+++ b/mixin/lib/civimix-schema@5.85.beta1/src/CiviMixSchema.php
@@ -0,0 +1,46 @@
+regex, $class, $m)) {
+ $absPath = __DIR__ . DIRECTORY_SEPARATOR . $m[2] . '.php';
+ class_alias(get_class(require $absPath), $class);
+ }
+ }
+
+ /**
+ * @param string $extensionKey
+ * Ex: 'org.civicrm.flexmailer'
+ * @return \CiviMix\Schema\SchemaHelperInterface
+ */
+ public function getHelper(string $extensionKey) {
+ $store = &\Civi::$statics['CiviMixSchema-helpers'];
+ if (!isset($store[$extensionKey])) {
+ $class = get_class(require __DIR__ . '/SchemaHelper.php');
+ $store[$extensionKey] = new $class($extensionKey);
+ }
+ return $store[$extensionKey];
+ }
+
+};
diff --git a/mixin/lib/civimix-schema@5.85.beta1/src/DAO.php b/mixin/lib/civimix-schema@5.85.beta1/src/DAO.php
new file mode 100644
index 0000000..3b82b27
--- /dev/null
+++ b/mixin/lib/civimix-schema@5.85.beta1/src/DAO.php
@@ -0,0 +1,350 @@
+ $field) {
+ $this->$name = NULL;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function keys(): array {
+ $keys = [];
+ foreach (static::getEntityDefinition()['getFields']() as $name => $field) {
+ if (!empty($field['primary_key'])) {
+ $keys[] = $name;
+ }
+ }
+ return $keys;
+ }
+
+ public static function getEntityTitle($plural = FALSE) {
+ $info = static::getEntityInfo();
+ return ($plural && isset($info['title_plural'])) ? $info['title_plural'] : $info['title'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getEntityPaths(): array {
+ $definition = static::getEntityDefinition();
+ if (isset($definition['getPaths'])) {
+ return $definition['getPaths']();
+ }
+ return [];
+ }
+
+ public static function getLabelField(): ?string {
+ return static::getEntityInfo()['label_field'] ?? NULL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getEntityDescription(): ?string {
+ return static::getEntityInfo()['description'] ?? NULL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getTableName() {
+ return static::getEntityDefinition()['table'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLog(): bool {
+ return static::getEntityInfo()['log'] ?? FALSE;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getEntityIcon(string $entityName, ?int $entityId = NULL): ?string {
+ return static::getEntityInfo()['icon'] ?? NULL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected static function getTableAddVersion(): string {
+ return static::getEntityInfo()['add'] ?? '1.0';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getExtensionName(): ?string {
+ return static::getEntityDefinition()['module'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function &fields() {
+ $fields = [];
+ foreach (static::getSchemaFields() as $field) {
+ $key = $field['uniqueName'] ?? $field['name'];
+ unset($field['uniqueName']);
+ $fields[$key] = $field;
+ }
+ return $fields;
+ }
+
+ private static function getSchemaFields(): array {
+ if (!isset(\Civi::$statics[static::class]['fields'])) {
+ \Civi::$statics[static::class]['fields'] = static::loadSchemaFields();
+ }
+ return \Civi::$statics[static::class]['fields'];
+ }
+
+ private static function loadSchemaFields(): array {
+ $fields = [];
+ $entityDef = static::getEntityDefinition();
+ $baoName = \CRM_Core_DAO_AllCoreTables::getBAOClassName(static::class);
+
+ foreach ($entityDef['getFields']() as $fieldName => $fieldSpec) {
+ $field = [
+ 'name' => $fieldName,
+ 'type' => !empty($fieldSpec['data_type']) ? \CRM_Utils_Type::getValidTypes()[$fieldSpec['data_type']] : static::getCrmTypeFromSqlType($fieldSpec['sql_type']),
+ 'title' => $fieldSpec['title'],
+ 'description' => $fieldSpec['description'] ?? NULL,
+ ];
+ if (!empty($fieldSpec['required'])) {
+ $field['required'] = TRUE;
+ }
+ if (strpos($fieldSpec['sql_type'], 'decimal(') === 0) {
+ $precision = self::getFieldLength($fieldSpec['sql_type']);
+ $field['precision'] = array_map('intval', explode(',', $precision));
+ }
+ foreach (['maxlength', 'size', 'rows', 'cols'] as $attr) {
+ if (isset($fieldSpec['input_attrs'][$attr])) {
+ $field[$attr] = $fieldSpec['input_attrs'][$attr];
+ unset($fieldSpec['input_attrs'][$attr]);
+ }
+ }
+ if (strpos($fieldSpec['sql_type'], 'char(') !== FALSE) {
+ $length = self::getFieldLength($fieldSpec['sql_type']);
+ if (!isset($field['size'])) {
+ $field['size'] = constant(static::getDefaultSize($length));
+ }
+ if (!isset($field['maxlength'])) {
+ $field['maxlength'] = $length;
+ }
+ }
+ $usage = $fieldSpec['usage'] ?? [];
+ $field['usage'] = [
+ 'import' => in_array('import', $usage),
+ 'export' => in_array('export', $usage),
+ 'duplicate_matching' => in_array('duplicate_matching', $usage),
+ 'token' => in_array('token', $usage),
+ ];
+ if ($field['usage']['import']) {
+ $field['import'] = TRUE;
+ }
+ $field['where'] = $entityDef['table'] . '.' . $field['name'];
+ if ($field['usage']['export'] || (!$field['usage']['export'] && $field['usage']['import'])) {
+ $field['export'] = $field['usage']['export'];
+ }
+ if (!empty($fieldSpec['contact_type'])) {
+ $field['contactType'] = $fieldSpec['contact_type'];
+ }
+ if (!empty($fieldSpec['permission'])) {
+ $field['permission'] = $fieldSpec['permission'];
+ }
+ if (array_key_exists('default', $fieldSpec)) {
+ $field['default'] = isset($fieldSpec['default']) ? (string) $fieldSpec['default'] : NULL;
+ if (is_bool($fieldSpec['default'])) {
+ $field['default'] = $fieldSpec['default'] ? '1' : '0';
+ }
+ }
+ $field['table_name'] = $entityDef['table'];
+ $field['entity'] = $entityDef['name'];
+ $field['bao'] = $baoName;
+ $field['localizable'] = intval($fieldSpec['localizable'] ?? 0);
+ if (!empty($fieldSpec['localize_context'])) {
+ $field['localize_context'] = (string) $fieldSpec['localize_context'];
+ }
+ if (!empty($fieldSpec['entity_reference'])) {
+ if (!empty($fieldSpec['entity_reference']['entity'])) {
+ $field['FKClassName'] = static::getDAONameForEntity($fieldSpec['entity_reference']['entity']);
+ }
+ if (!empty($fieldSpec['entity_reference']['dynamic_entity'])) {
+ $field['DFKEntityColumn'] = $fieldSpec['entity_reference']['dynamic_entity'];
+ }
+ $field['FKColumnName'] = $fieldSpec['entity_reference']['key'] ?? 'id';
+ }
+ if (!empty($fieldSpec['component'])) {
+ $field['component'] = $fieldSpec['component'];
+ }
+ if (!empty($fieldSpec['serialize'])) {
+ $field['serialize'] = $fieldSpec['serialize'];
+ }
+ if (!empty($fieldSpec['unique_name'])) {
+ $field['uniqueName'] = $fieldSpec['unique_name'];
+ }
+ if (!empty($fieldSpec['unique_title'])) {
+ $field['unique_title'] = $fieldSpec['unique_title'];
+ }
+ if (!empty($fieldSpec['deprecated'])) {
+ $field['deprecated'] = TRUE;
+ }
+ if (!empty($fieldSpec['input_attrs'])) {
+ $field['html'] = \CRM_Utils_Array::rekey($fieldSpec['input_attrs'], function($str) {
+ return \CRM_Utils_String::convertStringToCamel($str, FALSE);
+ });
+ }
+ if (!empty($fieldSpec['input_type'])) {
+ $field['html']['type'] = $fieldSpec['input_type'];
+ }
+ if (!empty($fieldSpec['pseudoconstant'])) {
+ $field['pseudoconstant'] = \CRM_Utils_Array::rekey($fieldSpec['pseudoconstant'], function($str) {
+ return \CRM_Utils_String::convertStringToCamel($str, FALSE);
+ });
+ if (!isset($field['pseudoconstant']['optionEditPath']) && !empty($field['pseudoconstant']['optionGroupName'])) {
+ $field['pseudoconstant']['optionEditPath'] = 'civicrm/admin/options/' . $field['pseudoconstant']['optionGroupName'];
+ }
+ }
+ if (!empty($fieldSpec['primary_key']) || !empty($fieldSpec['readonly'])) {
+ $field['readonly'] = TRUE;
+ }
+ $field['add'] = $fieldSpec['add'] ?? NULL;
+ $fields[$fieldName] = $field;
+ }
+ \CRM_Core_DAO_AllCoreTables::invoke(static::class, 'fields_callback', $fields);
+ return $fields;
+ }
+
+ private static function getFieldLength($sqlType): ?string {
+ $open = strpos($sqlType, '(');
+ if ($open) {
+ return substr($sqlType, $open + 1, -1);
+ }
+ return NULL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function indices(bool $localize = TRUE): array {
+ $definition = static::getEntityDefinition();
+ $indices = [];
+ if (isset($definition['getIndices'])) {
+ $fields = $definition['getFields']();
+ foreach ($definition['getIndices']() as $name => $info) {
+ $index = [
+ 'name' => $name,
+ 'field' => [],
+ 'localizable' => FALSE,
+ ];
+ foreach ($info['fields'] as $fieldName => $length) {
+ if (!empty($fields[$fieldName]['localizable'])) {
+ $index['localizable'] = TRUE;
+ }
+ if (is_int($length)) {
+ $fieldName .= "($length)";
+ }
+ $index['field'][] = $fieldName;
+ }
+ if (!empty($info['unique'])) {
+ $index['unique'] = TRUE;
+ }
+ $index['sig'] = ($definition['table']) . '::' . intval($info['unique'] ?? 0) . '::' . implode('::', $index['field']);
+ $indices[$name] = $index;
+ }
+ }
+ return ($localize && $indices) ? \CRM_Core_DAO_AllCoreTables::multilingualize(static::class, $indices) : $indices;
+ }
+
+ public static function getEntityDefinition(): array {
+ if (!isset(\Civi::$statics[static::class]['definition'])) {
+ $class = new \ReflectionClass(static::class);
+ $file = substr(basename($class->getFileName()), 0, -4) . '.entityType.php';
+ $folder = dirname($class->getFileName(), 4) . '/schema/';
+ $path = $folder . $file;
+ \Civi::$statics[static::class]['definition'] = include $path;
+ }
+ return \Civi::$statics[static::class]['definition'];
+ }
+
+ private static function getEntityInfo(): array {
+ return static::getEntityDefinition()['getInfo']();
+ }
+
+ private static function getDefaultSize($length) {
+ // Infer from tag if was not explicitly set or was invalid
+ // This map is slightly different from CRM_Core_Form_Renderer::$_sizeMapper
+ // Because we usually want fields to render as smaller than their maxlength
+ $sizes = [
+ 2 => 'TWO',
+ 4 => 'FOUR',
+ 6 => 'SIX',
+ 8 => 'EIGHT',
+ 16 => 'TWELVE',
+ 32 => 'MEDIUM',
+ 64 => 'BIG',
+ ];
+ foreach ($sizes as $size => $name) {
+ if ($length <= $size) {
+ return "CRM_Utils_Type::$name";
+ }
+ }
+ return 'CRM_Utils_Type::HUGE';
+ }
+
+ private static function getCrmTypeFromSqlType(string $sqlType): int {
+ [$type] = explode('(', $sqlType);
+ switch ($type) {
+ case 'varchar':
+ case 'char':
+ return \CRM_Utils_Type::T_STRING;
+
+ case 'datetime':
+ return \CRM_Utils_Type::T_DATE + \CRM_Utils_Type::T_TIME;
+
+ case 'decimal':
+ return \CRM_Utils_Type::T_MONEY;
+
+ case 'double':
+ return \CRM_Utils_Type::T_FLOAT;
+
+ case 'int unsigned':
+ case 'tinyint':
+ return \CRM_Utils_Type::T_INT;
+
+ default:
+ return constant('CRM_Utils_Type::T_' . strtoupper($type));
+ }
+ }
+
+ private static function getDAONameForEntity($entity) {
+ if (is_callable(['CRM_Core_DAO_AllCoreTables', 'getDAONameForEntity'])) {
+ return \CRM_Core_DAO_AllCoreTables::getDAONameForEntity($entity);
+ }
+ else {
+ return \CRM_Core_DAO_AllCoreTables::getFullName($entity);
+ }
+ }
+
+};
diff --git a/mixin/lib/civimix-schema@5.85.beta1/src/SchemaHelper.php b/mixin/lib/civimix-schema@5.85.beta1/src/SchemaHelper.php
new file mode 100644
index 0000000..46dc311
--- /dev/null
+++ b/mixin/lib/civimix-schema@5.85.beta1/src/SchemaHelper.php
@@ -0,0 +1,138 @@
+key = $key;
+ }
+
+ public function install(): void {
+ $this->runSqls([$this->generateInstallSql()]);
+ }
+
+ public function uninstall(): void {
+ $this->runSqls([$this->generateUninstallSql()]);
+ }
+
+ public function generateInstallSql(): ?string {
+ return $this->getSqlGenerator()->getCreateTablesSql();
+ }
+
+ public function generateUninstallSql(): string {
+ return $this->getSqlGenerator()->getDropTablesSql();
+ }
+
+ public function hasSchema(): bool {
+ return file_exists($this->getExtensionDir() . '/schema');
+ }
+
+ /**
+ * Converts an entity or field definition to SQL statement.
+ *
+ * @param array $defn
+ * The definition array, which can either represent
+ * an entity with fields or a single database column.
+ * @return string
+ * The generated SQL statement, which is either an SQL command
+ * for creating a table with constraints or for defining a single column.
+ */
+ public function arrayToSql(array $defn): string {
+ $generator = $this->getSqlGenerator();
+ // Entity array: generate entire table
+ if (isset($defn['getFields'])) {
+ return $generator->generateCreateTableWithConstraintSql($defn);
+ }
+ // Field array: generate single column
+ else {
+ return $generator->generateFieldSql($defn);
+ }
+ }
+
+ /**
+ * Create table (if not exists) from a given php schema file.
+ *
+ * The original entityType.php file should be copied to a directory (e.g. `my_extension/upgrade/schema`)
+ * and prefixed with the version-added.
+ *
+ * @param string $filePath
+ * Relative path to copied schema file (relative to extension directory).
+ * @return bool
+ * @throws \CRM_Core_Exception
+ */
+ public function createEntityTable(string $filePath): bool {
+ $absolutePath = $this->getExtensionDir() . DIRECTORY_SEPARATOR . $filePath;
+ $entityDefn = include $absolutePath;
+ $sql = $this->arrayToSql($entityDefn);
+ \CRM_Core_DAO::executeQuery($sql, [], TRUE, NULL, FALSE, FALSE);
+ return TRUE;
+ }
+
+ /**
+ * Task to add or change a column definition, based on the php schema spec.
+ *
+ * @param string $entityName
+ * @param string $fieldName
+ * @param array $fieldSpec
+ * As definied in the .entityType.php file for $entityName
+ * @return bool
+ * @throws \CRM_Core_Exception
+ */
+ public function alterSchemaField(string $entityName, string $fieldName, array $fieldSpec): bool {
+ $tableName = method_exists('Civi', 'entity')
+ ? \Civi::entity($entityName)->getMeta('table')
+ : \CRM_Core_DAO_AllCoreTables::getTableForEntityName($entityName);
+ $fieldSql = $this->arrayToSql($fieldSpec);
+ if (\CRM_Core_BAO_SchemaHandler::checkIfFieldExists($tableName, $fieldName, FALSE)) {
+ $query = "ALTER TABLE `$tableName` CHANGE `$fieldName` `$fieldName` $fieldSql";
+ }
+ else {
+ $query = "ALTER TABLE `$tableName` ADD COLUMN `$fieldName` $fieldSql";
+ }
+ \CRM_Core_DAO::executeQuery($query, [], TRUE, NULL, FALSE, FALSE);
+ return TRUE;
+ }
+
+ /**
+ * @param array $sqls
+ * List of SQL scripts.
+ */
+ private function runSqls(array $sqls): void {
+ foreach ($sqls as $sql) {
+ \CRM_Utils_File::runSqlQuery(CIVICRM_DSN, $sql);
+ }
+ }
+
+ protected function getExtensionDir(): string {
+ if ($this->key === 'civicrm') {
+ $r = new \ReflectionClass('CRM_Core_ClassLoader');
+ return dirname($r->getFileName(), 3);
+ }
+ $system = \CRM_Extension_System::singleton();
+ return $system->getMapper()->keyToBasePath($this->key);
+ }
+
+ private function getSqlGenerator() {
+ if ($this->sqlGenerator === NULL) {
+ $gen = require __DIR__ . '/SqlGenerator.php';
+ $this->sqlGenerator = $gen::createFromFolder($this->key, $this->getExtensionDir() . '/schema', $this->key === 'civicrm');
+ }
+ return $this->sqlGenerator;
+ }
+
+};
diff --git a/mixin/lib/civimix-schema@5.85.beta1/src/SchemaHelperInterface.php b/mixin/lib/civimix-schema@5.85.beta1/src/SchemaHelperInterface.php
new file mode 100644
index 0000000..d14b91c
--- /dev/null
+++ b/mixin/lib/civimix-schema@5.85.beta1/src/SchemaHelperInterface.php
@@ -0,0 +1,36 @@
+entities = array_filter($entities, function($entity) {
+ return !empty($entity['table']);
+ });
+ $this->findExternalTable = $findExternalTable ?: function() {
+ return NULL;
+ };
+ }
+
+ public function getEntities(): array {
+ return $this->entities;
+ }
+
+ public function getCreateTablesSql(): string {
+ $sql = '';
+ foreach ($this->entities as $entity) {
+ $sql .= $this->generateCreateTableSql($entity);
+ }
+ foreach ($this->entities as $entity) {
+ $sql .= $this->generateConstraintsSql($entity);
+ }
+ return $sql;
+ }
+
+ public function getCreateTableSql(string $entityName): string {
+ $sql = $this->generateCreateTableSql($this->entities[$entityName]);
+ $sql .= $this->generateConstraintsSql($this->entities[$entityName]);
+ return $sql;
+ }
+
+ public function getDropTablesSql(): string {
+ $sql = "SET FOREIGN_KEY_CHECKS=0;\n";
+ foreach ($this->entities as $entity) {
+ $sql .= "DROP TABLE IF EXISTS `{$entity['table']}`;\n";
+ }
+ $sql .= "SET FOREIGN_KEY_CHECKS=1;\n";
+ return $sql;
+ }
+
+ public function generateCreateTableWithConstraintSql(array $entity): string {
+ $definition = $this->getTableDefinition($entity);
+ $constraints = $this->getTableConstraints($entity);
+ $sql = "CREATE TABLE IF NOT EXISTS `{$entity['table']}` (\n " .
+ implode(",\n ", $definition);
+ if ($constraints) {
+ $sql .= ",\n " . implode(",\n ", $constraints);
+ }
+ $sql .= "\n)\n" . $this->getTableOptions() . ";\n";
+ return $sql;
+ }
+
+ private function generateCreateTableSql(array $entity): string {
+ $definition = $this->getTableDefinition($entity);
+ $sql = "CREATE TABLE `{$entity['table']}` (\n " .
+ implode(",\n ", $definition) .
+ "\n)\n" .
+ $this->getTableOptions() . ";\n";
+ return $sql;
+ }
+
+ private function getTableDefinition(array $entity): array {
+ $definition = [];
+ $primaryKeys = [];
+ foreach ($entity['getFields']() as $fieldName => $field) {
+ if (!empty($field['primary_key'])) {
+ $primaryKeys[] = "`$fieldName`";
+ }
+ $definition[] = "`$fieldName` " . self::generateFieldSql($field);
+ }
+ if ($primaryKeys) {
+ $definition[] = 'PRIMARY KEY (' . implode(', ', $primaryKeys) . ')';
+ }
+ $indices = isset($entity['getIndices']) ? $entity['getIndices']() : [];
+ foreach ($indices as $indexName => $index) {
+ $indexFields = [];
+ foreach ($index['fields'] as $fieldName => $length) {
+ $indexFields[] = "`$fieldName`" . (is_int($length) ? "($length)" : '');
+ }
+ $definition[] = (!empty($index['unique']) ? 'UNIQUE ' : '') . "INDEX `$indexName`(" . implode(', ', $indexFields) . ')';
+ }
+ return $definition;
+ }
+
+ private function generateConstraintsSql(array $entity): string {
+ $constraints = $this->getTableConstraints($entity);
+ $sql = '';
+ if ($constraints) {
+ $sql .= "ALTER TABLE `{$entity['table']}`\n ";
+ $sql .= 'ADD ' . implode(",\n ADD ", $constraints) . ";\n";
+ }
+ return $sql;
+ }
+
+ private function getTableConstraints(array $entity): array {
+ $constraints = [];
+ foreach ($entity['getFields']() as $fieldName => $field) {
+ if (!empty($field['entity_reference']['entity'])) {
+ $fkName = \CRM_Core_BAO_SchemaHandler::getIndexName($entity['table'], $fieldName);
+ $constraint = "CONSTRAINT `FK_$fkName` FOREIGN KEY (`$fieldName`)" .
+ " REFERENCES `" . $this->getTableForEntity($field['entity_reference']['entity']) . "`(`{$field['entity_reference']['key']}`)";
+ if (!empty($field['entity_reference']['on_delete'])) {
+ $constraint .= " ON DELETE {$field['entity_reference']['on_delete']}";
+ }
+ $constraints[] = $constraint;
+ }
+ }
+ return $constraints;
+ }
+
+ public static function generateFieldSql(array $field): string {
+ $fieldSql = $field['sql_type'];
+ if (!empty($field['collate'])) {
+ $fieldSql .= " COLLATE {$field['collate']}";
+ }
+ // Required fields and booleans cannot be null
+ // FIXME: For legacy support this doesn't force boolean fields to be NOT NULL... but it really should.
+ if (!empty($field['required'])) {
+ $fieldSql .= ' NOT NULL';
+ }
+ else {
+ $fieldSql .= ' NULL';
+ }
+ if (!empty($field['auto_increment'])) {
+ $fieldSql .= " AUTO_INCREMENT";
+ }
+ $fieldSql .= self::getDefaultSql($field);
+ if (!empty($field['description'])) {
+ $fieldSql .= " COMMENT '" . \CRM_Core_DAO::escapeString($field['description']) . "'";
+ }
+ return $fieldSql;
+ }
+
+ private static function getDefaultSql(array $field): string {
+ // Booleans always have a default
+ if ($field['sql_type'] === 'boolean') {
+ $field += ['default' => FALSE];
+ }
+ if (!array_key_exists('default', $field)) {
+ return '';
+ }
+ if (is_null($field['default'])) {
+ $default = 'NULL';
+ }
+ elseif (is_bool($field['default'])) {
+ $default = $field['default'] ? 'TRUE' : 'FALSE';
+ }
+ elseif (!is_string($field['default']) || str_starts_with($field['default'], 'CURRENT_TIMESTAMP')) {
+ $default = $field['default'];
+ }
+ else {
+ $default = "'" . \CRM_Core_DAO::escapeString($field['default']) . "'";
+ }
+ return ' DEFAULT ' . $default;
+ }
+
+ private function getTableForEntity(string $entityName): string {
+ return $this->entities[$entityName]['table'] ?? call_user_func($this->findExternalTable, $entityName);
+ }
+
+ /**
+ * Get general/default options for use in CREATE TABLE (eg character set, collation).
+ */
+ private function getTableOptions(): string {
+ if (!Civi\Core\Container::isContainerBooted()) {
+ // Pre-installation environment ==> aka new install
+ $collation = CRM_Core_BAO_SchemaHandler::DEFAULT_COLLATION;
+ }
+ else {
+ // What character-set is used for CiviCRM core schema? What collation?
+ // This depends on when the DB was *initialized*:
+ // - civicrm-core >= 5.33 has used `CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
+ // - civicrm-core 4.3-5.32 has used `CHARACTER SET utf8 COLLATE utf8_unicode_ci`
+ // - civicrm-core <= 4.2 -- I haven't checked, but it's probably the same.
+ // Some systems have migrated (eg APIv3's `System.utf8conversion`), but (as of Feb 2024)
+ // we haven't made any effort to push to this change.
+ $collation = \CRM_Core_BAO_SchemaHandler::getInUseCollation();
+ }
+
+ $characterSet = (stripos($collation, 'utf8mb4') !== FALSE) ? 'utf8mb4' : 'utf8';
+ return "ENGINE=InnoDB DEFAULT CHARACTER SET {$characterSet} COLLATE {$collation} ROW_FORMAT=DYNAMIC";
+ }
+
+};
diff --git a/mixin/lib/pathload-0.php b/mixin/lib/pathload-0.php
new file mode 100644
index 0000000..76d6425
--- /dev/null
+++ b/mixin/lib/pathload-0.php
@@ -0,0 +1,711 @@
+top = $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/menu-xml@1.0.0.mixin.php b/mixin/menu-xml@1.0.0.mixin.php
new file mode 100644
index 0000000..4c0b227
--- /dev/null
+++ b/mixin/menu-xml@1.0.0.mixin.php
@@ -0,0 +1,31 @@
+addListener('hook_civicrm_xmlMenu', function ($e) use ($mixInfo) {
+ if (!$mixInfo->isActive()) {
+ return;
+ }
+
+ $files = (array) glob($mixInfo->getPath('xml/Menu/*.xml'));
+ foreach ($files as $file) {
+ $e->files[] = $file;
+ }
+ });
+
+};
diff --git a/mixin/polyfill.php b/mixin/polyfill.php
new file mode 100644
index 0000000..be40364
--- /dev/null
+++ b/mixin/polyfill.php
@@ -0,0 +1,101 @@
+')) {
+ $mixinVers[$name] = $ver;
+ }
+ }
+ $mixins = [];
+ foreach ($mixinVers as $name => $ver) {
+ $mixins[] = "$name@$ver";
+ }
+
+ // Imitate CRM_Extension_MixInfo.
+ $mixInfo = new class() {
+
+ /**
+ * @var string
+ */
+ public $longName;
+
+ /**
+ * @var string
+ */
+ public $shortName;
+
+ public $_basePath;
+
+ public function getPath($file = NULL) {
+ return $this->_basePath . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file));
+ }
+
+ public function isActive() {
+ return \CRM_Extension_System::singleton()->getMapper()->isActiveModule($this->shortName);
+ }
+
+ };
+ $mixInfo->longName = $longName;
+ $mixInfo->shortName = $shortName;
+ $mixInfo->_basePath = $basePath;
+
+ // Imitate CRM_Extension_BootCache.
+ $bootCache = new class() {
+
+ public function define($name, $callback) {
+ $envId = \CRM_Core_Config_Runtime::getId();
+ $oldExtCachePath = \Civi::paths()->getPath("[civicrm.compile]/CachedExtLoader.{$envId}.php");
+ $stat = stat($oldExtCachePath);
+ $file = Civi::paths()->getPath('[civicrm.compile]/CachedMixin.' . md5($name . ($stat['mtime'] ?? 0)) . '.php');
+ if (file_exists($file)) {
+ return include $file;
+ }
+ else {
+ $data = $callback();
+ file_put_contents($file, '<' . "?php\nreturn " . var_export($data, TRUE) . ';');
+ return $data;
+ }
+ }
+
+ };
+
+ // Imitate CRM_Extension_MixinLoader::run()
+ // Parse all live mixins before trying to scan any classes.
+ global $_CIVIX_MIXIN_POLYFILL;
+ foreach ($mixins as $mixin) {
+ // If the exact same mixin is defined by multiple exts, just use the first one.
+ if (!isset($_CIVIX_MIXIN_POLYFILL[$mixin])) {
+ $_CIVIX_MIXIN_POLYFILL[$mixin] = include_once $basePath . '/mixin/' . $mixin . '.mixin.php';
+ }
+ }
+ foreach ($mixins as $mixin) {
+ // If there's trickery about installs/uninstalls/resets, then we may need to register a second time.
+ if (!isset(\Civi::$statics[$longName][$mixin])) {
+ \Civi::$statics[$longName][$mixin] = 1;
+ $func = $_CIVIX_MIXIN_POLYFILL[$mixin];
+ $func($mixInfo, $bootCache);
+ }
+ }
+};
diff --git a/mixin/setting-php@1.0.0.mixin.php b/mixin/setting-php@1.0.0.mixin.php
new file mode 100644
index 0000000..7195af4
--- /dev/null
+++ b/mixin/setting-php@1.0.0.mixin.php
@@ -0,0 +1,32 @@
+addListener('hook_civicrm_alterSettingsFolders', function ($e) use ($mixInfo) {
+ // When deactivating on a polyfill/pre-mixin system, listeners may not cleanup automatically.
+ if (!$mixInfo->isActive()) {
+ return;
+ }
+
+ $settingsDir = $mixInfo->getPath('settings');
+ if (!in_array($settingsDir, $e->settingsFolders) && is_dir($settingsDir)) {
+ $e->settingsFolders[] = $settingsDir;
+ }
+ });
+
+};
diff --git a/mixin/smarty-v2@1.0.3.mixin.php b/mixin/smarty-v2@1.0.3.mixin.php
new file mode 100644
index 0000000..f8718c8
--- /dev/null
+++ b/mixin/smarty-v2@1.0.3.mixin.php
@@ -0,0 +1,78 @@
+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-v2.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/phpunit.xml.dist b/phpunit.xml.dist
index 0f9f25d..fc8f870 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,5 +1,5 @@
-
+
./tests/phpunit