vtenext/include/utils/ResourceVersion.php
2021-04-28 20:10:26 +02:00

498 lines
16 KiB
PHP

<?php
/*************************************
* SPDX-FileCopyrightText: 2009-2020 Vtenext S.r.l. <info@vtenext.com>
* SPDX-License-Identifier: AGPL-3.0-only
************************************/
/* crmv@94125 */
require_once('include/BaseClasses.php');
require_once('include/utils/Cache.php');
/**
* This class create versioned versions of resources (js, css, images...)
* so when files are changed or after an upgrade, they can be regenerated
* to ensure browsers will download the latest version
*/
class ResourceVersion extends SDKExtendableUniqueClass {
public $table = '';
public $enabled = false;
public $autoRefresh = false;
public $enableLog = false; // warning: enabling the log can impact performance
public $logFile = 'logs/resource_version.log';
// when true, the cache file can be written (usually on multi-webserver systems this is not reccomended)
// by default, this is true when called from cli
protected $enableCacheWrite = false; // crmv@144893
// crmv@140887
public $theme = false;
public $resourcesCDN = false;
private $cacheThemeImage = array();
private $cacheImageFile = array();
// crmv@140887e
public function __construct() {
global $table_prefix, $theme, $default_theme;
$this->table = $table_prefix.'_resource_version';
$this->enabled = PerformancePrefs::getBoolean('VERSION_RESOURCES', true) && Vtecrm_Utils::CheckTable($this->table);
$this->autoRefresh = PerformancePrefs::getBoolean('VERSION_RESOURCES_AUTOREFRESH', false);
$this->enableCacheWrite = (php_sapi_name() == 'cli'); // crmv@144893
// crmv@140887
$this->theme = $theme ?: $default_theme;
$this->resourcesCDN = PerformancePrefs::getBoolean('VERSION_RESOURCES_CDN', '');
// crmv@140887e
$this->log('ResourceVersion initialized, cache write is '.($this->enableCacheWrite ? 'enabled' : 'disabled'));
}
// crmv@144893
protected function log($text) {
if ($this->enableLog) {
file_put_contents($this->logFile, '['.date('Y-m-d H:i:s').'] '.$text."\n", FILE_APPEND | LOCK_EX);
}
}
public function enableCacheWrite() {
$this->enableCacheWrite = true;
$this->log('Enable writing to cache');
}
public function disableCacheWrite() {
$this->enableCacheWrite = false;
$this->log('Disable writing to cache');
}
// crmv@144893e
/**
* function createResources : check and recreate symliks to resources
* params: file
* step1: get resources with update_revision = 1
* step2: increase revision
* step3: check original resource if is readable and new symlink is not created yet
* step4: create directory containing new symlink, create symlink, update database with new symlink, delete old symlink
*/
public function createResources($file=''){
global $adb,$root_directory;
$cache_method = PerformancePrefs::get('VERSION_RESOURCES_METHOD','link');
$update_date = date('Y-m-d H:i:s');
if (!empty($this->resourcesCDN)) $cache_method = 'remote'; // crmv@140887
$this->log("Creating resources".($file ? " (file: $file)" : ''));
$columns = array('resource', 'revision', 'versioned_resource');
$adb->format_columns($columns);
$sql = "SELECT ".implode(', ', $columns)." FROM {$this->table} WHERE update_revision = 1";
$params = Array();
if (!empty($file)){
$sql.= " AND {$columns[0]} = ?";
$params[] = $file;
}
$res = $adb->pquery($sql,$params);
if ($res){
while($row=$adb->fetchByAssoc($res,-1,false)){
$initial_revision = false;
$update_cache = false;
$revision = (int)$row['revision'];
if ($revision == -1){
$initial_revision = true;
}
$revision++; //new revision
$sql_update = "UPDATE {$this->table} SET versioned_resource=?, update_revision=?, revision=?, filemtime=?, last_update=?, type=? WHERE {$columns[0]} = ?";
if ($initial_revision) {
$fileTime = file_exists($row['resource']) ? filemtime($row['resource']) : null; // crmv@140887
$upd_params = Array($row['resource'],0,$revision,$fileTime,$update_date,'original',$row['resource']);
$res_update = $adb->pquery($sql_update,$upd_params);
$update_cache = true;
$newresource = $row['resource'];
} elseif ($cache_method == 'link' || $cache_method == 'copy') {
$newresource = $this->generateName($row['resource'], $revision);
if ($this->isVersionValid($row['resource'], $newresource, $cache_method)) {
// remove the new resource
@unlink($newresource);
$newresource_exists = false;
}
if (file_exists($row['resource'])) {
$ok = $this->generateVersion($row['resource'], $newresource, $cache_method);
if ($ok) {
$upd_params = Array($newresource,0,$revision,filemtime($row['resource']),$update_date,$cache_method,$row['resource']);
$res_update = $adb->pquery($sql_update,$upd_params);
if (!empty($row['versioned_resource']) && $row['versioned_resource'] != $row['resource']) {
// remove old version
@unlink($row['versioned_resource']);
}
$update_cache = true;
} else {
// unable to copy, use the original file
$upd_params = Array($row['resource'],0,$revision,filemtime($row['resource']),$update_date,'original',$row['resource']);
$res_update = $adb->pquery($sql_update,$upd_params);
$update_cache = true;
$newresource = $row['resource'];
}
} else {
// some problem, maybe the original file doesn't exist
if (!file_exists($row['resource'])) {
$this->removeResource($row['resource']);
}
}
}
// crmv@140887
if ($cache_method == 'remote') {
$newresource = $this->generateName($row['resource'], $revision);
$upd_params = Array($newresource,0,$revision,null,$update_date,$cache_method,$row['resource']);
$res_update = $adb->pquery($sql_update,$upd_params);
$update_cache = true;
}
// crmv@140887e
}
}
if (!empty($file)) {
if ($update_cache && !empty($newresource)){
// crmv@144893
if ($this->enableCacheWrite) {
$resources_cache_obj = $this->cacheResources();
$resources_cache = $resources_cache_obj->get();
$fileTime = file_exists($file) ? filemtime($file) : null; // crmv@140887
$resources_cache[$file] = array($newresource, $fileTime);
$resources_cache_obj->set($resources_cache);
}
// crmv@144893e
return $newresource;
}
} else {
//reset cache
$resources_cache_obj = $this->cacheResources(true);
$cache_arr = $resources_cache_obj->get();
$resources_cache_obj->set($cache_arr);
}
}
/**
* Generates the name for the versioned file (either link or real copy)
*/
// crmv@140887
protected function generateName($file, $revision) {
$isImageFile = $this->isImageFile($file);
if ($isImageFile) {
$file = $this->generateImageName($file, $this->theme, $revision);
}
$path_info = pathinfo($file);
$newpath = $path_info['dirname'];
if (!empty($this->resourcesCDN)) {
$newresource = $this->resourcesCDN."/".$newpath.'/'.$path_info['filename'].".".$path_info['extension'];
} else {
$newresource = $newpath."/".$path_info['filename']."_v".$revision.".".$path_info['extension'];
}
return $newresource;
}
protected function generateImageName($file, $theme, $revision = 0) {
$imagepath = $this->cacheThemeImage[$file][$theme][$revision] ?? null;
if ($imagepath === null) {
// Check in theme specific folder
if (file_exists("themes/$theme/images/$file")) {
$imagepath = "themes/$theme/images/$file";
} else if (file_exists("themes/images/$file")) {
// Search in common image folder
$imagepath = "themes/images/$file";
} else if (file_exists("themes/images/modulesimg/$file")) {
$imagepath = "themes/images/modulesimg/$file";
} else {
// Not found anywhere? Return whatever is sent
$imagepath = $file;
}
$this->cacheThemeImage[$file][$theme][$revision] = $imagepath;
}
return $imagepath;
}
public function isImageFile($file) { //crmv@155337
$isImage = $this->cacheImageFile[$file] ?? null;
if ($isImage === null) {
$imageTypes = array('gif', 'jpg', 'jpeg', 'png', 'bmp');
$extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$isImage = in_array($extension, $imageTypes);
$this->cacheImageFile[$file] = $isImage;
}
return $isImage;
}
// crmv@140887e
protected function isResourceChanged($file, $versioned, $timestamp = null) {
$changed = (empty($timestamp) || ($timestamp < filemtime($file)) || !empty($this->resourcesCDN)); // crmv@140887
// you can also add some kind of hash to check for changes, but might slow down things badly!
return $changed;
}
/**
* Return true if the versioned file is valid
*/
protected function isVersionValid($file, $versioned, $type) {
if ($type == 'link') {
$path_info = pathinfo($file);
$valid = (!empty($versioned) && is_link($versioned) && @readlink($versioned) && file_exists($path_info['dirname']."/".@readlink($versioned)));
} elseif ($type == 'copy') {
$valid = (!empty($versioned) && is_file($versioned));
} else {
throw new Exception("Unknown versioning type");
}
return $valid;
}
/**
* Physically create the link or copy the file
*/
protected function generateVersion($file, $versioned, $type) {
$path_info = pathinfo($file);
if ($type == 'link') {
$ok = @symlink($path_info['basename'],$versioned);
} elseif ($type == 'copy') {
$ok = @copy($path_info['basename'],$versioned);
} else {
throw new Exception("Unknown versioning type");
}
$this->log("New version generated ($type): ".$versioned);
return $ok;
}
/**
* function createResource : create or update cache resource
* params: $file: path of the original resource, $force_create: default false, force call createResources function
*/
public function createResource($file, $force_create = false) {
global $adb;
$column = "resource";
$adb->format_columns($column);
$sql_check = "SELECT {$column}, update_revision FROM {$this->table} WHERE {$column} = ?";
$res_check = $adb->pquery($sql_check, [$file]);
if ($res_check) {
if ($adb->num_rows($res_check) > 0) {
$upd = $adb->query_result_no_html($res_check, 0, 'update_revision');
if ($upd != '1') {
$adb->pquery("UPDATE {$this->table} SET update_revision = 1 WHERE {$column} = ?", [$file]);
}
} else {
if ($adb->isMysql()) {
$adb->pquery("INSERT IGNORE INTO {$this->table} ({$column}, update_revision) VALUES (?, ?)", [$file, 1]);
} else {
$result = $adb->pquery("SELECT 1 FROM {$this->table} WHERE {$column} = ?", [$file]);
if ($result && $adb->num_rows($result) == 0) {
$adb->pquery("INSERT INTO {$this->table} ({$column}, update_revision) VALUES (?, ?)", [$file, 1]);
}
}
}
$this->log("Resource set for update: " . $file);
}
if ($force_create) {
$this->createResources($file);
}
}
/**
* function cacheResources : create or update cached resources object
* params: $recreate: boolean to force rebuild cache
*/
public function cacheResources($recreate=false){
global $adb;
$cache = Cache::getInstance('cacheResources');
$resources_cache = $cache->get();
if (($resources_cache === false || $recreate) && $this->enableCacheWrite){ // crmv@144893
$resources_cache = Array();
$columns = array('resource', 'versioned_resource', 'type', 'filemtime');
$adb->format_columns($columns);
$sql = "SELECT ".implode(', ', $columns)." FROM {$this->table}";
$res = $adb->pquery($sql,Array());
if ($res){
while($row=$adb->fetchByAssoc($res,-1,false)){
switch($row['type']){
case 'original':
case 'remote': // crmv@140887
$resources_cache[$row['resource']] = array($row['versioned_resource'], $row['filemtime']);
break;
case 'link':
case 'copy':
if ($this->isVersionValid($row['resource'], $row['versioned_resource'], $row['type'])) {
$resources_cache[$row['resource']] = array($row['versioned_resource'], $row['filemtime']);
}
break;
default:
break;
}
}
}
$cache->set($resources_cache);
}
return $cache;
}
/**
* function getResource : get versioned resource
* params: $file: path of the original resource
* step1: create Array with versioned resources if it is not present
* step2: return versioned resource or $file if it is not present
*/
public function getResource($file) {
global $adb;
// crmv@140887
if ($this->isImageFile($file)) {
$file = $this->generateImageName($file, $this->theme, null);
}
// crmv@140887e
if (!$this->enabled) return $file; // crmv@167753 - moved here
$resources_cache = $this->cacheResources()->get();
if (!empty($resources_cache) && isset($resources_cache[$file])) {
$newfile = $resources_cache[$file][0];
if ($this->autoRefresh) {
// check for changes
$filets = $resources_cache[$file][1];
if ($this->isResourceChanged($file, $newfile, $filets)) {
$newfile = $this->createResource($file,true);
}
}
// crmv@144893
// flag for update only if the cache is valid but the file is missing
} elseif (is_array($resources_cache)) {
$newfile = $this->createResource($file,$this->autoRefresh); // crmv@140887
}
// crmv@144893e
//$this->log("Requested resource: $file");
if (!empty($newfile)){
return $newfile;
} else {
return $file;
}
}
/**
* function updateResources : update versioned resources
* params: none
* step1: for every resource not in pending state control last modified date if it is newer than the versioned one put into creation queue (call createResource)
* step2: call createResources (create or update versioned resources that are in pending state)
*/
public function updateResources() {
global $adb;
$this->log("Updating resources...");
$sql = "SELECT ".$adb->format_column('resource').",versioned_resource,filemtime FROM {$this->table} WHERE update_revision = 0"; // crmv@165801
$res = $adb->query($sql);
if ($res){
while($row = $adb->fetchByAssoc($res,-1,false)){
if (file_exists($row['resource'])){
if ($this->isResourceChanged($row['resource'], $row['versioned_resource'], $row['filemtime'])) {
$this->createResource($row['resource']);
}
} else {
// file deleted, remove the versioning line
$this->removeResource($row['resource']);
}
}
}
$this->createResources();
}
/**
*
*/
public function removeResource($filename) {
global $adb;
$sqldel = "DELETE FROM {$this->table} WHERE ".$adb->format_column('resource')." = ?"; // crmv@165801
$adb->pquery($sqldel, array($filename));
$this->log("Resource removed: ".$filename);
}
/**
* function resetResources : reset versioned resources
* params: none
* for every resource delete entry and try to delete copy/link
*/
public function resetResources(){
global $adb;
$this->log("Resetting resources...");
$sql = "SELECT ".$adb->format_column('resource').",versioned_resource,type FROM {$this->table}"; // crmv@165801
$res = $adb->query($sql);
if ($res){
while($row = $adb->fetchByAssoc($res,-1,false)){
switch($row['type']){
case 'original':
case 'remote': // crmv@140887
//do nothing
break;
case 'link':
case 'copy':
if ($this->isVersionValid($row['resource'], $row['versioned_resource'], $row['type'])) {
@unlink($row['versioned_resource']);
}
break;
default:
break;
}
}
}
$sql = "DELETE FROM {$this->table}";
$adb->query($sql);
}
// crmv@140887
public function getJSResources() {
$resources = array('clock_bg.gif', 'close_details.png', 'open_details.png', 'btnL3Calendar.gif',
'listview_folder.png', 'folderback.png',
'modules/SDK/examples/uitypeSocial/img/bkico.png', 'modules/SDK/examples/uitypeSocial/img/fbico.png',
'modules/SDK/examples/uitypeSocial/img/gpico.png', 'modules/SDK/examples/uitypeSocial/img/liico.png',
'modules/SDK/examples/uitypeSocial/img/orico.png', 'modules/SDK/examples/uitypeSocial/img/qzico.png',
'modules/SDK/examples/uitypeSocial/img/twico.png', 'modules/SDK/examples/uitypeSocial/img/ytico.png',
'offstar.gif', 'onstar.gif', 'modules/Calendar/wdCalendar/css/images/calendar/task.png',
'clear_field.gif', 'delete.gif', 'movecol_up.gif', 'movecol_down.gif', 'movecol_del.gif',
);
$resourcesJS = array();
foreach ($resources as $resource) {
$newfile = $this->getResource($resource);
$resourcesJS[$resource] = $newfile;
}
return $resourcesJS;
}
// crmv@140887e
}
/**
* Handy function to get the resource versioned name
* Just a wrapper to the ResourceVersion::getResource method
*/
function resourcever($filename) {
$RV = ResourceVersion::getInstance();
$filename = $RV->getResource($filename);
return $filename;
}