diff --git a/wa-installer/lib/classes/wainstallerapps.class.php b/wa-installer/lib/classes/wainstallerapps.class.php index 428c6734f..260876c2e 100644 --- a/wa-installer/lib/classes/wainstallerapps.class.php +++ b/wa-installer/lib/classes/wainstallerapps.class.php @@ -1,2655 +1,2655 @@ -identity_hash; - } - - /** - * Get install promo_id - * @return string - */ - public function getPromoId() - { - return $this->promo_id; - } - - private static function getServerSignature($raw = false) - { - $signature = array( - 'php' => preg_replace('@([^0-9\\.].*)$@', '', phpversion()), - 'c' => PHP_INT_SIZE, - 'api' => PHP_SAPI, - ); - if (function_exists('php_uname')) { - $signature['os'] = @php_uname('s'); - $signature['r'] = @php_uname('r'); - } elseif (defined('PHP_OS')) { - $signature['os'] = constant('PHP_OS'); - } - - try { - if (class_exists('waDbConnector')) { - $adapter = waDbConnector::getConnection(); - $signature['sql_adapter'] = strtolower(preg_replace('@^waDb(.+)Adapter$@', '$1', get_class($adapter))); - if ($result = $adapter->query('SELECT @@version')) { - if ($version = $adapter->fetch($result)) { - $signature['sql'] = preg_replace('@([^0-9\\.].*)$@', '', reset($version)); - } - } - } - } catch (Exception $ex) { - - } - - return $raw ? $signature : base64_encode(json_encode($signature)); - } - - private static $app_domains = array(); - - private static function getDomains($apps, $raw = false) - { - $d = null; - $a = array(); - if (function_exists('wa')) { - $wa = wa(); - if ($wa && is_object($wa) && method_exists($wa, 'getRouting')) { - if (($routing = $wa->getRouting()) && method_exists($routing, 'getByApp')) { - $d = array(); - foreach ($apps as $app_id) { - if (!in_array($app_id, array('installer', 'webasyst', 'site',))) { - $app_domains = $routing->getByApp($app_id); - foreach ($app_domains as $domain => $route) { - if ($i = strpos($domain, '/')) { - $domain = substr($domain, 0, $i); - } - if (strpos($domain, '.')) { - $domain = preg_replace('@:\d+@', '', $domain); - if (!in_array($domain, $d)) { - $d[] = $domain; - } - if (!isset(self::$app_domains[$app_id])) { - self::$app_domains[$app_id] = array(); - } - if (!in_array($domain, self::$app_domains[$app_id])) { - self::$app_domains[$app_id][] = $domain; - } - - $id = array_search($domain, $d); - if (!isset($s[$app_id])) { - $s[$app_id] = array(); - } - $s[$app_id][] = $id; - } - } - if (!empty($s[$app_id])) { - $a[$app_id] = 0; - foreach ($s[$app_id] as $id) { - $a[$app_id] += 1 << $id; - } - } - } - } - } - } - } - if (!$raw && $d) { - $d = array_slice($d, 0, 10); - $d = implode(':', $d); - } - - $result = compact('a', 'd'); - return $raw ? $result : http_build_query($result); - } - - /** - * Get install domain - * @return mixed - */ - public function getDomain() - { - static $domain = null; - if ($domain === null) { - $domain = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; - $domain = preg_replace('@(^www\.|:\d+$)@', '', $domain); - } - return $domain; - } - - private static function init() - { - if (!isset(self::$root_path)) { - @ini_set("magic_quotes_runtime", 0); - /** @noinspection PhpDeprecationInspection */ - if (version_compare('5.4', PHP_VERSION, '>') && function_exists('set_magic_quotes_runtime') && get_magic_quotes_runtime()) { - /** @noinspection PhpDeprecationInspection */ - @set_magic_quotes_runtime(false); - } - @ini_set('register_globals', 'off'); - - self::$root_path = preg_replace('@([/\\\\]+)@', '/', dirname(__FILE__).'/'); - //XXX fix root path definition - self::$root_path = preg_replace('@(/)wa-installer/lib/classes/?$@', '$1', self::$root_path); - } - - } - - public static function setLocale($locale = null) - { - if (!is_null($locale) && $locale) { - self::$locale = $locale; - } - self::init(); - } - - public function __construct($license = null, $locale = null, $ttl = 600, $force = false, $token = null) - { - self::init(); - $this->license = $license; - $this->token = $token; - /* identity hash */ - $this->identity_hash = self::getGenericConfig('identity_hash'); - $this->beta = self::getGenericConfig('beta'); - if (in_array($this->beta, array(true, '1', 1), true)) { - $this->beta = 'beta'; - } - $this->promo_id = self::getGenericConfig('promo_id'); - if (!$this->identity_hash) { - $this->updateGenericConfig(); - $this->identity_hash = self::getGenericConfig('identity_hash'); - } - self::setLocale($locale); - self::$cache_ttl = max(0, $ttl); - self::$force = $force; - - /* enabled items list */ - if (false && file_exists(self::$root_path.self::CONFIG_APPS)) { - $this->installed_apps = include(self::$root_path.self::CONFIG_APPS); - foreach ($this->installed_apps as $app_id => & $enabled) { - if ($enabled) { - $this->installed_extras[$app_id] = array(); - $this->installed_extras[$app_id]['plugins'] = self::getConfig(sprintf(self::CONFIG_APP_PLUGINS, $app_id)); - $this->installed_extras[$app_id]['themes'] = self::getFolders(sprintf(self::ITEM_EXTRAS_PATH, $app_id, 'themes')); - $this->installed_extras[$app_id]['widgets'] = self::getFolders(sprintf(self::ITEM_EXTRAS_PATH, $app_id, 'widgets')); - $build_path = self::$root_path.sprintf(self::ITEM_BUILD, $app_id); - if (file_exists($build_path)) { - $enabled = max(1, include($build_path)); - } else { - $enabled = 1; - } - - } - unset($enabled); - } - } - - if (!file_exists(self::$root_path.self::CONFIG_SOURCES) && class_exists('waSystem')) { - wa('installer')->event('sources_not_found'); - } - - $path = self::$root_path . self::CONFIG_SOURCES_UPDATABLE; - if (!is_readable($path)) { - $path = self::$root_path . self::CONFIG_SOURCES; - } - - if (is_readable($path)) { - if (class_exists('waConfigCache')) { - $config_cache = waConfigCache::getInstance(); - $this->sources = $this->sources_all_zones = $config_cache->includeFile($path, false); - } else { - $this->sources = $this->sources_all_zones = include($path); - } - - if (isset($this->sources['zones'])) { - $installer_zone = self::getGenericConfig('zone_jail', 'auto'); - if ((empty($installer_zone) || $installer_zone === 'auto') && class_exists('waAppSettingsModel')) { - try { - $installer_zone = $this->getAppSettingsModel()->get('webasyst', self::ENDPOINTS_ZONE_KEY); - } catch (Exception $e) { - // do nothing - } - if (empty($installer_zone) && wa()->getLocale() === 'ru_RU') { - $installer_zone = 'ru'; - } - } - if (!empty($installer_zone) && !empty($this->sources['zones'][$installer_zone])) { - $this->sources = $this->sources['zones'][$installer_zone]; - } - unset($this->sources['zones']); - } - } else { - $this->sources = $this->sources_all_zones = []; - } - - //TODO USE config or etc - $this->extras_list['plugins'] = array( - 'info' => 'plugin', - 'subpath' => 'lib/config/', - ); - $this->extras_list['widgets'] = array( - 'info' => 'theme', - 'subpath' => '', - ); - $this->extras_list['widgets'] = array( - 'info' => 'widget', - 'subpath' => 'lib/config/', - ); - } - - private function getVendors() - { - return array(self::VENDOR_SELF); - } - - private function getSources($key, $vendors = array(), $zone = null) - { - if (!is_array($vendors) && $vendors) { - $vendors = array($vendors); - } - - if (!$this->sources) { - throw new Exception('Empty sources list'); - } - - $sources = $this->sources; - if (!empty($zone)) { - if ($zone === 'default') { - $sources = $this->sources_all_zones; - unset($sources['zones']); - } elseif (!empty($this->sources_all_zones['zones'][$zone])) { - $sources = $this->sources_all_zones['zones'][$zone]; - } - } - - if (empty($sources[$key])) { - throw new Exception(sprintf('Not found sources for %s', $key)); - } - - $result = array(); - foreach ((array)$sources[$key] as $vendor => $source) { - if (!$vendor) { - $vendor = self::VENDOR_SELF; - } - if (!$vendors || in_array($vendor, $vendors)) { - $result[$vendor] = $source; - } - } - return $result; - } - - /** - * - * @param $requirements - * @param bool $update_config - * @param bool $action - * @return boolean - */ - public static function checkRequirements(&$requirements, $update_config = false, $action = false) - { - self::init(); - if (is_null($requirements)) { - $requirements = self::getRequirements('wa-installer/lib/config/requirements.php', 'installer'); - } - $passed = true; - $config = array(); - $actions = array( - self::ACTION_CRITICAL_UPDATE, - self::ACTION_UPDATE, - self::ACTION_INSTALL, - true, - ); - $update = $action && in_array($action, $actions, true); - foreach ($requirements as $subject => & $requirement) { - $requirement['passed'] = false; - $requirement['note'] = null; - $requirement['warning'] = false; - $requirement['update'] = $update; - - waInstallerRequirements::test($subject, $requirement); - - $passed = $requirement['passed'] && $passed; - if ($update_config && isset($requirement['config'])) { - $config[$requirement['config']] = $requirement['value']; - } - if ($requirement['note'] && isset($requirement['allow_skip']) && $requirement['allow_skip']) { - unset($requirement); - unset($requirements[$subject]); - } else { - unset($requirement); - } - } - if ($update_config) { - try { - self::updateGenericConfig($config); - } catch (Exception $e) { - $requirements[] = array( - 'name' => '', - 'passed' => false, - 'warning' => $e->getMessage(), - 'description' => '', - 'note' => '', - ); - $passed = false; - } - } - - return $passed; - } - - /** - * - * @param $item - * @param $config - * @return boolean - */ - private static function checkVendor($item, $config = null) - { - $applicable = false; - if (is_null($config) && isset($item['installed'])) { - $config = $item['installed']; - } - if ($config) { - if (isset($config['vendor'])) { - if (isset($item['vendor'])) { - $applicable = (strcasecmp($config['vendor'], $item['vendor']) == 0) ? true : false; - if ($applicable) { - if (isset($item['edition'])) { - $applicable = (strcasecmp($config['edition'], $item['edition']) == 0) ? true : false; - } elseif (!empty($config['edition'])) { - $applicable = false; - } - } - } else { - //TODO - } - - } else { - //XXX allow update, while vendor missing - $applicable = false; - } - } - return $applicable; - } - - /** - * - * @param $path - * @param $slug - * @return array - */ - private static function getRequirements($path, $slug) - { - $requirements = self::getConfig($path); - $fields = array('name', 'description'); - foreach ($requirements as & $requirement) { - foreach ($fields as $field) { - if (isset($requirement[$field]) && is_array($requirement[$field])) { - if (self::$locale && isset($requirement[$field][self::$locale])) { - $value = $requirement[$field][self::$locale]; - } elseif (isset($requirement[$field]['en_US'])) { - $value = $requirement[$field]['en_US']; - } else { - $value = array_shift($requirement[$field]); - } - $requirement[$field] = $value; - } elseif (!isset($requirement[$field])) { - $requirement[$field] = ''; - } else { - $requirement[$field] = _wd($slug, $requirement[$field]); - } - } - if (!isset($requirement['strict'])) { - $requirement['strict'] = false; - } - unset($requirement); - } - return $requirements; - } - - public static function getGenericConfig($property = null, $default = false) - { - self::init(); - $config = self::getConfig(self::CONFIG_GENERIC); - if ($property) { - return isset($config[$property]) ? $config[$property] : $default; - } else { - return $config; - } - } - - /** - * - * Enumerate local items - * @param string $path wa-apps/[%app_id%/(themes|plugins|widgets)/] or wa-plugins/(payment|shipping|sms) - * - * @param array $options - * - * @param array [string]array $options['items'] - * @param array [string]boolean $options['plugins'] - * @param array [string]boolean $options['themes'] - * @param array [string]boolean $options['widgets'] - * @param array [string]boolean $options['list'] - * - * @param array $filter - * @param array [string]mixed $filter['locale'] - * @param array [string]mixed $filter['vendor'] - * @param array [string]boolean $options['plugins'] - * @param array [string]boolean $options['themes'] - * @return array - * @since 2.0 - */ - protected function enumerate($path, $options = array(), $filter = array()) - { - $items = array(); - $path = self::formatPath($path); - $folders = self::getFolders($path); - foreach ($folders as $slug => $exist) { - if ($slug) { - if (isset($options['items']) && !isset($options['items'][$slug])) { - #skip not applicable items - continue; - } - $item = $this->info($path, $slug, $filter); - if ($item) { - if (isset($options['items'])) { - $item['enabled'] = !empty($options['items'][$slug]); - } - $items[$slug] = $item; - } - } - } - return $items; - } - - private function info($path, $slug, $filter = array()) - { - $item = null; - $config_path = self::getConfigPath(trim($path.'/'.$slug, '/')); - $config = $this->getConfig($config_path); - if ($config) { - $config += array( - 'vendor' => self::VENDOR_UNKNOWN, - ); - } - if (empty($filter) || self::filter($config, $filter)) { - $item = array( - 'id' => preg_replace('@^.+/([^/]+)$@', '$1', $slug), - 'slug' => trim(preg_replace('@^/?wa-apps/@', '', $path.'/'.$slug), '/'), - 'path' => $path.'/'.$slug, - 'config_path' => $config_path, - 'installed' => $config ? $config : false, - ); - if ($item['installed']) { - self::fixItemVersion($item); - } - } - return $item; - } - - private static function getConfigPath($path) - { - $path = self::formatPath($path); - if (preg_match('@^wa-apps/[^/]+(/.+|$)@', $path)) { - #it apps or apps extras - if (preg_match('@^wa-apps/([^/]+)/(themes|plugins|widgets)/([^/]+)$@', $path, $matches)) { - switch ($matches[2]) { - case 'themes': - $path .= '/theme.xml'; - break; - case 'plugins': - $path .= '/lib/config/plugin.php'; - break; - case 'widgets': - $path .= '/lib/config/widget.php'; - break; - } - } elseif (preg_match('@^wa-apps/([^/]+)/?$@', $path, $matches)) { - $path .= '/lib/config/app.php'; - } else { - $path = false; - } - } elseif (preg_match('@^wa-plugins/[^/]+/[^/]+@', $path)) { - #system plugins - $path = preg_replace('@^wa-plugins/([^/]+)/plugins/(.+)$@', 'wa-plugins/$1/$2', $path); - $path .= '/lib/config/plugin.php'; - } elseif (preg_match('@^(wa-widgets|webasyst/widgets)/([^/]+)/?$@', $path, $matches)) { - - $path = 'wa-widgets/'.$matches[2].'/lib/config/widget.php'; - } else { - $path = false; - } - return $path; - } - - private static function formatPath($path) - { - $path = preg_replace('@([/\\\\]+)@', '/', $path); - return preg_replace('@([/\\\\]+)$@', '', $path); - } - - /** - * - * Filter item by params at filter - * @param array $item - * @param array $filter - * @return bool - */ - private static function filter($item, $filter = array()) - { - $applicable = true; - foreach ($filter as $field => $value) { - if (isset($item[$field])) { - $v = $item[$field]; - if (is_array($value)) { - if (is_array($item[$field])) { - if (!array_intersect($v, $value)) { - $applicable = false; - break; - } - } elseif (!in_array($v, $value)) { - $applicable = false; - break; - } - } elseif ($value === true) { /* value required */ - if (empty($v)) { - $applicable = false; - break; - } - - } elseif ($value === false) { /* value required */ - $applicable = false; - break; - } else { - if (is_array($value)) { - if (!in_array($value, $v)) { - $applicable = false; - break; - } - } elseif ($v != $value) { - $applicable = false; - break; - } - } - } elseif ($value === true) { /* value required */ - $applicable = false; - break; - } - } - return $applicable; - } - - private function extend(&$item, $options = array()) - { - if ($item) { - if (!empty($options['translate_name']) && isset($item['installed']['name'])) { - $translated_name = self::translateName($item, $options['translate_name']); - if (!empty($translated_name)) { - $item['installed']['name'] = $translated_name; - } - } - if (!empty($options['requirements']) && !isset($item['requirements'])) { - $item['requirements'] = self::getRequirements(sprintf(self::ITEM_REQUIREMENTS, $item['slug']), $item['slug']); - } - self::fixItemCurrent($item, null, array(), empty($options['local'])); - $item['applicable'] = true; - - if (!empty($item['requirements']) && !empty($options['requirements'])) { - $item['applicable'] = $this->checkRequirements($item['requirements'], false, ifset($options['action'])); - } - if (!empty($options['action'])) { - $item['action'] = self::applicableAction($item); - } - } - } - - public function getItems($options = array()) - { - static $items = null; - if ($items === null) { - $options += array( - 'installed' => true, - ); - $extra_types = array( - 'plugins' => array(), - 'themes' => array(), - 'widgets' => array(), - ); - $items = $this->getApps($options); - foreach ($items as $app) { - if (!empty($app['installed']['plugins'])) { - $extra_types['plugins'][] = $app['id']; - } - - if (!empty($app['installed']['themes'])) { - $extra_types['themes'][] = $app['id']; - } - - $extra_types['widgets'][] = $app['id']; - } - $options['status'] = true; - foreach ($extra_types as $type => $extras_apps) { - $extras = $this->getExtras($extras_apps, $type, $options); - foreach ($extras as $app_id => $app) { - foreach ($app[$type] as $extra) { - if (($type != 'themes') || ($extra['id'] != 'default')) { - $items[$app_id][$type][$extra['id']] = $extra; - } - } - } - } - } - return $items; - } - - /** - * @param string $vendor - * @param boolean $check_updates - * @return array[string] - * @since 2.0 - * @todo filter vendor - */ - public function getVersions($vendor = null, $check_updates = false) - { - if (empty($vendor)) { - $vendor = self::VENDOR_SELF; - - } - $versions = array(); - $options = array( - 'widgets' => true, - ); - $apps = $this->getItems($options); - $apps += $this->getSystemItems(true); - - foreach ($apps as $slug => $app) { - if (!empty($app['installed']) && (strpos($slug, 'wa-plugins/') !== 0)) { - $versions[$slug] = $app['installed']['version']; - } - - foreach (array('plugins', 'themes', 'widgets') as $type) { - if (!empty($app[$type])) { - foreach ($app[$type] as $item) { - $versions[$item['slug']] = ifempty($item, 'installed', 'version', ''); - } - } - - } - } - - if ($check_updates) { - $available = $this->query('versions/?slug='.implode(',', array_keys($versions)), $vendor); - foreach ($versions as $slug => $version) { - if (empty($available[$slug]) || version_compare($version, $available[$slug], '>=')) { - unset($versions[$slug]); - } - } - } - - return $versions; - } - - /** - * @param string $vendor - * @param array $new_items list of new items - * @return array - */ - public function getUpdates($vendor = null, $new_items = array()) - { - if (empty($vendor)) { - $vendor = self::VENDOR_SELF; - } - $query = array(); - $versions = $this->getVersions($vendor, false); - if ($new_items) { - foreach ($new_items as $slug => $item) { - $slug = preg_replace('@^wa-plugins/([^/]+)/plugins/(.+)$@', 'wa-plugins/$1/$2', $slug); - $version = isset($versions[$slug]) ? $versions[$slug] : '0'; - $query[$slug] = sprintf('%s=%s', (sprintf('v[%s]', $slug)), rawurlencode($version)); - } - //XXX TODO compare local & new edition & version - } else { - foreach ($versions as $slug => $version) { - $slug = preg_replace('@^wa-plugins/([^/]+)/plugins/(.+)$@', 'wa-plugins/$1/$2', $slug); - $query[$slug] = sprintf('%s=%s', (sprintf('v[%s]', $slug)), rawurlencode($version)); - } - } - if ($query) { - $url = 'updates/?'.implode('&', $query); - $updates = $this->query($url, $vendor, false); - $items = $this->getItems(); - - $items += $this->getSystemItems(true); - //XXX check current edition:versions - foreach ($updates as $app_id => &$item) { - foreach (array('plugins', 'themes', 'widgets') as $type) { - if (!empty($item[$type])) { - foreach ($item[$type] as $extras_id => &$extras_item) { - if (!empty($items[$app_id][$type][$extras_id])) { - $extras_item = array_merge($items[$app_id][$type][$extras_id], $extras_item); - } - $extras_item['app'] = $app_id; - $extras_item['vendor'] = $vendor; - $extras_item['slug'] = $slug = $app_id.'/'.$type.'/'.$extras_id; - if (empty($extras_item['name'])) { - $extras_item['name'] = empty($extras_item['installed']['name']) ? $extras_item['slug'] : $extras_item['installed']['name']; - } - $extras_item['action'] = self::applicableAction($extras_item); - $extras_item['applicable'] = self::checkRequirements($extras_item['requirements'], false, $extras_item['action']); - $this->buildUrl($extras_item['download_url']); - unset($extras_item); - } - } - } - - if (isset($item['version'])) { - - if (!empty($items[$app_id])) { - if (isset($items[$app_id]['plugins'])) { - unset($items[$app_id]['plugins']); - } - if (isset($items[$app_id]['themes'])) { - unset($items[$app_id]['themes']); - } - if (isset($items[$app_id]['widgets'])) { - unset($items[$app_id]['widgets']); - } - $item = array_merge($items[$app_id], $item); - } - $item['vendor'] = $vendor; - $item['slug'] = $slug = $app_id; - $item['action'] = self::applicableAction($item); - $item['applicable'] = self::checkRequirements($item['requirements'], false, $item['action']); - $item['domains'] = isset(self::$app_domains[$app_id]) ? self::$app_domains[$app_id] : array(); - $this->buildUrl($item['download_url']); - } elseif (!empty($items[$app_id])) { - if (empty($item['name'])) { - if (!empty($items[$app_id]['installed']) && is_array($items[$app_id]['installed'])) { - $item['name'] = ifset($items[$app_id]['installed']['name'], ifset($items[$app_id]['name'], $app_id)); - } else { - $item['name'] = ifset($items[$app_id]['name'], $app_id); - } - } - - if (empty($item['icons']) && !empty($items[$app_id]['installed'])) { - if (is_array($items[$app_id]['installed'])) { - $item['icon'] = ifset($items[$app_id]['installed']['icon'], ifset($items[$app_id]['installed']['icons'], array())); - $item['icons'] = $item['icon']; - } - } - $item['action'] = self::ACTION_NONE; - } - unset($item); - } - } else { - $updates = array(); - } - return $updates; - } - - private function getSystemItems($extras = false) - { - $items = array( - 'wa-plugins/sms' => array( - 'slug' => 'wa-plugins/sms', - 'id' => 'sms', - 'icon' => array( - 16 => 'icon16 mobile', - ), - 'name' => _w('SMS'), - 'installed' => true, - 'vendor' => 'webasyst', - 'virtual' => true, - ), - 'wa-plugins/payment' => array( - 'slug' => 'wa-plugins/payment', - 'id' => 'payment', - 'icon' => array( - 16 => 'icon16 dollar', - ), - 'name' => _w('Payment'), - 'installed' => true, - 'vendor' => 'webasyst', - 'virtual' => true, - ), - 'wa-plugins/shipping' => array( - 'slug' => 'wa-plugins/shipping', - 'id' => 'shipping', - 'icon' => array( - 16 => 'icon16 box', - ), - 'name' => _w('Shipping'), - 'installed' => true, - 'vendor' => 'webasyst', - 'virtual' => true, - ), - ); - foreach ($items as $id => &$item) { - - $item['plugins'] = array(); - $item['icons'] = $item['icon']; - if (empty($item['plugins']) && false) { - unset($items[$id]); - } - unset($item); - } - if ($extras) { - $enum_options = array( - 'installed' => true, - ); - $plugins = $this->getExtras(array_keys($items), 'plugins', $enum_options); - - foreach ($plugins as $id => $item) { - if (isset($items[$id]) && !empty($item['plugins'])) { - $items[$id]['plugins'] = $item['plugins']; - } - - } - } - return $items; - } - - public function getServerData($name = null) - { - if ($name) { - return isset($this->server_data[$name]) ? $this->server_data[$name] : null; - } else { - return $this->server_data; - } - } - - /** - * - * @param array $options - * - * @param array [string]string $options['status'] item status at config (enabled|disabled|true - items from config|false - all available); default are enabled - * @param array [string]boolean $options['requirements'] check apps requirements; default are false - * @param array [string]boolean $options['installed'] get local apps only if true; get remote apps if false; merge if not set - * - * @param array $filter - * @param array [string]string $filter['extras'] - * @return array - * @since 2.0 - */ - public function getApps($options = array(), $filter = array()) - { - /*get local apps*/ - $installed = array(); - $options += array( - 'status' => self::STATUS_ENABLED, - ); - $enum_options = array(); - if (isset($options['status'])) { - $installed_apps = self::getConfig(self::CONFIG_APPS); - if ($options['status'] === true) { - $enum_options['items'] = $installed_apps; - } elseif ($options['status'] === false) { - $installed_apps = array_merge(array_fill_keys(array_keys(self::getFolders('wa-apps')), false), $installed_apps); - $enum_options['items'] = $installed_apps; - } else { - switch ($options['status']) { - case self::STATUS_ENABLED: - $installed_apps = array_filter($installed_apps, 'intval'); - $enum_options['items'] = $installed_apps; - break; - case self::STATUS_DISABLED: - $installed_apps = array_diff_assoc($installed_apps, array_filter($installed_apps, 'intval')); - $enum_options['items'] = $installed_apps; - break; - } - } - } - - $enum_filter = array(); - if (!empty($filter['extras'])) { - if (!is_array($filter['extras'])) { - $enum_filter[$filter['extras']] = true; - } - } - if (!isset($options['installed']) || !empty($options['installed'])) { - if (!empty($filter['extras']) && is_array($filter['extras'])) { - $apps = array(); - foreach ($filter['extras'] as $extras_type) { - $enum_filter[$extras_type] = true; - $apps += $this->enumerate('wa-apps', $enum_options, $enum_filter); - unset($enum_filter[$extras_type]); - } - } else { - $apps = $this->enumerate('wa-apps', $enum_options, $enum_filter); - } - - $installed = $apps; - } else { - $apps = array(); - } - - if (!empty($options['system'])) { - $apps += $this->getSystemItems(); - } - - if (!empty($options['widgets'])) { - $apps += array( - 'webasyst' => array( - 'name' => 'Webasyst framework', - 'id' => 'webasyst', - 'slug' => 'webasyst', - 'enabled' => true, - ), - ); - } - - - /* get available apps */ - if (empty($options['installed'])) { - $list_filter = array(); - - if (!empty($filter['extras'])) { - $list_filter[$filter['extras']] = true; - } - - if (isset($options['local']) && empty($options['local'])) { - $apps = array(); - } - - foreach ($this->getVendors() as $vendor) { - $url = 'apps/'; - if (!empty($options['filter'])) { - $filter = array(); - foreach ($options['filter'] as $param => $value) { - $filter[] = "filter[{$param}]={$value}"; - } - $url .= '?'.implode('&', $filter); - } - $list = $this->query($url, $vendor); - foreach ($list as $app_id => $app) { - if (strpos($app_id, '::') === 0) { - $this->server_data[substr($app_id, 2)] = $app; - } else { - if (empty($list_filter) || self::filter($app, $list_filter)) { - - if (!empty($installed[$app_id])) { - $apps[$app_id] = array_merge($installed[$app_id], $app); - } else { - $app += array( - 'id' => $app_id, - 'slug' => $app_id, - 'vendor' => $vendor, - 'installed' => null, - ); - $apps[$app_id] = $app; - } - } elseif (!empty($installed[$app_id])) { - unset($apps[$app_id]); - } - } - } - } - } else { - if (isset($options['local'])) { - trigger_error(sprintf('Parameter $option[%s] has no effect at %s', 'local', __METHOD__), E_USER_NOTICE); - } - } - - foreach ($apps as & $app) { - $this->extend($app, $options); - } - unset($app); - - /* optional check applicable updates */ - - return $apps; - - } - - /** - * @param $app - * @param $type - * @param array $options - * @return array - * @throws Exception - * @since 2.0 - */ - public function getExtras($app, $type, $options = array()) - { - if (!in_array($type, array('plugins', 'themes', 'widgets'))) { - throw new Exception('Invalid extras type'); - } - /*get local extras*/ - $options += array( - 'status' => self::STATUS_ENABLED, - ); - - $enum_options = array(); - if (isset($options['status'])) { - if ($type == 'plugins') { - foreach ((array)$app as $app_id) { - if (strpos($app_id, 'wa-plugins/') === false) { - $installed = self::getConfig(sprintf(self::CONFIG_APP_PLUGINS, $app_id)); - if ($options['status'] === true) { - $enum_options[$app_id] = array('items' => $installed); - } elseif ($options['status'] === false) { - $path = $this->getExtrasPath($app_id, $type); - $installed = array_merge(array_fill_keys(array_keys(self::getFolders($path)), false), $installed); - $enum_options[$app_id] = array('items' => $installed); - } else { - switch ($options['status']) { - case self::STATUS_ENABLED: - $installed = array_filter($installed, 'intval'); - $enum_options[$app_id] = array('items' => $installed); - break; - case self::STATUS_DISABLED: - $installed = array_diff_assoc($installed, array_filter($installed, 'intval')); - $enum_options[$app_id] = array('items' => $installed); - break; - } - } - } - } - } - } - - if (!isset($options['installed']) || !empty($options['installed'])) { - $extras = array(); - foreach ((array)$app as $app_id) { - $path = $this->getExtrasPath($app_id, $type); - if ($path) { - $enum_filter = array(); - if (!empty($options['filter'])) { - if (!empty($options['filter']['vendor'])) { - $enum_filter['vendor'] = $options['filter']['vendor']; - } - } - - $items = $this->enumerate($path, isset($enum_options[$app_id]) ? $enum_options[$app_id] : array(), $enum_filter); - - $extend_options = array(); - if (!empty($options['translate_titles'])) { - $extend_options = array( - 'translate_name' => array( - 'type' => $type, - 'app_id' => $app_id, - ) - ); - } - - foreach ($items as &$extras_item) { - self::extend($extras_item, $extend_options); - unset($extras_item); - } - $extras[$app_id] = array( - $type => $items, - ); - unset($items); - } - } - $installed = $extras; - } else { - $installed = array(); - $extras = array(); - } - - /* get available apps */ - if (empty($options['installed'])) { - if (isset($options['local']) && empty($options['local'])) { - $installed = $extras; - $extras = array(); - } - - foreach ($this->getVendors() as $vendor) { - - $url = $type.'/?app_id='.implode(',', (array)$app); - if (!empty($options['filter'])) { - foreach ($options['filter'] as $param => $value) { - $url .= "&filter[{$param}]={$value}"; - } - } - if (!empty($options['inherited'])) { - $url .= '&inherited='.implode(',', (array)$options['inherited']); - } - - $list = $this->query($url, $vendor); - - $count = count(self::$sort); - foreach ($list as $app_id => $available_extras) { - if (strpos($app_id, '::') === 0) { - $this->server_data[substr($app_id, 2)] = $available_extras; - } else { - if (!isset($extras[$app_id])) { - if (!isset($options['apps']) || !empty($options['apps'])) { - $enum_filter = array($type => true); - $extras[$app_id] = $this->info('wa-apps', $app_id, $enum_filter); - } else { - $extras[$app_id] = array($type => array()); - } - } - foreach ($available_extras[$type] as $extras_id => $extras_item) { - if (!isset(self::$sort[$extras_id])) { - self::$sort[$extras_id] = ++$count; - } - if (!empty($installed[$app_id][$type][$extras_id])) { - $_installed = $installed[$app_id][$type][$extras_id]; - if (!empty($extras_item['inherited'])) { - foreach ($extras_item['inherited'] as $inherited_app_id => $inherited) { - - $inherited_extras_id = preg_replace('@(^|^.+/)([^/]+)$@', '$2', $inherited['slug']); - if (empty($installed[$inherited_app_id][$type][$inherited_extras_id])) { - $_installed = array( - 'id' => $extras_id, - 'slug' => $app_id.'/'.$type.'/'.$extras_id, - 'vendor' => $vendor, - 'installed' => null, - ); - break; - } - } - - } - $extras[$app_id][$type][$extras_id] = array_merge($_installed, $extras_item); - } else { - $extras_item += array( - 'id' => $extras_id, - 'slug' => $app_id.'/'.$type.'/'.$extras_id, - 'vendor' => $vendor, - 'installed' => null, - ); - $extras[$app_id][$type][$extras_id] = $extras_item; - } - } - } - } - - foreach ($extras as &$available_extras) { - uksort($available_extras[$type], array(__CLASS__, 'sort')); - unset($available_extras); - } - } - } - - return $extras; - } - - private static function sort($a, $b) - { - static $min = 0; - if (!isset(self::$sort[$a])) { - self::$sort[$a] = --$min; - } - if (!isset(self::$sort[$b])) { - self::$sort[$b] = --$min; - } - return max(-1, min(1, (self::$sort[$a] - self::$sort[$b]))); - } - - /** - * @param $slug - * @param array $options - * @return array(); - * @throws Exception - * @since 2.0 - */ - public function getItemInfo($slug, $options = array()) - { - $info = null; - $id = preg_replace('@/.*$@', '', $slug); - if (empty($options['local'])) { - switch ($id) { - case 'wa-plugins': - $info = $this->query('app/'.$slug.'/'); - break; - default: - //TODO send installation hash - $url = 'app/'; - if (!empty($options['inherited'])) { - $url .= implode(',', (array)$options['inherited']).','; - } - $url .= $slug; - $info = $this->query($url = preg_replace('@,?\\*@', '', $url).'/'); - break; - } - } else { - $info = array(); - } - - if ($info || !empty($options['local'])) { - if ($id == '*') { - $slug = preg_replace('@^\\*/@', 'site/', $slug); - } - $exists = !!$info; - $info += array( - 'installed' => null, - 'applicable' => null, - 'slug' => $slug, - ); - - $installed_apps = self::getConfig(self::CONFIG_APPS); - if (!empty($installed_apps[$id]) || ($id == 'wa-plugins') || ($id == 'webasyst') || ($id == '*')) { - $filter = array(); - if (!empty($options['vendor'])) { - $filter['vendor'] = $options['vendor']; - } - switch ($id) { - case 'webasyst': - $key = ''; - break; - case 'wa-plugins': - $key = ''; - break; - default: - $key = 'wa-apps'; - break; - } - $info = array_merge($info, $this->info($key, $info['slug'], $filter)); - if (!$exists && empty($info['installed'])) { - $info = array(); - } elseif (!empty($info['inherited'])) { - foreach ($info['inherited'] as $inherited_app_id => $inherited) { - $inherited_info = $this->info(($inherited_app_id == 'wa-plugins') ? '' : 'wa-apps', $inherited['slug'], $filter); - if (empty($inherited_info['installed'])) { - $info['installed'] = false; - break; - } - } - } - } - $this->extend($info, $options); - } - return $info; - - } - - /** - * Fix item images and icons names - * @param $item - * @param null $id - * @return void - */ - private static function fixItemIcon(&$item, $id = null) - { - if (!$id) { - $id = (isset($item['id']) && $item['id']) ? $item['id'] : $item['slug']; - } - if (isset($item['installed']) && !empty($item['installed']) !== false) { - $l = &$item['installed']; - $sizes = array(48, 24, 16); - if (!empty($l['icon']) && !is_array($l['icon'])) { - $l['icon'] = array(48 => $l['icon']); - } - - if (!isset($l['img'])) { - if (!isset($l['icon'])) { - $l['img'] = sprintf(self::ITEM_ICON, $id); - } elseif (isset($l['icon'][reset($sizes)])) { - $l['img'] = $l['icon'][reset($sizes)]; - } else { - $l['img'] = sprintf(self::ITEM_ICON, $id); - } - } - if (!isset($l['icon'])) { - $l['icon'] = array(); - } - - foreach ($sizes as $size) { - if (!isset($l['icon'][$size])) { - $l['icon'][$size] = $l['img']; - } - } - - - foreach ($l['icon'] as &$i) { - $i = wa()->getRootUrl().'wa-apps/'.$item['slug'].'/'.$i; - }; - unset($i); - if (empty($item['icons'])) { - $item['icons'] = $l['icon']; - } - if (!isset($item['icon'])) { - if (!empty($item['icons'])) { - $icon_id = max(array_keys($item['icons'])); - $item['icon'] = $item['icons'][$icon_id]; - } - } - - } - - } - - private function buildUrl(&$path) - { - if (is_array($path)) { - $is_url = true; - foreach ($path as &$chunk) { - $is_url = $this->buildUrl($chunk) && $is_url; - unset($chunk); - } - } else { - $is_url = preg_match('@^https?://@', $path); - if (($this->license || $this->identity_hash || $this->beta) && $is_url && $this->originalUrl($path)) { - $query = parse_url($path, PHP_URL_QUERY); - if ($this->license) { - $query = $query.($query ? '&' : '').'license='.$this->license; - } - if ($this->identity_hash) { - $query = $query.($query ? '&' : '').'hash='.$this->identity_hash; - } - if ($identity_hash = $this->getGenericConfig('previous_hash')) { - $query = $query.($query ? '&' : '').'previous_hash='.$identity_hash; - } - if (!empty($this->token)) { - $query = $query.($query ? '&' : '').'token='.$this->token; - } - if ($this->promo_id) { - $query = $query.($query ? '&' : '').'promo_id='.$this->promo_id; - } - $domain = $this->getDomain(); - if ($domain) { - $query = $query.($query ? '&' : '').'domain='.urlencode(base64_encode($domain)); - } - if (preg_match('@/(download|archive)/@', $path)) { - $query = $query.($query ? '&' : '').'signature='.urlencode(self::getServerSignature()); - - if ($this->beta && preg_match('@/archive/@', $path)) { - $path = preg_replace('@/(archive)/@', "/\$1/{$this->beta}/", $path, 1); - } - } - - if (preg_match('@/versions/\?@', $path)) { - if ($this->beta) { - $path = preg_replace('@/versions/\?@', "/versions/{$this->beta}/?", $path, 1); - } - } - - if (preg_match('@/updates/\?@', $path)) { - if ($this->beta) { - $path = preg_replace('@/updates/\?@', "/updates/{$this->beta}/?", $path, 1); - } - parse_str($path, $raw); - if (!empty($raw['v']) && ($raw['v'] = array_filter($raw['v']))) { - $stack = debug_backtrace(); - foreach ($stack as $s) { - if (isset($s['class']) && isset($s['function'])) { - if (md5($s['class'].$s['function']) == '4e78e0ba4240bbbcb818f122201cc5b6') { - $raw['v'] = array(); - break; - } - } - } - if ($apps = array_filter(array_keys($raw['v']), array($this, 'buildUrlCallback'))) { - $query = $query.($query ? '&' : '').self::getDomains($apps); - } - } - } - - $path = preg_replace("@\?.*$@", '', $path); - $path .= '?'.$query; - } - if (self::$locale && $is_url) { - $query = parse_url($path, PHP_URL_QUERY); - $query .= '&locale='.self::$locale; - $path = preg_replace("@\?.*$@", '', $path); - $path .= '?'.$query; - } - } - return $is_url; - } - - private function buildUrlCallback($a) - { - return strpos($a, "/") === false; - } - - private function originalUrl($url) - { - static $original_host; - if (!$original_host) { - foreach ((array)$this->sources['apps'] as $vendor => $source) { - if (!$vendor) { - $vendor = self::VENDOR_SELF; - } - if ($vendor == self::VENDOR_SELF) { - $original_host = parse_url($source, PHP_URL_HOST); - $original_host = preg_replace('@(^www\.|:\d+$)@', '', $original_host); - } - } - } - $host = parse_url($url, PHP_URL_HOST); - return $original_host && (($original_host == $host) || (strpos($host, '.'.$original_host))); - } - - /** - * - * @param array $item - * @param string $id - * @param array $fields - * @param bool $no_translate - * @todo rename and use corrected names - */ - private static function fixItemCurrent(&$item, $id = null, $fields = array(), $no_translate = false) - { - if (!$id) { - $id = empty($item['id']) ? ($item['slug']) : $item['id']; - } - - $item['action'] = self::ACTION_NONE; - if (empty($item['requirements'])) { - $item['requirements'] = array(); - } - $item['applicable'] = true; - - $fields += array( - 'id', - 'slug', - 'name', - 'description', - 'vendor_name' => 'vendor', - 'system', - 'vendor', - 'commercial', - 'version', - ); - - foreach ($fields as $field => $source) { - if (is_numeric($field)) { - $field = $source; - } - if (empty($item[$field])) { - if (!empty($item['installed'][$source])) { - $item[$field] = $item['installed'][$source]; - } elseif (!isset($item[$field])) { - $item[$field] = ''; - } - } - - } - $ml_fileds = array('name', 'description'); - foreach ($ml_fileds as $field) { - if (isset($item[$field])) { - if (is_array($item[$field])) { - $item[$field] = isset($item[$field][self::$locale]) ? $item[$field][self::$locale] : current($item[$field]); - } elseif (!empty($item[$field]) && function_exists('_wd') && !$no_translate) { - $item[$field] = _wd($id, $item[$field]); - } - } - } - if (empty($item['app'])) { - if (preg_match('@^(.+)/(plugins|widgets)/@', $item['slug'], $matches)) { - $item['app'] = $matches[1]; - } elseif (preg_match('@^wa-widgets/@', $item['slug'], $matches)) { - $item['app'] = 'webasyst'; - } - } - - $remap = array( - 'vendor_name' => 'vendor', - ); - foreach ($remap as $target => $source) { - if (empty($item[$target])) { - $item[$target] = $item[$source]; - } - } - if (!preg_match('@^wa-plugins/@', $item['slug'])) { - self::fixItemIcon($item); - } - if (isset($item['installed']['version']) && ($item['installed']['version'] === '0.0.0.0')) { - if (isset($item['version']) && preg_match('@^1\.0\.\d+$@', $item['version'])) { - $item['installed']['version'] = $item['version']; - } - } - } - - private static function fixItemVersion(&$item, $id = null, $extras_info = null) - { - if ((!isset($item['installed']['version']) || !$item['installed']['version']) && !empty($item['installed'])) { - if (isset($item['version'])) { - $item['installed']['version'] = $item['version']; - } else { - $item['installed']['version'] = '0.0.0'; - } - } - - if (is_null($id) && isset($item['slug'])) { - $id = $item['slug']; - } - if ($id && !empty($item['installed'])) { - if (empty($item['config_path'])) { - if (is_null($extras_info)) { - $build_path = self::$root_path.sprintf(self::ITEM_BUILD, $id); - } else { - $build_path = self::$root_path.sprintf(self::ITEM_EXTRAS_PATH, $id, $extras_info['subpath']).'build.php'; - } - } else { - $build_path = self::$root_path.dirname($item['config_path']).'/build.php'; - } - if (file_exists($build_path) && ($build = include($build_path))) { - $item['installed']['version'] .= ".{$build}"; - } elseif (preg_match('/((^|\\.)[\\d]+){2,3}$/', $item['installed']['version'])) { - $item['installed']['version'] .= ".p"; - } - } - } - - protected static function translateName($item, $options) - { - $translation = ''; - if ($options['type'] == 'themes' && class_exists('waTheme')) { - $theme = new waTheme($item['id'], $options['app_id']); - $translation = $theme->getName(); - } elseif (class_exists('waLocalePHPAdapter')) { - $locale_name = ''; - if ($options['type'] == 'widgets') { - $locale_name = sprintf('%s_widget_%s', $options['app_id'], $item['id']); - } elseif ($options['type'] == 'plugins') { - if (strpos($options['app_id'], '/') === false) { - $locale_name = sprintf('%s_%s', $options['app_id'], $item['id']); - } else { - $system_plugin_type = explode('/', $options['app_id'])[1]; - $locale_name = sprintf('%s_%s', $system_plugin_type, $item['id']); - } - } - $locale_adapter = new waLocalePHPAdapter(); - $root_path = wa()->getConfig()->getRootPath(); - $path_to_locale = sprintf('%s/%s/locale', $root_path, $item['path']); - $locale_adapter->load(wa()->getLocale(), $path_to_locale, $locale_name, false); - $translation = $locale_adapter->dgettext($locale_name, $item['installed']['name']); - } - return $translation; - } - - private function sortAppsCallback($a, $b) - { - $a['order'] = self::getActionPriority($a['action']); - $b['order'] = self::getActionPriority($b['action']); - $result = max(-1, min(1, ($b['order'] - $a['order']))); - if ($result == 0) { - //XXX hardcoded order - if ($a['slug'] == 'installer') { - $result = 1; - } elseif ($b['slug'] == 'installer') { - $result = -1; - } else { - if (isset($a['priority']) && isset($b['priority'])) { - $result = max(-1, min(1, ($b['priority'] - $a['priority']))); - } - if ($result == 0) { - $ap = (int)!empty($a['payware']); - $bp = (int)!empty($b['payware']); - if ($ap != $bp) { - $result = $bp - $ap; - } - } - if ($result == 0) { - $result = strcmp($a['name'], $b['name']); - } - } - } - return $result; - } - - private static function getActionPriority($action) - { - $priority = null; - switch ($action) { - case self::ACTION_INSTALL: - $priority = 6; - break; - case self::ACTION_CRITICAL_UPDATE: - $priority = 5; - break; - case self::ACTION_UPDATE: - $priority = 4; - break; - case self::ACTION_INBUILT: - $priority = 3; - break; - case self::ACTION_REPAIR: - $priority = 2; - break; - case self::ACTION_NONE: - $priority = 1; - break; - } - return $priority; - } - - private static function getFolders($path, $pattern = '/^[a-z_\-\d][a-z_\-\d\.]*$/i') - { - $paths = array(); - if (file_exists(self::$root_path.$path)) { - $directoryContent = scandir(self::$root_path.$path); - foreach ($directoryContent as $item_path) { - if (preg_match($pattern, $item_path) && is_dir(self::$root_path.$path.'/'.$item_path)) { - $paths[$item_path] = true; - } - } - } - return $paths; - } - - /** - * opcache workaround - * @var array - */ - private static $configs = array(); - - private static function getConfig($path) - { - $config = array(); - $ml_fields = array('name', 'description'); - if ($path) { - $_path = self::$root_path.$path; - //hack for theme xml - $path_xml = preg_replace('@\.php$@', '.xml', $_path); - if (file_exists($path_xml)) { - if (!function_exists('simplexml_load_file')) { - throw new Exception('PHP extension SimpleXML required'); - } - if ($xml = @simplexml_load_file($path_xml)) { - - foreach ($ml_fields as $field) { - $config[$field] = array(); - } - foreach ($xml->attributes() as $field => $value) { - $config[$field] = (string)$value; - } - - foreach ($ml_fields as $field) { - if ($xml->$field) { - foreach ($xml->$field as $value) { - if ($locale = (string)$value['locale']) { - $config[$field][$locale] = (string)$value; - } - } - } - } - } - } elseif (file_exists($_path)) { - if (!isset(self::$configs[$path])) { - $config = include($_path); - if (!is_array($config)) { - $config = array(); - } - self::$configs[$path] = $config; - } else { - $config = self::$configs[$path]; - } - } - } - - // Build parent theme slug - if (!empty($config['parent_theme_id'])) { - $parent = $config['parent_theme_id']; - list($app_id, $parent_theme_id) = explode(':', $parent); - if (!empty($app_id) && !empty($parent_theme_id)) { - $config['parent_theme_slug'] = "{$app_id}/themes/{$parent_theme_id}"; - } - } - - foreach ($ml_fields as $field) { - if (isset($config[$field]) && is_array($config[$field])) { - $key = array_intersect(array(self::$locale, 'en_US',), array_keys($config[$field])); - $config[$field] = $key ? $config[$field][reset($key)] : reset($config[$field]); - } - } - return $config; - } - - private static function setConfig($path, $config) - { - if (!(is_array($config))) { - throw new Exception('Invalid config'); - } - if (!self::mkdir(dirname($path))) { - throw new Exception('Error make path '.$path); - } - $fp = @fopen(self::$root_path.$path, 'w'); - if (!$fp) { - throw new Exception('Error while save config at '.$path); - } - if (!@flock($fp, LOCK_EX)) { - fclose($fp); - throw new Exception('Unable to lock config file '.$path); - } - fwrite($fp, "updateRoutingConfig($app_id, false); - } elseif ($routing) { - $this->updateRoutingConfig($app_id, $routing); - } - self::setConfig(self::CONFIG_APPS, $config); - return $current; - } - - /** - * - * @param $app_id string - * @param $plugin_id string - * @param $enabled boolean or null to remove - * @return array - * @throws Exception - */ - public function updateAppPluginsConfig($app_id, $plugin_id, $enabled = true) - { - $config = array($plugin_id => $enabled); - $path = sprintf(self::CONFIG_APP_PLUGINS, $app_id); - $config = array_merge(self::getConfig($path), $config); - if (is_null($enabled)) { - unset($config[$plugin_id]); - } - return self::setConfig($path, $config); - } - - /** - * - * @param $app_id string - * @param $routing array - * @param $domain string - * @return string|string[] - * @throws Exception - */ - private function updateRoutingConfig($app_id = 'default', $routing = array(), $domain = null) - { - $result = null; - $current_routing = self::getConfig(self::CONFIG_ROUTING); - if (!$routing) { - foreach ($current_routing as $domain => & $routes) { - if (is_array($routes)) { - foreach ($routes as $route_id => $route) { - if (is_array($route)) { - if (isset($route['app']) && ($route['app'] == $app_id)) { - $routes[$route_id]['temporarily_off'] = true; - } - } else { //route is string - $parts = array_filter(explode('/', $route), 'strlen'); - $route = array_shift($parts); - if ($route == $app_id) { - unset($routes[$route_id]); - } - } - } - } - unset($routes); - } - } else { - if (is_null($domain)) { - if (!empty($current_routing)) { - $domains = array_keys($current_routing); - foreach ($domains as $domain) { - $this->updateRoutingConfig($app_id, $routing, $domain); - } - return $domains; - } else { - $domain = $_SERVER['HTTP_HOST']; - if (isset($_SERVER['SCRIPT_NAME']) && $_SERVER['SCRIPT_NAME']) { - $root_url = $_SERVER['SCRIPT_NAME']; - } elseif (isset($_SERVER['PHP_SELF']) && $_SERVER['PHP_SELF']) { - $root_url = $_SERVER['PHP_SELF']; - } else { - $root_url = '/'; - } - $root_url = preg_replace('!/[^/]*$!', '/', $root_url); - $root_url = trim($root_url, '/'); - if ($root_url) { - $domain .= '/'.$root_url; - } - } - } - - if (!isset($current_routing[$domain])) { - $current_routing[$domain] = array(); - } elseif (!is_array($current_routing[$domain])) { - // When routing is a string, it's domain of redirect-type. - // Nothing to update! - return $domain; - } - - $root_owned = false; - foreach ($current_routing[$domain] as $route) { - $url = is_array($route) ? $route['url'] : $route; - if (strpos($url, '*') === 0) { - $root_owned = true; - break; - } - } - if (($app_id == 'site')) { - $routing['url'] = $root_owned ? "{$app_id}/*" : '*'; - } - - $rule_exists = false; - foreach ($current_routing[$domain] as $_id => $route) { - if (is_array($route)) { //route is array - if (isset($route['app']) && ($route['app'] == $app_id)) { - $rule_exists = true; - unset($current_routing[$domain][$_id]['temporarily_off']); - } - } else { //route is string - $parts = array_filter(explode('/', $route), 'strlen'); - $route = array_shift($parts); - if ($route == $app_id) { - $rule_exists = true; - } - } - } - - if (!$rule_exists) { - if ($root_owned) { - array_unshift($current_routing[$domain], $routing); - } else { - $current_routing[$domain][] = $routing; - } - } - } - self::setConfig(self::CONFIG_ROUTING, $current_routing); - return $domain; - } - - /** - * Update database settings - * @param $config array - * @param $id - * @return void - * @throws Exception - */ - public function updateDbConfig($config = array(), $id = 'default') - { - $config = array($id => $config); - $config = array_merge(self::getConfig(self::CONFIG_DB), $config); - self::setConfig(self::CONFIG_DB, $config); - } - - /** - * Update database settings - * - * @param $config array - * @return array - * @internal param $id - */ - private static function updateGenericConfig($config = array()) - { - $default = array( - 'debug' => false, - 'identity_hash' => md5(mt_rand().__FILE__.(function_exists('php_uname') ? php_uname() : '').phpversion().mt_rand()), - ); - $current = self::getConfig(self::CONFIG_GENERIC); - if (isset($config['identity_hash'])) { - $log = array( - 'Regenerate identity hash procedure.', - ); - if ($config['identity_hash']) { - if (!empty($current['previous_hash'])) { - $log[] = sprintf('Hold previous change %s->%s.', $current['previous_hash'], $current['identity_hash']); - } else { - $config['previous_hash'] = $current['identity_hash']; - $log[] = sprintf('Change hash %s->%s.', $current['identity_hash'], $default['identity_hash']); - unset($current['identity_hash']); - } - } elseif (!empty($current['previous_hash'])) { - $log[] = sprintf('Remove obsolete hash %s.', $current['previous_hash']); - unset($current['previous_hash']); - } else { - $log[] = 'Attempt to remove deleted obsolete hash.'; - unset($current['previous_hash']); - } - if (class_exists('waLog')) { - //waLog::log(implode("\n", $log), 'installer/identity_hash.log'); - } - unset($config['identity_hash']); - } - $config = array_merge($default, $current, $config); - return self::setConfig(self::CONFIG_GENERIC, $config); - } - - /** - * - * Setup generic options - * @param array $options - * @return void - */ - public static function setGenericOptions($options = array()) - { - self::init(); - $allowed_options = array( - 'mod_rewrite' => true, - 'debug' => true, - 'license_key' => true, - 'default_host_domain' => true, - 'ui' => true, - ); - $options = array_intersect_key($options, $allowed_options); - if ($options) { - self::updateGenericConfig($options); - } - } - - /** - * Register applications at config and add routing for it - * - * @param $slug - * @param $domain string domain ID fo - * @param bool|string $edition string application edition - * @return void - * @throws Exception - */ - public function installWebAsystItem($slug, $domain = null, $edition = true) - { - $slugs = explode('/', $slug); - if (count($slugs) == 3) { - switch ($slugs[1]) { - case 'plugins': - $this->updateAppPluginsConfig($slugs[0], $slugs[2]); - break; - case 'themes': - break; - case 'widgets': - //do nothing - break; - default: - throw new Exception("Invalid subject for method ".__METHOD__); - } - } elseif (!preg_match('@^wa-plugins/@', $slug)) { - $this->installWebAsystApp($slug, $domain, $edition); - } - } - - /** - * Register applications at config and add routing for it - * - * @param $app_id string application slug - * @param $domain string domain ID fo - * @param bool|string $edition string application edition - * @return void - */ - public function installWebAsystApp($app_id, $domain = null, $edition = true) - { - $prev = $this->updateAppConfig($app_id, $edition); - $config = self::getConfig(sprintf(self::ITEM_CONFIG, $app_id)); - if (!$prev && !empty($config['frontend'])) { - $routing = array( - 'url' => $app_id.'/*', - 'app' => $app_id, - 'locale' => self::$locale, - ); - if (!empty($config['routing_params']) && is_array($config['routing_params'])) { - foreach ($config['routing_params'] as $routing_param => $routing_param_value) { - if (is_callable($routing_param_value)) { - $config['routing_params'][$routing_param] = call_user_func($routing_param_value); - } - } - $routing = array_merge($routing, $config['routing_params']); - } - $domain = $this->updateRoutingConfig($app_id, $routing, $domain); - foreach ((array)$domain as $_domain) { - $this->addAppRobots($app_id, $routing, $_domain); - } - } - } - - private function addAppRobots($app_id, $routing, $domain) - { - - $path = self::$root_path.sprintf(self::ITEM_ROBOTS, $app_id); - if (file_exists($path)) { - $app_raw_robots = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - $app_robots = array(); - $user_agent = false; - foreach ($app_raw_robots as $str) { - $str = trim($str); - if ($str[1] == '#') { - continue; - } - $str = explode(':', $str, 2); - if (strtolower(trim($str[0])) == 'user-agent') { - $user_agent = trim($str[1]); - } else { - $app_robots[$user_agent][] = array_map('trim', $str); - } - } - - $domain = $domain.'/'.str_replace('/?', '/', preg_replace('/\.?\*$/i', '', $routing['url'])); - $url = preg_replace('@^[^/]+/?@', '/', $domain); - $domain = preg_replace('@/.*$@', '/', $domain); - - $robots = array(); - foreach ($app_robots as $user_agent => $rows) { - $robots[] = 'User-agent: '.$user_agent."\n"; - $robots[] = "# wa ".$app_id." ".$routing['url']."\n"; - foreach ($rows as $row) { - if (strpos($row[1], '[URL]') !== false) { - $row[1] = str_replace('[URL]', $url, $row[1]); - } - $robots[] .= $row[0].": ".$row[1]."\n"; - } - $robots[] .= "# wa ".$app_id."\n"; - $robots[] = "\n"; - } - if (class_exists('waConfig')) { - $robots_path = waConfig::get('wa_path_data')."/public/site/data/{$domain}"; - $robots_path = substr($robots_path, strlen(waSystem::getInstance()->getConfig()->getRootPath()) + 1); - } else { - $robots_path = "wa-data/public/site/data/{$domain}"; - } - - self::mkdir($robots_path); - - $robots_path .= '/robots.txt'; - - if ($fp = @fopen(self::$root_path.$robots_path, 'a')) { - fwrite($fp, "\n".implode("", $robots)); - fclose($fp); - } else { - //TODO log error - } - } - } - - private static function mkdir($target_path, $mode = 0777) - { - if (!@file_exists(self::$root_path.$target_path)) { - if (!@mkdir(self::$root_path.$target_path, $mode & 0777, true)) { - throw new Exception("Error occurred while creating a directory {$target_path} at ".self::$root_path); - } - } elseif (!@is_dir(self::$root_path.$target_path)) { - throw new Exception("Error occurred while creating a directory {$target_path} - it's a file"); - - } elseif (!@is_writable(self::$root_path.$target_path)) { - throw new Exception("Directory {$target_path} unwritable"); - } - if (preg_match('@^/?(wa-data/protected|wa-log|wa-cache|wa-config)(/|$)@', $target_path, $matches)) { - $htaccess_path = $matches[1].'/.htaccess'; - if (!@file_exists(self::$root_path.$htaccess_path)) { - if ($fp = @fopen(self::$root_path.$htaccess_path, 'w')) { - @fwrite($fp, "Deny from all\n"); - @fclose($fp); - } else { - throw new Exception("Error while trying to protect a directory {$target_path} with htaccess"); - } - } - } - return true; - } - - /** - * - * Get applicable action - * @param array $item - * @return string - */ - private static function applicableAction($item) - { - if (!empty($item['installed'])) { - if (!empty($item['installed']['version'])) { - if (isset($item['vendor']) && isset($item['installed']['vendor']) && ($item['vendor'] != $item['installed']['vendor'])) { - $action = self::ACTION_INSTALL; - } elseif (isset($item['edition']) && ($item['edition'] != $item['installed']['edition'])) { - $action = self::ACTION_INSTALL; - } elseif (version_compare($item['version'], $item['installed']['version'], '>')) { - if (!empty($item['inbuilt'])) { - $action = self::ACTION_INBUILT; - } else { - if (isset($item['critical']) && version_compare($item['critical'], $item['installed']['version'], '>')) { - $action = self::ACTION_CRITICAL_UPDATE; - } else { - $action = self::ACTION_UPDATE; - } - } - } else { - $action = self::ACTION_REPAIR; - } - } else { - $action = self::ACTION_UPDATE; - } - } elseif (!empty($item['download_url'])) { - $action = self::ACTION_INSTALL; - } else { - $action = self::ACTION_NONE; - } - return $action; - } - - /** - * - * Query to updates server - * @param string $query - * @param string $vendor - * @param boolean $values return simple array or with keys - * @return bool|mixed|string - * @since 2.0 - */ - private function query($query, $vendor = self::VENDOR_SELF, $values = false) - { - /** @var waInstallerFile $file */ - static $file; - $result = false; - - $updates_url = $this->buildUpdatesUrl('2.0', $vendor); - $url = $updates_url.$query; - if ($this->buildUrl($url)) { - if (!$file) { - $file = new waInstallerFile(); - } - $result = $file->getData($url, 'json', $values); - $headers = $file->getHeaders(); - if (isset($headers['identity_hash'])) { - $config = array( - 'identity_hash' => intval($headers['identity_hash']), - ); - self::updateGenericConfig($config); - } - } - return $result; - } - - /** - * - * Verify that updates allowed - * @throws Exception - */ - public function checkUpdates() - { - if (!$this->sources) { - throw new Exception('Empty sources list'); - } - $requirements = array(); - $requirements['php.curl'] = array( - 'description' => 'Get updates information from update servers', - 'strict' => true, - ); - $requirements['phpini.allow_url_fopen'] = array( - 'description' => 'Get updates information from update servers', - 'strict' => true, - 'value' => 1, - ); - $requirements['rights'] = array( - 'subject' => '.', - 'strict' => true, - 'value' => true, - ); - $messages = array(); - if (!self::checkRequirements($requirements)) { - foreach ($requirements as $requirement) { - if (!$requirement['passed']) { - $messages[] = $requirement['name'].' '.$requirement['warning']; - } else { - $messages = array(); - break; - } - } - if ($messages) { - throw new Exception(implode("\n", $messages)); - } - } - } - - /** - * Actualize sources endpoints for all zones - * Check & update current endpoints zone - */ - public function syncSourcesEndpoints($force = false) - { - $updatable_sources_config_path = self::$root_path . self::CONFIG_SOURCES_UPDATABLE; - if (file_exists($updatable_sources_config_path) && !is_writable($updatable_sources_config_path)) { - if (class_exists('waLog')) { - waLog::log('Cannot update sources: ' . $updatable_sources_config_path . ' is not writable.'); - } - return; - } - - if (!class_exists('waAppSettingsModel') || !class_exists('waNet') || !class_exists('waUtils')) { - return; - } - - $last_sync_time = $this->getAppSettingsModel()->get('webasyst', self::ENDPOINTS_SYNC_TIME_KEY, 0); - if (!$force && time() - $last_sync_time <= self::ENDPOINTS_SYNC_TIMEOUT) { - // already synced - return; - } - - $result = $this->getEndpoints('updates'); - - if (isset($result['status']) && $result['status'] === 'ok' && !empty($result['data']['endpoints'])) { - $sources = $result['data']['endpoints']; - if ($sources != $this->sources_all_zones) { - // update sources config - try { - waUtils::varExportToFile($sources, $updatable_sources_config_path); - } catch (Exception $e) { - if (class_exists('waLog')) { - waLog::log('Failed saving sources endpoints: ' . $e->getMessage()); - } - } - $this->sources_all_zones = $sources; - - $zone_jail = self::getGenericConfig('zone_jail', null); - if (!empty($this->sources_all_zones['zone_jail']) && empty($zone_jail)) { - self::updateGenericConfig(['zone_jail' => $this->sources_all_zones['zone_jail']]); - } - } - } - $this->getAppSettingsModel()->set('webasyst', self::ENDPOINTS_SYNC_TIME_KEY, time()); - } - - private function waNetQuery($url, $timeout = 5) - { - if (empty($url)) { - return null; - } - - return (new waNet([ - 'timeout' => $timeout, - 'format' => waNet::FORMAT_JSON - ]))->query($url); - } - - public function zonedNetQuery(callable $get_url, $timeout = 5, ...$params) - { - $result = null; - $url = $get_url(...$params); - try { - $result = $this->waNetQuery($url, $timeout); - } catch (waNetTimeoutException $e) { - // current endpoints zone is not valid or not accessable - // try other endpoints zones - if (class_exists('installerHelper')) { - $zone_detect_result = installerHelper::getInstaller()->detectBestZone(); - } - if (!empty($zone_detect_result['is_zone_changed'])) { - $url = $get_url(...$params); - try { - $result = $this->waNetQuery($url, $timeout); - } catch (Exception $e) { - if (class_exists('waLog')) { - waLog::log('Failed getting url: ' . $url . ' : ' . $e->getMessage()); - } - } - } - } catch (Exception $e) { - if (class_exists('waLog')) { - waLog::log('Failed getting url: ' . $url . ' : ' . $e->getMessage()); - } - } - return $result; - } - - public function getEndpoints($app) - { - return $this->zonedNetQuery([$this, 'getEndpointsUrl'], 5, $app); - } - - public function detectBestZone() - { - if (empty($this->sources_all_zones['zones'])) { - return null; - } - - $zone_jail = self::getGenericConfig('zone_jail', 'auto'); - if (!empty($zone_jail) && $zone_jail !== 'auto') { - return [ - 'is_zone_changed' => false, - 'zone' => $zone_jail, - ]; - } - - $endpoints_zones = array_keys($this->sources_all_zones['zones']); - $endpoints_zones[] = 'default'; - $zoned_urls = array_reduce($endpoints_zones, function ($result, $zone) { - $result[$zone] = $this->getEndpointsUrl('no', $zone); - return $result; - }, []); - $zoned_urls = array_unique($zoned_urls); - - try { - $multi = curl_multi_init(); - - foreach ($zoned_urls as $zone => $url) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_USERAGENT, sprintf('Webasyst-Framework/%s', wa()->getVersion('webasyst'))); - curl_setopt($ch, CURLOPT_NOBODY, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); - curl_multi_add_handle($multi, $ch); - } - unset($inst); - - $zoned_urls = array_flip($zoned_urls); - $zone = null; - $active = null; - do { - $mrc = curl_multi_exec($multi, $active); - } while ($mrc == CURLM_CALL_MULTI_PERFORM); - - while ($active && $mrc == CURLM_OK) { - if (curl_multi_select($multi) == -1) { - usleep(100); - continue; - } - - do { - $mrc = curl_multi_exec($multi, $active); - } while ($mrc == CURLM_CALL_MULTI_PERFORM); - - if ($mhinfo = curl_multi_info_read($multi)) { - $chinfo = curl_getinfo($mhinfo['handle']); - if ($chinfo['http_code'] == 200) { - $zone = $zoned_urls[$chinfo['url']]; - break; - } - curl_multi_remove_handle($multi, $mhinfo['handle']); - curl_close($mhinfo['handle']); - } - } - curl_multi_close($multi); - } catch (Exception $e) { - return null; - } - - $is_zone_changed = false; - if (!empty($zone) && class_exists('waAppSettingsModel') && - $this->getAppSettingsModel()->get('webasyst', self::ENDPOINTS_ZONE_KEY) !== $zone - ) { - // zone changed - $is_zone_changed = true; - $this->getAppSettingsModel()->set('webasyst', self::ENDPOINTS_ZONE_KEY, $zone); - $this->sources = $this->sources_all_zones; - if (isset($this->sources['zones']) && !empty($this->sources['zones'][$zone])) { - $this->sources = $this->sources['zones'][$zone]; - } - unset($this->sources['zones']); - - if (wa()->appExists('installer')) { - wa('installer')->getConfig()->clearInitDataCache(); - } - } - - return [ - 'is_zone_changed' => $is_zone_changed, - 'zone' => $zone, - ]; - } - - private function getExtrasPath($app_id, $type = 'plugins') - { - if (preg_match('@^wa-plugins/@', $app_id)) { - if ($type !== 'plugins') { - $path = null; - } else { - $path = $app_id; - } - } elseif (('webasyst' == $app_id) && ($type == 'widgets')) { - $path = 'wa-widgets/'; - } else { - $path = 'wa-apps/'.$app_id.'/'.$type; - } - return $path; - } - - /** - * Build url to remote updates server - * @param string $version - * @param string $vendor - * @param null|string $api_method - * @return false|string - * @throws Exception - * @since 3.0 - */ - private function buildUpdatesUrl($version = '3.0', $vendor = self::VENDOR_SELF, $api_method = null, $zone = null) - { - $result = false; - $sources = $this->getSources(self::LIST_APPS, $vendor, $zone); - if (!empty($sources[$vendor])) { - $result = preg_replace('@apps/list/$@', "{$version}/", $sources[$vendor]); - } - - if ($api_method && is_string($api_method)) { - $result .= $api_method.'/'; - } - - return $result; - } - - /** - * Get the address with the information to initialize the Installer app - * @return string - * @throws Exception - * @since 3.0 - */ - public function getInstallerInitUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/init'); - } catch (Exception $e) { - throw new Exception('Installer app cannot be initialized'); - } - } - - public function getInstallerTokenUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/token'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get token'); - } - } - - public function getInstallerFactUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/fact'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to send changes to the inst package'); - } - } - - public function getStoreReviewCoreUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/review/core.init.djs'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to Store Product rate JS API'); - } - } - - public function getInstallerAnnounceUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/announce/2'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get announcements'); - } - } - - public function getInstallerLicenseUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/license'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get license'); - } - } - - public function getInstallerProductsUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/products'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get products'); - } - } - - public function getInstallationStaticIDUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/id'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get static id'); - } - } - - public function getInstallationBetaTestProductDisconnectUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/beta_test_product/disconnect'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get static id'); - } - } - - public function getCheckProductLeaseStatusUrl() - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/product_lease_status'); - } catch (Exception $e) { - throw new Exception('Unable to build URL to get product lease status'); - } - } - - public function getEndpointsUrl($app = null, $zone = null) - { - try { - return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'config/endpoints', $zone) . '?zoned=1&app=' . $app; - } catch (Exception $e) { - throw new Exception('Unable to build URL to get endpoints'); - } - } - - private function getAppSettingsModel() - { - if (!class_exists('waAppSettingsModel')) { - return null; - } - if (empty($this->app_settings_model)) { - $this->app_settings_model = new waAppSettingsModel(); - } - return $this->app_settings_model; - } -} +identity_hash; + } + + /** + * Get install promo_id + * @return string + */ + public function getPromoId() + { + return $this->promo_id; + } + + private static function getServerSignature($raw = false) + { + $signature = array( + 'php' => preg_replace('@([^0-9\\.].*)$@', '', phpversion()), + 'c' => PHP_INT_SIZE, + 'api' => PHP_SAPI, + ); + if (function_exists('php_uname')) { + $signature['os'] = @php_uname('s'); + $signature['r'] = @php_uname('r'); + } elseif (defined('PHP_OS')) { + $signature['os'] = constant('PHP_OS'); + } + + try { + if (class_exists('waDbConnector')) { + $adapter = waDbConnector::getConnection(); + $signature['sql_adapter'] = strtolower(preg_replace('@^waDb(.+)Adapter$@', '$1', get_class($adapter))); + if ($result = $adapter->query('SELECT @@version')) { + if ($version = $adapter->fetch($result)) { + $signature['sql'] = preg_replace('@([^0-9\\.].*)$@', '', reset($version)); + } + } + } + } catch (Exception $ex) { + + } + + return $raw ? $signature : base64_encode(json_encode($signature)); + } + + private static $app_domains = array(); + + private static function getDomains($apps, $raw = false) + { + $d = null; + $a = array(); + if (function_exists('wa')) { + $wa = wa(); + if ($wa && is_object($wa) && method_exists($wa, 'getRouting')) { + if (($routing = $wa->getRouting()) && method_exists($routing, 'getByApp')) { + $d = array(); + foreach ($apps as $app_id) { + if (!in_array($app_id, array('installer', 'webasyst', 'site',))) { + $app_domains = $routing->getByApp($app_id); + foreach ($app_domains as $domain => $route) { + if ($i = strpos($domain, '/')) { + $domain = substr($domain, 0, $i); + } + if (strpos($domain, '.')) { + $domain = preg_replace('@:\d+@', '', $domain); + if (!in_array($domain, $d)) { + $d[] = $domain; + } + if (!isset(self::$app_domains[$app_id])) { + self::$app_domains[$app_id] = array(); + } + if (!in_array($domain, self::$app_domains[$app_id])) { + self::$app_domains[$app_id][] = $domain; + } + + $id = array_search($domain, $d); + if (!isset($s[$app_id])) { + $s[$app_id] = array(); + } + $s[$app_id][] = $id; + } + } + if (!empty($s[$app_id])) { + $a[$app_id] = 0; + foreach ($s[$app_id] as $id) { + $a[$app_id] += 1 << $id; + } + } + } + } + } + } + } + if (!$raw && $d) { + $d = array_slice($d, 0, 10); + $d = implode(':', $d); + } + + $result = compact('a', 'd'); + return $raw ? $result : http_build_query($result); + } + + /** + * Get install domain + * @return mixed + */ + public function getDomain() + { + static $domain = null; + if ($domain === null) { + $domain = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; + $domain = preg_replace('@(^www\.|:\d+$)@', '', $domain); + } + return $domain; + } + + private static function init() + { + if (!isset(self::$root_path)) { + @ini_set("magic_quotes_runtime", 0); + /** @noinspection PhpDeprecationInspection */ + if (version_compare('5.4', PHP_VERSION, '>') && function_exists('set_magic_quotes_runtime') && get_magic_quotes_runtime()) { + /** @noinspection PhpDeprecationInspection */ + @set_magic_quotes_runtime(false); + } + @ini_set('register_globals', 'off'); + + self::$root_path = preg_replace('@([/\\\\]+)@', '/', dirname(__FILE__).'/'); + //XXX fix root path definition + self::$root_path = preg_replace('@(/)wa-installer/lib/classes/?$@', '$1', self::$root_path); + } + + } + + public static function setLocale($locale = null) + { + if (!is_null($locale) && $locale) { + self::$locale = $locale; + } + self::init(); + } + + public function __construct($license = null, $locale = null, $ttl = 600, $force = false, $token = null) + { + self::init(); + $this->license = $license; + $this->token = $token; + /* identity hash */ + $this->identity_hash = self::getGenericConfig('identity_hash'); + $this->beta = self::getGenericConfig('beta'); + if (in_array($this->beta, array(true, '1', 1), true)) { + $this->beta = 'beta'; + } + $this->promo_id = self::getGenericConfig('promo_id'); + if (!$this->identity_hash) { + $this->updateGenericConfig(); + $this->identity_hash = self::getGenericConfig('identity_hash'); + } + self::setLocale($locale); + self::$cache_ttl = max(0, $ttl); + self::$force = $force; + + /* enabled items list */ + if (false && file_exists(self::$root_path.self::CONFIG_APPS)) { + $this->installed_apps = include(self::$root_path.self::CONFIG_APPS); + foreach ($this->installed_apps as $app_id => & $enabled) { + if ($enabled) { + $this->installed_extras[$app_id] = array(); + $this->installed_extras[$app_id]['plugins'] = self::getConfig(sprintf(self::CONFIG_APP_PLUGINS, $app_id)); + $this->installed_extras[$app_id]['themes'] = self::getFolders(sprintf(self::ITEM_EXTRAS_PATH, $app_id, 'themes')); + $this->installed_extras[$app_id]['widgets'] = self::getFolders(sprintf(self::ITEM_EXTRAS_PATH, $app_id, 'widgets')); + $build_path = self::$root_path.sprintf(self::ITEM_BUILD, $app_id); + if (file_exists($build_path)) { + $enabled = max(1, include($build_path)); + } else { + $enabled = 1; + } + + } + unset($enabled); + } + } + + if (!file_exists(self::$root_path.self::CONFIG_SOURCES) && class_exists('waSystem')) { + wa('installer')->event('sources_not_found'); + } + + $path = self::$root_path . self::CONFIG_SOURCES_UPDATABLE; + if (!is_readable($path)) { + $path = self::$root_path . self::CONFIG_SOURCES; + } + + if (is_readable($path)) { + if (class_exists('waConfigCache')) { + $config_cache = waConfigCache::getInstance(); + $this->sources = $this->sources_all_zones = $config_cache->includeFile($path, false); + } else { + $this->sources = $this->sources_all_zones = include($path); + } + + if (isset($this->sources['zones'])) { + $installer_zone = self::getGenericConfig('zone_jail', 'auto'); + if ((empty($installer_zone) || $installer_zone === 'auto') && class_exists('waAppSettingsModel')) { + try { + $installer_zone = $this->getAppSettingsModel()->get('webasyst', self::ENDPOINTS_ZONE_KEY); + } catch (Exception $e) { + // do nothing + } + if (empty($installer_zone) && wa()->getLocale() === 'ru_RU') { + $installer_zone = 'ru'; + } + } + if (!empty($installer_zone) && !empty($this->sources['zones'][$installer_zone])) { + $this->sources = $this->sources['zones'][$installer_zone]; + } + unset($this->sources['zones']); + } + } else { + $this->sources = $this->sources_all_zones = []; + } + + //TODO USE config or etc + $this->extras_list['plugins'] = array( + 'info' => 'plugin', + 'subpath' => 'lib/config/', + ); + $this->extras_list['widgets'] = array( + 'info' => 'theme', + 'subpath' => '', + ); + $this->extras_list['widgets'] = array( + 'info' => 'widget', + 'subpath' => 'lib/config/', + ); + } + + private function getVendors() + { + return array(self::VENDOR_SELF); + } + + private function getSources($key, $vendors = array(), $zone = null) + { + if (!is_array($vendors) && $vendors) { + $vendors = array($vendors); + } + + if (!$this->sources) { + throw new Exception('Empty sources list'); + } + + $sources = $this->sources; + if (!empty($zone)) { + if ($zone === 'default') { + $sources = $this->sources_all_zones; + unset($sources['zones']); + } elseif (!empty($this->sources_all_zones['zones'][$zone])) { + $sources = $this->sources_all_zones['zones'][$zone]; + } + } + + if (empty($sources[$key])) { + throw new Exception(sprintf('Not found sources for %s', $key)); + } + + $result = array(); + foreach ((array)$sources[$key] as $vendor => $source) { + if (!$vendor) { + $vendor = self::VENDOR_SELF; + } + if (!$vendors || in_array($vendor, $vendors)) { + $result[$vendor] = $source; + } + } + return $result; + } + + /** + * + * @param $requirements + * @param bool $update_config + * @param bool $action + * @return boolean + */ + public static function checkRequirements(&$requirements, $update_config = false, $action = false):bool + { + self::init(); + if (is_null($requirements)) { + $requirements = self::getRequirements('wa-installer/lib/config/requirements.php', 'installer'); + } + $passed = true; + $config = array(); + $actions = array( + self::ACTION_CRITICAL_UPDATE, + self::ACTION_UPDATE, + self::ACTION_INSTALL, + true, + ); + $update = $action && in_array($action, $actions, true); + foreach ($requirements as $subject => & $requirement) { + $requirement['passed'] = false; + $requirement['note'] = null; + $requirement['warning'] = false; + $requirement['update'] = $update; + + waInstallerRequirements::test($subject, $requirement); + + $passed = $requirement['passed'] && $passed; + if ($update_config && isset($requirement['config'])) { + $config[$requirement['config']] = $requirement['value']; + } + if ($requirement['note'] && isset($requirement['allow_skip']) && $requirement['allow_skip']) { + unset($requirement); + unset($requirements[$subject]); + } else { + unset($requirement); + } + } + if ($update_config) { + try { + self::updateGenericConfig($config); + } catch (Exception $e) { + $requirements[] = array( + 'name' => '', + 'passed' => false, + 'warning' => $e->getMessage(), + 'description' => '', + 'note' => '', + ); + $passed = false; + } + } + + return $passed; + } + + /** + * + * @param $item + * @param $config + * @return boolean + */ + private static function checkVendor($item, $config = null) + { + $applicable = false; + if (is_null($config) && isset($item['installed'])) { + $config = $item['installed']; + } + if ($config) { + if (isset($config['vendor'])) { + if (isset($item['vendor'])) { + $applicable = (strcasecmp($config['vendor'], $item['vendor']) == 0) ? true : false; + if ($applicable) { + if (isset($item['edition'])) { + $applicable = (strcasecmp($config['edition'], $item['edition']) == 0) ? true : false; + } elseif (!empty($config['edition'])) { + $applicable = false; + } + } + } else { + //TODO + } + + } else { + //XXX allow update, while vendor missing + $applicable = false; + } + } + return $applicable; + } + + /** + * + * @param $path + * @param $slug + * @return array + */ + private static function getRequirements($path, $slug) + { + $requirements = self::getConfig($path); + $fields = array('name', 'description'); + foreach ($requirements as & $requirement) { + foreach ($fields as $field) { + if (isset($requirement[$field]) && is_array($requirement[$field])) { + if (self::$locale && isset($requirement[$field][self::$locale])) { + $value = $requirement[$field][self::$locale]; + } elseif (isset($requirement[$field]['en_US'])) { + $value = $requirement[$field]['en_US']; + } else { + $value = array_shift($requirement[$field]); + } + $requirement[$field] = $value; + } elseif (!isset($requirement[$field])) { + $requirement[$field] = ''; + } else { + $requirement[$field] = _wd($slug, $requirement[$field]); + } + } + if (!isset($requirement['strict'])) { + $requirement['strict'] = false; + } + unset($requirement); + } + return $requirements; + } + + public static function getGenericConfig($property = null, $default = false) + { + self::init(); + $config = self::getConfig(self::CONFIG_GENERIC); + if ($property) { + return isset($config[$property]) ? $config[$property] : $default; + } else { + return $config; + } + } + + /** + * + * Enumerate local items + * @param string $path wa-apps/[%app_id%/(themes|plugins|widgets)/] or wa-plugins/(payment|shipping|sms) + * + * @param array $options + * + * @param array [string]array $options['items'] + * @param array [string]boolean $options['plugins'] + * @param array [string]boolean $options['themes'] + * @param array [string]boolean $options['widgets'] + * @param array [string]boolean $options['list'] + * + * @param array $filter + * @param array [string]mixed $filter['locale'] + * @param array [string]mixed $filter['vendor'] + * @param array [string]boolean $options['plugins'] + * @param array [string]boolean $options['themes'] + * @return array + * @since 2.0 + */ + protected function enumerate($path, $options = array(), $filter = array()) + { + $items = array(); + $path = self::formatPath($path); + $folders = self::getFolders($path); + foreach ($folders as $slug => $exist) { + if ($slug) { + if (isset($options['items']) && !isset($options['items'][$slug])) { + #skip not applicable items + continue; + } + $item = $this->info($path, $slug, $filter); + if ($item) { + if (isset($options['items'])) { + $item['enabled'] = !empty($options['items'][$slug]); + } + $items[$slug] = $item; + } + } + } + return $items; + } + + private function info($path, $slug, $filter = array()) + { + $item = null; + $config_path = self::getConfigPath(trim($path.'/'.$slug, '/')); + $config = $this->getConfig($config_path); + if ($config) { + $config += array( + 'vendor' => self::VENDOR_UNKNOWN, + ); + } + if (empty($filter) || self::filter($config, $filter)) { + $item = array( + 'id' => preg_replace('@^.+/([^/]+)$@', '$1', $slug), + 'slug' => trim(preg_replace('@^/?wa-apps/@', '', $path.'/'.$slug), '/'), + 'path' => $path.'/'.$slug, + 'config_path' => $config_path, + 'installed' => $config ? $config : false, + ); + if ($item['installed']) { + self::fixItemVersion($item); + } + } + return $item; + } + + private static function getConfigPath($path) + { + $path = self::formatPath($path); + if (preg_match('@^wa-apps/[^/]+(/.+|$)@', $path)) { + #it apps or apps extras + if (preg_match('@^wa-apps/([^/]+)/(themes|plugins|widgets)/([^/]+)$@', $path, $matches)) { + switch ($matches[2]) { + case 'themes': + $path .= '/theme.xml'; + break; + case 'plugins': + $path .= '/lib/config/plugin.php'; + break; + case 'widgets': + $path .= '/lib/config/widget.php'; + break; + } + } elseif (preg_match('@^wa-apps/([^/]+)/?$@', $path, $matches)) { + $path .= '/lib/config/app.php'; + } else { + $path = false; + } + } elseif (preg_match('@^wa-plugins/[^/]+/[^/]+@', $path)) { + #system plugins + $path = preg_replace('@^wa-plugins/([^/]+)/plugins/(.+)$@', 'wa-plugins/$1/$2', $path); + $path .= '/lib/config/plugin.php'; + } elseif (preg_match('@^(wa-widgets|webasyst/widgets)/([^/]+)/?$@', $path, $matches)) { + + $path = 'wa-widgets/'.$matches[2].'/lib/config/widget.php'; + } else { + $path = false; + } + return $path; + } + + private static function formatPath($path) + { + $path = preg_replace('@([/\\\\]+)@', '/', $path); + return preg_replace('@([/\\\\]+)$@', '', $path); + } + + /** + * + * Filter item by params at filter + * @param array $item + * @param array $filter + * @return bool + */ + private static function filter($item, $filter = array()) + { + $applicable = true; + foreach ($filter as $field => $value) { + if (isset($item[$field])) { + $v = $item[$field]; + if (is_array($value)) { + if (is_array($item[$field])) { + if (!array_intersect($v, $value)) { + $applicable = false; + break; + } + } elseif (!in_array($v, $value)) { + $applicable = false; + break; + } + } elseif ($value === true) { /* value required */ + if (empty($v)) { + $applicable = false; + break; + } + + } elseif ($value === false) { /* value required */ + $applicable = false; + break; + } else { + if (is_array($value)) { + if (!in_array($value, $v)) { + $applicable = false; + break; + } + } elseif ($v != $value) { + $applicable = false; + break; + } + } + } elseif ($value === true) { /* value required */ + $applicable = false; + break; + } + } + return $applicable; + } + + private function extend(&$item, $options = array()) + { + if ($item) { + if (!empty($options['translate_name']) && isset($item['installed']['name'])) { + $translated_name = self::translateName($item, $options['translate_name']); + if (!empty($translated_name)) { + $item['installed']['name'] = $translated_name; + } + } + if (!empty($options['requirements']) && !isset($item['requirements'])) { + $item['requirements'] = self::getRequirements(sprintf(self::ITEM_REQUIREMENTS, $item['slug']), $item['slug']); + } + self::fixItemCurrent($item, null, array(), empty($options['local'])); + $item['applicable'] = true; + + if (!empty($item['requirements']) && !empty($options['requirements'])) { + $item['applicable'] = $this->checkRequirements($item['requirements'], false, ifset($options['action'])); + } + if (!empty($options['action'])) { + $item['action'] = self::applicableAction($item); + } + } + } + + public function getItems($options = array()) + { + static $items = null; + if ($items === null) { + $options += array( + 'installed' => true, + ); + $extra_types = array( + 'plugins' => array(), + 'themes' => array(), + 'widgets' => array(), + ); + $items = $this->getApps($options); + foreach ($items as $app) { + if (!empty($app['installed']['plugins'])) { + $extra_types['plugins'][] = $app['id']; + } + + if (!empty($app['installed']['themes'])) { + $extra_types['themes'][] = $app['id']; + } + + $extra_types['widgets'][] = $app['id']; + } + $options['status'] = true; + foreach ($extra_types as $type => $extras_apps) { + $extras = $this->getExtras($extras_apps, $type, $options); + foreach ($extras as $app_id => $app) { + foreach ($app[$type] as $extra) { + if (($type != 'themes') || ($extra['id'] != 'default')) { + $items[$app_id][$type][$extra['id']] = $extra; + } + } + } + } + } + return $items; + } + + /** + * @param string $vendor + * @param boolean $check_updates + * @return array[string] + * @since 2.0 + * @todo filter vendor + */ + public function getVersions($vendor = null, $check_updates = false) + { + if (empty($vendor)) { + $vendor = self::VENDOR_SELF; + + } + $versions = array(); + $options = array( + 'widgets' => true, + ); + $apps = $this->getItems($options); + $apps += $this->getSystemItems(true); + + foreach ($apps as $slug => $app) { + if (!empty($app['installed']) && (strpos($slug, 'wa-plugins/') !== 0)) { + $versions[$slug] = $app['installed']['version']; + } + + foreach (array('plugins', 'themes', 'widgets') as $type) { + if (!empty($app[$type])) { + foreach ($app[$type] as $item) { + $versions[$item['slug']] = ifempty($item, 'installed', 'version', ''); + } + } + + } + } + + if ($check_updates) { + $available = $this->query('versions/?slug='.implode(',', array_keys($versions)), $vendor); + foreach ($versions as $slug => $version) { + if (empty($available[$slug]) || version_compare($version, $available[$slug], '>=')) { + unset($versions[$slug]); + } + } + } + + return $versions; + } + + /** + * @param string $vendor + * @param array $new_items list of new items + * @return array + */ + public function getUpdates($vendor = null, $new_items = array()) + { + if (empty($vendor)) { + $vendor = self::VENDOR_SELF; + } + $query = array(); + $versions = $this->getVersions($vendor, false); + if ($new_items) { + foreach ($new_items as $slug => $item) { + $slug = preg_replace('@^wa-plugins/([^/]+)/plugins/(.+)$@', 'wa-plugins/$1/$2', $slug); + $version = isset($versions[$slug]) ? $versions[$slug] : '0'; + $query[$slug] = sprintf('%s=%s', (sprintf('v[%s]', $slug)), rawurlencode($version)); + } + //XXX TODO compare local & new edition & version + } else { + foreach ($versions as $slug => $version) { + $slug = preg_replace('@^wa-plugins/([^/]+)/plugins/(.+)$@', 'wa-plugins/$1/$2', $slug); + $query[$slug] = sprintf('%s=%s', (sprintf('v[%s]', $slug)), rawurlencode($version)); + } + } + if ($query) { + $url = 'updates/?'.implode('&', $query); + $updates = $this->query($url, $vendor, false); + $items = $this->getItems(); + + $items += $this->getSystemItems(true); + //XXX check current edition:versions + foreach ($updates as $app_id => &$item) { + foreach (array('plugins', 'themes', 'widgets') as $type) { + if (!empty($item[$type])) { + foreach ($item[$type] as $extras_id => &$extras_item) { + if (!empty($items[$app_id][$type][$extras_id])) { + $extras_item = array_merge($items[$app_id][$type][$extras_id], $extras_item); + } + $extras_item['app'] = $app_id; + $extras_item['vendor'] = $vendor; + $extras_item['slug'] = $slug = $app_id.'/'.$type.'/'.$extras_id; + if (empty($extras_item['name'])) { + $extras_item['name'] = empty($extras_item['installed']['name']) ? $extras_item['slug'] : $extras_item['installed']['name']; + } + $extras_item['action'] = self::applicableAction($extras_item); + $extras_item['applicable'] = self::checkRequirements($extras_item['requirements'], false, $extras_item['action']); + $this->buildUrl($extras_item['download_url']); + unset($extras_item); + } + } + } + + if (isset($item['version'])) { + + if (!empty($items[$app_id])) { + if (isset($items[$app_id]['plugins'])) { + unset($items[$app_id]['plugins']); + } + if (isset($items[$app_id]['themes'])) { + unset($items[$app_id]['themes']); + } + if (isset($items[$app_id]['widgets'])) { + unset($items[$app_id]['widgets']); + } + $item = array_merge($items[$app_id], $item); + } + $item['vendor'] = $vendor; + $item['slug'] = $slug = $app_id; + $item['action'] = self::applicableAction($item); + $item['applicable'] = self::checkRequirements($item['requirements'], false, $item['action']); + $item['domains'] = isset(self::$app_domains[$app_id]) ? self::$app_domains[$app_id] : array(); + $this->buildUrl($item['download_url']); + } elseif (!empty($items[$app_id])) { + if (empty($item['name'])) { + if (!empty($items[$app_id]['installed']) && is_array($items[$app_id]['installed'])) { + $item['name'] = ifset($items[$app_id]['installed']['name'], ifset($items[$app_id]['name'], $app_id)); + } else { + $item['name'] = ifset($items[$app_id]['name'], $app_id); + } + } + + if (empty($item['icons']) && !empty($items[$app_id]['installed'])) { + if (is_array($items[$app_id]['installed'])) { + $item['icon'] = ifset($items[$app_id]['installed']['icon'], ifset($items[$app_id]['installed']['icons'], array())); + $item['icons'] = $item['icon']; + } + } + $item['action'] = self::ACTION_NONE; + } + unset($item); + } + } else { + $updates = array(); + } + return $updates; + } + + private function getSystemItems($extras = false) + { + $items = array( + 'wa-plugins/sms' => array( + 'slug' => 'wa-plugins/sms', + 'id' => 'sms', + 'icon' => array( + 16 => 'icon16 mobile', + ), + 'name' => _w('SMS'), + 'installed' => true, + 'vendor' => 'webasyst', + 'virtual' => true, + ), + 'wa-plugins/payment' => array( + 'slug' => 'wa-plugins/payment', + 'id' => 'payment', + 'icon' => array( + 16 => 'icon16 dollar', + ), + 'name' => _w('Payment'), + 'installed' => true, + 'vendor' => 'webasyst', + 'virtual' => true, + ), + 'wa-plugins/shipping' => array( + 'slug' => 'wa-plugins/shipping', + 'id' => 'shipping', + 'icon' => array( + 16 => 'icon16 box', + ), + 'name' => _w('Shipping'), + 'installed' => true, + 'vendor' => 'webasyst', + 'virtual' => true, + ), + ); + foreach ($items as $id => &$item) { + + $item['plugins'] = array(); + $item['icons'] = $item['icon']; + if (empty($item['plugins']) && false) { + unset($items[$id]); + } + unset($item); + } + if ($extras) { + $enum_options = array( + 'installed' => true, + ); + $plugins = $this->getExtras(array_keys($items), 'plugins', $enum_options); + + foreach ($plugins as $id => $item) { + if (isset($items[$id]) && !empty($item['plugins'])) { + $items[$id]['plugins'] = $item['plugins']; + } + + } + } + return $items; + } + + public function getServerData($name = null) + { + if ($name) { + return isset($this->server_data[$name]) ? $this->server_data[$name] : null; + } else { + return $this->server_data; + } + } + + /** + * + * @param array $options + * + * @param array [string]string $options['status'] item status at config (enabled|disabled|true - items from config|false - all available); default are enabled + * @param array [string]boolean $options['requirements'] check apps requirements; default are false + * @param array [string]boolean $options['installed'] get local apps only if true; get remote apps if false; merge if not set + * + * @param array $filter + * @param array [string]string $filter['extras'] + * @return array + * @since 2.0 + */ + public function getApps($options = array(), $filter = array()) + { + /*get local apps*/ + $installed = array(); + $options += array( + 'status' => self::STATUS_ENABLED, + ); + $enum_options = array(); + if (isset($options['status'])) { + $installed_apps = self::getConfig(self::CONFIG_APPS); + if ($options['status'] === true) { + $enum_options['items'] = $installed_apps; + } elseif ($options['status'] === false) { + $installed_apps = array_merge(array_fill_keys(array_keys(self::getFolders('wa-apps')), false), $installed_apps); + $enum_options['items'] = $installed_apps; + } else { + switch ($options['status']) { + case self::STATUS_ENABLED: + $installed_apps = array_filter($installed_apps, 'intval'); + $enum_options['items'] = $installed_apps; + break; + case self::STATUS_DISABLED: + $installed_apps = array_diff_assoc($installed_apps, array_filter($installed_apps, 'intval')); + $enum_options['items'] = $installed_apps; + break; + } + } + } + + $enum_filter = array(); + if (!empty($filter['extras'])) { + if (!is_array($filter['extras'])) { + $enum_filter[$filter['extras']] = true; + } + } + if (!isset($options['installed']) || !empty($options['installed'])) { + if (!empty($filter['extras']) && is_array($filter['extras'])) { + $apps = array(); + foreach ($filter['extras'] as $extras_type) { + $enum_filter[$extras_type] = true; + $apps += $this->enumerate('wa-apps', $enum_options, $enum_filter); + unset($enum_filter[$extras_type]); + } + } else { + $apps = $this->enumerate('wa-apps', $enum_options, $enum_filter); + } + + $installed = $apps; + } else { + $apps = array(); + } + + if (!empty($options['system'])) { + $apps += $this->getSystemItems(); + } + + if (!empty($options['widgets'])) { + $apps += array( + 'webasyst' => array( + 'name' => 'Webasyst framework', + 'id' => 'webasyst', + 'slug' => 'webasyst', + 'enabled' => true, + ), + ); + } + + + /* get available apps */ + if (empty($options['installed'])) { + $list_filter = array(); + + if (!empty($filter['extras'])) { + $list_filter[$filter['extras']] = true; + } + + if (isset($options['local']) && empty($options['local'])) { + $apps = array(); + } + + foreach ($this->getVendors() as $vendor) { + $url = 'apps/'; + if (!empty($options['filter'])) { + $filter = array(); + foreach ($options['filter'] as $param => $value) { + $filter[] = "filter[{$param}]={$value}"; + } + $url .= '?'.implode('&', $filter); + } + $list = $this->query($url, $vendor); + foreach ($list as $app_id => $app) { + if (strpos($app_id, '::') === 0) { + $this->server_data[substr($app_id, 2)] = $app; + } else { + if (empty($list_filter) || self::filter($app, $list_filter)) { + + if (!empty($installed[$app_id])) { + $apps[$app_id] = array_merge($installed[$app_id], $app); + } else { + $app += array( + 'id' => $app_id, + 'slug' => $app_id, + 'vendor' => $vendor, + 'installed' => null, + ); + $apps[$app_id] = $app; + } + } elseif (!empty($installed[$app_id])) { + unset($apps[$app_id]); + } + } + } + } + } else { + if (isset($options['local'])) { + trigger_error(sprintf('Parameter $option[%s] has no effect at %s', 'local', __METHOD__), E_USER_NOTICE); + } + } + + foreach ($apps as & $app) { + $this->extend($app, $options); + } + unset($app); + + /* optional check applicable updates */ + + return $apps; + + } + + /** + * @param $app + * @param $type + * @param array $options + * @return array + * @throws Exception + * @since 2.0 + */ + public function getExtras($app, $type, $options = array()) + { + if (!in_array($type, array('plugins', 'themes', 'widgets'))) { + throw new Exception('Invalid extras type'); + } + /*get local extras*/ + $options += array( + 'status' => self::STATUS_ENABLED, + ); + + $enum_options = array(); + if (isset($options['status'])) { + if ($type == 'plugins') { + foreach ((array)$app as $app_id) { + if (strpos($app_id, 'wa-plugins/') === false) { + $installed = self::getConfig(sprintf(self::CONFIG_APP_PLUGINS, $app_id)); + if ($options['status'] === true) { + $enum_options[$app_id] = array('items' => $installed); + } elseif ($options['status'] === false) { + $path = $this->getExtrasPath($app_id, $type); + $installed = array_merge(array_fill_keys(array_keys(self::getFolders($path)), false), $installed); + $enum_options[$app_id] = array('items' => $installed); + } else { + switch ($options['status']) { + case self::STATUS_ENABLED: + $installed = array_filter($installed, 'intval'); + $enum_options[$app_id] = array('items' => $installed); + break; + case self::STATUS_DISABLED: + $installed = array_diff_assoc($installed, array_filter($installed, 'intval')); + $enum_options[$app_id] = array('items' => $installed); + break; + } + } + } + } + } + } + + if (!isset($options['installed']) || !empty($options['installed'])) { + $extras = array(); + foreach ((array)$app as $app_id) { + $path = $this->getExtrasPath($app_id, $type); + if ($path) { + $enum_filter = array(); + if (!empty($options['filter'])) { + if (!empty($options['filter']['vendor'])) { + $enum_filter['vendor'] = $options['filter']['vendor']; + } + } + + $items = $this->enumerate($path, isset($enum_options[$app_id]) ? $enum_options[$app_id] : array(), $enum_filter); + + $extend_options = array(); + if (!empty($options['translate_titles'])) { + $extend_options = array( + 'translate_name' => array( + 'type' => $type, + 'app_id' => $app_id, + ) + ); + } + + foreach ($items as &$extras_item) { + self::extend($extras_item, $extend_options); + unset($extras_item); + } + $extras[$app_id] = array( + $type => $items, + ); + unset($items); + } + } + $installed = $extras; + } else { + $installed = array(); + $extras = array(); + } + + /* get available apps */ + if (empty($options['installed'])) { + if (isset($options['local']) && empty($options['local'])) { + $installed = $extras; + $extras = array(); + } + + foreach ($this->getVendors() as $vendor) { + + $url = $type.'/?app_id='.implode(',', (array)$app); + if (!empty($options['filter'])) { + foreach ($options['filter'] as $param => $value) { + $url .= "&filter[{$param}]={$value}"; + } + } + if (!empty($options['inherited'])) { + $url .= '&inherited='.implode(',', (array)$options['inherited']); + } + + $list = $this->query($url, $vendor); + + $count = count(self::$sort); + foreach ($list as $app_id => $available_extras) { + if (strpos($app_id, '::') === 0) { + $this->server_data[substr($app_id, 2)] = $available_extras; + } else { + if (!isset($extras[$app_id])) { + if (!isset($options['apps']) || !empty($options['apps'])) { + $enum_filter = array($type => true); + $extras[$app_id] = $this->info('wa-apps', $app_id, $enum_filter); + } else { + $extras[$app_id] = array($type => array()); + } + } + foreach ($available_extras[$type] as $extras_id => $extras_item) { + if (!isset(self::$sort[$extras_id])) { + self::$sort[$extras_id] = ++$count; + } + if (!empty($installed[$app_id][$type][$extras_id])) { + $_installed = $installed[$app_id][$type][$extras_id]; + if (!empty($extras_item['inherited'])) { + foreach ($extras_item['inherited'] as $inherited_app_id => $inherited) { + + $inherited_extras_id = preg_replace('@(^|^.+/)([^/]+)$@', '$2', $inherited['slug']); + if (empty($installed[$inherited_app_id][$type][$inherited_extras_id])) { + $_installed = array( + 'id' => $extras_id, + 'slug' => $app_id.'/'.$type.'/'.$extras_id, + 'vendor' => $vendor, + 'installed' => null, + ); + break; + } + } + + } + $extras[$app_id][$type][$extras_id] = array_merge($_installed, $extras_item); + } else { + $extras_item += array( + 'id' => $extras_id, + 'slug' => $app_id.'/'.$type.'/'.$extras_id, + 'vendor' => $vendor, + 'installed' => null, + ); + $extras[$app_id][$type][$extras_id] = $extras_item; + } + } + } + } + + foreach ($extras as &$available_extras) { + uksort($available_extras[$type], array(__CLASS__, 'sort')); + unset($available_extras); + } + } + } + + return $extras; + } + + private static function sort($a, $b) + { + static $min = 0; + if (!isset(self::$sort[$a])) { + self::$sort[$a] = --$min; + } + if (!isset(self::$sort[$b])) { + self::$sort[$b] = --$min; + } + return max(-1, min(1, (self::$sort[$a] - self::$sort[$b]))); + } + + /** + * @param $slug + * @param array $options + * @return array(); + * @throws Exception + * @since 2.0 + */ + public function getItemInfo($slug, $options = array()) + { + $info = null; + $id = preg_replace('@/.*$@', '', $slug); + if (empty($options['local'])) { + switch ($id) { + case 'wa-plugins': + $info = $this->query('app/'.$slug.'/'); + break; + default: + //TODO send installation hash + $url = 'app/'; + if (!empty($options['inherited'])) { + $url .= implode(',', (array)$options['inherited']).','; + } + $url .= $slug; + $info = $this->query($url = preg_replace('@,?\\*@', '', $url).'/'); + break; + } + } else { + $info = array(); + } + + if ($info || !empty($options['local'])) { + if ($id == '*') { + $slug = preg_replace('@^\\*/@', 'site/', $slug); + } + $exists = !!$info; + $info += array( + 'installed' => null, + 'applicable' => null, + 'slug' => $slug, + ); + + $installed_apps = self::getConfig(self::CONFIG_APPS); + if (!empty($installed_apps[$id]) || ($id == 'wa-plugins') || ($id == 'webasyst') || ($id == '*')) { + $filter = array(); + if (!empty($options['vendor'])) { + $filter['vendor'] = $options['vendor']; + } + switch ($id) { + case 'webasyst': + $key = ''; + break; + case 'wa-plugins': + $key = ''; + break; + default: + $key = 'wa-apps'; + break; + } + $info = array_merge($info, $this->info($key, $info['slug'], $filter)); + if (!$exists && empty($info['installed'])) { + $info = array(); + } elseif (!empty($info['inherited'])) { + foreach ($info['inherited'] as $inherited_app_id => $inherited) { + $inherited_info = $this->info(($inherited_app_id == 'wa-plugins') ? '' : 'wa-apps', $inherited['slug'], $filter); + if (empty($inherited_info['installed'])) { + $info['installed'] = false; + break; + } + } + } + } + $this->extend($info, $options); + } + return $info; + + } + + /** + * Fix item images and icons names + * @param $item + * @param null $id + * @return void + */ + private static function fixItemIcon(&$item, $id = null) + { + if (!$id) { + $id = (isset($item['id']) && $item['id']) ? $item['id'] : $item['slug']; + } + if (isset($item['installed']) && !empty($item['installed']) !== false) { + $l = &$item['installed']; + $sizes = array(48, 24, 16); + if (!empty($l['icon']) && !is_array($l['icon'])) { + $l['icon'] = array(48 => $l['icon']); + } + + if (!isset($l['img'])) { + if (!isset($l['icon'])) { + $l['img'] = sprintf(self::ITEM_ICON, $id); + } elseif (isset($l['icon'][reset($sizes)])) { + $l['img'] = $l['icon'][reset($sizes)]; + } else { + $l['img'] = sprintf(self::ITEM_ICON, $id); + } + } + if (!isset($l['icon'])) { + $l['icon'] = array(); + } + + foreach ($sizes as $size) { + if (!isset($l['icon'][$size])) { + $l['icon'][$size] = $l['img']; + } + } + + + foreach ($l['icon'] as &$i) { + $i = wa()->getRootUrl().'wa-apps/'.$item['slug'].'/'.$i; + }; + unset($i); + if (empty($item['icons'])) { + $item['icons'] = $l['icon']; + } + if (!isset($item['icon'])) { + if (!empty($item['icons'])) { + $icon_id = max(array_keys($item['icons'])); + $item['icon'] = $item['icons'][$icon_id]; + } + } + + } + + } + + private function buildUrl(&$path) + { + if (is_array($path)) { + $is_url = true; + foreach ($path as &$chunk) { + $is_url = $this->buildUrl($chunk) && $is_url; + unset($chunk); + } + } else { + $is_url = preg_match('@^https?://@', $path); + if (($this->license || $this->identity_hash || $this->beta) && $is_url && $this->originalUrl($path)) { + $query = parse_url($path, PHP_URL_QUERY); + if ($this->license) { + $query = $query.($query ? '&' : '').'license='.$this->license; + } + if ($this->identity_hash) { + $query = $query.($query ? '&' : '').'hash='.$this->identity_hash; + } + if ($identity_hash = $this->getGenericConfig('previous_hash')) { + $query = $query.($query ? '&' : '').'previous_hash='.$identity_hash; + } + if (!empty($this->token)) { + $query = $query.($query ? '&' : '').'token='.$this->token; + } + if ($this->promo_id) { + $query = $query.($query ? '&' : '').'promo_id='.$this->promo_id; + } + $domain = $this->getDomain(); + if ($domain) { + $query = $query.($query ? '&' : '').'domain='.urlencode(base64_encode($domain)); + } + if (preg_match('@/(download|archive)/@', $path)) { + $query = $query.($query ? '&' : '').'signature='.urlencode(self::getServerSignature()); + + if ($this->beta && preg_match('@/archive/@', $path)) { + $path = preg_replace('@/(archive)/@', "/\$1/{$this->beta}/", $path, 1); + } + } + + if (preg_match('@/versions/\?@', $path)) { + if ($this->beta) { + $path = preg_replace('@/versions/\?@', "/versions/{$this->beta}/?", $path, 1); + } + } + + if (preg_match('@/updates/\?@', $path)) { + if ($this->beta) { + $path = preg_replace('@/updates/\?@', "/updates/{$this->beta}/?", $path, 1); + } + parse_str($path, $raw); + if (!empty($raw['v']) && ($raw['v'] = array_filter($raw['v']))) { + $stack = debug_backtrace(); + foreach ($stack as $s) { + if (isset($s['class']) && isset($s['function'])) { + if (md5($s['class'].$s['function']) == '4e78e0ba4240bbbcb818f122201cc5b6') { + $raw['v'] = array(); + break; + } + } + } + if ($apps = array_filter(array_keys($raw['v']), array($this, 'buildUrlCallback'))) { + $query = $query.($query ? '&' : '').self::getDomains($apps); + } + } + } + + $path = preg_replace("@\?.*$@", '', $path); + $path .= '?'.$query; + } + if (self::$locale && $is_url) { + $query = parse_url($path, PHP_URL_QUERY); + $query .= '&locale='.self::$locale; + $path = preg_replace("@\?.*$@", '', $path); + $path .= '?'.$query; + } + } + return $is_url; + } + + private function buildUrlCallback($a) + { + return strpos($a, "/") === false; + } + + private function originalUrl($url) + { + static $original_host; + if (!$original_host) { + foreach ((array)$this->sources['apps'] as $vendor => $source) { + if (!$vendor) { + $vendor = self::VENDOR_SELF; + } + if ($vendor == self::VENDOR_SELF) { + $original_host = parse_url($source, PHP_URL_HOST); + $original_host = preg_replace('@(^www\.|:\d+$)@', '', $original_host); + } + } + } + $host = parse_url($url, PHP_URL_HOST); + return $original_host && (($original_host == $host) || (strpos($host, '.'.$original_host))); + } + + /** + * + * @param array $item + * @param string $id + * @param array $fields + * @param bool $no_translate + * @todo rename and use corrected names + */ + private static function fixItemCurrent(&$item, $id = null, $fields = array(), $no_translate = false) + { + if (!$id) { + $id = empty($item['id']) ? ($item['slug']) : $item['id']; + } + + $item['action'] = self::ACTION_NONE; + if (empty($item['requirements'])) { + $item['requirements'] = array(); + } + $item['applicable'] = true; + + $fields += array( + 'id', + 'slug', + 'name', + 'description', + 'vendor_name' => 'vendor', + 'system', + 'vendor', + 'commercial', + 'version', + ); + + foreach ($fields as $field => $source) { + if (is_numeric($field)) { + $field = $source; + } + if (empty($item[$field])) { + if (!empty($item['installed'][$source])) { + $item[$field] = $item['installed'][$source]; + } elseif (!isset($item[$field])) { + $item[$field] = ''; + } + } + + } + $ml_fileds = array('name', 'description'); + foreach ($ml_fileds as $field) { + if (isset($item[$field])) { + if (is_array($item[$field])) { + $item[$field] = isset($item[$field][self::$locale]) ? $item[$field][self::$locale] : current($item[$field]); + } elseif (!empty($item[$field]) && function_exists('_wd') && !$no_translate) { + $item[$field] = _wd($id, $item[$field]); + } + } + } + if (empty($item['app'])) { + if (preg_match('@^(.+)/(plugins|widgets)/@', $item['slug'], $matches)) { + $item['app'] = $matches[1]; + } elseif (preg_match('@^wa-widgets/@', $item['slug'], $matches)) { + $item['app'] = 'webasyst'; + } + } + + $remap = array( + 'vendor_name' => 'vendor', + ); + foreach ($remap as $target => $source) { + if (empty($item[$target])) { + $item[$target] = $item[$source]; + } + } + if (!preg_match('@^wa-plugins/@', $item['slug'])) { + self::fixItemIcon($item); + } + if (isset($item['installed']['version']) && ($item['installed']['version'] === '0.0.0.0')) { + if (isset($item['version']) && preg_match('@^1\.0\.\d+$@', $item['version'])) { + $item['installed']['version'] = $item['version']; + } + } + } + + private static function fixItemVersion(&$item, $id = null, $extras_info = null) + { + if ((!isset($item['installed']['version']) || !$item['installed']['version']) && !empty($item['installed'])) { + if (isset($item['version'])) { + $item['installed']['version'] = $item['version']; + } else { + $item['installed']['version'] = '0.0.0'; + } + } + + if (is_null($id) && isset($item['slug'])) { + $id = $item['slug']; + } + if ($id && !empty($item['installed'])) { + if (empty($item['config_path'])) { + if (is_null($extras_info)) { + $build_path = self::$root_path.sprintf(self::ITEM_BUILD, $id); + } else { + $build_path = self::$root_path.sprintf(self::ITEM_EXTRAS_PATH, $id, $extras_info['subpath']).'build.php'; + } + } else { + $build_path = self::$root_path.dirname($item['config_path']).'/build.php'; + } + if (file_exists($build_path) && ($build = include($build_path))) { + $item['installed']['version'] .= ".{$build}"; + } elseif (preg_match('/((^|\\.)[\\d]+){2,3}$/', $item['installed']['version'])) { + $item['installed']['version'] .= ".p"; + } + } + } + + protected static function translateName($item, $options) + { + $translation = ''; + if ($options['type'] == 'themes' && class_exists('waTheme')) { + $theme = new waTheme($item['id'], $options['app_id']); + $translation = $theme->getName(); + } elseif (class_exists('waLocalePHPAdapter')) { + $locale_name = ''; + if ($options['type'] == 'widgets') { + $locale_name = sprintf('%s_widget_%s', $options['app_id'], $item['id']); + } elseif ($options['type'] == 'plugins') { + if (strpos($options['app_id'], '/') === false) { + $locale_name = sprintf('%s_%s', $options['app_id'], $item['id']); + } else { + $system_plugin_type = explode('/', $options['app_id'])[1]; + $locale_name = sprintf('%s_%s', $system_plugin_type, $item['id']); + } + } + $locale_adapter = new waLocalePHPAdapter(); + $root_path = wa()->getConfig()->getRootPath(); + $path_to_locale = sprintf('%s/%s/locale', $root_path, $item['path']); + $locale_adapter->load(wa()->getLocale(), $path_to_locale, $locale_name, false); + $translation = $locale_adapter->dgettext($locale_name, $item['installed']['name']); + } + return $translation; + } + + private function sortAppsCallback($a, $b) + { + $a['order'] = self::getActionPriority($a['action']); + $b['order'] = self::getActionPriority($b['action']); + $result = max(-1, min(1, ($b['order'] - $a['order']))); + if ($result == 0) { + //XXX hardcoded order + if ($a['slug'] == 'installer') { + $result = 1; + } elseif ($b['slug'] == 'installer') { + $result = -1; + } else { + if (isset($a['priority']) && isset($b['priority'])) { + $result = max(-1, min(1, ($b['priority'] - $a['priority']))); + } + if ($result == 0) { + $ap = (int)!empty($a['payware']); + $bp = (int)!empty($b['payware']); + if ($ap != $bp) { + $result = $bp - $ap; + } + } + if ($result == 0) { + $result = strcmp($a['name'], $b['name']); + } + } + } + return $result; + } + + private static function getActionPriority($action) + { + $priority = null; + switch ($action) { + case self::ACTION_INSTALL: + $priority = 6; + break; + case self::ACTION_CRITICAL_UPDATE: + $priority = 5; + break; + case self::ACTION_UPDATE: + $priority = 4; + break; + case self::ACTION_INBUILT: + $priority = 3; + break; + case self::ACTION_REPAIR: + $priority = 2; + break; + case self::ACTION_NONE: + $priority = 1; + break; + } + return $priority; + } + + private static function getFolders($path, $pattern = '/^[a-z_\-\d][a-z_\-\d\.]*$/i') + { + $paths = array(); + if (file_exists(self::$root_path.$path)) { + $directoryContent = scandir(self::$root_path.$path); + foreach ($directoryContent as $item_path) { + if (preg_match($pattern, $item_path) && is_dir(self::$root_path.$path.'/'.$item_path)) { + $paths[$item_path] = true; + } + } + } + return $paths; + } + + /** + * opcache workaround + * @var array + */ + private static $configs = array(); + + private static function getConfig($path) + { + $config = array(); + $ml_fields = array('name', 'description'); + if ($path) { + $_path = self::$root_path.$path; + //hack for theme xml + $path_xml = preg_replace('@\.php$@', '.xml', $_path); + if (file_exists($path_xml)) { + if (!function_exists('simplexml_load_file')) { + throw new Exception('PHP extension SimpleXML required'); + } + if ($xml = @simplexml_load_file($path_xml)) { + + foreach ($ml_fields as $field) { + $config[$field] = array(); + } + foreach ($xml->attributes() as $field => $value) { + $config[$field] = (string)$value; + } + + foreach ($ml_fields as $field) { + if ($xml->$field) { + foreach ($xml->$field as $value) { + if ($locale = (string)$value['locale']) { + $config[$field][$locale] = (string)$value; + } + } + } + } + } + } elseif (file_exists($_path)) { + if (!isset(self::$configs[$path])) { + $config = include($_path); + if (!is_array($config)) { + $config = array(); + } + self::$configs[$path] = $config; + } else { + $config = self::$configs[$path]; + } + } + } + + // Build parent theme slug + if (!empty($config['parent_theme_id'])) { + $parent = $config['parent_theme_id']; + list($app_id, $parent_theme_id) = explode(':', $parent); + if (!empty($app_id) && !empty($parent_theme_id)) { + $config['parent_theme_slug'] = "{$app_id}/themes/{$parent_theme_id}"; + } + } + + foreach ($ml_fields as $field) { + if (isset($config[$field]) && is_array($config[$field])) { + $key = array_intersect(array(self::$locale, 'en_US',), array_keys($config[$field])); + $config[$field] = $key ? $config[$field][reset($key)] : reset($config[$field]); + } + } + return $config; + } + + private static function setConfig($path, $config) + { + if (!(is_array($config))) { + throw new Exception('Invalid config'); + } + if (!self::mkdir(dirname($path))) { + throw new Exception('Error make path '.$path); + } + $fp = @fopen(self::$root_path.$path, 'w'); + if (!$fp) { + throw new Exception('Error while save config at '.$path); + } + if (!@flock($fp, LOCK_EX)) { + fclose($fp); + throw new Exception('Unable to lock config file '.$path); + } + fwrite($fp, "updateRoutingConfig($app_id, false); + } elseif ($routing) { + $this->updateRoutingConfig($app_id, $routing); + } + self::setConfig(self::CONFIG_APPS, $config); + return $current; + } + + /** + * + * @param $app_id string + * @param $plugin_id string + * @param $enabled boolean or null to remove + * @return array + * @throws Exception + */ + public function updateAppPluginsConfig($app_id, $plugin_id, $enabled = true) + { + $config = array($plugin_id => $enabled); + $path = sprintf(self::CONFIG_APP_PLUGINS, $app_id); + $config = array_merge(self::getConfig($path), $config); + if (is_null($enabled)) { + unset($config[$plugin_id]); + } + return self::setConfig($path, $config); + } + + /** + * + * @param $app_id string + * @param $routing array + * @param $domain string + * @return string|string[] + * @throws Exception + */ + private function updateRoutingConfig($app_id = 'default', $routing = array(), $domain = null) + { + $result = null; + $current_routing = self::getConfig(self::CONFIG_ROUTING); + if (!$routing) { + foreach ($current_routing as $domain => & $routes) { + if (is_array($routes)) { + foreach ($routes as $route_id => $route) { + if (is_array($route)) { + if (isset($route['app']) && ($route['app'] == $app_id)) { + $routes[$route_id]['temporarily_off'] = true; + } + } else { //route is string + $parts = array_filter(explode('/', $route), 'strlen'); + $route = array_shift($parts); + if ($route == $app_id) { + unset($routes[$route_id]); + } + } + } + } + unset($routes); + } + } else { + if (is_null($domain)) { + if (!empty($current_routing)) { + $domains = array_keys($current_routing); + foreach ($domains as $domain) { + $this->updateRoutingConfig($app_id, $routing, $domain); + } + return $domains; + } else { + $domain = $_SERVER['HTTP_HOST']; + if (isset($_SERVER['SCRIPT_NAME']) && $_SERVER['SCRIPT_NAME']) { + $root_url = $_SERVER['SCRIPT_NAME']; + } elseif (isset($_SERVER['PHP_SELF']) && $_SERVER['PHP_SELF']) { + $root_url = $_SERVER['PHP_SELF']; + } else { + $root_url = '/'; + } + $root_url = preg_replace('!/[^/]*$!', '/', $root_url); + $root_url = trim($root_url, '/'); + if ($root_url) { + $domain .= '/'.$root_url; + } + } + } + + if (!isset($current_routing[$domain])) { + $current_routing[$domain] = array(); + } elseif (!is_array($current_routing[$domain])) { + // When routing is a string, it's domain of redirect-type. + // Nothing to update! + return $domain; + } + + $root_owned = false; + foreach ($current_routing[$domain] as $route) { + $url = is_array($route) ? $route['url'] : $route; + if (strpos($url, '*') === 0) { + $root_owned = true; + break; + } + } + if (($app_id == 'site')) { + $routing['url'] = $root_owned ? "{$app_id}/*" : '*'; + } + + $rule_exists = false; + foreach ($current_routing[$domain] as $_id => $route) { + if (is_array($route)) { //route is array + if (isset($route['app']) && ($route['app'] == $app_id)) { + $rule_exists = true; + unset($current_routing[$domain][$_id]['temporarily_off']); + } + } else { //route is string + $parts = array_filter(explode('/', $route), 'strlen'); + $route = array_shift($parts); + if ($route == $app_id) { + $rule_exists = true; + } + } + } + + if (!$rule_exists) { + if ($root_owned) { + array_unshift($current_routing[$domain], $routing); + } else { + $current_routing[$domain][] = $routing; + } + } + } + self::setConfig(self::CONFIG_ROUTING, $current_routing); + return $domain; + } + + /** + * Update database settings + * @param $config array + * @param $id + * @return void + * @throws Exception + */ + public function updateDbConfig($config = array(), $id = 'default') + { + $config = array($id => $config); + $config = array_merge(self::getConfig(self::CONFIG_DB), $config); + self::setConfig(self::CONFIG_DB, $config); + } + + /** + * Update database settings + * + * @param $config array + * @return array + * @internal param $id + */ + private static function updateGenericConfig($config = array()) + { + $default = array( + 'debug' => false, + 'identity_hash' => md5(mt_rand().__FILE__.(function_exists('php_uname') ? php_uname() : '').phpversion().mt_rand()), + ); + $current = self::getConfig(self::CONFIG_GENERIC); + if (isset($config['identity_hash'])) { + $log = array( + 'Regenerate identity hash procedure.', + ); + if ($config['identity_hash']) { + if (!empty($current['previous_hash'])) { + $log[] = sprintf('Hold previous change %s->%s.', $current['previous_hash'], $current['identity_hash']); + } else { + $config['previous_hash'] = $current['identity_hash']; + $log[] = sprintf('Change hash %s->%s.', $current['identity_hash'], $default['identity_hash']); + unset($current['identity_hash']); + } + } elseif (!empty($current['previous_hash'])) { + $log[] = sprintf('Remove obsolete hash %s.', $current['previous_hash']); + unset($current['previous_hash']); + } else { + $log[] = 'Attempt to remove deleted obsolete hash.'; + unset($current['previous_hash']); + } + if (class_exists('waLog')) { + //waLog::log(implode("\n", $log), 'installer/identity_hash.log'); + } + unset($config['identity_hash']); + } + $config = array_merge($default, $current, $config); + return self::setConfig(self::CONFIG_GENERIC, $config); + } + + /** + * + * Setup generic options + * @param array $options + * @return void + */ + public static function setGenericOptions($options = array()) + { + self::init(); + $allowed_options = array( + 'mod_rewrite' => true, + 'debug' => true, + 'license_key' => true, + 'default_host_domain' => true, + 'ui' => true, + ); + $options = array_intersect_key($options, $allowed_options); + if ($options) { + self::updateGenericConfig($options); + } + } + + /** + * Register applications at config and add routing for it + * + * @param $slug + * @param $domain string domain ID fo + * @param bool|string $edition string application edition + * @return void + * @throws Exception + */ + public function installWebAsystItem($slug, $domain = null, $edition = true) + { + $slugs = explode('/', $slug); + if (count($slugs) == 3) { + switch ($slugs[1]) { + case 'plugins': + $this->updateAppPluginsConfig($slugs[0], $slugs[2]); + break; + case 'themes': + break; + case 'widgets': + //do nothing + break; + default: + throw new Exception("Invalid subject for method ".__METHOD__); + } + } elseif (!preg_match('@^wa-plugins/@', $slug)) { + $this->installWebAsystApp($slug, $domain, $edition); + } + } + + /** + * Register applications at config and add routing for it + * + * @param $app_id string application slug + * @param $domain string domain ID fo + * @param bool|string $edition string application edition + * @return void + */ + public function installWebAsystApp($app_id, $domain = null, $edition = true) + { + $prev = $this->updateAppConfig($app_id, $edition); + $config = self::getConfig(sprintf(self::ITEM_CONFIG, $app_id)); + if (!$prev && !empty($config['frontend'])) { + $routing = array( + 'url' => $app_id.'/*', + 'app' => $app_id, + 'locale' => self::$locale, + ); + if (!empty($config['routing_params']) && is_array($config['routing_params'])) { + foreach ($config['routing_params'] as $routing_param => $routing_param_value) { + if (is_callable($routing_param_value)) { + $config['routing_params'][$routing_param] = call_user_func($routing_param_value); + } + } + $routing = array_merge($routing, $config['routing_params']); + } + $domain = $this->updateRoutingConfig($app_id, $routing, $domain); + foreach ((array)$domain as $_domain) { + $this->addAppRobots($app_id, $routing, $_domain); + } + } + } + + private function addAppRobots($app_id, $routing, $domain) + { + + $path = self::$root_path.sprintf(self::ITEM_ROBOTS, $app_id); + if (file_exists($path)) { + $app_raw_robots = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $app_robots = array(); + $user_agent = false; + foreach ($app_raw_robots as $str) { + $str = trim($str); + if ($str[1] == '#') { + continue; + } + $str = explode(':', $str, 2); + if (strtolower(trim($str[0])) == 'user-agent') { + $user_agent = trim($str[1]); + } else { + $app_robots[$user_agent][] = array_map('trim', $str); + } + } + + $domain = $domain.'/'.str_replace('/?', '/', preg_replace('/\.?\*$/i', '', $routing['url'])); + $url = preg_replace('@^[^/]+/?@', '/', $domain); + $domain = preg_replace('@/.*$@', '/', $domain); + + $robots = array(); + foreach ($app_robots as $user_agent => $rows) { + $robots[] = 'User-agent: '.$user_agent."\n"; + $robots[] = "# wa ".$app_id." ".$routing['url']."\n"; + foreach ($rows as $row) { + if (strpos($row[1], '[URL]') !== false) { + $row[1] = str_replace('[URL]', $url, $row[1]); + } + $robots[] .= $row[0].": ".$row[1]."\n"; + } + $robots[] .= "# wa ".$app_id."\n"; + $robots[] = "\n"; + } + if (class_exists('waConfig')) { + $robots_path = waConfig::get('wa_path_data')."/public/site/data/{$domain}"; + $robots_path = substr($robots_path, strlen(waSystem::getInstance()->getConfig()->getRootPath()) + 1); + } else { + $robots_path = "wa-data/public/site/data/{$domain}"; + } + + self::mkdir($robots_path); + + $robots_path .= '/robots.txt'; + + if ($fp = @fopen(self::$root_path.$robots_path, 'a')) { + fwrite($fp, "\n".implode("", $robots)); + fclose($fp); + } else { + //TODO log error + } + } + } + + private static function mkdir($target_path, $mode = 0777) + { + if (!@file_exists(self::$root_path.$target_path)) { + if (!@mkdir(self::$root_path.$target_path, $mode & 0777, true)) { + throw new Exception("Error occurred while creating a directory {$target_path} at ".self::$root_path); + } + } elseif (!@is_dir(self::$root_path.$target_path)) { + throw new Exception("Error occurred while creating a directory {$target_path} - it's a file"); + + } elseif (!@is_writable(self::$root_path.$target_path)) { + throw new Exception("Directory {$target_path} unwritable"); + } + if (preg_match('@^/?(wa-data/protected|wa-log|wa-cache|wa-config)(/|$)@', $target_path, $matches)) { + $htaccess_path = $matches[1].'/.htaccess'; + if (!@file_exists(self::$root_path.$htaccess_path)) { + if ($fp = @fopen(self::$root_path.$htaccess_path, 'w')) { + @fwrite($fp, "Deny from all\n"); + @fclose($fp); + } else { + throw new Exception("Error while trying to protect a directory {$target_path} with htaccess"); + } + } + } + return true; + } + + /** + * + * Get applicable action + * @param array $item + * @return string + */ + private static function applicableAction($item) + { + if (!empty($item['installed'])) { + if (!empty($item['installed']['version'])) { + if (isset($item['vendor']) && isset($item['installed']['vendor']) && ($item['vendor'] != $item['installed']['vendor'])) { + $action = self::ACTION_INSTALL; + } elseif (isset($item['edition']) && ($item['edition'] != $item['installed']['edition'])) { + $action = self::ACTION_INSTALL; + } elseif (version_compare($item['version'], $item['installed']['version'], '>')) { + if (!empty($item['inbuilt'])) { + $action = self::ACTION_INBUILT; + } else { + if (isset($item['critical']) && version_compare($item['critical'], $item['installed']['version'], '>')) { + $action = self::ACTION_CRITICAL_UPDATE; + } else { + $action = self::ACTION_UPDATE; + } + } + } else { + $action = self::ACTION_REPAIR; + } + } else { + $action = self::ACTION_UPDATE; + } + } elseif (!empty($item['download_url'])) { + $action = self::ACTION_INSTALL; + } else { + $action = self::ACTION_NONE; + } + return $action; + } + + /** + * + * Query to updates server + * @param string $query + * @param string $vendor + * @param boolean $values return simple array or with keys + * @return bool|mixed|string + * @since 2.0 + */ + private function query($query, $vendor = self::VENDOR_SELF, $values = false) + { + /** @var waInstallerFile $file */ + static $file; + $result = false; + + $updates_url = $this->buildUpdatesUrl('2.0', $vendor); + $url = $updates_url.$query; + if ($this->buildUrl($url)) { + if (!$file) { + $file = new waInstallerFile(); + } + $result = $file->getData($url, 'json', $values); + $headers = $file->getHeaders(); + if (isset($headers['identity_hash'])) { + $config = array( + 'identity_hash' => intval($headers['identity_hash']), + ); + self::updateGenericConfig($config); + } + } + return $result; + } + + /** + * + * Verify that updates allowed + * @throws Exception + */ + public function checkUpdates() + { + if (!$this->sources) { + throw new Exception('Empty sources list'); + } + $requirements = array(); + $requirements['php.curl'] = array( + 'description' => 'Get updates information from update servers', + 'strict' => true, + ); + $requirements['phpini.allow_url_fopen'] = array( + 'description' => 'Get updates information from update servers', + 'strict' => true, + 'value' => 1, + ); + $requirements['rights'] = array( + 'subject' => '.', + 'strict' => true, + 'value' => true, + ); + $messages = array(); + if (!self::checkRequirements($requirements)) { + foreach ($requirements as $requirement) { + if (!$requirement['passed']) { + $messages[] = $requirement['name'].' '.$requirement['warning']; + } else { + $messages = array(); + break; + } + } + if ($messages) { + throw new Exception(implode("\n", $messages)); + } + } + } + + /** + * Actualize sources endpoints for all zones + * Check & update current endpoints zone + */ + public function syncSourcesEndpoints($force = false) + { + $updatable_sources_config_path = self::$root_path . self::CONFIG_SOURCES_UPDATABLE; + if (file_exists($updatable_sources_config_path) && !is_writable($updatable_sources_config_path)) { + if (class_exists('waLog')) { + waLog::log('Cannot update sources: ' . $updatable_sources_config_path . ' is not writable.'); + } + return; + } + + if (!class_exists('waAppSettingsModel') || !class_exists('waNet') || !class_exists('waUtils')) { + return; + } + + $last_sync_time = $this->getAppSettingsModel()->get('webasyst', self::ENDPOINTS_SYNC_TIME_KEY, 0); + if (!$force && time() - $last_sync_time <= self::ENDPOINTS_SYNC_TIMEOUT) { + // already synced + return; + } + + $result = $this->getEndpoints('updates'); + + if (isset($result['status']) && $result['status'] === 'ok' && !empty($result['data']['endpoints'])) { + $sources = $result['data']['endpoints']; + if ($sources != $this->sources_all_zones) { + // update sources config + try { + waUtils::varExportToFile($sources, $updatable_sources_config_path); + } catch (Exception $e) { + if (class_exists('waLog')) { + waLog::log('Failed saving sources endpoints: ' . $e->getMessage()); + } + } + $this->sources_all_zones = $sources; + + $zone_jail = self::getGenericConfig('zone_jail', null); + if (!empty($this->sources_all_zones['zone_jail']) && empty($zone_jail)) { + self::updateGenericConfig(['zone_jail' => $this->sources_all_zones['zone_jail']]); + } + } + } + $this->getAppSettingsModel()->set('webasyst', self::ENDPOINTS_SYNC_TIME_KEY, time()); + } + + private function waNetQuery($url, $timeout = 5) + { + if (empty($url)) { + return null; + } + + return (new waNet([ + 'timeout' => $timeout, + 'format' => waNet::FORMAT_JSON + ]))->query($url); + } + + public function zonedNetQuery(callable $get_url, $timeout = 5, ...$params) + { + $result = null; + $url = $get_url(...$params); + try { + $result = $this->waNetQuery($url, $timeout); + } catch (waNetTimeoutException $e) { + // current endpoints zone is not valid or not accessable + // try other endpoints zones + if (class_exists('installerHelper')) { + $zone_detect_result = installerHelper::getInstaller()->detectBestZone(); + } + if (!empty($zone_detect_result['is_zone_changed'])) { + $url = $get_url(...$params); + try { + $result = $this->waNetQuery($url, $timeout); + } catch (Exception $e) { + if (class_exists('waLog')) { + waLog::log('Failed getting url: ' . $url . ' : ' . $e->getMessage()); + } + } + } + } catch (Exception $e) { + if (class_exists('waLog')) { + waLog::log('Failed getting url: ' . $url . ' : ' . $e->getMessage()); + } + } + return $result; + } + + public function getEndpoints($app) + { + return $this->zonedNetQuery([$this, 'getEndpointsUrl'], 5, $app); + } + + public function detectBestZone() + { + if (empty($this->sources_all_zones['zones'])) { + return null; + } + + $zone_jail = self::getGenericConfig('zone_jail', 'auto'); + if (!empty($zone_jail) && $zone_jail !== 'auto') { + return [ + 'is_zone_changed' => false, + 'zone' => $zone_jail, + ]; + } + + $endpoints_zones = array_keys($this->sources_all_zones['zones']); + $endpoints_zones[] = 'default'; + $zoned_urls = array_reduce($endpoints_zones, function ($result, $zone) { + $result[$zone] = $this->getEndpointsUrl('no', $zone); + return $result; + }, []); + $zoned_urls = array_unique($zoned_urls); + + try { + $multi = curl_multi_init(); + + foreach ($zoned_urls as $zone => $url) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_USERAGENT, sprintf('Webasyst-Framework/%s', wa()->getVersion('webasyst'))); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_multi_add_handle($multi, $ch); + } + unset($inst); + + $zoned_urls = array_flip($zoned_urls); + $zone = null; + $active = null; + do { + $mrc = curl_multi_exec($multi, $active); + } while ($mrc == CURLM_CALL_MULTI_PERFORM); + + while ($active && $mrc == CURLM_OK) { + if (curl_multi_select($multi) == -1) { + usleep(100); + continue; + } + + do { + $mrc = curl_multi_exec($multi, $active); + } while ($mrc == CURLM_CALL_MULTI_PERFORM); + + if ($mhinfo = curl_multi_info_read($multi)) { + $chinfo = curl_getinfo($mhinfo['handle']); + if ($chinfo['http_code'] == 200) { + $zone = $zoned_urls[$chinfo['url']]; + break; + } + curl_multi_remove_handle($multi, $mhinfo['handle']); + curl_close($mhinfo['handle']); + } + } + curl_multi_close($multi); + } catch (Exception $e) { + return null; + } + + $is_zone_changed = false; + if (!empty($zone) && class_exists('waAppSettingsModel') && + $this->getAppSettingsModel()->get('webasyst', self::ENDPOINTS_ZONE_KEY) !== $zone + ) { + // zone changed + $is_zone_changed = true; + $this->getAppSettingsModel()->set('webasyst', self::ENDPOINTS_ZONE_KEY, $zone); + $this->sources = $this->sources_all_zones; + if (isset($this->sources['zones']) && !empty($this->sources['zones'][$zone])) { + $this->sources = $this->sources['zones'][$zone]; + } + unset($this->sources['zones']); + + if (wa()->appExists('installer')) { + wa('installer')->getConfig()->clearInitDataCache(); + } + } + + return [ + 'is_zone_changed' => $is_zone_changed, + 'zone' => $zone, + ]; + } + + private function getExtrasPath($app_id, $type = 'plugins') + { + if (preg_match('@^wa-plugins/@', $app_id)) { + if ($type !== 'plugins') { + $path = null; + } else { + $path = $app_id; + } + } elseif (('webasyst' == $app_id) && ($type == 'widgets')) { + $path = 'wa-widgets/'; + } else { + $path = 'wa-apps/'.$app_id.'/'.$type; + } + return $path; + } + + /** + * Build url to remote updates server + * @param string $version + * @param string $vendor + * @param null|string $api_method + * @return false|string + * @throws Exception + * @since 3.0 + */ + private function buildUpdatesUrl($version = '3.0', $vendor = self::VENDOR_SELF, $api_method = null, $zone = null) + { + $result = false; + $sources = $this->getSources(self::LIST_APPS, $vendor, $zone); + if (!empty($sources[$vendor])) { + $result = preg_replace('@apps/list/$@', "{$version}/", $sources[$vendor]); + } + + if ($api_method && is_string($api_method)) { + $result .= $api_method.'/'; + } + + return $result; + } + + /** + * Get the address with the information to initialize the Installer app + * @return string + * @throws Exception + * @since 3.0 + */ + public function getInstallerInitUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/init'); + } catch (Exception $e) { + throw new Exception('Installer app cannot be initialized'); + } + } + + public function getInstallerTokenUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/token'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get token'); + } + } + + public function getInstallerFactUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/fact'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to send changes to the inst package'); + } + } + + public function getStoreReviewCoreUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/review/core.init.djs'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to Store Product rate JS API'); + } + } + + public function getInstallerAnnounceUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/announce/2'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get announcements'); + } + } + + public function getInstallerLicenseUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/license'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get license'); + } + } + + public function getInstallerProductsUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/products'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get products'); + } + } + + public function getInstallationStaticIDUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/id'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get static id'); + } + } + + public function getInstallationBetaTestProductDisconnectUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/beta_test_product/disconnect'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get static id'); + } + } + + public function getCheckProductLeaseStatusUrl() + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'installer/product_lease_status'); + } catch (Exception $e) { + throw new Exception('Unable to build URL to get product lease status'); + } + } + + public function getEndpointsUrl($app = null, $zone = null) + { + try { + return $this->buildUpdatesUrl('3.0', self::VENDOR_SELF, 'config/endpoints', $zone) . '?zoned=1&app=' . $app; + } catch (Exception $e) { + throw new Exception('Unable to build URL to get endpoints'); + } + } + + private function getAppSettingsModel() + { + if (!class_exists('waAppSettingsModel')) { + return null; + } + if (empty($this->app_settings_model)) { + $this->app_settings_model = new waAppSettingsModel(); + } + return $this->app_settings_model; + } +} diff --git a/wa-installer/lib/classes/wainstallerrequirements.class.php b/wa-installer/lib/classes/wainstallerrequirements.class.php index 5058f8da5..9cd5afe72 100644 --- a/wa-installer/lib/classes/wainstallerrequirements.class.php +++ b/wa-installer/lib/classes/wainstallerrequirements.class.php @@ -12,8 +12,11 @@ * @package wa-installer */ -class waInstallerRequirements +final class waInstallerRequirements { + private const INTERNAL_REQUIREMENT_KEYS = ['passed', 'note', 'warning', 'update']; + private const DB_SERVER_NAMES = ['mariadb' => 'MariaDB', 'mysql' => 'MySQL']; + private $root; private $wa_locale; @@ -32,7 +35,7 @@ private static function getInstance() private function __construct() { - $this->root = dirname(__FILE__).'/../../../'; + $this->root = dirname(__FILE__) . '/../../../'; $this->root = preg_replace('@([/\\\\]+)@', '/', $this->root); while (preg_match('@/\.\./@', $this->root)) { $this->root = preg_replace('@/[^/]+/\.\./@', '/', $this->root); @@ -47,28 +50,271 @@ private function __clone() /** * * Run test case - * @param string $case - * @param array $requirement + * @param string|int $case + * @param array $requirement + * @param int $level Deepness level + * @throws Exception */ - public static function test($case, &$requirement) + public static function test($case, &$requirement, int $level = 0) { - $subject = null; - if (strpos($case, '.') !== false) { - list($case, $subject) = explode('.', $case, 2); - } - if (empty($subject) && !empty($requirement['subject'])) { - $subject = $requirement['subject']; + $instance = self::getInstance(); + + ['case' => $case, 'subject' => $subject] = $instance->extractCaseAndSubject($case, $requirement, $level); + + $camel_case = preg_split('/[-_]/', $case); + if($camel_case) { + $method = 'test' . implode('', array_map('ucfirst', $camel_case)); + } else { + $method = 'test' . ucfirst($case); } - $method = 'test'.ucfirst($case); - $instance = self::getInstance(); if (!method_exists($instance, $method)) { $method = 'testDefault'; $subject = $case; } + return $instance->$method($subject, $requirement); } + /** + * + * @param string $subject + * @param array $requirement + * @return bool + * @throws Exception + */ + private function testOneOf(string $subject, array &$requirement): bool + { + $internal_fields = [ + 'passed' => $requirement['passed'], + 'note' => $requirement['note'], + 'warning' => $requirement['warning'], + 'update' => $requirement['update'] + ]; + + $result = false; + + foreach ($requirement as $key => &$item) { + if (in_array($key, self::INTERNAL_REQUIREMENT_KEYS, true)) { + continue; + } + $item = array_merge($item, $internal_fields); + $result = self::test($key, $item, 1); + $requirement['passed'] = $result; + $requirement['note'] = $item['note']; + $requirement['warning'] = $item['warning']; + $requirement['update'] = $item['update']; + if ($item['passed']) { + break; + } + } + unset($item); + + return $result; + } + + /** + * @param string $subject database server code (mysql, MariaDB e.t.c.) + * @param array $requirement + * @return bool + * @throws waDbException + */ + private function testDb(string $subject, array &$requirement): bool + { + $is_strict = !empty($requirement['strict']); + $requirement['passed'] = !$is_strict; + $this->castDatabaseServer($subject, $requirement); + + $required_db_servers = array_map(fn($v)=>strtolower($v), $requirement['server'] ?? []); + + ['version' => $db_server_version, 'server' => $db_server] = $this->detectDatabaseServer(); + self::setDefaultDescription($requirement, 'Database'); + if (!$required_db_servers || in_array($db_server, $required_db_servers, true)) { + $this->castVersion($requirement); + if (isset($requirement['version'])) { + $relation = $this->getRelation($requirement['version']); + if ($requirement['passed'] = version_compare($db_server_version, $requirement['version'], $relation)) { + $requirement['note'] = $db_server_version; + } else { + $requirement['warning'] = $is_strict + ? $this->wa_locale->_('You use %s server version %s but version %s %s required') + : $this->wa_locale->_('You use %s server version %s but version %s %s recommended'); + $requirement['warning'] = sprintf( + $requirement['warning'], + self::DB_SERVER_NAMES[$db_server] ?? $db_server, + $db_server_version, + $relation, + $requirement['version'] + ); + } + } else { + $requirement['passed'] = true; + } + if ($requirement['passed'] && !empty($requirement['engine'])) { + $this->castDatabaseTableEngine($requirement); + $supported_engines = $this->getSupportedTableEngines(); + $required_engines_lc = array_map(fn($val) => strtolower($val), $requirement['engine']); + $supported_engines_lc = array_map(fn($val) => strtolower($val), $supported_engines); + if (empty(array_intersect($required_engines_lc, $supported_engines_lc))) { + $requirement['passed'] = false; + $requirement['warning'] = $is_strict + ? $this->wa_locale->_('Your %s server supports %s table engines but %s required') + : $this->wa_locale->_('Your %s server supports %s table engines but %s recommended'); + $requirement['warning'] = sprintf( + $requirement['warning'], + self::DB_SERVER_NAMES[$db_server] ?? $db_server, + $this->stringToList($supported_engines), + $this->stringToList($requirement['engine'], $this->wa_locale->_('or')) + ); + } + } + } else { + $requirement['passed'] = false; + $requirement['warning'] = sprintf( + $is_strict + ? $this->wa_locale->_('Your database server is %s but %s required') + : $this->wa_locale->_('Your database server is %s but %s recommended'), + self::DB_SERVER_NAMES[$db_server] ?? $db_server, + $this->stringToList($requirement['server'], $this->wa_locale->_('or')) + ); + } + + return $requirement['passed']; + } + + /** + * Perhaps can be moved into waString + * Implode array items into the string, where two last items are joined with $and + * + * @param array $list + * @param string|null $and + * @param string $separator + * @return string + * @example ['a', 'b', 'c', 'd'] => 'a, b, c and d' + * + */ + private function stringToList(array $list, ?string $and = null, string $separator = ', '): string + { + if ($and === null) { + $and = $this->wa_locale->_('and'); + } + if (count($list) > 1) { + return implode($separator, array_slice($list, 0, -1)) . ' ' . $and . ' ' . array_pop($list); + } + + return array_pop($list); + } + + private function castDatabaseServer(string $subject, &$requirement): void + { + $server = null; + + if ($subject) { + $server = [trim($subject)]; + } elseif (!empty($requirement['server'])) { + if (is_string($requirement['server'])) { + $server = explode(',', $requirement['server']); + } elseif (is_array($requirement['server'])) { + $server = $requirement['server']; + } + } + + if($server) { + $server = array_map(fn($v) => trim($v), $server); + $requirement['server'] = array_values($server); + } + } + + /** + * Cast a list of required db table engines to the unified array + * + * @param array $requirement + * @return void + */ + private function castDatabaseTableEngine(array &$requirement): void + { + if (isset($requirement['engine'])) { + if (is_string($requirement['engine'])) { + $engines = explode(',', $requirement['engine']); + $requirement['engine'] = array_map(fn($e) => trim($e), $engines); + } + } + } + + /** + * List supported database table engines like MyISAM, InnoDB + * MySQL dialect aware! + * + * @return array + * @throws waDbException + */ + private function getSupportedTableEngines(): array + { + $engines = (new waModel)->query('SHOW ENGINES')->fetchAll(); + $engines = array_filter($engines, fn($e) => in_array(strtolower($e['Support']), ['yes', 'default'], true)); + + return array_values(array_map(fn($e) => $e['Engine'], $engines)); + } + + + /** + * Currently we don't differ mysql and percona engines + * + * @return array{version:string, server:string} + * @throws waDbException + */ + private function detectDatabaseServer(): array + { + // @todo check used db adapter — is mysql/mysqli or not + + $database_data = (new waModel)->query('SELECT VERSION() AS `version`, @@version_comment AS `version_comment`')->fetchAssoc(); + + return [ + 'version' => $database_data['version'] ?? '', + 'server' => stripos($database_data['version_comment'] ?? '', 'mariadb') === false ? 'mysql' : 'mariadb' + ]; + } + + /** + * @param $case + * @param $requirement + * @param $level + * @return array + * @throws Exception + */ + private function extractCaseAndSubject($case, $requirement, $level = 0): array + { + $subject = ''; + + if (is_numeric($case)) { + $is_one_of = !$level + && is_array($requirement) + && empty(array_filter( + $requirement, + fn($key) => !is_numeric($key) && !in_array($key, self::INTERNAL_REQUIREMENT_KEYS, true), + ARRAY_FILTER_USE_KEY + )); + + if ($is_one_of) { + $case = 'one_of'; + } else { + return $this->extractCaseAndSubject($requirement[0] ?? '', $requirement, $level); + } + } elseif (is_string($case)) { + if (strpos($case, '.') !== false) { + [$case, $subject] = explode('.', $case, 2); + } + } else { + throw new Exception('Invalid requirement format'); + } + + if (!$subject && !empty($requirement['subject'])) { + $subject = $requirement['subject']; + } + + return ['case' => $case, 'subject' => $subject]; + } + public function __call($name, $args) { if (preg_match('/^test(\w+)$/', $name, $matches)) { @@ -85,8 +331,8 @@ private function getAppsConfig() $apps = array(); if (class_exists('waConfig')) { $path = waConfig::get('wa_path_config'); - if ($path && file_exists($path.'/apps.php')) { - $apps = include($path.'/apps.php'); + if ($path && file_exists($path . '/apps.php')) { + $apps = include($path . '/apps.php'); } } } @@ -100,8 +346,8 @@ private function getAppsConfig() private function appVersion($app_id) { $app_id = preg_replace('@\\/@', '', strtolower($app_id)); - $path = $this->root.'wa-apps/'.$app_id.'/lib/config/app.php'; - $build_path = $this->root.'wa-apps/'.$app_id.'/lib/config/build.php'; + $path = $this->root . 'wa-apps/' . $app_id . '/lib/config/app.php'; + $build_path = $this->root . 'wa-apps/' . $app_id . '/lib/config/build.php'; $version = false; if (file_exists($path)) { $apps = $this->getAppsConfig(); @@ -177,7 +423,7 @@ private function testPhpini($subject, &$requirement) if (empty($requirement['strict'])) { $format = $this->wa_locale->_('Value of PHP configuration parameter %s: %s. Recommended value: %s.'); } - $requirement['warning'] = sprintf($format, $subject, $value, $relation.$requirement['value']); + $requirement['warning'] = sprintf($format, $subject, $value, $relation . $requirement['value']); } else { $requirement['passed'] = true; if ($value === true) { @@ -323,8 +569,8 @@ private function testRights($folders, &$requirement) self::setDefaultDescription($requirement, 'Files access rights'); //TODO make it recursive foreach ($folders as $folder) { - $path = $this->root.$folder; - $folder_name = $root_path.preg_replace('@^\.?/?@', '/', $folder); + $path = $this->root . $folder; + $folder_name = $root_path . preg_replace('@^\.?/?@', '/', $folder); if (file_exists($path)) { //XXX skip symbolic link @@ -357,7 +603,7 @@ private function testServer($subject, &$requirement) $requirement['warning'] = false; $requirement['value'] = empty($requirement['strict']); - $server = (isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '').(isset($_SERVER['SERVER_SIGNATURE']) ? $_SERVER['SERVER_SIGNATURE'] : ''); + $server = (isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '') . (isset($_SERVER['SERVER_SIGNATURE']) ? $_SERVER['SERVER_SIGNATURE'] : ''); if ($subject) { //check server module self::setDefaultDescription($requirement, array('Server module %s', htmlentities($subject, ENT_QUOTES, 'utf-8'))); @@ -390,7 +636,7 @@ private function testServer($subject, &$requirement) * * Verify MD5 hashes * @param string $pattern - * @param array $requirement + * @param array $requirement */ private function testMd5($pattern, &$requirement) { @@ -398,7 +644,7 @@ private function testMd5($pattern, &$requirement) $requirement['passed'] = empty($requirement['strict']); $requirement['note'] = false; $requirement['warning'] = false; - $md5_path = $this->root.'.files.md5'; + $md5_path = $this->root . '.files.md5'; if ($pattern) { //check files by mask @@ -415,8 +661,8 @@ private function testMd5($pattern, &$requirement) unset($char); } - $cleanup_pattern = '@({'.implode('|', $meta_characters).')@'; - $command_pattern = '@({'.implode('|', $command_characters).')@'; + $cleanup_pattern = '@({' . implode('|', $meta_characters) . ')@'; + $command_pattern = '@({' . implode('|', $command_characters) . ')@'; $pattern = preg_replace($cleanup_pattern, '\\\\$1', $pattern); $pattern = preg_replace($command_pattern, '.$1', $pattern); $hash_pattern = "@^([\\da-f]{32})\\s+\\*({$pattern})$@m"; @@ -425,7 +671,7 @@ private function testMd5($pattern, &$requirement) if (preg_match_all($hash_pattern, $hashes, $file_matches)) { $requirement['passed'] = true; foreach ($file_matches[2] as $id => $file) { - $path = $this->root.$file; + $path = $this->root . $file; if (file_exists($path)) { $md5_hash = md5_file($path); if ($file_matches[1][$id] != $md5_hash) {