714 lines
22 KiB
PHP
714 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* Piwik - Open source web analytics
|
|
*
|
|
* @link http://piwik.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
|
|
namespace Piwik;
|
|
|
|
use Exception;
|
|
|
|
/**
|
|
* Singleton that provides read & write access to Piwik's INI configuration.
|
|
*
|
|
* This class reads and writes to the `config/config.ini.php` file. If config
|
|
* options are missing from that file, this class will look for their default
|
|
* values in `config/global.ini.php`.
|
|
*
|
|
* ### Examples
|
|
*
|
|
* **Getting a value:**
|
|
*
|
|
* // read the minimum_memory_limit option under the [General] section
|
|
* $minValue = Config::getInstance()->General['minimum_memory_limit'];
|
|
*
|
|
* **Setting a value:**
|
|
*
|
|
* // set the minimum_memory_limit option
|
|
* Config::getInstance()->General['minimum_memory_limit'] = 256;
|
|
* Config::getInstance()->forceSave();
|
|
*
|
|
* **Setting an entire section:**
|
|
*
|
|
* Config::getInstance()->MySection = array('myoption' => 1);
|
|
* Config::getInstance()->forceSave();
|
|
*
|
|
* @method static \Piwik\Config getInstance()
|
|
*/
|
|
class Config extends Singleton
|
|
{
|
|
const DEFAULT_LOCAL_CONFIG_PATH = '/config/config.ini.php';
|
|
const DEFAULT_COMMON_CONFIG_PATH = '/config/common.config.ini.php';
|
|
const DEFAULT_GLOBAL_CONFIG_PATH = '/config/global.ini.php';
|
|
|
|
/**
|
|
* Contains configuration files values
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $initialized = false;
|
|
protected $configGlobal = array();
|
|
protected $configLocal = array();
|
|
protected $configCommon = array();
|
|
protected $configCache = array();
|
|
protected $pathGlobal = null;
|
|
protected $pathCommon = null;
|
|
protected $pathLocal = null;
|
|
|
|
/**
|
|
* @var boolean
|
|
*/
|
|
protected $isTest = false;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
|
|
{
|
|
$this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath();
|
|
$this->pathCommon = $pathCommon ?: self::getCommonConfigPath();
|
|
$this->pathLocal = $pathLocal ?: self::getLocalConfigPath();
|
|
}
|
|
|
|
/**
|
|
* Returns the path to the local config file used by this instance.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getLocalPath()
|
|
{
|
|
return $this->pathLocal;
|
|
}
|
|
|
|
/**
|
|
* Returns the path to the global config file used by this instance.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getGlobalPath()
|
|
{
|
|
return $this->pathGlobal;
|
|
}
|
|
|
|
/**
|
|
* Returns the path to the common config file used by this instance.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCommonPath()
|
|
{
|
|
return $this->pathCommon;
|
|
}
|
|
|
|
/**
|
|
* Enable test environment
|
|
*
|
|
* @param string $pathLocal
|
|
* @param string $pathGlobal
|
|
* @param string $pathCommon
|
|
*/
|
|
public function setTestEnvironment($pathLocal = null, $pathGlobal = null, $pathCommon = null, $allowSaving = false)
|
|
{
|
|
if (!$allowSaving) {
|
|
$this->isTest = true;
|
|
}
|
|
|
|
$this->clear();
|
|
|
|
$this->pathLocal = $pathLocal ?: Config::getLocalConfigPath();
|
|
$this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
|
|
$this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
|
|
|
|
$this->init();
|
|
|
|
// this proxy will not record any data in the production database.
|
|
// this provides security for Piwik installs and tests were setup.
|
|
if (isset($this->configGlobal['database_tests'])
|
|
|| isset($this->configLocal['database_tests'])
|
|
) {
|
|
$this->__get('database_tests');
|
|
$this->configCache['database'] = $this->configCache['database_tests'];
|
|
}
|
|
|
|
// Ensure local mods do not affect tests
|
|
if (empty($pathGlobal)) {
|
|
$this->configCache['log'] = $this->configGlobal['log'];
|
|
$this->configCache['Debug'] = $this->configGlobal['Debug'];
|
|
$this->configCache['mail'] = $this->configGlobal['mail'];
|
|
$this->configCache['General'] = $this->configGlobal['General'];
|
|
$this->configCache['Segments'] = $this->configGlobal['Segments'];
|
|
$this->configCache['Tracker'] = $this->configGlobal['Tracker'];
|
|
$this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs'];
|
|
$this->configCache['Deletereports'] = $this->configGlobal['Deletereports'];
|
|
}
|
|
|
|
// for unit tests, we set that no plugin is installed. This will force
|
|
// the test initialization to create the plugins tables, execute ALTER queries, etc.
|
|
$this->configCache['PluginsInstalled'] = array('PluginsInstalled' => array());
|
|
|
|
// DevicesDetection plugin is not yet enabled by default
|
|
if (isset($configGlobal['Plugins'])) {
|
|
$this->configCache['Plugins'] = $this->configGlobal['Plugins'];
|
|
$this->configCache['Plugins']['Plugins'][] = 'DevicesDetection';
|
|
}
|
|
if (isset($configGlobal['Plugins_Tracker'])) {
|
|
$this->configCache['Plugins_Tracker'] = $this->configGlobal['Plugins_Tracker'];
|
|
$this->configCache['Plugins_Tracker']['Plugins_Tracker'][] = 'DevicesDetection';
|
|
}
|
|
|
|
// to avoid weird session error in travis
|
|
if (empty($pathGlobal)) {
|
|
$configArray = &$this->configCache;
|
|
} else {
|
|
$configArray = &$this->configLocal;
|
|
}
|
|
$configArray['General']['session_save_handler'] = 'dbtables';
|
|
}
|
|
|
|
/**
|
|
* Returns absolute path to the global configuration file
|
|
*
|
|
* @return string
|
|
*/
|
|
protected static function getGlobalConfigPath()
|
|
{
|
|
return PIWIK_USER_PATH . self::DEFAULT_GLOBAL_CONFIG_PATH;
|
|
}
|
|
|
|
/**
|
|
* Returns absolute path to the common configuration file.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getCommonConfigPath()
|
|
{
|
|
return PIWIK_USER_PATH . self::DEFAULT_COMMON_CONFIG_PATH;
|
|
}
|
|
|
|
/**
|
|
* Returns absolute path to the local configuration file
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getLocalConfigPath()
|
|
{
|
|
$path = self::getByDomainConfigPath();
|
|
if ($path) {
|
|
return $path;
|
|
}
|
|
return PIWIK_USER_PATH . self::DEFAULT_LOCAL_CONFIG_PATH;
|
|
}
|
|
|
|
private static function getLocalConfigInfoForHostname($hostname)
|
|
{
|
|
$perHostFilename = $hostname . '.config.ini.php';
|
|
$pathDomainConfig = PIWIK_USER_PATH . '/config/' . $perHostFilename;
|
|
|
|
return array('file' => $perHostFilename, 'path' => $pathDomainConfig);
|
|
}
|
|
|
|
public function getConfigHostnameIfSet()
|
|
{
|
|
if ($this->getByDomainConfigPath() === false) {
|
|
return false;
|
|
}
|
|
return $this->getHostname();
|
|
}
|
|
|
|
public function getClientSideOptions()
|
|
{
|
|
$general = $this->General;
|
|
|
|
return array(
|
|
'action_url_category_delimiter' => $general['action_url_category_delimiter'],
|
|
'autocomplete_min_sites' => $general['autocomplete_min_sites'],
|
|
'datatable_export_range_as_day' => $general['datatable_export_range_as_day']
|
|
);
|
|
}
|
|
|
|
protected static function getByDomainConfigPath()
|
|
{
|
|
$host = self::getHostname();
|
|
$hostConfig = self::getLocalConfigInfoForHostname($host);
|
|
|
|
if (Filesystem::isValidFilename($hostConfig['file'])
|
|
&& file_exists($hostConfig['path'])
|
|
) {
|
|
return $hostConfig['path'];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected static function getHostname()
|
|
{
|
|
$host = Url::getHost($checkIfTrusted = false); // Check trusted requires config file which is not ready yet
|
|
return $host;
|
|
}
|
|
|
|
/**
|
|
* If set, Piwik will use the hostname config no matter if it exists or not. Useful for instance if you want to
|
|
* create a new hostname config:
|
|
*
|
|
* $config = Config::getInstance();
|
|
* $config->forceUsageOfHostnameConfig('piwik.example.com');
|
|
* $config->save();
|
|
*
|
|
* @param string $hostname eg piwik.example.com
|
|
* @return string
|
|
* @throws \Exception In case the domain contains not allowed characters
|
|
*/
|
|
public function forceUsageOfLocalHostnameConfig($hostname)
|
|
{
|
|
$hostConfig = static::getLocalConfigInfoForHostname($hostname);
|
|
|
|
if (!Filesystem::isValidFilename($hostConfig['file'])) {
|
|
throw new Exception('Hostname is not valid');
|
|
}
|
|
|
|
$this->pathLocal = $hostConfig['path'];
|
|
$this->configLocal = array();
|
|
$this->initialized = false;
|
|
return $this->pathLocal;
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the local configuration file is writable.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isFileWritable()
|
|
{
|
|
return is_writable($this->pathLocal);
|
|
}
|
|
|
|
/**
|
|
* Clear in-memory configuration so it can be reloaded
|
|
*/
|
|
public function clear()
|
|
{
|
|
$this->configGlobal = array();
|
|
$this->configLocal = array();
|
|
$this->configCache = array();
|
|
$this->initialized = false;
|
|
}
|
|
|
|
/**
|
|
* Read configuration from files into memory
|
|
*
|
|
* @throws Exception if local config file is not readable; exits for other errors
|
|
*/
|
|
public function init()
|
|
{
|
|
$this->initialized = true;
|
|
$reportError = SettingsServer::isTrackerApiRequest();
|
|
|
|
// read defaults from global.ini.php
|
|
if (!is_readable($this->pathGlobal) && $reportError) {
|
|
Piwik_ExitWithMessage(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal)));
|
|
}
|
|
|
|
$this->configGlobal = _parse_ini_file($this->pathGlobal, true);
|
|
|
|
if (empty($this->configGlobal) && $reportError) {
|
|
Piwik_ExitWithMessage(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
|
|
}
|
|
|
|
$this->configCommon = _parse_ini_file($this->pathCommon, true);
|
|
|
|
// Check config.ini.php last
|
|
$this->checkLocalConfigFound();
|
|
|
|
$this->configLocal = _parse_ini_file($this->pathLocal, true);
|
|
if (empty($this->configLocal) && $reportError) {
|
|
Piwik_ExitWithMessage(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
|
|
}
|
|
}
|
|
|
|
public function existsLocalConfig()
|
|
{
|
|
return is_readable($this->pathLocal);
|
|
}
|
|
|
|
public function checkLocalConfigFound()
|
|
{
|
|
if (!$this->existsLocalConfig()) {
|
|
throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathLocal)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode HTML entities
|
|
*
|
|
* @param mixed $values
|
|
* @return mixed
|
|
*/
|
|
protected function decodeValues($values)
|
|
{
|
|
if (is_array($values)) {
|
|
foreach ($values as &$value) {
|
|
$value = $this->decodeValues($value);
|
|
}
|
|
return $values;
|
|
}
|
|
return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
|
|
}
|
|
|
|
/**
|
|
* Encode HTML entities
|
|
*
|
|
* @param mixed $values
|
|
* @return mixed
|
|
*/
|
|
protected function encodeValues($values)
|
|
{
|
|
if (is_array($values)) {
|
|
foreach ($values as &$value) {
|
|
$value = $this->encodeValues($value);
|
|
}
|
|
} else {
|
|
$values = htmlentities($values, ENT_COMPAT, 'UTF-8');
|
|
}
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Returns a configuration value or section by name.
|
|
*
|
|
* @param string $name The value or section name.
|
|
* @return string|array The requested value requested. Returned by reference.
|
|
* @throws Exception If the value requested not found in either `config.ini.php` or
|
|
* `global.ini.php`.
|
|
* @api
|
|
*/
|
|
public function &__get($name)
|
|
{
|
|
if (!$this->initialized) {
|
|
$this->init();
|
|
|
|
// must be called here, not in init(), since setTestEnvironment() calls init(). (this avoids
|
|
// infinite recursion)
|
|
Piwik::postTestEvent('Config.createConfigSingleton',
|
|
array($this, &$this->configCache, &$this->configLocal));
|
|
}
|
|
|
|
// check cache for merged section
|
|
if (isset($this->configCache[$name])) {
|
|
$tmp =& $this->configCache[$name];
|
|
return $tmp;
|
|
}
|
|
|
|
$section = $this->getFromGlobalConfig($name);
|
|
$sectionCommon = $this->getFromCommonConfig($name);
|
|
if(empty($section) && !empty($sectionCommon)) {
|
|
$section = $sectionCommon;
|
|
} elseif(!empty($section) && !empty($sectionCommon)) {
|
|
$section = $this->array_merge_recursive_distinct($section, $sectionCommon);
|
|
}
|
|
|
|
if (isset($this->configLocal[$name])) {
|
|
// local settings override the global defaults
|
|
$section = $section
|
|
? array_merge($section, $this->configLocal[$name])
|
|
: $this->configLocal[$name];
|
|
}
|
|
|
|
if ($section === null && $name = 'superuser') {
|
|
$user = $this->getConfigSuperUserForBackwardCompatibility();
|
|
return $user;
|
|
} else if ($section === null) {
|
|
throw new Exception("Error while trying to read a specific config file entry <strong>'$name'</strong> from your configuration files.</b>If you just completed a Piwik upgrade, please check that the file config/global.ini.php was overwritten by the latest Piwik version.");
|
|
}
|
|
|
|
// cache merged section for later
|
|
$this->configCache[$name] = $this->decodeValues($section);
|
|
$tmp =& $this->configCache[$name];
|
|
|
|
return $tmp;
|
|
}
|
|
|
|
/**
|
|
* @deprecated since version 2.0.4
|
|
*/
|
|
public function getConfigSuperUserForBackwardCompatibility()
|
|
{
|
|
try {
|
|
$db = Db::get();
|
|
$user = $db->fetchRow("SELECT login, email, password
|
|
FROM " . Common::prefixTable("user") . "
|
|
WHERE superuser_access = 1
|
|
ORDER BY date_registered ASC LIMIT 1");
|
|
|
|
if (!empty($user)) {
|
|
$user['bridge'] = 1;
|
|
return $user;
|
|
}
|
|
} catch (Exception $e) {}
|
|
|
|
return array();
|
|
}
|
|
|
|
public function getFromGlobalConfig($name)
|
|
{
|
|
if (isset($this->configGlobal[$name])) {
|
|
return $this->configGlobal[$name];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function getFromCommonConfig($name)
|
|
{
|
|
if (isset($this->configCommon[$name])) {
|
|
return $this->configCommon[$name];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets a configuration value or section.
|
|
*
|
|
* @param string $name This section name or value name to set.
|
|
* @param mixed $value
|
|
* @api
|
|
*/
|
|
public function __set($name, $value)
|
|
{
|
|
$this->configCache[$name] = $value;
|
|
}
|
|
|
|
/**
|
|
* Comparison function
|
|
*
|
|
* @param mixed $elem1
|
|
* @param mixed $elem2
|
|
* @return int;
|
|
*/
|
|
public static function compareElements($elem1, $elem2)
|
|
{
|
|
if (is_array($elem1)) {
|
|
if (is_array($elem2)) {
|
|
return strcmp(serialize($elem1), serialize($elem2));
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (is_array($elem2)) {
|
|
return -1;
|
|
}
|
|
|
|
if ((string)$elem1 === (string)$elem2) {
|
|
return 0;
|
|
}
|
|
|
|
return ((string)$elem1 > (string)$elem2) ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* Compare arrays and return difference, such that:
|
|
*
|
|
* $modified = array_merge($original, $difference);
|
|
*
|
|
* @param array $original original array
|
|
* @param array $modified modified array
|
|
* @return array differences between original and modified
|
|
*/
|
|
public function array_unmerge($original, $modified)
|
|
{
|
|
// return key/value pairs for keys in $modified but not in $original
|
|
// return key/value pairs for keys in both $modified and $original, but values differ
|
|
// ignore keys that are in $original but not in $modified
|
|
|
|
return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements'));
|
|
}
|
|
|
|
/**
|
|
* Dump config
|
|
*
|
|
* @param array $configLocal
|
|
* @param array $configGlobal
|
|
* @param array $configCommon
|
|
* @param array $configCache
|
|
* @return string
|
|
*/
|
|
public function dumpConfig($configLocal, $configGlobal, $configCommon, $configCache)
|
|
{
|
|
$dirty = false;
|
|
|
|
$output = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
|
|
$output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
|
|
|
|
if (!$configCache) {
|
|
return false;
|
|
}
|
|
|
|
// If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values
|
|
if(!empty($configCommon)) {
|
|
$configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon);
|
|
}
|
|
|
|
if ($configLocal) {
|
|
foreach ($configLocal as $name => $section) {
|
|
if (!isset($configCache[$name])) {
|
|
$configCache[$name] = $this->decodeValues($section);
|
|
}
|
|
}
|
|
}
|
|
|
|
$sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache)));
|
|
|
|
foreach ($sectionNames as $section) {
|
|
if (!isset($configCache[$section])) {
|
|
continue;
|
|
}
|
|
|
|
// Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php)
|
|
|
|
// get local and cached config
|
|
$local = isset($configLocal[$section]) ? $configLocal[$section] : array();
|
|
$config = $configCache[$section];
|
|
|
|
// remove default values from both (they should not get written to local)
|
|
if (isset($configGlobal[$section])) {
|
|
$config = $this->array_unmerge($configGlobal[$section], $configCache[$section]);
|
|
$local = $this->array_unmerge($configGlobal[$section], $local);
|
|
}
|
|
|
|
// if either local/config have non-default values and the other doesn't,
|
|
// OR both have values, but different values, we must write to config.ini.php
|
|
if (empty($local) xor empty($config)
|
|
|| (!empty($local)
|
|
&& !empty($config)
|
|
&& self::compareElements($config, $configLocal[$section]))
|
|
) {
|
|
$dirty = true;
|
|
}
|
|
|
|
// no point in writing empty sections, so skip if the cached section is empty
|
|
if (empty($config)) {
|
|
continue;
|
|
}
|
|
|
|
$output .= "[$section]\n";
|
|
|
|
foreach ($config as $name => $value) {
|
|
$value = $this->encodeValues($value);
|
|
|
|
if (is_numeric($name)) {
|
|
$name = $section;
|
|
$value = array($value);
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
foreach ($value as $currentValue) {
|
|
$output .= $name . "[] = \"$currentValue\"\n";
|
|
}
|
|
} else {
|
|
if (!is_numeric($value)) {
|
|
$value = "\"$value\"";
|
|
}
|
|
$output .= $name . ' = ' . $value . "\n";
|
|
}
|
|
}
|
|
|
|
$output .= "\n";
|
|
}
|
|
|
|
if ($dirty) {
|
|
return $output;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Write user configuration file
|
|
*
|
|
* @param array $configLocal
|
|
* @param array $configGlobal
|
|
* @param array $configCommon
|
|
* @param array $configCache
|
|
* @param string $pathLocal
|
|
* @param bool $clear
|
|
*
|
|
* @throws \Exception if config file not writable
|
|
*/
|
|
protected function writeConfig($configLocal, $configGlobal, $configCommon, $configCache, $pathLocal, $clear = true)
|
|
{
|
|
if ($this->isTest) {
|
|
return;
|
|
}
|
|
|
|
$output = $this->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache);
|
|
if ($output !== false) {
|
|
$success = @file_put_contents($pathLocal, $output);
|
|
if (!$success) {
|
|
throw $this->getConfigNotWritableException();
|
|
}
|
|
}
|
|
|
|
if ($clear) {
|
|
$this->clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes the current configuration to the **config.ini.php** file. Only writes options whose
|
|
* values are different from the default.
|
|
*
|
|
* @api
|
|
*/
|
|
public function forceSave()
|
|
{
|
|
$this->writeConfig($this->configLocal, $this->configGlobal, $this->configCommon, $this->configCache, $this->pathLocal);
|
|
}
|
|
|
|
/**
|
|
* @throws \Exception
|
|
*/
|
|
public function getConfigNotWritableException()
|
|
{
|
|
$path = "config/" . basename($this->pathLocal);
|
|
return new Exception(Piwik::translate('General_ConfigFileIsNotWritable', array("(" . $path . ")", "")));
|
|
}
|
|
|
|
/**
|
|
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
|
|
* keys to arrays rather than overwriting the value in the first array with the duplicate
|
|
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
|
|
* this happens (documented behavior):
|
|
*
|
|
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
|
|
* => array('key' => array('org value', 'new value'));
|
|
*
|
|
* array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
|
|
* Matching keys' values in the second array overwrite those in the first array, as is the
|
|
* case with array_merge, i.e.:
|
|
*
|
|
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
|
|
* => array('key' => array('new value'));
|
|
*
|
|
* Parameters are passed by reference, though only for performance reasons. They're not
|
|
* altered by this function.
|
|
*
|
|
* @param array $array1
|
|
* @param array $array2
|
|
* @return array
|
|
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
|
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
|
*/
|
|
function array_merge_recursive_distinct ( array &$array1, array &$array2 )
|
|
{
|
|
$merged = $array1;
|
|
foreach ( $array2 as $key => &$value ) {
|
|
if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) ) {
|
|
$merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value );
|
|
} else {
|
|
$merged [$key] = $value;
|
|
}
|
|
}
|
|
return $merged;
|
|
}
|
|
}
|