mirror of
https://github.com/VTECRM/vtenext.git
synced 2026-02-26 16:18:47 +00:00
1511 lines
44 KiB
PHP
1511 lines
44 KiB
PHP
<?php
|
|
/*************************************
|
|
* SPDX-FileCopyrightText: 2009-2020 Vtenext S.r.l. <info@vtenext.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
************************************/
|
|
|
|
/* crmv@181161 crmv@182073 crmv@183486 */
|
|
|
|
require_once('Update.php');
|
|
require_once('modules/Emails/mail.php');
|
|
|
|
class AutoUpdater {
|
|
|
|
const STATUS_IDLE = 0;
|
|
const STATUS_WAIT_PACKAGE = 10;
|
|
const STATUS_DOWNLOADING = 20;
|
|
const STATUS_CHECK_FILES = 30;
|
|
const STATUS_NOT_UPDATABLE = 40;
|
|
const STATUS_UPDATABLE = 50;
|
|
const STATUS_POSTPONED = 60;
|
|
const STATUS_REFUSED = 70;
|
|
const STATUS_SCHEDULED = 80;
|
|
const STATUS_UPDATING = 90;
|
|
const STATUS_SUCCESS = 100;
|
|
const STATUS_FAILURE = 110;
|
|
|
|
const REASON_FILES = 'files_changed';
|
|
const REASON_NEED_PHP_70 = 'need_php_70';
|
|
const REASON_OS_NOT_SUPPORTED = 'os_not_supported';
|
|
|
|
public $table = '';
|
|
public $statuses_table = '';
|
|
public $remind_table = '';
|
|
public $seen_table = '';
|
|
|
|
public $updateServer = '';
|
|
|
|
// if the user doesn't make any choice, show at the next login after this amount of time
|
|
// set to 0 to disable
|
|
public $popupQuietTime = 1800;
|
|
|
|
// timeouts in seconds after which each state is reset
|
|
public $status_timeouts = array(
|
|
self::STATUS_WAIT_PACKAGE => array('timeout' => 86400, 'goto' => self::STATUS_IDLE),
|
|
self::STATUS_DOWNLOADING => array('timeout' => 7200, 'goto' => self::STATUS_WAIT_PACKAGE),
|
|
self::STATUS_CHECK_FILES => array('timeout' => 7200, 'goto' => self::STATUS_NOT_UPDATABLE),
|
|
);
|
|
|
|
public $hashes_file = 'modules/Update/md5_hashes.txt'; // crmv@183486
|
|
|
|
public function __construct() {
|
|
global $table_prefix;
|
|
|
|
$this->table = $table_prefix.'_autoupdate';
|
|
$this->statuses_table = $table_prefix.'_autoupdate_statuses';
|
|
$this->remind_table = $table_prefix.'_autoupdate_reminder';
|
|
$this->seen_table = $table_prefix.'_autoupdate_seen';
|
|
|
|
eval(Users::m_de_cryption());
|
|
eval($hash_version[23]);
|
|
|
|
if (empty($this->updateServer)) {
|
|
if ($this->isCommunity()) {
|
|
$this->updateServer = 'https://autoupdatece.vtecrm.net/';
|
|
} else {
|
|
$this->updateServer = 'https://autoupdate.vtecrm.net/';
|
|
}
|
|
}
|
|
}
|
|
|
|
public function createTables() {
|
|
global $adb;
|
|
|
|
// crmv@183486
|
|
if(!Vtecrm_Utils::CheckTable($this->table)) {
|
|
$schema = '<?xml version="1.0"?>
|
|
<schema version="0.3">
|
|
<table name="'.$this->table.'">
|
|
<opt platform="mysql">ENGINE=InnoDB</opt>
|
|
<field name="id" type="I" size="11">
|
|
<KEY/>
|
|
</field>
|
|
<field name="status" type="I" size="11">
|
|
<NOTNULL/>
|
|
</field>
|
|
<field name="reason" type="C" size="31" />
|
|
<field name="last_check_time" type="T">
|
|
<DEFAULT value="0000-00-00 00:00:00"/>
|
|
</field>
|
|
<field name="new_revision" type="I" size="11"/>
|
|
<field name="new_version" type="C" size="31"/>
|
|
<field name="scheduled_time" type="T">
|
|
<DEFAULT value="0000-00-00 00:00:00"/>
|
|
</field>
|
|
<field name="scheduled_users" type="XL"/>
|
|
<field name="scheduled_message" type="XL"/>
|
|
<field name="comments_ids" type="C" size="255"/>
|
|
<field name="userid" type="I" size="19"/>
|
|
</table>
|
|
</schema>';
|
|
$schema_obj = new adoSchema($adb->database);
|
|
$schema_obj->ExecuteSchema($schema_obj->ParseSchemaString($schema));
|
|
}
|
|
// crmv@183486e
|
|
|
|
if(!Vtecrm_Utils::CheckTable($this->statuses_table)) {
|
|
$schema = '<?xml version="1.0"?>
|
|
<schema version="0.3">
|
|
<table name="'.$this->statuses_table.'">
|
|
<opt platform="mysql">ENGINE=InnoDB</opt>
|
|
<field name="status" type="I" size="11">
|
|
<KEY/>
|
|
</field>
|
|
<field name="last_change" type="T">
|
|
<DEFAULT value="0000-00-00 00:00:00"/>
|
|
</field>
|
|
</table>
|
|
</schema>';
|
|
$schema_obj = new adoSchema($adb->database);
|
|
$schema_obj->ExecuteSchema($schema_obj->ParseSchemaString($schema));
|
|
}
|
|
|
|
if(!Vtecrm_Utils::CheckTable($this->remind_table)) {
|
|
$schema = '<?xml version="1.0"?>
|
|
<schema version="0.3">
|
|
<table name="'.$this->remind_table.'">
|
|
<opt platform="mysql">ENGINE=InnoDB</opt>
|
|
<field name="userid" type="I" size="19">
|
|
<KEY/>
|
|
</field>
|
|
<field name="revision" type="I" size="11"/>
|
|
<field name="remind_after" type="T">
|
|
<DEFAULT value="0000-00-00 00:00:00"/>
|
|
</field>
|
|
</table>
|
|
</schema>';
|
|
$schema_obj = new adoSchema($adb->database);
|
|
$schema_obj->ExecuteSchema($schema_obj->ParseSchemaString($schema));
|
|
}
|
|
|
|
if(!Vtecrm_Utils::CheckTable($this->seen_table)) {
|
|
$schema = '<?xml version="1.0"?>
|
|
<schema version="0.3">
|
|
<table name="'.$this->seen_table.'">
|
|
<opt platform="mysql">ENGINE=InnoDB</opt>
|
|
<field name="userid" type="I" size="19">
|
|
<KEY/>
|
|
</field>
|
|
<field name="seen_time" type="T">
|
|
<DEFAULT value="0000-00-00 00:00:00"/>
|
|
</field>
|
|
</table>
|
|
</schema>';
|
|
$schema_obj = new adoSchema($adb->database);
|
|
$schema_obj->ExecuteSchema($schema_obj->ParseSchemaString($schema));
|
|
}
|
|
}
|
|
|
|
// crmv@183486
|
|
/**
|
|
* Create md5 hashes to be passed to vteUpdater to skip the overwritten files
|
|
*/
|
|
public function createFileHashes() {
|
|
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
|
|
exec('md5sum ./modules/Update/*.* > '.$this->hashes_file);
|
|
exec('md5sum ./modules/Update/language/*.* >> '.$this->hashes_file);
|
|
exec('md5sum ./Smarty/templates/modules/Update/*.tpl >> '.$this->hashes_file);
|
|
}
|
|
}
|
|
// crmv@183486e
|
|
|
|
public function isCommunity() {
|
|
global $enterprise_mode;
|
|
return ($enterprise_mode === 'VTENEXTCE');
|
|
}
|
|
|
|
// --------------------------------- DB functions ---------------------------------
|
|
|
|
public function getStatus() {
|
|
global $adb;
|
|
|
|
$status = false;
|
|
|
|
$res = $adb->query("SELECT status FROM {$this->table}");
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
$status = intval($adb->query_result_no_html($res, 0, 'status'));
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
public function getInfo($field = null) {
|
|
global $adb;
|
|
|
|
$row = array();
|
|
$res = $adb->query("SELECT ".($field ?: '*')." FROM {$this->table}");
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
$row = $adb->FetchByAssoc($res, -1, false);
|
|
}
|
|
|
|
if ($field) $row = $row[$field];
|
|
|
|
return $row;
|
|
}
|
|
|
|
public function getRemindInfo($userid) {
|
|
global $adb;
|
|
|
|
$row = array();
|
|
$res = $adb->pquery("SELECT * FROM {$this->remind_table} WHERE userid = ?", array($userid));
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
$row = $adb->FetchByAssoc($res, -1, false);
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
public function getSeenTime($userid) {
|
|
global $adb;
|
|
|
|
$time = null;
|
|
$res = $adb->pquery("SELECT seen_time FROM {$this->seen_table} WHERE userid = ?", array($userid));
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
$time = $adb->query_result_no_html($res, 0, 'seen_time');
|
|
}
|
|
|
|
return $time;
|
|
}
|
|
|
|
public function setLastCheck($date = null) {
|
|
global $adb;
|
|
|
|
if (empty($date)) $date = date('Y-m-d H:i:s');
|
|
$adb->pquery("UPDATE {$this->table} SET last_check_time = ?", array($date));
|
|
}
|
|
|
|
public function setStatus($status, $fields = array()) {
|
|
global $adb;
|
|
|
|
unset($fields['status']);
|
|
$r = $adb->pquery("UPDATE {$this->table} SET status = ? WHERE status != ?", array($status, $status));
|
|
|
|
if ($adb->getAffectedRowCount($r) > 0) {
|
|
// ok, the status changed, set the changed time
|
|
$this->setStatusChangeTime($status);
|
|
}
|
|
|
|
if (count($fields) > 0) {
|
|
$update = array();
|
|
foreach ($fields as $field => $val) {
|
|
$update[] = $field .' = ?';
|
|
}
|
|
$adb->pquery("UPDATE {$this->table} SET ".implode(', ', $update), $fields);
|
|
}
|
|
}
|
|
|
|
public function getStatusDuration($status) {
|
|
global $adb;
|
|
|
|
$now = time();
|
|
|
|
$r = $adb->pquery("SELECT last_change FROM {$this->statuses_table} WHERE status = ?", array($status));
|
|
if ($r && $adb->num_rows($r) > 0) {
|
|
$lc = $adb->query_result_no_html($r, 0, 'last_change');
|
|
|
|
return ($now - strtotime($lc));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function setStatusChangeTime($status) {
|
|
global $adb;
|
|
|
|
$time = date('Y-m-d H:i:s');
|
|
|
|
$r = $adb->pquery("UPDATE {$this->statuses_table} SET last_change = ? WHERE status = ?", array($time, $status));
|
|
if ($adb->getAffectedRowCount($r) == 0) {
|
|
// no row present, insert!
|
|
$adb->pquery("INSERT INTO {$this->statuses_table} (status, last_change) VALUES (?,?)", array($status, $time));
|
|
}
|
|
}
|
|
|
|
protected function insertFirstStatus() {
|
|
global $adb;
|
|
|
|
$params = array(1, self::STATUS_IDLE);
|
|
$adb->pquery("INSERT INTO {$this->table} (id, status) VALUES (".generateQuestionMarks($params).")", $params);
|
|
}
|
|
|
|
// --------------------------------- Utility functions ---------------------------------
|
|
|
|
// crmv@199352
|
|
/**
|
|
* Reset the cron lastrun time to have it run now
|
|
*/
|
|
public function forceCron() {
|
|
global $adb, $table_prefix;
|
|
|
|
$time = date('Y-m-d 00:00:00', time()-3600*24*7); // 7 days ago
|
|
$res = $adb->pquery("UPDATE {$table_prefix}_cronjobs SET lastrun = ? WHERE cronname = ?", array($time, 'CheckUpdates'));
|
|
|
|
if ($res && $adb->getAffectedRowCount($res) > 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// crmv@199352e
|
|
|
|
/**
|
|
* Check whether autoupdate can run on this system
|
|
*/
|
|
public function canAutoupdate() {
|
|
global $adb;
|
|
|
|
// updateServer must be populated correctly
|
|
if (empty($this->updateServer)) {
|
|
$this->logWarning("Update server is empty");
|
|
return false;
|
|
}
|
|
|
|
// only with mysql I can autoupdate
|
|
$dbType = $adb->dbType;
|
|
if ($dbType != 'mysql' && $dbType != 'mysqli') {
|
|
return false;
|
|
}
|
|
|
|
// Windows is not supported in Business
|
|
// in Community, let notify and show instructions for manual update
|
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && !$this->isCommunity()) {
|
|
return false;
|
|
}
|
|
|
|
// VTE dir must be writable
|
|
if (!is_writable('./') || !is_writable('modules/') || !is_writable('config.inc.php')) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function shouldShowPopup($user) {
|
|
$status = $this->getStatus();
|
|
|
|
if ($status == self::STATUS_UPDATABLE || $status == self::STATUS_NOT_UPDATABLE) {
|
|
if ($this->popupQuietTime > 0) {
|
|
// check if the user has seen recently the popup
|
|
// if he never answered, show again at the next login, but after some time
|
|
$lastSeen = $this->getSeenTime($user->id);
|
|
if ($lastSeen) {
|
|
$limit = date('Y-m-d H:i:s', time() - $this->popupQuietTime);
|
|
if ($lastSeen > $limit) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} elseif ($status == self::STATUS_POSTPONED) {
|
|
$rinfo = $this->getRemindInfo($user->id);
|
|
$time = $rinfo['remind_after'];
|
|
if ($time) {
|
|
$now = date('Y-m-d H:i:s');
|
|
if ($now >= $time) {
|
|
return true;
|
|
}
|
|
} else {
|
|
// reminded by someone else, don't show anything
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function shouldUpdateNow() {
|
|
$status = $this->getStatus();
|
|
|
|
if ($status === self::STATUS_SCHEDULED) {
|
|
$now = date('Y-m-d H:i:s');
|
|
$when = $this->getInfo('scheduled_time');
|
|
return ($when <= $now);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getReminderOptions() {
|
|
$list = array(
|
|
'in_4_hours' => getTranslatedString('LBL_REMIND_IN_4_HOURS', 'Update'), // crmv@183486
|
|
'tomorrow' => getTranslatedString('LBL_REMIND_TOMORROW', 'Update'),
|
|
'next_week' => getTranslatedString('LBL_REMIND_NEXT_WEEK', 'Update'),
|
|
);
|
|
return $list;
|
|
}
|
|
|
|
public function canIgnoreUpdate($user) {
|
|
$status = $this->getStatus();
|
|
|
|
return in_array($status, array(self::STATUS_UPDATABLE, self::STATUS_POSTPONED, self::STATUS_NOT_UPDATABLE));
|
|
}
|
|
|
|
public function canRemindUpdate($user) {
|
|
return $this->canIgnoreUpdate($user);
|
|
}
|
|
|
|
public function canScheduleUpdate($user) {
|
|
return $this->canIgnoreUpdate($user);
|
|
}
|
|
|
|
public function canCancelUpdate($user) {
|
|
$status = $this->getStatus();
|
|
return ($status === self::STATUS_SCHEDULED);
|
|
}
|
|
|
|
public function setPopupSeen($user) {
|
|
global $adb, $table_prefix;
|
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
$res = $adb->pquery("UPDATE {$this->seen_table} SET seen_time = ? WHERE userid = ?", array($now, $user->id));
|
|
if ($res && $adb->getAffectedRowCount($res) == 0) {
|
|
$res = $adb->pquery("INSERT INTO {$this->seen_table} (userid, seen_time) VALUES (?,?)", array($user->id, $now));
|
|
}
|
|
}
|
|
|
|
public function ignoreUpdate($user) {
|
|
$this->setStatus(AutoUpdater::STATUS_REFUSED);
|
|
}
|
|
|
|
public function remindUpdate($user, $when) {
|
|
global $adb, $table_prefix;
|
|
|
|
$list = $this->getReminderOptions();
|
|
|
|
if (!array_key_exists($when, $list)) throw new Exception("Invalid value for when parameter");
|
|
|
|
$info = $this->getInfo();
|
|
$revision = $info['new_revision'];
|
|
|
|
switch ($when) {
|
|
case 'in_4_hours': // crmv@183486
|
|
$after = date('Y-m-d H:i:s', time()+3600*4); // crmv@183486
|
|
break;
|
|
case 'tomorrow':
|
|
// after the user's start hour
|
|
$startHour = getSingleFieldValue($table_prefix.'_users', 'start_hour', 'id', $user->id) ?: '08:00';
|
|
$after = date('Y-m-d ', strtotime('+1 day')).substr(trim($startHour), 0, 5).':00';
|
|
break;
|
|
case 'next_week':
|
|
// using first day of the week and start hour
|
|
$startHour = getSingleFieldValue($table_prefix.'_users', 'start_hour', 'id', $user->id) ?: '08:00';
|
|
$startDay = getSingleFieldValue($table_prefix.'_users', 'weekstart', 'id', $user->id);
|
|
if ($startDay === '') $startDay = '1';
|
|
$startDayName = ($startDay == '1' ? 'Monday' : 'Sunday');
|
|
$after = date('Y-m-d ', strtotime('next '.$startDayName)).substr(trim($startHour), 0, 5).':00';
|
|
break;
|
|
default:
|
|
throw new Exception("Unknown value for when parameter");
|
|
}
|
|
|
|
$params = array($user->id, $revision, $after);
|
|
|
|
$adb->pquery("DELETE FROM {$this->remind_table} WHERE userid = ?", array($user->id));
|
|
$adb->pquery("INSERT INTO {$this->remind_table} (userid, revision, remind_after) VALUES (?,?,?)", $params);
|
|
|
|
$this->setStatus(AutoUpdater::STATUS_POSTPONED);
|
|
}
|
|
|
|
public function scheduleUpdate($user, $data) {
|
|
global $adb;
|
|
|
|
// delete all reminders from remind table
|
|
$adb->query("DELETE FROM {$this->remind_table}");
|
|
|
|
// set scheduling info
|
|
$adb->pquery("UPDATE {$this->table} SET scheduled_time = ?, userid = ?", array($data['date'], $user->id));
|
|
$adb->updateClob($this->table, 'scheduled_message', 'id = 1', $data['message']);
|
|
$adb->updateClob($this->table, 'scheduled_users', 'id = 1', Zend_Json::encode($data['alert']));
|
|
|
|
// crmv@183486
|
|
$commentids = $this->sendUpdateAlert($user, $data);
|
|
|
|
$this->setStatus(AutoUpdater::STATUS_SCHEDULED, array('comments_ids' => Zend_Json::encode($commentids)));
|
|
// crmv@183486e
|
|
}
|
|
|
|
public function getDiffFile() {
|
|
global $enterprise_current_build;
|
|
|
|
$path = "vte_updater/$enterprise_current_build/differences.log";
|
|
|
|
if (file_exists($path)) return $path;
|
|
return null;
|
|
}
|
|
|
|
// crmv@183486
|
|
public function cancelUpdate($user) {
|
|
global $adb;
|
|
|
|
// find previous state
|
|
$res = $adb->limitpQuery(
|
|
"SELECT status FROM {$this->statuses_table} WHERE status IN (?,?) ORDER BY last_change DESC",
|
|
0, 1,
|
|
array(self::STATUS_UPDATABLE, self::STATUS_NOT_UPDATABLE)
|
|
);
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
$prevStatus = $adb->query_result_no_html($res, 0, 'status');
|
|
} else {
|
|
// guess the safest one
|
|
$prevStatus = self::STATUS_NOT_UPDATABLE;
|
|
}
|
|
|
|
$this->setStatus($prevStatus);
|
|
|
|
$info = $this->getInfo();
|
|
$schedulerId = $info['userid'];
|
|
$users = Zend_Json::decode($info['scheduled_users']);
|
|
|
|
$allUsers = $this->extractUsers($users);
|
|
if (count($allUsers) == 0) return;
|
|
|
|
$myname = getUserFullName($user->id);
|
|
$email = getUserEmail($user->id);
|
|
|
|
// extract emails
|
|
$emails = array();
|
|
foreach ($allUsers as $userid => $userinfo) {
|
|
if ($userinfo['email']) $emails[] = $userinfo['email'];
|
|
}
|
|
|
|
list($sdate, $stime) = explode(' ', $info['scheduled_time']);
|
|
|
|
$msg = getTranslatedString('LBL_CANCEL_BODY', 'Update');
|
|
$msg = trim(str_replace(
|
|
array(
|
|
'{date}',
|
|
'{hour}',
|
|
),
|
|
array(
|
|
substr($sdate, 0, 10),
|
|
substr($stime, 0, 5),
|
|
),
|
|
$msg
|
|
));
|
|
|
|
// send the answer to the comment
|
|
$commentids = Zend_Json::decode($info['comments_ids']) ?: array();
|
|
foreach ($commentids as $commentid) {
|
|
$focus = CRMEntity::getInstance('ModComments');
|
|
$focus->retrieve_entity_info($commentid, 'ModComments');
|
|
// add myself if I wasn't included and I wasn't the author
|
|
if ($focus->column_fields['assigned_user_id'] != $user->id) {
|
|
$focus->addUsers(array($user->id));
|
|
}
|
|
// and add reply!
|
|
$focus = CRMEntity::getInstance('ModComments');
|
|
$focus->column_fields['commentcontent'] = $msg;
|
|
$focus->column_fields['assigned_user_id'] = $user->id;
|
|
$focus->column_fields['parent_comments'] = $commentid;
|
|
$focus->save('ModComments');
|
|
}
|
|
|
|
// send emails!
|
|
if (count($emails) > 0) {
|
|
// make the message more html like
|
|
$msg = nl2br($msg);
|
|
$r = send_mail('Update', $emails, $myname, $email, 'vtenext update canceled', $msg);
|
|
}
|
|
}
|
|
|
|
protected function sendUpdateAlert($user, $data) {
|
|
global $adb, $table_prefix, $site_URL;
|
|
|
|
$users = $data['alert'];
|
|
$msg = $data['message'];
|
|
|
|
$allUsers = $this->extractUsers($users);
|
|
if (count($allUsers) == 0) return;
|
|
|
|
$myname = getUserFullName($user->id);
|
|
$email = getUserEmail($user->id);
|
|
|
|
// divide users in admin/non-admin and extract emails
|
|
$adminUsers = $nonAdminUsers = array();
|
|
$adminEmails = $nonAdminEmails = array();
|
|
foreach ($allUsers as $userid => $userinfo) {
|
|
if ($userinfo['is_admin']) {
|
|
$adminUsers[] = $userid;
|
|
if ($userinfo['email']) $adminEmails[] = $userinfo['email'];
|
|
} else {
|
|
$nonAdminUsers[] = $userid;
|
|
if ($userinfo['email']) $nonAdminEmails[] = $userinfo['email'];
|
|
}
|
|
}
|
|
|
|
$commentids = array();
|
|
|
|
if (count($adminUsers) > 0) {
|
|
$cancelUrl = $site_URL.'/index.php?module=Update&action=CancelUpdate&parenttab=Settings';
|
|
$cancelText = getTranslatedString('LBL_UPDATE_DEFAULT_CANCEL_TEXT', 'Update');
|
|
$cancelText = str_replace('%s', $cancelUrl, $cancelText);
|
|
|
|
// now replace vars in the message (different for admin/non-admin)
|
|
$msgAdmin = trim(str_replace(
|
|
array(
|
|
'{date}',
|
|
'{hour}',
|
|
'{cancel_text}'
|
|
),
|
|
array(
|
|
substr($data['date'], 0, 10), // I know, I shoudld format according to the user, but then I'd have to send N messages...
|
|
substr($data['date'], 11, 5),
|
|
$cancelText
|
|
),
|
|
$msg
|
|
));
|
|
|
|
// for notifications, text only messages!
|
|
$msgAdminNot = strip_tags($msgAdmin);
|
|
$msgAdminNot = preg_replace('/.$/', ': '.$cancelUrl.' .', $msgAdminNot);
|
|
|
|
// alert by notification
|
|
$focus = CRMEntity::getInstance('ModComments');
|
|
$focus->column_fields['commentcontent'] = $msgAdminNot;
|
|
$focus->column_fields['assigned_user_id'] = $user->id;
|
|
$focus->column_fields['visibility_comm'] = 'Users';
|
|
|
|
// remove myself from recipients now
|
|
$k = array_search($user->id, $adminUsers);
|
|
if ($k !== false) {
|
|
unset($adminUsers[$k]);
|
|
}
|
|
$_REQUEST['users_comm'] = implode('|', $adminUsers); // uugh, ugly trick!!
|
|
$focus->save('ModComments');
|
|
$commentids[] = intval($focus->id);
|
|
|
|
// alert by email
|
|
if (count($adminEmails) > 0) {
|
|
// make the message more html like
|
|
$msgAdmin = nl2br($msgAdmin);
|
|
$r = send_mail('Update', $adminEmails, $myname, $email, 'vtenext update', $msgAdmin);
|
|
}
|
|
}
|
|
|
|
if (count($nonAdminUsers) > 0) {
|
|
$msgNonAdmin = trim(str_replace(
|
|
array(
|
|
'{date}',
|
|
'{hour}',
|
|
'{cancel_text}'
|
|
),
|
|
array(
|
|
substr($data['date'], 0, 10), // I know, I shoudld format according to the user, but then I'd have to send N messages...
|
|
substr($data['date'], 11, 5),
|
|
''
|
|
),
|
|
$msg
|
|
));
|
|
|
|
// alert by notification
|
|
$focus = CRMEntity::getInstance('ModComments');
|
|
$focus->column_fields['commentcontent'] = $msgNonAdmin;
|
|
$focus->column_fields['assigned_user_id'] = $user->id;
|
|
$focus->column_fields['visibility_comm'] = 'Users';
|
|
$_REQUEST['users_comm'] = implode('|', $nonAdminUsers); // uugh, ugly trick!!
|
|
$focus->save('ModComments');
|
|
$commentids[] = intval($focus->id);
|
|
|
|
// alert by email
|
|
if (count($nonAdminEmails) > 0) {
|
|
// make the message more html like
|
|
$msgNonAdmin = nl2br($msgNonAdmin);
|
|
$r = send_mail('Update', $nonAdminEmails, $myname, $email, 'vtenext update', $msgNonAdmin);
|
|
}
|
|
}
|
|
|
|
return $commentids;
|
|
}
|
|
|
|
/**
|
|
* Extract a list of all users from the users/group arrays
|
|
*/
|
|
protected function extractUsers($users) {
|
|
global $adb, $table_prefix;
|
|
|
|
$allUsers = array();
|
|
if (is_array($users['users'])) {
|
|
$allUsers = $users['users'];
|
|
}
|
|
if (in_array('all', $allUsers)) {
|
|
$allUsers = array();
|
|
$res = $adb->query("SELECT id, COALESCE(email1, email2) as email, is_admin FROM {$table_prefix}_users WHERE status = 'Active' AND deleted = 0");
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
while ($row = $adb->fetchByAssoc($res, -1, false)) {
|
|
$id = intval($row['id']);
|
|
$allUsers[$id] = array('email' => $row['email'], 'is_admin' => ($row['is_admin'] == 'on'));
|
|
}
|
|
}
|
|
} else {
|
|
if (is_array($users['groups'])) {
|
|
foreach ($users['groups'] as $groupid) {
|
|
$class = new GetGroupUsers();
|
|
$class->getAllUsersInGroup($groupid, true, true);
|
|
$allUsers = array_merge($allUsers, $class->group_users);
|
|
}
|
|
}
|
|
|
|
$allUsers = array_filter(array_unique(array_map('intval', $allUsers)));
|
|
|
|
if (count($allUsers) > 0) {
|
|
$res = $adb->pquery("SELECT id, COALESCE(email1, email2) as email, is_admin FROM {$table_prefix}_users WHERE status = 'Active' AND id IN (".generateQuestionMarks($allUsers).")", $allUsers);
|
|
if ($res && $adb->num_rows($res) > 0) {
|
|
while ($row = $adb->fetchByAssoc($res, -1, false)) {
|
|
$id = intval($row['id']);
|
|
$allUsers[$id] = array('email' => $row['email'], 'is_admin' => ($row['is_admin'] == 'on'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $allUsers;
|
|
}
|
|
// crmv@183486e
|
|
|
|
|
|
public function validateSchedule($post, &$data = array(), &$error = '') {
|
|
$status = $this->getStatus();
|
|
$reason = $this->getInfo('reason');
|
|
|
|
$date = substr($post['schedule_date'], 0, 10);
|
|
$hour = substr($post['schedule_hour'], 0, 5);
|
|
|
|
// TODO: translate errors!
|
|
|
|
if ($status == self::STATUS_NOT_UPDATABLE && $reason == self::REASON_FILES) {
|
|
if ($post['alert_changes'] !== 'on') {
|
|
$error = "You must consent to overwrite files";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (empty($date)) {
|
|
$error = "Invalid date";
|
|
return false;
|
|
}
|
|
|
|
if (empty($hour)) {
|
|
$error = "Invalid hour";
|
|
return false;
|
|
}
|
|
|
|
$dbDate = getValidDBInsertDateValue($date);
|
|
if ($dbDate < date('Y-m-d')) {
|
|
$error = getTranslatedString('LBL_DATE_IS_PAST', 'Update');
|
|
return false;
|
|
} elseif ($dbDate.' '.$hour.':00' <= date('Y-m-d H:i:s', time()+600)) {
|
|
$error = getTranslatedString('LBL_DATE_TOO_CLOSE', 'Update');
|
|
return false;
|
|
}
|
|
|
|
$data['date'] = $dbDate.' '.$hour.':00';
|
|
|
|
if ($post['schedule_alert'] == 'on') {
|
|
$users = Zend_Json::decode($post['schedule_users']);
|
|
$msg = $post['schedule_message'];
|
|
|
|
if (empty($users)) {
|
|
$error = "You must select at least one user";
|
|
return false;
|
|
}
|
|
|
|
if (empty($msg)) {
|
|
$error = "You must provide a message";
|
|
return false;
|
|
}
|
|
|
|
$data['alert'] = array();
|
|
foreach ($users as $us) {
|
|
list($type, $id) = explode('::', $us);
|
|
$data['alert'][$type][] = ($id === 'all' ? $id : intval($id)); // crmv@183486
|
|
}
|
|
$data['message'] = $msg;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --------------------------------- WS functions ---------------------------------
|
|
|
|
protected function updateWSCall($service, $params = array()) {
|
|
$url = $this->updateServer.'ws.php?wsname='.$service;
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
|
curl_setopt($ch, CURLOPT_HEADER, 0);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
|
|
$content = curl_exec($ch);
|
|
$errno = curl_errno($ch);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($errno > 0) {
|
|
throw new Exception("Unable to communicate with update server: $error");
|
|
}
|
|
|
|
if (!$content) throw new Exception("Invalid answer from update server");
|
|
|
|
$content = Zend_Json::decode($content);
|
|
if (!$content) throw new Exception("Invalid answer from update server");
|
|
|
|
if ($content['success'] !== true) throw new Exception("Update server returned error: ".$content['error']);
|
|
|
|
return $content;
|
|
}
|
|
|
|
public function getLatestRevision() {
|
|
$content = $this->updateWSCall('get_latest_revision');
|
|
$newrev = intval($content['result']);
|
|
|
|
return $newrev;
|
|
}
|
|
|
|
public function getUpdateRanges() {
|
|
$ranges = $this->updateWSCall('get_safe_update_ranges');
|
|
return $ranges['result'];
|
|
}
|
|
|
|
public function getRevisionInfo($revision) {
|
|
$info = $this->updateWSCall('get_revision_info', array('revision' => $revision));
|
|
return $info['result'];
|
|
}
|
|
|
|
|
|
// --------------------------------- Main logic ---------------------------------
|
|
|
|
/**
|
|
* State-machine handler
|
|
*/
|
|
public function statusHandler() {
|
|
$status = $this->getStatus();
|
|
|
|
if ($status === false) {
|
|
// never run before! set the default empty line
|
|
$this->insertFirstStatus();
|
|
return;
|
|
}
|
|
|
|
// timeout check
|
|
if (array_key_exists($status, $this->status_timeouts)) {
|
|
$timeinfo = $this->status_timeouts[$status];
|
|
$duration = $this->getStatusDuration($status);
|
|
if ($duration && $duration > $timeinfo['timeout']) { // crmv@201821
|
|
$goto = $timeinfo['goto'];
|
|
$this->logWarning("Status $status hit timeout limit, setting it back to $goto");
|
|
$this->setStatus($goto);
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch ($status) {
|
|
case self::STATUS_IDLE:
|
|
case self::STATUS_REFUSED:
|
|
if ($this->canAutoupdate()) {
|
|
$this->checkUpdates($status);
|
|
}
|
|
break;
|
|
case self::STATUS_WAIT_PACKAGE:
|
|
$info = $this->getInfo();
|
|
$this->downloadAndCheck($info['new_revision']);
|
|
break;
|
|
case self::STATUS_NOT_UPDATABLE:
|
|
// the user will be asked what to do
|
|
$reason = $this->getInfo('reason');
|
|
if ($reason == self::REASON_NEED_PHP_70) {
|
|
$this->checkPhpVersion('7.0');
|
|
}
|
|
break;
|
|
case self::STATUS_SUCCESS:
|
|
// last update was ok, reset the status
|
|
$this->resetStatus();
|
|
break;
|
|
}
|
|
}
|
|
|
|
public function checkPhpVersion($needed) {
|
|
$ok = version_compare(phpversion(), $needed, '>=');
|
|
if ($ok) {
|
|
// good, reset the status so I can check again
|
|
$this->logInfo('PHP has been updated, resetting the status');
|
|
$this->setStatus(self::STATUS_IDLE, array('reason' => ''));
|
|
}
|
|
return $ok;
|
|
}
|
|
|
|
/**
|
|
* Reset the status to 0, remove reminders/seen
|
|
*/
|
|
public function resetStatus() {
|
|
global $adb;
|
|
|
|
$adb->pquery("UPDATE {$this->table} SET reason = NULL, new_revision = 0, new_version = NULL, scheduled_time = ?, scheduled_users = NULL, scheduled_message = NULL, userid = 0, comments_ids = NULL", array('0000-00-00 00:00:00'));
|
|
$adb->query("DELETE FROM {$this->seen_table}");
|
|
$adb->query("DELETE FROM {$this->remind_table}");
|
|
$this->setStatus(self::STATUS_IDLE);
|
|
}
|
|
|
|
public function checkUpdates($status = null) {
|
|
global $adb;
|
|
|
|
if (is_null($status)) {
|
|
$status = $this->getStatus();
|
|
}
|
|
|
|
$this->setLastCheck();
|
|
|
|
$newrev = $this->getLatestRevision();
|
|
|
|
global $enterprise_current_build;
|
|
|
|
if ($newrev <= $enterprise_current_build) return false; // nothing to update
|
|
|
|
$newrev = $this->decideNextRevision($newrev);
|
|
|
|
$info = $this->getRevisionInfo($newrev);
|
|
$newversion = $info['version'];
|
|
|
|
// ok, set the new revision
|
|
$adb->pquery("UPDATE {$this->table} SET new_revision = ?, new_version = ?", array($newrev, $newversion));
|
|
|
|
// ok I have an update!!
|
|
if ($status == self::STATUS_REFUSED) {
|
|
// if I had refused, check if this update is newer than the old one
|
|
$info = $this->getInfo();
|
|
|
|
if ($newrev > $info['new_revision']) {
|
|
$this->logInfo("Found new version ($newrev) after the refused one, checking for compatibility...");
|
|
return $this->downloadAndCheck($newrev);
|
|
}
|
|
} else {
|
|
// otherwise
|
|
$this->logInfo("Found new version available ($newrev), checking for compatibility...");
|
|
return $this->downloadAndCheck($newrev);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function downloadAndCheck($revision) {
|
|
global $enterprise_current_build;
|
|
|
|
// check if already downloaded
|
|
$basename = "vte_updater/packages/vte{$enterprise_current_build}-{$revision}";
|
|
if (is_readable($basename.'.tgz') && is_readable($basename.'src.tgz') && is_readable($basename.'del.tgz')) {
|
|
$this->logInfo("Files already downloaded, using them");
|
|
|
|
} else {
|
|
$params = array(
|
|
'start_revision' => $enterprise_current_build,
|
|
'dest_revision' => $revision,
|
|
);
|
|
$content = $this->updateWSCall('is_package_available', $params);
|
|
$avail = ($content['result'] == '1');
|
|
|
|
if (!$avail) {
|
|
$status = $this->getStatus();
|
|
if ($status == self::STATUS_WAIT_PACKAGE || $this->isCommunity()) {
|
|
$this->logInfo("Package is not available yet, keep waiting...");
|
|
if ($status != self::STATUS_WAIT_PACKAGE) {
|
|
$this->setStatus(self::STATUS_WAIT_PACKAGE, array('new_revision' => $revision));
|
|
}
|
|
} else {
|
|
$this->logInfo("Package is not available, requesting it...");
|
|
$this->requestPackage($revision);
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
$this->logInfo("Package is available, downloading it...");
|
|
|
|
$this->downloadPackage($revision);
|
|
}
|
|
|
|
// on Windows only manual update is supported
|
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
|
$this->setStatus(self::STATUS_NOT_UPDATABLE, array('new_revision' => $revision, 'reason' => self::REASON_OS_NOT_SUPPORTED));
|
|
$this->logInfo("On Windows only manual update is possible");
|
|
$this->sendNotification();
|
|
} else {
|
|
$this->checkVteUpdater();
|
|
$this->checkUpdatability($revision);
|
|
}
|
|
}
|
|
|
|
protected function requestPackage($revision) {
|
|
global $site_URL, $application_unique_key, $enterprise_current_build;
|
|
|
|
$this->setStatus(self::STATUS_WAIT_PACKAGE, array('new_revision' => $revision));
|
|
|
|
$idlic = getMorphsuitNo();
|
|
$lic = getSavedMorphsuit();
|
|
$hash = hash('sha256', $lic.$application_unique_key);
|
|
|
|
if (empty($idlic)) throw new Exception("This installation doesn't have a valid license");
|
|
|
|
$params = array(
|
|
'username' => 'LIC_'.$idlic,
|
|
'hashedkey' => $hash,
|
|
'start_revision' => $enterprise_current_build,
|
|
'dest_revision' => $revision,
|
|
'vte_url' => $site_URL,
|
|
'email' => 'nomail@example.com',
|
|
);
|
|
$content = $this->updateWSCall('request_package', $params);
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function downloadPackage($revision) {
|
|
global $site_URL, $application_unique_key, $enterprise_current_build;
|
|
|
|
$this->setStatus(self::STATUS_DOWNLOADING, array('new_revision' => $revision));
|
|
|
|
if ($this->isCommunity()) {
|
|
|
|
$params = array(
|
|
'start_revision' => $enterprise_current_build,
|
|
'dest_revision' => $revision,
|
|
'vte_url' => $site_URL,
|
|
);
|
|
|
|
} else {
|
|
$idlic = getMorphsuitNo();
|
|
$lic = getSavedMorphsuit();
|
|
$hash = hash('sha256', $lic.$application_unique_key);
|
|
|
|
if (empty($idlic)) throw new Exception("This installation doesn't have a valid license");
|
|
|
|
$params = array(
|
|
'username' => 'LIC_'.$idlic,
|
|
'hashedkey' => $hash,
|
|
'start_revision' => $enterprise_current_build,
|
|
'dest_revision' => $revision,
|
|
'vte_url' => $site_URL,
|
|
);
|
|
}
|
|
|
|
$answer = $this->updateWSCall('request_download', $params);
|
|
$files = $answer['result'];
|
|
|
|
if (empty($files)) throw new Exception("The update server didn't returned valid files");
|
|
|
|
// make dest dirs
|
|
if (!is_dir('vte_updater/packages')) {
|
|
if (!mkdir('vte_updater/packages', 0755, true)) {
|
|
throw new Exception("Unable to create the destination directory");
|
|
}
|
|
}
|
|
|
|
$basename = "vte_updater/packages/vte{$enterprise_current_build}-{$revision}";
|
|
|
|
// check if absolute url
|
|
if (substr($files['upd'], 0, 4) == 'http') {
|
|
$host = '';
|
|
} else {
|
|
$host = $this->updateServer;
|
|
}
|
|
$this->downloadFile($host.$files['upd'], $basename.'.tgz');
|
|
$this->downloadFile($host.$files['src'], $basename.'src.tgz');
|
|
if ($files['del']) {
|
|
$this->downloadFile($host.$files['del'], $basename.'del.tgz');
|
|
}
|
|
|
|
$this->logInfo("Files downloaded");
|
|
|
|
return true;
|
|
}
|
|
|
|
public function checkUpdatability($revision) {
|
|
|
|
$this->setStatus(self::STATUS_CHECK_FILES);
|
|
|
|
$this->logInfo("Checking updatability of this VTE...");
|
|
|
|
if (file_exists($this->hashes_file) && is_readable($this->hashes_file)) {
|
|
$hashOpts = '--file-hashes='.$this->hashes_file;
|
|
} else {
|
|
$hashOpts = '';
|
|
}
|
|
|
|
$out = array();
|
|
exec("./vteUpdater.sh -b -d $revision --only-file-check $hashOpts", $out, $ret);
|
|
|
|
$log = implode("\n", $out);
|
|
echo $log."\n";
|
|
|
|
if (strpos($log, 'Some differences were found') !== false) {
|
|
// not updatable
|
|
$this->setStatus(self::STATUS_NOT_UPDATABLE, array('reason' => self::REASON_FILES));
|
|
$this->logInfo("Update might overwrite files. User must decide what to do");
|
|
$this->sendNotification();
|
|
} elseif (strpos($log, 'No differences found') !== false) {
|
|
// no diff
|
|
$this->setStatus(self::STATUS_UPDATABLE);
|
|
$this->logInfo("Update is applicable! A popup/notification will be shown to admin users");
|
|
$this->sendNotification();
|
|
} elseif (strpos($log, 'The destination revision supports only PHP >= 7.0') !== false) {
|
|
// php too old to update!
|
|
$this->setStatus(self::STATUS_NOT_UPDATABLE, array('reason' => self::REASON_NEED_PHP_70));
|
|
$this->logInfo("Update cannot continue, but notify user anyway and later show alert");
|
|
$this->sendNotification();
|
|
} else {
|
|
// unknown error
|
|
throw new Exception("vteUpdater didn't answer as expected");
|
|
}
|
|
|
|
}
|
|
|
|
// notify admin users about the availability of an update
|
|
protected function sendNotification() {
|
|
global $site_URL, $table_prefix; // crmv@183486
|
|
global $HELPDESK_SUPPORT_NAME, $HELPDESK_SUPPORT_EMAIL_ID;
|
|
|
|
// this works with the old class as well as with the new one
|
|
require_once('modules/ModNotifications/ModNotifications.php');
|
|
$notfocus = ModNotifications::getInstance('ModNotifications');
|
|
|
|
// add notification type
|
|
$notfocus->addNotificationType('UPDATE_AVAILABLE', 'UPDATE_AVAILABLE');
|
|
|
|
// and add translation if missing (for old versions)
|
|
$text = getTranslatedString('UPDATE_AVAILABLE', 'ModNotifications');
|
|
$url = $site_URL.'/index.php?module=Update&action=ViewUpdate&parenttab=Settings';
|
|
if (empty($text) || strpos($text, $url) === false) {
|
|
$tpl = getTranslatedString('LBL_NOTIFICATION_TPL_TEXT', 'Update');
|
|
$tpl = str_replace('{url}', $url, $tpl);
|
|
SDK::setLanguageEntries('ModNotifications', 'UPDATE_AVAILABLE', array(
|
|
'it_it' => $tpl,
|
|
'en_us' => $tpl,
|
|
));
|
|
$text = $tpl; // there is a cache somewhere, so I have to use this value
|
|
}
|
|
|
|
$users = $this->getAllAdminIds();
|
|
|
|
foreach ($users as $userid) {
|
|
$this->logInfo("Notifying user #$userid");
|
|
|
|
$subject = 'vtenext update available';
|
|
$body = $text;
|
|
$ret = $notfocus->saveFastNotification(
|
|
array(
|
|
'assigned_user_id' => $userid,
|
|
'related_to' => '',
|
|
'mod_not_type' => 'UPDATE_AVAILABLE',
|
|
'subject' => $subject,
|
|
'description' => $body,
|
|
)
|
|
);
|
|
if (empty($ret)) {
|
|
$this->logWarning("Unable to notify user #$userid");
|
|
}
|
|
|
|
// crmv@183486
|
|
// and send also and email if it was a vte notification
|
|
$notifyType = getSingleFieldValue($table_prefix.'_users', 'notify_me_via', 'id', $userid);
|
|
if ($notifyType != 'Emails') {
|
|
$email = getUserEmail($userid);
|
|
if ($email) {
|
|
$r = send_mail('Update', $email, $HELPDESK_SUPPORT_NAME, $HELPDESK_SUPPORT_EMAIL_ID, $subject, $body);
|
|
if ($r != 1) {
|
|
$this->logWarning("Unable to notify user #$userid by email");
|
|
}
|
|
}
|
|
}
|
|
// crmv@183486e
|
|
}
|
|
|
|
}
|
|
|
|
protected function getAllAdminIds() {
|
|
global $adb, $table_prefix;
|
|
|
|
$ids = array();
|
|
$res = $adb->pquery("SELECT id FROM {$table_prefix}_users WHERE status = ? AND is_admin = ?", array('Active', 'on'));
|
|
while ($row = $adb->fetchByAssoc($res, -1, false)) {
|
|
$ids[] = $row['id'];
|
|
}
|
|
|
|
return $ids;
|
|
}
|
|
|
|
protected function checkVteUpdater() {
|
|
if (is_file('vteUpdater.sh')) {
|
|
// update it!
|
|
$this->logInfo("Updating vteUpdater.sh...");
|
|
|
|
// make it executable, just in case!
|
|
chmod('vteUpdater.sh', 0755);
|
|
|
|
// check version
|
|
$out = exec('./vteUpdater.sh --version');
|
|
if (preg_match('/version ([0-9.-]+)/', $out, $matches)) {
|
|
$version = $matches[1];
|
|
}
|
|
if (empty($version)) throw new Exception('Unable to determine vteUpdater version');
|
|
|
|
if (version_compare($version, '1.28') < 0) {
|
|
// doesn't support only-upgrade flag and community version, download it from scratch!
|
|
$this->downloadUpdater();
|
|
} else {
|
|
// ok, self check!
|
|
exec('./vteUpdater.sh --only-upgrade');
|
|
}
|
|
} else {
|
|
$this->logInfo("vteUpdater.sh missing, downloading it...");
|
|
// download vte updater!!
|
|
$this->downloadUpdater();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function downloadUpdater() {
|
|
$answer = $this->updateWSCall('request_script_download', array('name' => 'main'));
|
|
$url = $answer['result']['url'];
|
|
$this->downloadFile($this->updateServer.$url, 'vteUpdater.tgz');
|
|
|
|
$output = array();
|
|
exec('tar -xf vteUpdater.tgz', $output, $ret);
|
|
if ($ret != 0) {
|
|
throw new Exception("Unable to decompress vteUpdater: ".implode("\n", $output));
|
|
}
|
|
unlink('vteUpdater.tgz');
|
|
|
|
if (!is_file('vteUpdater.sh')) throw new Exception('Unable to decompress vteUpdater');
|
|
|
|
chmod('vteUpdater.sh', 0755);
|
|
return true;
|
|
}
|
|
|
|
protected function downloadFile($url, $dest) {
|
|
$src = fopen($url, 'r');
|
|
$dest_res = fopen($dest, 'w');
|
|
|
|
$r = stream_copy_to_stream($src, $dest_res);
|
|
fclose($src);
|
|
fclose($dest_res);
|
|
if ($r === false) throw new Exception("Unable to download file ".$url);
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function decideNextRevision($latest) {
|
|
global $enterprise_current_build;
|
|
|
|
$ranges = $this->getUpdateRanges();
|
|
|
|
// check the update ranges and choose the best destination revision
|
|
foreach ($ranges as $range) {
|
|
if ($range['rev_start'] <= $enterprise_current_build && $range['rev_end'] >= $enterprise_current_build) {
|
|
return $range['rev_dest'];
|
|
}
|
|
}
|
|
|
|
return $latest;
|
|
}
|
|
|
|
public function getFreePackageUrl() {
|
|
global $enterprise_current_build, $site_URL;
|
|
|
|
$revision = $this->getInfo('new_revision');
|
|
|
|
$params = array(
|
|
'start_revision' => $enterprise_current_build,
|
|
'dest_revision' => $revision,
|
|
'vte_url' => $site_URL,
|
|
'format' => 'zip', // crmv@183486
|
|
);
|
|
|
|
$answer = $this->updateWSCall('request_download', $params);
|
|
$files = $answer['result'];
|
|
|
|
if (substr($files['upd'], 0, 4) == 'http') {
|
|
$host = '';
|
|
} else {
|
|
$host = $this->updateServer;
|
|
}
|
|
|
|
$url = $host.$files['upd'];
|
|
|
|
return $url;
|
|
}
|
|
|
|
public function startUpdate() {
|
|
global $enterprise_current_build;
|
|
$oldVersion = $enterprise_current_build;
|
|
|
|
$this->setStatus(self::STATUS_UPDATING);
|
|
|
|
$revision = $this->getInfo('new_revision');
|
|
$user = $this->getInfo('userid');
|
|
|
|
$this->logInfo('Starting vte update');
|
|
|
|
$cj = CronJob::getByName('DoUpdate');
|
|
|
|
// wait for cron to finish
|
|
$this->cronFreezeAll();
|
|
$this->unfreezeCron('DoUpdate'); // and unfreeze myself
|
|
|
|
$r = $this->waitForAllCron(120, array($cj->getId())); // wait 2 mins, excluding myself!
|
|
if (!$r) $this->logWarning("There seems to be cron running, but updating anyway...");
|
|
|
|
$emailTabid = getTabid('Emails'); // trick to avoid new tabdata cache problem
|
|
|
|
$retcode = 0;
|
|
passthru("./vteUpdater.sh -b -d $revision --skip-upgrade --skip-file-check --skip-cron", $retcode);
|
|
|
|
if ($retcode == 0) {
|
|
// all ok!
|
|
$this->setStatus(self::STATUS_SUCCESS);
|
|
$this->logInfo('Update succesful!');
|
|
$this->cronUnfreezeAll();
|
|
$outcome = 'ok';
|
|
} elseif ($retcode == 3) {
|
|
// fail, rollback ok
|
|
$this->setStatus(self::STATUS_FAILURE);
|
|
$this->cronUnfreezeAll();
|
|
$outcome = 'fail_rollback';
|
|
} else {
|
|
// fail, vte currupted! manual action needed!
|
|
$this->setStatus(self::STATUS_FAILURE);
|
|
$this->cronUnfreezeAll();
|
|
|
|
// set maintenance mode anyway!
|
|
try {
|
|
$this->setMaintenanceMode();
|
|
} catch (Exception $e) {
|
|
$this->logError($e->getMessage());
|
|
}
|
|
|
|
$outcome = 'fail';
|
|
}
|
|
|
|
$this->notifyOutcome($outcome, $user, $oldVersion);
|
|
|
|
// now forcefully terminate cron to avoid executing other crons with old files in memory
|
|
$cj->clearPid();
|
|
$cj->setStatus(CronJob::$STATUS_EMPTY);
|
|
die();
|
|
}
|
|
|
|
protected function notifyOutcome($outcome, $userid, $oldVersion) {
|
|
$email = getUserEmail($userid);
|
|
|
|
if ($email) {
|
|
global $HELPDESK_SUPPORT_NAME, $HELPDESK_SUPPORT_EMAIL_ID;
|
|
|
|
if ($outcome == 'ok') {
|
|
$msg = getTranslatedString('LBL_UPDATE_MESSAGE_OK', 'Update');
|
|
} elseif ($outcome == 'fail_rollback') {
|
|
$msg = getTranslatedString('LBL_UPDATE_MESSAGE_FAIL_RB', 'Update');
|
|
} elseif ($outcome == 'fail') {
|
|
$msg = getTranslatedString('LBL_UPDATE_MESSAGE_FAIL', 'Update');
|
|
}
|
|
$msg = str_replace('{name}', getUserFullName($userid), $msg);
|
|
|
|
// find notes log
|
|
$attachments = array();
|
|
|
|
$workdir = "vte_updater/$oldVersion/";
|
|
|
|
$updatelogs = glob($workdir.'*_vteupdate.log');
|
|
if (is_array($updatelogs) && count($updatelogs)) {
|
|
$updatelog = array_pop($updatelogs);
|
|
$attachments[] = array(
|
|
'sourcetype' => 'file',
|
|
'content' => $updatelog,
|
|
'filename' => 'update.log',
|
|
);
|
|
}
|
|
|
|
$noteslogs = glob($workdir.'*_update_notes.log');
|
|
if (is_array($noteslogs) && count($noteslogs)) {
|
|
$notelog = array_pop($noteslogs);
|
|
$attachments[] = array(
|
|
'sourcetype' => 'file',
|
|
'content' => $notelog,
|
|
'filename' => 'notes.log',
|
|
);
|
|
}
|
|
|
|
$r = send_mail('Update', $email, $HELPDESK_SUPPORT_NAME, $HELPDESK_SUPPORT_EMAIL_ID, 'vtenext update result', $msg, '', '', $attachments);
|
|
|
|
if ($r != 1) {
|
|
$this->logWarning('Unable to send outcome email');
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function setMaintenanceMode() {
|
|
$file = 'maintenance.php';
|
|
|
|
if (!is_writable($file)) {
|
|
throw new Exception('Maintanance file is not writable');
|
|
}
|
|
|
|
$maint = file_get_contents($file);
|
|
$maint = preg_replace('/\$vte_maintenance\s*=.*/', '$vte_maintenance = true;');
|
|
if (!file_put_contents($file, $maint)) { // false or 0 is an error
|
|
throw new Exception('Unable to write maintenance file');
|
|
}
|
|
|
|
$this->logInfo('Maintenance mode activated');
|
|
}
|
|
|
|
// --------------------------------- Cron compatibility ---------------------------------
|
|
// these are needed only when backporting the AutoUpdater to a vte that doesn't have the
|
|
// needed patches in CronUtils
|
|
|
|
protected function cronFreezeAll() {
|
|
|
|
$CU = CronUtils::getInstance();
|
|
if (method_exists($CU, 'freezeAllActive')) {
|
|
$CU->freezeAllActive();
|
|
} else {
|
|
global $adb, $table_prefix;
|
|
$adb->query("UPDATE {$table_prefix}_cronjobs SET active = 2 WHERE active = 1");
|
|
}
|
|
}
|
|
|
|
protected function cronUnfreezeAll() {
|
|
$CU = CronUtils::getInstance();
|
|
if (method_exists($CU, 'unfreezeAll')) {
|
|
$CU->unfreezeAll();
|
|
} else {
|
|
global $adb, $table_prefix;
|
|
$adb->query("UPDATE {$table_prefix}_cronjobs SET active = 1 WHERE active = 2");
|
|
}
|
|
}
|
|
|
|
protected function unfreezeCron($cronname) {
|
|
$cj = CronJob::getByName('DoUpdate');
|
|
if ($cj) {
|
|
if (method_exists($cj, 'unfreeze')) {
|
|
$cj->unfreeze();
|
|
} else {
|
|
global $adb, $table_prefix;
|
|
$adb->pquery("UPDATE {$table_prefix}_cronjobs SET active = 2 WHERE cronid = ?", array($cj->getId()));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function waitForAllCron($timeout = 300, $skipids = array()) {
|
|
|
|
$CU = CronUtils::getInstance();
|
|
|
|
if (method_exists($CU, 'waitForAllCron')) {
|
|
return $CU->waitForAllCron($timeout, $skipids);
|
|
} else {
|
|
global $adb, $table_prefix;
|
|
|
|
$sql = "SELECT COUNT(*) as count FROM {$table_prefix}_cronjobs WHERE active = 1 AND status = ?";
|
|
$params = array(CronJob::$STATUS_PROCESSING);
|
|
|
|
if (count($skipids) > 0) {
|
|
$sql .= " AND cronid NOT IN (".generateQuestionMarks($skipids).")";
|
|
$params = array_merge($params, $skipids);
|
|
}
|
|
|
|
$time = microtime(true);
|
|
$res = $adb->pquery($sql, $params);
|
|
$count = $adb->query_result_no_html($res, 0, 'count');
|
|
|
|
while ($count > 0) {
|
|
$t = microtime(true);
|
|
if ($t-$time > $timeout) return false;
|
|
|
|
sleep(3);
|
|
|
|
$res = $adb->pquery($sql, $params);
|
|
$count = $adb->query_result_no_html($res, 0, 'count');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
// --------------------------------- Log functions ---------------------------------
|
|
|
|
protected function logInfo($msg) {
|
|
echo "[INFO] ".$msg."\n";
|
|
return true;
|
|
}
|
|
|
|
protected function logWarning($msg) {
|
|
echo "[WARNING] ".$msg."\n";
|
|
return true;
|
|
}
|
|
|
|
protected function logError($msg) {
|
|
echo "[ERROR] ".$msg."\n";
|
|
return false;
|
|
}
|
|
|
|
} |