diff --git a/README.md b/README.md index dee16a2d..2a8964f9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This repository contains the core of Mibew Messenger application. ## Server requirements 1. A webserver or web hosting account running on any major Operating System -2. PHP (5.3.3 and above) with PDO, pdo_mysql and gd extensions +2. PHP (5.3.3 and above) with PDO, pdo_mysql, cURL and gd extensions 3. MySQL 5.0 and above ## Build from sources diff --git a/src/mibew/README.txt b/src/mibew/README.txt index 0452dd06..ed0f5292 100644 --- a/src/mibew/README.txt +++ b/src/mibew/README.txt @@ -6,7 +6,7 @@ REQUIREMENTS * Apache web server 1.3.34 or above with the ability to use local .htaccess files (mod_rewrite module is optional, but recommended) * MySQL database 5.0 or above - * PHP 5.3.3 or above with PDO, pdo_mysql and gd extensions + * PHP 5.3.3 or above with PDO, pdo_mysql, cURL and gd extensions INSTALLATION diff --git a/src/mibew/configs/database_schema.yml b/src/mibew/configs/database_schema.yml index 160c1fea..3f3d5ef9 100644 --- a/src/mibew/configs/database_schema.yml +++ b/src/mibew/configs/database_schema.yml @@ -316,3 +316,19 @@ plugin: enabled: "tinyint NOT NULL DEFAULT 0" unique_keys: name: [name] + +# Contains info about all available updates +availableupdate: + fields: + # Artificial ID + id: "INT NOT NULL auto_increment PRIMARY KEY" + # Can be either "core" or fully qualified plugin's name + target: "varchar(255) NOT NULL" + # The latest available version of the plugin + version: "varchar(255) NOT NULL" + # A URL where the new version can be downloaded + url: "text" + # Description of the update + description: "text" + unique_keys: + target: [target] diff --git a/src/mibew/configs/routing.yml b/src/mibew/configs/routing.yml index 872a664b..7f67e159 100644 --- a/src/mibew/configs/routing.yml +++ b/src/mibew/configs/routing.yml @@ -759,6 +759,13 @@ update_run: _access_check: Mibew\AccessControl\Check\PermissionsCheck _access_permissions: [CAN_ADMINISTRATE] +update_check: + path: /update/check + defaults: + _controller: Mibew\Controller\UpdateController::checkUpdatesAction + _access_check: Mibew\AccessControl\Check\PermissionsCheck + _access_permissions: [CAN_ADMINISTRATE] + ## Users (visitors avaiting page) users: path: /operator/users diff --git a/src/mibew/js/source/about.js b/src/mibew/js/source/about.js deleted file mode 100644 index 7636e2e8..00000000 --- a/src/mibew/js/source/about.js +++ /dev/null @@ -1,40 +0,0 @@ -/*! - * This file is a part of Mibew Messenger. - * - * Copyright 2005-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -(function (Mibew, $) { - Mibew.updateVersion = function(data) { - if (!data.core || !data.core.stable) { - return; - } - - $(document).ready(function() { - var currentVersion = $("#current-version").html(), - core = data.core.stable; - - if (currentVersion != core.version) { - if (currentVersion < core.version) { - $("#current-version").css("color", "red"); - } - $("#latest-version").html(core.version + ", Download " + core.title + ""); - } else { - $("#current-version").css("color", "green"); - $("#latest-version").html(core.version); - } - }); - } -})(Mibew, jQuery); diff --git a/src/mibew/libs/classes/Mibew/Controller/AboutController.php b/src/mibew/libs/classes/Mibew/Controller/AboutController.php index b9ac3c12..1c907fe8 100644 --- a/src/mibew/libs/classes/Mibew/Controller/AboutController.php +++ b/src/mibew/libs/classes/Mibew/Controller/AboutController.php @@ -19,7 +19,7 @@ namespace Mibew\Controller; -use Mibew\Asset\AssetManagerInterface; +use Mibew\Maintenance\AvailableUpdate; use Symfony\Component\HttpFoundation\Request; /** @@ -43,16 +43,11 @@ class AboutController extends AbstractController 'version' => MIBEW_VERSION, 'title' => getlocal('About'), 'menuid' => 'about', + 'availableUpdates' => $this->getAvailableUpdates(), ), prepare_menu($this->getOperator()) ); - $this->getAssetManager()->attachJs('js/compiled/about.js'); - $this->getAssetManager()->attachJs( - 'https://mibew.org/api/updates', - AssetManagerInterface::ABSOLUTE_URL - ); - return $this->render('about', $page); } @@ -68,7 +63,7 @@ class AboutController extends AbstractController */ protected function getExtensionsInfo() { - $required_extensions = array('PDO', 'pdo_mysql', 'gd'); + $required_extensions = array('PDO', 'pdo_mysql', 'gd', 'curl'); $info = array(); foreach ($required_extensions as $ext) { if (!extension_loaded($ext)) { @@ -86,4 +81,39 @@ class AboutController extends AbstractController return $info; } + + /** + * Builds list of available updates to display in the template. + * + * @return array List of updates data. Each item of the list is associative + * array with the following keys: + * - "title": string, title of the update. + * - "version": string, the latest available version. + * - "url": string, URL of the page the updated version can be downloaded + * from. + * - "description": string, description of the update. + */ + protected function getAvailableUpdates() + { + $updates = AvailableUpdate::all(); + if (!$updates) { + return array(); + } + + $data = array(); + foreach ($updates as $update) { + $title = ($update->target == 'core') + ? 'Mibew' + : getlocal('{0} plugin', array($update->target)); + + $data[] = array( + 'title' => $title, + 'version' => $update->version, + 'url' => $update->url, + 'description' => $update->description, + ); + } + + return $data; + } } diff --git a/src/mibew/libs/classes/Mibew/Controller/Settings/FeaturesController.php b/src/mibew/libs/classes/Mibew/Controller/Settings/FeaturesController.php index a7bfa1d1..e0590c59 100644 --- a/src/mibew/libs/classes/Mibew/Controller/Settings/FeaturesController.php +++ b/src/mibew/libs/classes/Mibew/Controller/Settings/FeaturesController.php @@ -111,6 +111,7 @@ class FeaturesController extends AbstractController 'showonlineoperators', 'enablecaptcha', 'trackoperators', + 'autocheckupdates', ); } } diff --git a/src/mibew/libs/classes/Mibew/Controller/UpdateController.php b/src/mibew/libs/classes/Mibew/Controller/UpdateController.php index eb28121d..7bc5a4bc 100644 --- a/src/mibew/libs/classes/Mibew/Controller/UpdateController.php +++ b/src/mibew/libs/classes/Mibew/Controller/UpdateController.php @@ -19,7 +19,9 @@ namespace Mibew\Controller; +use Mibew\Maintenance\UpdateChecker; use Mibew\Maintenance\Updater; +use Mibew\Settings; use Mibew\Style\PageStyle; use Symfony\Component\HttpFoundation\Request; @@ -33,6 +35,11 @@ class UpdateController extends AbstractController */ protected $updater = null; + /** + * @var UpdateChecker|null + */ + protected $updateChecker = null; + /** * Renders update intro page. * @@ -74,6 +81,26 @@ class UpdateController extends AbstractController return $this->render('update_progress', $parameters); } + /** + * Runs the Update checker. + * + * @param Request $request Incoming request. + * @return Response|string Rendered page contents or Symfony's response + * object. + */ + public function checkUpdatesAction(Request $request) + { + $checker = $this->getUpdateChecker(); + $success = $checker->run(); + if (!$success) { + foreach ($checker->getErrors() as $error) { + trigger_error('Update checking failed: ' . $error, E_USER_WARNING); + } + } + + return $this->redirect($this->generateUrl('about')); + } + /** * {@inheritdoc} */ @@ -99,4 +126,22 @@ class UpdateController extends AbstractController return $this->updater; } + + /** + * Returns an instance of Update Checker. + * + * @return UpdateChecker + */ + protected function getUpdateChecker() + { + if (is_null($this->updateChecker)) { + $this->updateChecker = new UpdateChecker(); + $id = Settings::get('_instance_id'); + if ($id) { + $this->updateChecker->setInstanceId($id); + } + } + + return $this->updateChecker; + } } diff --git a/src/mibew/libs/classes/Mibew/Maintenance/AvailableUpdate.php b/src/mibew/libs/classes/Mibew/Maintenance/AvailableUpdate.php new file mode 100644 index 00000000..5e08f80b --- /dev/null +++ b/src/mibew/libs/classes/Mibew/Maintenance/AvailableUpdate.php @@ -0,0 +1,253 @@ +query( + "SELECT * FROM {availableupdate} WHERE id = :id", + array(':id' => $id), + array('return_rows' => Database::RETURN_ONE_ROW) + ); + + // There is no update with such id in database + if (!$info) { + return false; + } + + // Create and populate update object + $update = new self(); + $update->populateFromDbFields($info); + + return $update; + } + + /** + * Loads update by its target. + * + * @param string $target Target of the update to load. + * @return boolean|AvailableUpdate Returns an AvailableUpdate instance or + * boolean false on failure. + */ + public static function loadByTarget($target) + { + // Check the target + if (empty($target)) { + return false; + } + + // Load update info + $info = Database::getInstance()->query( + "SELECT * FROM {availableupdate} WHERE target = :target", + array(':target' => $target), + array('return_rows' => Database::RETURN_ONE_ROW) + ); + + // There is no update with such target in database + if (!$info) { + return false; + } + + // Create and populate update object + $update = new self(); + $update->populateFromDbFields($info); + + return $update; + } + + /** + * Loads available updates. + * + * @return array List of AvailableUpdate instances. + * + * @throws \RuntimeException If something went wrong and the list could not + * be loaded. + */ + public static function all() + { + $rows = Database::getInstance()->query( + "SELECT * FROM {availableupdate}", + null, + array('return_rows' => Database::RETURN_ALL_ROWS) + ); + + if ($rows === false) { + throw new \RuntimeException('List of available updates cannot be retrieved.'); + } + + $updates = array(); + foreach ($rows as $item) { + $update = new self(); + $update->populateFromDbFields($item); + $updates[] = $update; + } + + return $updates; + } + + /** + * Class constructor. + */ + public function __construct() + { + // Set default values + $this->id = false; + $this->target = null; + $this->version = null; + $this->url = ''; + $this->description = ''; + } + + /** + * Remove record about available update from the database. + */ + public function delete() + { + if (!$this->id) { + throw new \RuntimeException('You cannot delete an update without id'); + } + + Database::getInstance()->query( + "DELETE FROM {availableupdate} WHERE id = :id LIMIT 1", + array(':id' => $this->id) + ); + } + + /** + * Save the update to the database. + */ + public function save() + { + $db = Database::getInstance(); + + if (!$this->target) { + throw new \RuntimeException('Update\'s target was not set'); + } + + if (!$this->url) { + throw new \RuntimeException('Update\'s URL was not set'); + } + + if (!$this->version) { + throw new \RuntimeException('Update\'s version was not set'); + } + + if (!$this->id) { + // This update is new. + $db->query( + ("INSERT INTO {availableupdate} (target, version, url, description) " + . "VALUES (:target, :version, :url, :description)"), + array( + ':target' => $this->target, + ':version' => $this->version, + ':url' => $this->url, + ':description' => $this->description, + ) + ); + $this->id = $db->insertedId(); + } else { + // Update existing update + $db->query( + ("UPDATE {availableupdate} SET target = :target, url = :url, " + . "version = :version, description = :description " + . "WHERE id = :id"), + array( + ':id' => $this->id, + ':target' => $this->target, + ':version' => $this->version, + ':url' => $this->url, + ':description' => $this->description, + ) + ); + } + } + + /** + * Sets update's fields according to the fields from Database. + * + * @param array $db_fields Associative array of database fields which keys + * are fields names and the values are fields values. + */ + protected function populateFromDbFields($db_fields) + { + $this->id = $db_fields['id']; + $this->target = $db_fields['target']; + $this->version = $db_fields['version']; + $this->url = $db_fields['url']; + $this->description = $db_fields['description']; + } +} diff --git a/src/mibew/libs/classes/Mibew/Maintenance/CronWorker.php b/src/mibew/libs/classes/Mibew/Maintenance/CronWorker.php index 423ddf67..731754ee 100644 --- a/src/mibew/libs/classes/Mibew/Maintenance/CronWorker.php +++ b/src/mibew/libs/classes/Mibew/Maintenance/CronWorker.php @@ -36,6 +36,13 @@ class CronWorker */ protected $cache = null; + /** + * An instance of update checker. + * + * @var UpdateChecker|null + */ + protected $updateChecker = null; + /** * List of errors. * @@ -54,10 +61,15 @@ class CronWorker * Class constructor. * * @param PoolInterface $cache An instance of cache pool. + * @param UpdateChecker $update_checker An instance of update checker. */ - public function __construct(PoolInterface $cache) + public function __construct(PoolInterface $cache, UpdateChecker $update_checker = null) { $this->cache = $cache; + + if (!is_null($update_checker)) { + $this->updateChecker = $update_checker; + } } /** @@ -71,6 +83,9 @@ class CronWorker try { set_time_limit(0); + // Update time of last cron run + Settings::set('_last_cron_run', time()); + // Remove stale cached items $this->cache->purge(); @@ -83,8 +98,18 @@ class CronWorker $dispatcher = EventDispatcher::getInstance(); $dispatcher->triggerEvent(Events::CRON_RUN); - // Update time of last cron run - Settings::set('_last_cron_run', time()); + if (Settings::get('autocheckupdates') == '1') { + // Run the update checker + $update_checker = $this->getUpdateChecker(); + if (!$update_checker->run()) { + $this->errors = array_merge( + $this->errors, + $update_checker->getErrors() + ); + + return false; + } + } } catch (\Exception $e) { $this->log[] = $e->getMessage(); @@ -114,4 +139,24 @@ class CronWorker { return $this->log; } + + /** + * Retrives an instance of Update Checker attached to the worker. + * + * If there was no attached checker it creates a new one. + * + * @return UpdateChecker + */ + protected function getUpdateChecker() + { + if (is_null($this->updateChecker)) { + $this->updateChecker = new UpdateChecker(); + $id = Settings::get('_instance_id'); + if ($id) { + $this->updateChecker->setInstanceId($id); + } + } + + return $this->updateChecker; + } } diff --git a/src/mibew/libs/classes/Mibew/Maintenance/Installer.php b/src/mibew/libs/classes/Mibew/Maintenance/Installer.php index 773bbfd7..12b9551d 100644 --- a/src/mibew/libs/classes/Mibew/Maintenance/Installer.php +++ b/src/mibew/libs/classes/Mibew/Maintenance/Installer.php @@ -440,6 +440,46 @@ class Installer return false; } + // Generate Unique ID for Mibew Instance + try { + list($count) = $db->query( + 'SELECT COUNT(*) FROM {config} WHERE vckey = :key', + array(':key' => '_instance_id'), + array( + 'return_rows' => Database::RETURN_ONE_ROW, + 'fetch_type' => Database::FETCH_NUM, + ) + ); + + if ($count == 0) { + $db->query( + 'INSERT INTO {config} (vckey, vcvalue) VALUES (:key, :value)', + array( + ':key' => '_instance_id', + ':value' => Utils::generateInstanceId(), + ) + ); + } else { + // The option is already in the database. It seems that + // something went wrong with the previous installation attempt. + // Just update the instance ID. + $db->query( + 'UPDATE {config} SET vcvalue = :value WHERE vckey = :key', + array( + ':key' => '_instance_id', + ':value' => Utils::generateInstanceId(), + ) + ); + } + } catch (\Exception $e) { + $this->errors[] = getlocal( + 'Cannot store instance ID. Error {0}', + array($e->getMessage()) + ); + + return false; + } + return true; } @@ -489,7 +529,7 @@ class Installer */ protected function checkPhpExtensions() { - $extensions = array('PDO', 'pdo_mysql', 'gd'); + $extensions = array('PDO', 'pdo_mysql', 'gd', 'curl'); foreach ($extensions as $ext) { if (!extension_loaded($ext)) { diff --git a/src/mibew/libs/classes/Mibew/Maintenance/UpdateChecker.php b/src/mibew/libs/classes/Mibew/Maintenance/UpdateChecker.php new file mode 100644 index 00000000..f32e7e42 --- /dev/null +++ b/src/mibew/libs/classes/Mibew/Maintenance/UpdateChecker.php @@ -0,0 +1,371 @@ +url = $url; + } + + /** + * Retrieves URL of updates server. + * + * @return string + */ + public function getUrl() + { + return is_null($this->url) + ? 'https://mibew.org/api2/updates.json' + : $this->url; + } + + /** + * Sets Unique ID of the Mibew instance. + * + * @param string $id Unique ID that is 64 characters length at most. + * @throws \InvalidArgumentException + */ + public function setInstanceId($id) + { + if (strlen($id) > 64) { + throw new \InvalidArgumentException( + 'The ID is too long. It can be 64 characters length at most.' + ); + } + + // Make sure the ID is always a string. + $this->instanceId = $id ?: ''; + } + + /** + * Retrieve Unique ID of the Mibew instance. + * + * @return string + */ + public function getInstanceId() + { + return $this->instanceId; + } + + /** + * Retrieves list of errors that took place during update checking process. + * + * @return array List of errors. Each item in the list is a error string. + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Runs update checking process. + * + * @return boolean False on error and true otherwise. To get more info about + * error call {@link UpdateChecker::getErrors()} method. + */ + public function run() + { + $ch = curl_init($this->getUrl()); + + // TODO: set timeouts + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 5); + + $json = json_encode($this->getSystemInfo()); + curl_setopt($ch, CURLOPT_POSTFIELDS, $json); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Accept: application/json', + 'Content-Type: application/json', + 'Content-Length: ' . strlen($json) + )); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $body = curl_exec($ch); + $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curl_errno = curl_errno($ch); + $curl_error = curl_error($ch); + curl_close($ch); + + if ($curl_errno !== 0) { + // cURL request failed. + $this->errors[] = sprintf( + 'cURL error (#%u): %s', + $curl_errno, + $curl_error + ); + + return false; + } + + if ($response_code != 200) { + // Unexpected HTTP recieved. + $this->errors[] = sprintf( + 'Update server returns %u HTTP code instead of 200', + $response_code + ); + + return false; + } + + $updates = json_decode($body, true); + $json_error = json_last_error(); + if ($json_error !== JSON_ERROR_NONE) { + // Cannot parse JSON result. + $this->errors[] = $this->formatJsonError($json_error); + + return false; + } + + if (!$updates) { + // There are no available updates. + return true; + } + + return $this->processUpdates($updates); + } + + /** + * Retrieves set of system info that will be sent to updates server. + * + * @return array + */ + protected function getSystemInfo() + { + $info = array( + 'core' => MIBEW_VERSION, + 'plugins' => $this->getPluginsInfo(), + ); + + // Attach Instance ID to the info but only if it's not empty. + $id = $this->getInstanceId(); + if ($id) { + $info['uid'] = $id; + } + + return $info; + } + + /** + * Retrieves info about plugins available in the system. + * + * @return array Associative array of plugins info. Each key of the array is + * fully qualified plugin's name and each value is an array with the + * fillowing keys: + * - "version": string, version of the plugin which presents in the system. + * - "installed": boolean, indicates if the plugin is installed. + * - "enabled": boolean, indicates if the plugin is enabled. + */ + protected function getPluginsInfo() + { + if (is_null($this->pluginsInfo)) { + $this->pluginsInfo = []; + $names = PluginUtils::discoverPlugins(); + foreach ($names as $plugin_name) { + $info = new PluginInfo($plugin_name); + $this->pluginsInfo[$plugin_name] = array( + 'version' => $info->getVersion(), + 'installed' => $info->getState()->installed, + 'enabled' => $info->getState()->enabled, + ); + } + } + + return $this->pluginsInfo; + } + + /** + * Performs all actions that are needed to prepare store available updates. + * + * @param array $updates Asscociative array of available updates that is + * retrieved from the updates server. + * @return boolean False on error and true otherwise. To get more info about + * error call {@link UpdateChecker::getErrors()} method. + */ + protected function processUpdates($updates) + { + // Process updates of the core. + if (version_compare($updates['core']['version'], MIBEW_VERSION) > 0) { + $update = $updates['core']; + // Save info about update for the core only if its version changed + $success = $this->saveUpdate( + 'core', + $update['version'], + $update['download'], + empty($update['description']) ? '' : $update['description'] + ); + if (!$success) { + // Something went wrong. The error is already logged so just + // notify the outer code. + return false; + } + } + + // Process plugins updates. + $plugins_info = $this->getPluginsInfo(); + foreach ($updates['plugins'] as $plugin_name => $update) { + if (!isset($plugins_info[$plugin_name])) { + // It's strange. We recieve update info for a plugin that does + // not exist in the system. Just do nothing. + continue; + } + + $info = $plugins_info[$plugin_name]; + + if (version_compare($update['version'], $info['version']) <= 0) { + // Version of the plugin is not updated. Just do nothing. + continue; + } + + // Save the update + $success = $this->saveUpdate( + $plugin_name, + $update['version'], + $update['download'], + empty($update['description']) ? '' : $update['description'] + ); + if (!$success) { + // Something went wrong. The error is already logged so just + // notify the outer code. + return false; + } + } + + return true; + } + + /** + * Saves record about available update in the database. + * + * @param string $target Update's target. Can be either "core" or fully + * qualified plugin's name. + * @param string $version The latest version at the updates server. + * @param string $url URL of the page where the update can be downloaded. + * @param string $description Arbitrary update's description. + * @return boolean False on failure and true otherwise. To get more info + * about the error call {@link UpdateChecker::getErrors()} method. + */ + protected function saveUpdate($target, $version, $url, $description = '') + { + try { + $update = AvailableUpdate::loadByTarget($target); + if (!$update) { + // There is no such update in the database. Create a new one. + $update = new AvailableUpdate(); + $update->target = $target; + } + + $update->version = $version; + $update->url = $url; + $update->description = $description; + + $update->save(); + } catch (\Exception $e) { + $this->errors[] = 'Cannot save available update: ' + $e->getMessage(); + + return false; + } + + return true; + } + + /** + * Builds human-readable message about error in json_* PHP's function. + * + * @param int $error_code Error code returned by json_last_error + * @return string Human-readable error message. + */ + protected function formatJsonError($error_code) + { + $errors = array( + JSON_ERROR_DEPTH => 'JSON_ERROR_DEPTH', + JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH', + JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR', + JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX', + ); + + // Following constants may be unavailable for the current PHP version. + if (defined('JSON_ERROR_UTF8')) { + $errors[JSON_ERROR_UTF8] = 'JSON_ERROR_UTF8'; + } + + if (defined('JSON_ERROR_RECURSION')) { + $errors[JSON_ERROR_RECURSION] = 'JSON_ERROR_RECURSION'; + } + + if (defined('JSON_ERROR_INF_OR_NAN')) { + $errors[JSON_ERROR_INF_OR_NAN] = 'JSON_ERROR_INF_OR_NAN'; + } + + if (defined('JSON_ERROR_UNSUPPORTED_TYPE')) { + $errors[JSON_ERROR_UNSUPPORTED_TYPE] = 'JSON_ERROR_UNSUPPORTED_TYPE'; + } + + $msg = isset($errors[$error_code]) ? $errors[$error_code] : 'UNKNOWN'; + + return sprintf( + 'Could not parse response from update server. The error is: "%s"', + $msg + ); + } +} diff --git a/src/mibew/libs/classes/Mibew/Maintenance/Updater.php b/src/mibew/libs/classes/Mibew/Maintenance/Updater.php index 5552af41..763a4aa3 100644 --- a/src/mibew/libs/classes/Mibew/Maintenance/Updater.php +++ b/src/mibew/libs/classes/Mibew/Maintenance/Updater.php @@ -332,14 +332,33 @@ class Updater return false; } - // Alter locale table. try { + // Alter locale table. $db->query('ALTER TABLE {locale} ADD COLUMN name varchar(128) NOT NULL DEFAULT "" AFTER code'); $db->query('ALTER TABLE {locale} ADD COLUMN rtl tinyint NOT NULL DEFAULT 0'); $db->query('ALTER TABLE {locale} ADD COLUMN time_locale varchar(128) NOT NULL DEFAULT "en_US"'); $db->query('ALTER TABLE {locale} ADD COLUMN date_format text'); $db->query('ALTER TABLE {locale} ADD UNIQUE KEY code (code)'); + + // Create a table for available updates. + $db->query('CREATE TABLE {availableupdate} ( ' + . 'id INT NOT NULL auto_increment PRIMARY KEY, ' + . 'target varchar(255) NOT NULL, ' + . 'version varchar(255) NOT NULL, ' + . 'url text, ' + . 'description text, ' + . 'UNIQUE KEY target (target) ' + . ') charset utf8 ENGINE=InnoDb'); + + // Generate Unique ID of Mibew instance. + $db->query( + 'INSERT INTO {config} (vckey, vcvalue) VALUES (:key, :value)', + array( + ':key' => '_instance_id', + ':value' => Utils::generateInstanceId(), + ) + ); } catch (\Exception $e) { $this->errors[] = getlocal('Cannot update tables: {0}', $e->getMessage()); diff --git a/src/mibew/libs/classes/Mibew/Maintenance/Utils.php b/src/mibew/libs/classes/Mibew/Maintenance/Utils.php index 603be88e..ce122abb 100644 --- a/src/mibew/libs/classes/Mibew/Maintenance/Utils.php +++ b/src/mibew/libs/classes/Mibew/Maintenance/Utils.php @@ -101,6 +101,40 @@ class Utils return $updates; } + /** + * Generates random unique 64 characters length ID for Mibew instance. + * + * WARNING: This ID should not be used for any security/cryptographic. If + * you need an ID for such purpose you have to use PHP's + * {@link openssl_random_pseudo_bytes()} function instead. + * + * @return string + */ + public static function generateInstanceId() + { + $chars = '0123456789abcdefghijklmnopqrstuvwxyz'; + $rnd = (string)microtime(true); + + // Add ten random characters before and after the timestamp + $max_char = strlen($chars) - 1; + for ($i = 0; $i < 10; $i++) { + $rnd = $chars[rand(0, $max_char)] . $rnd . $chars[rand(0, $max_char)]; + } + + if (function_exists('hash')) { + // There is hash function that can give us 64-length hash. + return hash('sha256', $rnd); + } + + // We should build random 64 character length hash using old'n'good md5 + // function. + $middle = (int)floor(strlen($rnd) / 2); + $rnd_left = substr($rnd, 0, $middle); + $rnd_right = substr($rnd, $middle); + + return md5($rnd_left) . md5($rnd_right); + } + /** * This class should not be instantiated */ diff --git a/src/mibew/libs/classes/Mibew/Settings.php b/src/mibew/libs/classes/Mibew/Settings.php index 548d206a..2a547748 100644 --- a/src/mibew/libs/classes/Mibew/Settings.php +++ b/src/mibew/libs/classes/Mibew/Settings.php @@ -97,6 +97,7 @@ class Settings 'surveyaskgroup' => '1', 'surveyaskmessage' => '0', 'enablepopupnotification' => '0', + 'autocheckupdates' => '1', /* Check updates automatically */ 'showonlineoperators' => '0', 'enablecaptcha' => '0', 'online_timeout' => 30, /* Timeout (in seconds) when online operator becomes offline */ @@ -114,6 +115,10 @@ class Settings // underscore sign(_). // Unix timestamp when cron job ran last time. '_last_cron_run' => 0, + // Random unique ID which is used for getting info about new + // updates. This value is initialized during Installation or Update + // process. + '_instance_id' => '', ); // Load values from database diff --git a/src/mibew/styles/pages/default/templates_src/server_side/about.handlebars b/src/mibew/styles/pages/default/templates_src/server_side/about.handlebars index 6396699a..de8c2a27 100644 --- a/src/mibew/styles/pages/default/templates_src/server_side/about.handlebars +++ b/src/mibew/styles/pages/default/templates_src/server_side/about.handlebars @@ -22,11 +22,6 @@
-

{{l10n "Latest version:"}}

-
- -
-

{{l10n "Installed localizations:"}}

{{#each localizations}} {{this}} @@ -36,6 +31,26 @@

{{l10n "Environment:"}}

PHP {{phpVersion}} {{#each extensions}}{{@key}}{{#if loaded}}{{#if version}}/{{version}}{{/if}}{{else}}/absent{{/if}} {{/each}} + +

+ +

{{l10n "Available updates"}}

+ {{#if availableUpdates}} + {{#each availableUpdates}} +

{{title}} ({{version}})

+ {{#if description}} +
{{description}}
+ {{/if}} +
+ Download +
+ +
+ {{/each}} + {{else}} + There is no available updates.

+ {{/if}} + {{l10n "Check for available updates"}} +
+ +
+ +
+ +
+
+ {{#if canmodify}}