*
* This file is part of VersionX, a real estate property listings component
* for MODX Revolution.
*
* VersionX is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* VersionX is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* VersionX; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
* Suite 330, Boston, MA 02111-1307 USA
*
* @package versionx
*/
class VersionX {
public $modx;
private $chunks;
private $tvs = array();
public $config = array();
public $categoryCache = array();
public $debug = false;
public $charset;
/**
* @param \modX $modx
* @param array $config
*/
function __construct(modX $modx,array $config = array()) {
$this->modx =& $modx;
$basePath = $this->modx->getOption('versionx.core_path',$config,$this->modx->getOption('core_path').'components/versionx/');
$assetsUrl = $this->modx->getOption('versionx.assets_url',$config,$this->modx->getOption('assets_url').'components/versionx/');
$assetsPath = $this->modx->getOption('versionx.assets_path',$config,$this->modx->getOption('assets_path').'components/versionx/');
$this->config = array_merge(array(
'base_bath' => $basePath,
'core_path' => $basePath,
'model_path' => $basePath.'model/',
'processors_path' => $basePath.'processors/',
'elements_path' => $basePath.'elements/',
'templates_path' => $basePath.'templates/',
'assets_path' => $assetsPath,
'js_url' => $assetsUrl.'js/',
'css_url' => $assetsUrl.'css/',
'assets_url' => $assetsUrl,
'connector_url' => $assetsUrl.'connector.php',
'has_users_permission' => $this->modx->hasPermission('view_user'),
),$config);
require_once dirname(__DIR__) . '/docs/version.inc.php';
if (defined('VERSIONX_FULLVERSION')) {
$this->config['version'] = VERSIONX_FULLVERSION;
}
$modelPath = $this->config['model_path'];
$this->modx->addPackage('versionx', $modelPath);
$this->modx->lexicon->load('versionx:default');
$this->debug = $this->modx->getOption('versionx.debug',null,false);
}
/**
* Gets a Chunk and caches it; also falls back to file-based templates
* for easier debugging.
*
* @access public
* @param string $name The name of the Chunk
* @param array $properties The properties for the Chunk
* @return string The processed content of the Chunk
* @author Shaun "splittingred" McCormick
*/
public function getChunk($name,$properties = array()) {
$chunk = null;
if (!isset($this->chunks[$name])) {
$chunk = $this->_getTplChunk($name);
if (empty($chunk)) {
$chunk = $this->modx->getObject('modChunk',array('name' => $name),true);
if ($chunk == false) return false;
}
$this->chunks[$name] = $chunk->getContent();
} else {
$o = $this->chunks[$name];
$chunk = $this->modx->newObject('modChunk');
$chunk->setContent($o);
}
$chunk->setCacheable(false);
return $chunk->process($properties);
}
/**
* Returns a modChunk object from a template file.
*
* @access private
* @param string $name The name of the Chunk. Will parse to name.chunk.tpl
* @param string $postFix The postfix to append to the name
* @return modChunk/boolean Returns the modChunk object if found, otherwise false.
* @author Shaun "splittingred" McCormick
*/
private function _getTplChunk($name,$postFix = '.tpl') {
$chunk = false;
$f = $this->config['elements_path'].'chunks/'.strtolower($name).$postFix;
if (file_exists($f)) {
$o = file_get_contents($f);
/* @var modChunk $chunk */
$chunk = $this->modx->newObject('modChunk');
$chunk->set('name',$name);
$chunk->setContent($o);
}
return $chunk;
}
public function newVersionFor($class, $contentId, $mode)
{
switch ($class) {
case 'vxResource':
return $this->newResourceVersion($contentId, $mode);
case 'vxTemplate':
return $this->newTemplateVersion($contentId, $mode);
case 'vxChunk':
return $this->newChunkVersion($contentId, $mode);
case 'vxSnippet':
return $this->newSnippetVersion($contentId, $mode);
case 'vxPlugin':
return $this->newPluginVersion($contentId, $mode);
case 'vxTemplateVar':
return $this->newTemplateVarVersion($contentId, $mode);
}
$this->modx->log(modX::LOG_LEVEL_ERROR, 'Call to ' . __METHOD__ . ' with unrecognised class ' . $class);
return false;
}
/**
* Creates a new version of a Resource.
*
* @param int|modResource|modStaticResource $resource
* @param string $mode
* @return bool
*
*/
public function newResourceVersion($resource, $mode = 'upd') {
if ($resource instanceof modResource) {
// We're retrieving the resource again to clean up raw post data we don't want.
$resource = $this->modx->getObject('modResource',$resource->get('id'));
} else {
$resource = $this->modx->getObject('modResource',(int)$resource);
}
$rArray = $resource->toArray();
/* @var vxResource $version */
$version = $this->modx->newObject('vxResource');
$v = array(
'content_id' => $rArray['id'],
'user' => $this->modx->user->get('id'),
'mode' => $mode,
'title' => $rArray[$this->modx->getOption('resource_tree_node_name',null,'pagetitle')],
'context_key' => $rArray['context_key'],
'class' => $rArray['class_key'],
'content' => $resource->get('content'),
);
$version->fromArray($v);
unset ($rArray['id'],$rArray['content']);
$version->set('fields',$rArray);
$tvs = $resource->getTemplateVars();
$tvArray = array();
/* @var modTemplateVar $tv */
foreach ($tvs as $tv) {
$tvArray[] = $tv->get(array('id','value'));
}
$version->set('tvs',$tvArray);
if($this->checkLastVersion('vxResource', $version)) {
return $version->save();
}
return true;
}
/**
* Creates a new version of a Template.
*
* @param int|\modTemplate $template
* @param string $mode
* @return bool
*
*/
public function newTemplateVersion($template, $mode = 'upd') {
if ($template instanceof modTemplate) {
/* Fetch it again to prevent getting stuck with raw post data */
$template = $this->modx->getObject('modTemplate', $template->get('id'));
} else {
$template = $this->modx->getObject('modTemplate', (int)$template);
}
$tArray = $template->toArray();
/* @var vxTemplate $version */
$version = $this->modx->newObject('vxTemplate');
$v = array(
'content_id' => $tArray['id'],
'user' => $this->modx->user->get('id'),
'mode' => $mode,
);
$version->fromArray(array_merge($v,$tArray));
if($this->checkLastVersion('vxTemplate', $version)) {
return $version->save();
}
return true;
}
/**
* Creates a new version of a Template Variable.
*
* @param int|\modTemplateVar $tv
* @param string $mode
* @return bool
*
*/
public function newTemplateVarVersion($tv, $mode = 'upd') {
if ($tv instanceof modTemplateVar) {
/* Fetch it again to prevent getting stuck with raw post data */
$tv = $this->modx->getObject('modTemplateVar', $tv->get('id'));
} else {
$tv = $this->modx->getObject('modTemplateVar', (int)$tv);
}
$tArray = $tv->toArray();
/* @var modTemplateVar $version */
$version = $this->modx->newObject('vxTemplateVar');
$v = array(
'content_id' => $tArray['id'],
'user' => $this->modx->user->get('id'),
'mode' => $mode,
);
$version->fromArray(array_merge($v,$tArray));
if($this->checkLastVersion('vxTemplateVar', $version)) {
return $version->save();
}
return true;
}
/**
* Create a new version of a Chunk.
*
* @param int|\modChunk $chunk
* @param string $mode
* @return bool
*/
public function newChunkVersion($chunk, $mode = 'upd') {
if ($chunk instanceof modChunk) {
/* Fetch it again to prevent getting stuck with raw post data */
$chunk = $this->modx->getObject('modChunk', $chunk->get('id'));
} else {
$chunk = $this->modx->getObject('modChunk', (int)$chunk);
}
// prevents resource groups from failing in MODX versions prior to 2.2.14 (see github #8992 + fix)
if (!($chunk instanceof modChunk)) {
return false;
}
$cArray = $chunk->toArray();
/* @var vxChunk $version */
$version = $this->modx->newObject('vxChunk');
$v = array(
'content_id' => $cArray['id'],
'user' => $this->modx->user->get('id'),
'mode' => $mode,
);
$version->fromArray(array_merge($v,$cArray));
if($this->checkLastVersion('vxChunk', $version)) {
return $version->save();
}
return true;
}
/**
* Creates a new version of a Snippet.
*
* @param int|\modSnippet $snippet
* @param string $mode
* @return bool
*/
public function newSnippetVersion($snippet, $mode = 'upd') {
if ($snippet instanceof modSnippet) {
/* Fetch it again to prevent getting stuck with raw post data */
$snippet = $this->modx->getObject('modSnippet', $snippet->get('id'));
} else {
$snippet = $this->modx->getObject('modSnippet', (int)$snippet);
}
$sArray = $snippet->toArray();
/* @var vxSnippet $version */
$version = $this->modx->newObject('vxSnippet');
$v = array(
'content_id' => $sArray['id'],
'user' => $this->modx->user->get('id'),
'mode' => $mode,
);
$version->fromArray(array_merge($v,$sArray));
if($this->checkLastVersion('vxSnippet', $version)) {
return $version->save();
}
return true;
}
/**
* Creates a new version of a Plugin.
*
* @param int|\modPlugin $plugin
* @param string $mode
* @return bool
*/
public function newPluginVersion($plugin, $mode = 'upd') {
if ($plugin instanceof modPlugin) {
/* Fetch it again to prevent getting stuck with raw post data */
$plugin = $this->modx->getObject('modPlugin', $plugin->get('id'));
} else {
$plugin = $this->modx->getObject('modPlugin', (int)$plugin);
}
$pArray = $plugin->toArray();
/* @var vxPlugin $version */
$version = $this->modx->newObject('vxPlugin');
$v = array(
'content_id' => $pArray['id'],
'user' => $this->modx->user->get('id'),
'mode' => $mode,
);
$version->fromArray(array_merge($v,$pArray));
if($this->checkLastVersion('vxPlugin', $version)) {
return $version->save();
}
return true;
}
/**
* Gets & prepares version details for output.
*
* @param string $class
* @param int $id
* @param bool $json
* @param string $prefix
* @return bool|array
*/
public function getVersionDetails($class = 'vxResource',$id = 0, $json = false, $prefix = '') {
$v = $this->modx->getObject($class, ['version_id' => $id]);
/* @var xPDOObject $v */
if ($v instanceof $class) {
$vArray = $v->toArray();
$vArray['mode'] = $this->modx->lexicon('versionx.mode.'.$vArray['mode']);
/* Class specific processing */
switch ($class) {
case 'vxResource':
$vArray = array_merge($vArray,$vArray['fields']);
if ($vArray['parent'] != 0) {
/* @var modResource $parent */
$parent = $this->modx->getObject('modResource',$vArray['parent']);
if ($parent instanceof modResource) $vArray['parent'] = $parent->get('pagetitle') .' ('.$vArray['parent'].')';
}
/* Process content type */
/* @var modContentType $ct */
$ct = $this->modx->getObject('modContentType',$vArray['content_type']);
if ($ct instanceof modContentType)
$vArray['content_type'] = $ct->get('name');
$vArray['content'] = $this->_prepareCodeView($vArray['content']);
if ($vArray['content_dispo'] == 1) $vArray['content_dispo'] = $this->modx->lexicon('attachment');
else $vArray['content_dispo'] = $this->modx->lexicon('inline');
/* Process boolean values */
$vArray['published'] = (intval($vArray['published'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
$vArray['hidemenu'] = (intval($vArray['hidemenu'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
$vArray['isfolder'] = (intval($vArray['isfolder'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
$vArray['richtext'] = (intval($vArray['richtext'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
$vArray['searchable'] = (intval($vArray['searchable'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
$vArray['cacheable'] = (intval($vArray['cacheable'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
$vArray['deleted'] = (intval($vArray['deleted'])) ? $this->modx->lexicon('yes') : $this->modx->lexicon('no');
/* Process time stamps */
$df = $this->modx->config['manager_date_format'].' '.$this->modx->config['manager_time_format'];
$vArray['saved'] = ($vArray['saved'] != 0) ? date($df,strtotime($vArray['saved'])) : '';
$vArray['publishedon'] = ($vArray['publishedon'] != 0) ? date($df,strtotime($vArray['publishedon'])) : '';
$vArray['pub_date'] = ($vArray['pub_date'] != 0) ? date($df,strtotime($vArray['pub_date'])) : '';
$vArray['unpub_date'] = ($vArray['unpub_date'] != 0) ? date($df,strtotime($vArray['unpub_date'])) : '';
/* Get TV captions */
$tvArray = array();
foreach ($vArray['tvs'] as $tv) {
if (!isset($this->tvs[$tv['id']]) || empty($this->tvs[$tv['id']])) {
/* @var modTemplateVar $tvObj */
$tvObj = $this->modx->getObject('modTemplateVar',$tv['id']);
if ($tvObj instanceof modTemplateVar) {
$caption = $tvObj->get('caption');
if (empty($caption)) $caption = $tvObj->get('name');
$this->tvs[$tv['id']] = $caption;
} else {
$this->tvs[$tv['id']] = 'tv'.$tv['id'];
}
}
$tvArray[] = array_merge($tv,array('caption' => $this->tvs[$tv['id']]));
}
$vArray['tvs'] = $tvArray;
break;
case 'vxTemplateVar':
$vArray['category'] = $this->getCategory($vArray['category']);
if (is_array($vArray['input_properties'])) {
foreach ($vArray['input_properties'] as $key => $value) {
if ($decoded = $this->modx->fromJSON($value)) {
$vArray['input_properties'][$key] = $decoded;
}
}
}
if (is_array($vArray['output_properties'])) {
foreach ($vArray['output_properties'] as $key => $value) {
if ($decoded = $this->modx->fromJSON($value)) {
$vArray['output_properties'][$key] = $decoded;
}
}
}
break;
case 'vxTemplate':
$vArray['content'] = $this->_prepareCodeView($vArray['content']);
$vArray['category'] = $this->getCategory($vArray['category']);
break;
case 'vxChunk':
$vArray['snippet'] = $this->_prepareCodeView($vArray['snippet']);
$vArray['category'] = $this->getCategory($vArray['category']);
break;
case 'vxSnippet':
$vArray['snippet'] = $this->_prepareCodeView($vArray['snippet']);
$vArray['category'] = $this->getCategory($vArray['category']);
break;
case 'vxPlugin':
$vArray['plugincode'] = $this->_prepareCodeView($vArray['plugincode']);
$vArray['category'] = $this->getCategory($vArray['category']);
break;
}
/* @var modUserProfile $up */
$up = $this->modx->getObject('modUserProfile',array('internalKey' => $vArray['user']));
if ($up instanceof modUserProfile) $vArray['user'] = $up->get('fullname');
if (!empty($prefix)) {
$ta = array();
foreach ($vArray as $tk => $tv) {
$ta[$prefix.$tk] = $tv;
}
$vArray = $ta;
}
if ($json) return $this->modx->toJSON($vArray);
return $vArray;
}
return false;
}
/**
* @param $string
*
* @return string
*/
private function _prepareCodeView($string) {
$lines = explode("\n",$string);
foreach ($lines as $idx => $line) {
$pos = 0;
while( substr($line, $pos, 1) == ' ') {
$pos++;
}
$lines[$idx] = str_repeat(' ', $pos) . $this->htmlent(substr($line, $pos));
}
$lines = implode("
\n", $lines);
return $lines;
}
/**
* Checks the last saved version (if any).
* Returns true if there is no earlier version, or something is different.
* So if this returns true: go ahead and save the version.
* If this returns false: nothing changed, don't bother.
*
* @param string $class
* @param \xPDOObject $version
*
* @return bool
*/
protected function checkLastVersion($class = 'vxResource', xPDOObject $version) {
/* Get last version to make sure we've got some changes to save */
$c = $this->modx->newQuery($class);
$c->where(array('content_id' => $version->get('content_id')));
$c->sortby('version_id','DESC');
$c->limit(1);
$lastVersion = $this->modx->getCollection($class,$c);
$lastVersion = !empty($lastVersion) ? array_shift($lastVersion) : array();
/* @var vxResource $lastVersion */
/* If there's no earlier version, we can go ahead and
return true to indicate we need to save the version */
if (!($lastVersion instanceof $class)) {
if ($this->debug) $this->modx->log(xPDO::LOG_LEVEL_ERROR,"[VersionX] Saving a {$class} for ID {$version->get('content_id')}: No earlier version found.");
return true;
}
$newVersionArray = $version->toArray();
$lastVersionArray = $lastVersion->toArray();
/* Get rid of excluded vars for the specific object. */
$exclude = call_user_func(array($class,'getExcludeFields'));
if ($this->debug) $this->modx->log(modX::LOG_LEVEL_ERROR,'[VersionX checkLastVersion] Exclude fields: ' . print_r($exclude, true));
foreach ($exclude as $key => $value) {
if (is_array($value)) {
foreach ($value as $subfield) {
if (isset($newVersionArray[$key]) && isset($newVersionArray[$key][$subfield])) {
unset ($newVersionArray[$key][$subfield]);
}
if (isset($lastVersionArray[$key]) && isset($lastVersionArray[$key][$subfield])) {
unset ($lastVersionArray[$key][$subfield]);
}
}
} else {
if (isset($lastVersionArray[$value])) { unset($lastVersionArray[$value]); }
if (isset($newVersionArray[$value])) { unset($newVersionArray[$value]); }
}
}
$newVersionFlat = $this->flattenArray($newVersionArray);
$lastVersionFlat = $this->flattenArray($lastVersionArray);
if ($this->debug) {
$this->modx->log(modX::LOG_LEVEL_ERROR,'[VersionX checkLastVersion] New: ' . print_r($newVersionArray, true));
$this->modx->log(modX::LOG_LEVEL_ERROR,'[VersionX checkLastVersion] New Flattened: ' . $newVersionFlat);
$this->modx->log(modX::LOG_LEVEL_ERROR,'[VersionX checkLastVersion] Last: ' . print_r($lastVersionArray, true));
$this->modx->log(modX::LOG_LEVEL_ERROR,'[VersionX checkLastVersion] Last Flattened: ' . $newVersionFlat);
}
/* If the flattened arrays don't match there's a difference and we return true to indicate we need to save. */
if ($newVersionFlat != $lastVersionFlat) {
return true;
}
if ($this->debug) $this->modx->log(xPDO::LOG_LEVEL_ERROR,"[VersionX] Not saving a {$class} for ID {$version->get('content_id')}: No changes found.");
/* If we got here, there was a last version but it seemed nothing changes.
Return false to indicate to NOT save a new version. */
return false;
}
/**
* Flattens an array recursively.
* @param array $array
*
* @return array|string
*/
public function flattenArray(array $array = array()) {
if (!is_array($array)) return (string)$array;
$string = array();
foreach ($array as $key => $value) {
if (is_array($value)) {
$value = '{' . $this->flattenArray($value) .'}';
}
if (!empty($value)) {
$string[] = $key . ':' . $value;
}
}
$string = implode(',',$string);
return $string;
}
/**
* Outputs the JavaScript needed to add a tab to the panels.
*
* @param string $class
*/
public function outputVersionsTab ($class = 'vxResource') {
if (!class_exists($class)) {
$path = $this->config['model_path'].'versionx/'.strtolower($class).'.class.php';
if (file_exists($path)) {
require_once ($path);
}
if (!class_exists($class)) {
$this->modx->log(modX::LOG_LEVEL_ERROR,'[VersionX::outputVersionsTab] Error loading class '.$class);
return;
}
}
$langs = $this->_getLangs();
$jsurl = $this->config['js_url'].'mgr/';
/* Load class & set inVersion to true, indicating we're not looking at the VersionX controller. */
$this->modx->regClientStartupScript($jsurl.'versionx.class.js');
$this->modx->regClientStartupHTMLBlock('
');
/* Get the different individual JS to add to the page */
$tabjs = call_user_func(array($class,'getTabJavascript'));
if (is_array($tabjs)) {
foreach ($tabjs as $js) {
$this->modx->regClientStartupScript($jsurl.$js);
}
}
/* Get the template and register it */
$tplName = call_user_func(array($class,'getTabTpl'));
$tplFile = $this->config['templates_path'] . $tplName . '.tpl';
if (file_exists($tplFile)) {
$tpl = file_get_contents($tplFile);
if (!empty($tpl)) {
$this->modx->regClientStartupHTMLBlock($tpl);
}
}
}
/**
* Gets language strings for use on non-VersionX controllers.
* @return string
*/
public function _getLangs() {
$entries = $this->modx->lexicon->loadCache('versionx');
$langs = 'Ext.applyIf(MODx.lang,' . $this->modx->toJSON($entries) . ');';
return $langs;
}
/**
* @param $id
*
* @return string
*/
public function getCategory($id) {
if (!$id || $id == 0) return '';
if (isset($this->categoryCache[$id])) {
return $this->categoryCache[$id];
}
/* @var modCategory $category */
$category = $this->modx->getObject('modCategory',(int)$id);
if ($category) {
return $this->categoryCache[$id] = $category->get('category') . " ($id)";
} else {
return (string)$id;
}
}
/**
* Runs htmlentities() on the string with the proper character encoding.
* @param string $string
* @return string
*/
public function htmlent($string = '') {
if ($this->charset === null) {
$this->charset = $this->modx->getOption('modx_charset', null, 'UTF-8');
}
return htmlentities($string, ENT_QUOTES | ENT_SUBSTITUTE, $this->charset);
}
}