Create a way to automatically get info about available updates

This commit is contained in:
Dmitriy Simushev 2015-05-26 13:19:13 +00:00
parent 8e7fe6bcd7
commit 577c2be622
8 changed files with 635 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -316,3 +316,19 @@ plugin:
enabled: "tinyint NOT NULL DEFAULT 0"
unique_keys:
name: [name]
# Contains info about all available updates
available_update:
field:
# 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]

View File

@ -0,0 +1,253 @@
<?php
/*
* 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.
*/
namespace Mibew\Maintenance;
use Mibew\Database;
/**
* Represents a record about available update with all necessary info.
*/
class AvailableUpdate
{
/**
* Unique (for the current Mibew instance) update ID.
*
* @type int
*/
public $id;
/**
* String representing update target.
*
* It can be equal to either "core" or fully qualified plugin's name.
*
* @var string
*/
public $target;
/**
* The latest version the core/plugin can be updated to.
*
* @type string
*/
public $version;
/**
* The URL the update can be downloaded from.
*
* @type string
*/
public $url;
/**
* Arbitrary description of the update.
*
* @type string
*/
public $description;
/**
* Loads update by its ID.
*
* @param int $id ID of the update to load
* @return boolean|AvailableUpdate Returns an AvailableUpdate instance or
* boolean false on failure.
*/
public static function load($id)
{
// Check $id
if (empty($id)) {
return false;
}
// Load update's info
$info = Database::getInstance()->query(
"SELECT * FROM {available_update} 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 {available_update} 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 {available_updates}",
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 {available_update} 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 {available_update} (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 {available_update} 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'];
}
}

View File

@ -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,14 @@ 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;
$this->updateChecker = is_null($update_checker)
? new UpdateChecker()
: $update_checker;
}
/**
@ -71,6 +82,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 +97,15 @@ class CronWorker
$dispatcher = EventDispatcher::getInstance();
$dispatcher->triggerEvent(Events::CRON_RUN);
// Update time of last cron run
Settings::set('_last_cron_run', time());
// Run the update checker
if (!$this->updateChecker->run()) {
$this->errors = array_merge(
$this->errors,
$this->updateChecker->getErrors()
);
return false;
}
} catch (\Exception $e) {
$this->log[] = $e->getMessage();

View File

@ -489,7 +489,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)) {

View File

@ -0,0 +1,328 @@
<?php
/*
* 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.
*/
namespace Mibew\Maintenance;
use Mibew\Plugin\Utils as PluginUtils;
use Mibew\Plugin\PluginInfo;
/**
* Encapsulates available updates checking process.
*/
class UpdateChecker
{
/**
* URL of the updates server.
*
* @var string|null
*/
private $url = null;
/**
* A cache for plugins info array.
*
* @var array|null
*/
private $pluginsInfo = null;
/**
* List of errors that took place during updates checking.
*
* Each item of the list is a error string.
*
* @var array
*/
private $errors = [];
/**
* Sets URL of updates server.
*
* @param string $url New updates server's URL
*/
public function setUrl($url)
{
$this->url = $url;
}
/**
* Retrieves URL of updates server.
*
* @return string
*/
public function getUrl()
{
return is_null($this->url)
? 'https://mibew.org/api/auto-updates'
: $this->url;
}
/**
* 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()
{
return array(
'core' => MIBEW_VERSION,
'plugins' => $this->getPluginsInfo(),
);
}
/**
* 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
);
}
}

View File

@ -332,14 +332,24 @@ 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 {available_update} ( '
. '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');
} catch (\Exception $e) {
$this->errors[] = getlocal('Cannot update tables: {0}', $e->getMessage());