From f7d9aec1cfa5ef715c5f494defee901cfa988daa Mon Sep 17 00:00:00 2001 From: GeekoDev Date: Thu, 7 Jul 2022 16:10:56 +0200 Subject: [PATCH 1/6] Add files via upload Make vqmod to also take xml files which are in /vqmod/ folder from each extension directory, this way once vqmod is installed we can load a module package with its vqmod file inside directly. --- vqmod.php | 981 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 981 insertions(+) create mode 100644 vqmod.php diff --git a/vqmod.php b/vqmod.php new file mode 100644 index 0000000..6cca5cc --- /dev/null +++ b/vqmod.php @@ -0,0 +1,981 @@ += self::$_lastModifiedTime && filemtime($cacheFile) >= $file_last_modified) { + return $cacheFile; + } + + if(isset(self::$_filesModded[$sourcePath])) { + return self::$_filesModded[$sourcePath]['cached'] ? $cacheFile : $sourceFile; + } + + $changed = false; + $fileHash = sha1_file($sourcePath); + $fileData = file_get_contents($sourcePath); + + foreach(self::$_mods as $modObject) { + foreach($modObject->mods as $path => $mods) { + if(self::_checkMatch($path, $modificationsPath)) { + $modObject->applyMod($mods, $fileData); + } + } + } + + if (sha1($fileData) != $fileHash) { + $writePath = $cacheFile; + if(!file_exists($writePath) || is_writable($writePath)) { + file_put_contents($writePath, $fileData, LOCK_EX); + $changed = true; + } + } else { + file_put_contents(self::path(self::$checkedCache, true), $stripped_filename . PHP_EOL, FILE_APPEND | LOCK_EX); + + // Prevent checked.cache file from duplicating lines when checked folder is above vqmod directory - Thanks adrianolmedo + $lines = file(self::path(self::$checkedCache, true)); + $lines = array_unique($lines); + file_put_contents(self::path(self::$checkedCache, true), implode($lines)); + + self::$_doNotMod[] = $sourcePath; + } + + self::$_filesModded[$sourcePath] = array('cached' => $changed); + return $changed ? $writePath : $sourcePath; + } + + /** + * VQMod::path() + * + * @param string $path File path + * @param bool $skip_real If true path is full not relative + * @return bool, string + * @description Returns the full true path of a file if it exists, otherwise false + */ + public static function path($path, $skip_real = false) { + $tmp = self::$_cwd . $path; + $realpath = $skip_real ? $tmp : self::_realpath($tmp); + if(!$realpath) { + return false; + } + return $realpath; + } + + /** + * VQMod::getCwd() + * + * @return string + * @description Returns current working directory + */ + public static function getCwd() { + return self::$_cwd; + } + + /** + * VQMod::dirCheck() + * + * @param string $path + * @return null + * @description Creates $path folder if it doesn't exist + */ + public static function dirCheck($path) { + if(!is_dir($path)) { + if(!mkdir($path)) { + die('VQMod::dirCheck - CANNOT CREATE "' . $path . '" DIRECTORY'); + } + } + } + + /** + * VQMod::handleXMLError() + * + * @description Error handler for bad XML files + */ + public static function handleXMLError($errno, $errstr, $errfile, $errline) { + if ($errno == E_WARNING && (substr_count($errstr, 'DOMDocument::load()') > 0)) { + throw new DOMException(str_replace('DOMDocument::load()', '', $errstr)); + } else { + return false; + } + } + + /** + * VQMod::_getMods() + * + * @return null + * @description Gets list of XML files in vqmod xml folder for processing + */ + private static function _getMods() { + + self::$_modFileList = glob(self::path('vqmod/xml/', true) . '*.xml'); + + // @GeekoDev - Get files from opencart 4.x extension folders + if (defined('DIR_EXTENSION')) { + $opencartExtFileList = glob(DIR_EXTENSION . '*/vqmod/*.xml'); + self::$_modFileList = array_merge(self::$_modFileList, $opencartExtFileList); + } + // END - Get files from opencart 4.x extension folders + + foreach(self::$_modFileList as $file) { + if(file_exists($file)) { + $lastMod = filemtime($file); + if($lastMod > self::$_lastModifiedTime){ + self::$_lastModifiedTime = $lastMod; + } + } + } + + $xml_folder_time = filemtime(self::path('vqmod/xml')); + if($xml_folder_time > self::$_lastModifiedTime){ + self::$_lastModifiedTime = $xml_folder_time; + } + + $modCache = self::path(self::$modCache); + if(self::$_devMode || !file_exists($modCache)) { + self::$_lastModifiedTime = time(); + } elseif(file_exists($modCache) && filemtime($modCache) >= self::$_lastModifiedTime) { + $mods = file_get_contents($modCache); + if(!empty($mods)) + self::$_mods = unserialize($mods); + if(self::$_mods !== false) { + return; + } + } + + // Clear checked cache if rebuilding + file_put_contents(self::path(self::$checkedCache, true), '', LOCK_EX); + + if(self::$_modFileList) { + self::_parseMods(); + } else { + self::$log->write('VQMod::_getMods - NO XML FILES READABLE IN XML FOLDER'); + } + } + + /** + * VQMod::_parseMods() + * + * @return null + * @description Loops through xml files and attempts to load them as VQModObject's + */ + private static function _parseMods() { + + set_error_handler(array('VQMod', 'handleXMLError')); + + $dom = new DOMDocument('1.0', 'UTF-8'); + foreach(self::$_modFileList as $modFileKey => $modFile) { + if(file_exists($modFile)) { + try { + $dom->load($modFile); + $mod = $dom->getElementsByTagName('modification')->item(0); + $vqmver = $mod->getElementsByTagName('vqmver')->item(0); + + if($vqmver) { + $version_check = $vqmver->getAttribute('required'); + if(strtolower($version_check) == 'true') { + if(version_compare(self::$_vqversion, $vqmver->nodeValue, '<')) { + self::$log->write('VQMod::_parseMods - FILE "' . $modFile . '" REQUIRES VQMOD "' . $vqmver->nodeValue . '" OR ABOVE AND HAS BEEN SKIPPED'); + continue; + } + } + } + + self::$_mods[] = new VQModObject($mod, $modFile); + } catch (Exception $e) { + self::$log->write('VQMod::_parseMods - INVALID XML FILE: ' . $e->getMessage()); + } + } else { + self::$log->write('VQMod::_parseMods - FILE NOT FOUND: ' . $modFile); + } + } + + restore_error_handler(); + + $modCache = self::path(self::$modCache, true); + $result = file_put_contents($modCache, serialize(self::$_mods), LOCK_EX); + if(!$result) { + die('VQMod::_parseMods - "/vqmod/mods.cache" FILE NOT WRITEABLE'); + } + } + + /** + * VQMod::_loadProtected() + * + * @return null + * @description Loads protected list and adds them to _doNotMod array + */ + private static function _loadProtected() { + $file = self::path(self::$protectedFilelist); + if($file && is_file($file)) { + $paths = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if(!empty($paths)) { + foreach($paths as $path) { + $fullPath = self::path($path); + if($fullPath && !in_array($fullPath, self::$_doNotMod)) { + self::$_doNotMod[] = $fullPath; + } + } + } + } + } + + /** + * VQMod::_loadChecked() + * + * @return null + * @description Loads already checked files and adds them to _doNotMod array + */ + private static function _loadChecked() { + $file = self::path(self::$checkedCache); + if($file && is_file($file)) { + $paths = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if(!empty($paths)) { + foreach($paths as $path) { + $fullPath = self::path($path, true); + if($fullPath) { + self::$_doNotMod[] = $fullPath; + } + } + } + } + } + + /** + * VQMod::_cacheName() + * + * @param string $file Filename to be converted to cache filename + * @return string + * @description Returns cache file name for a path + */ + private static function _cacheName($file) { + return self::$_cachePathFull . 'vq2-' . preg_replace('~[/\\\\]+~', '_', $file); + } + + /** + * VQMod::_setCwd() + * + * @param string $path Path to be used as current working directory + * @return null + * @description Sets the current working directory variable + */ + private static function _setCwd($path) { + self::$_cwd = self::_realpath($path); + } + + /** + * VQMod::_realpath() + * + * @param string $file + * @return string + * @description Returns real path of any path, adding directory slashes if necessary + */ + private static function _realpath($file) { + $path = realpath($file); + if(!$path) { + return false; + } + + if(is_dir($path)) { + $path = rtrim($path, self::$directorySeparator) . self::$directorySeparator; + } + + return $path; + } + + /** + * VQMod::_checkMatch() + * + * @param string $modFilePath Modification path from a node + * @param string $checkFilePath File path + * @return bool + * @description Checks a modification path against a file path + */ + private static function _checkMatch($modFilePath, $checkFilePath) { + $modFilePath = str_replace('\\', '/', $modFilePath); + $checkFilePath = str_replace('\\', '/', $checkFilePath); + + if(self::$windows) { + $modFilePath = strtolower($modFilePath); + $checkFilePath = strtolower($checkFilePath); + } + + if($modFilePath == $checkFilePath) { + $return = true; + } elseif(strpos($modFilePath, '*') !== false) { + $return = true; + $modParts = explode('/', $modFilePath); + $checkParts = explode('/', $checkFilePath); + + if(count($modParts) !== count($checkParts)) { + $return = false; + } else { + + $toCheck = array_diff_assoc($modParts, $checkParts); + + foreach($toCheck as $k => $part) { + if($part === '*') { + continue; + } elseif(strpos($part, '*') !== false) { + $part = preg_replace_callback('~([^*]+)~', array('self', '_quotePath'), $part); + $part = str_replace('*', '[^/]*', $part); + $part = (bool) preg_match('~^' . $part . '$~', $checkParts[$k]); + + if($part) { + continue; + } + } elseif($part === $checkParts[$k]) { + continue; + } + + $return = false; + break; + } + + } + } else { + $return = false; + } + + return $return; + } + + /** + * VQMod::_quotePath() + * + * @param string $matches callback matches + * @return string + * @description apply's preg_quote to string from callback + */ + private static function _quotePath($matches) { + return preg_quote($matches[1], '~'); + } +} + +/** + * VQModLog + * @description Object to log information to a file + */ +class VQModLog { + private $_sep; + private $_defhash = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'; + private $_logs = array(); + + /** + * VQModLog::__construct() + * + * @return null + * @description Object instantiation method + */ + public function __construct() { + $this->_sep = str_repeat('-', 70); + } + + /** + * VQModLog::__destruct() + * + * @return null + * @description Logs any messages to the log file just before object is destroyed + */ + public function __destruct() { + if(empty($this->_logs) || VQMod::$logging == false) { + return; + } + + $logPath = VQMod::path(VQMod::$logFolder . date('w_D') . '.log', true); + + $txt = array(); + + $txt[] = str_repeat('-', 10) . ' Date: ' . date('Y-m-d H:i:s') . ' ~ IP : ' . (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'N/A') . ' ' . str_repeat('-', 10); + $txt[] = 'REQUEST URI : ' . $_SERVER['REQUEST_URI']; + + foreach($this->_logs as $count => $log) { + if($log['obj']) { + $vars = get_object_vars($log['obj']); + $txt[] = 'MOD DETAILS:'; + foreach($vars as $k => $v) { + if(is_string($v)) { + $txt[] = ' ' . str_pad($k, 10, ' ', STR_PAD_RIGHT) . ': ' . $v; + } + } + + } + + foreach($log['log'] as $msg) { + $txt[] = $msg; + } + + if ($count > count($this->_logs)-1) { + $txt[] = ''; + } + } + + $txt[] = $this->_sep; + $txt[] = str_repeat(PHP_EOL, 2); + $append = true; + + if(!file_exists($logPath)) { + $append = false; + } else { + $content = file_get_contents($logPath); + if(!empty($content) && strpos($content, ' Date: ' . date('Y-m-d ')) === false) { + $append = false; + } + } + + $result = file_put_contents($logPath, implode(PHP_EOL, $txt), ($append ? FILE_APPEND | LOCK_EX : LOCK_EX)); + if(!$result) { + die('VQModLog::__destruct - LOG FILE "' . $logPath . '" COULD NOT BE WRITTEN'); + } + } + + /** + * VQModLog::write() + * + * @param string $data Text to be added to log file + * @param VQModObject $obj Modification the error belongs to + * @return null + * @description Adds error to log object ready to be output + */ + public function write($data, VQModObject $obj = NULL) { + if($obj) { + $hash = sha1($obj->id); + } else { + $hash = $this->_defhash; + } + + if(empty($this->_logs[$hash])) { + $this->_logs[$hash] = array( + 'obj' => $obj, + 'log' => array() + ); + } + + if(VQMod::$fileModding) { + $this->_logs[$hash]['log'][] = PHP_EOL . 'File Name : ' . VQMod::$fileModding; + } + + $this->_logs[$hash]['log'][] = $data; + + } +} + +/** + * VQModObject + * @description Object for the that orchestrates each applied modification + */ +class VQModObject { + public $modFile = ''; + public $id = ''; + public $version = ''; + public $vqmver = ''; + public $author = ''; + public $mods = array(); + + private $_skip = false; + + /** + * VQModObject::__construct() + * + * @param DOMNode $node node + * @param string $modFile File modification is from + * @return null + * @description Loads modification meta information + */ + public function __construct(DOMNode $node, $modFile) { + if($node->hasChildNodes()) { + foreach($node->childNodes as $child) { + $name = (string) $child->nodeName; + if(isset($this->$name)) { + $this->$name = (string) $child->nodeValue; + } + } + } + + $this->modFile = $modFile; + $this->_parseMods($node); + } + + /** + * VQModObject::skip() + * + * @return bool + * @description Returns the skip status of a modification + */ + public function skip() { + return $this->_skip; + } + + /** + * VQModObject::applyMod() + * + * @param array $mods Array of search add nodes + * @param string $data File contents to be altered + * @return null + * @description Applies all modifications to the text data + */ + public function applyMod($mods, &$data) { + if($this->_skip) return; + $tmp = $data; + + foreach($mods as $mod) { + VQMod::$fileModding = $mod['fileToMod'] . '(' . $mod['opIndex'] . ')'; + if(!empty($mod['ignoreif'])) { + if($mod['ignoreif']->regex == 'true') { + if (preg_match($mod['ignoreif']->getContent(), $tmp)) { + continue; + } + } else { + if (strpos($tmp, $mod['ignoreif']->getContent()) !== false) { + continue; + } + } + } + + $indexCount = 0; + + $tmp = $this->_explodeData($tmp); + $lineMax = count($tmp) - 1; + + // tag attributes - Override attributes if set + foreach(array_keys((array)$mod['search']) as $key) { + if ($key == "\x0VQNode\x0_content") { continue; } + if ($key == "trim") { continue; } + if (isset($mod['add']->$key) && $mod['add']->$key) { + $mod['search']->$key = $mod['add']->$key; + } + } + + switch($mod['search']->position) { + case 'top': + $tmp[$mod['search']->offset] = $mod['add']->getContent() . $tmp[$mod['search']->offset]; + break; + + case 'bottom': + $offset = $lineMax - $mod['search']->offset; + if($offset < 0){ + $tmp[-1] = $mod['add']->getContent(); + } else { + $tmp[$offset] .= $mod['add']->getContent(); + } + break; + + default: + + $changed = false; + foreach($tmp as $lineNum => $line) { + if(strlen($mod['search']->getContent()) == 0) { + if($mod['error'] == 'log' || $mod['error'] == 'abort') { + VQMod::$log->write('VQModObject::applyMod - EMPTY SEARCH CONTENT ERROR', $this); + } + break; + } + + if($mod['search']->regex == 'true') { + $pos = @preg_match($mod['search']->getContent(), $line); + if($pos === false) { + if($mod['error'] == 'log' || $mod['error'] == 'abort' ) { + VQMod::$log->write('VQModObject::applyMod - INVALID REGEX ERROR - ' . $mod['search']->getContent(), $this); + } + break 2; + } elseif($pos == 0) { + $pos = false; + } + } else { + $pos = strpos($line, $mod['search']->getContent()); + } + + if($pos !== false) { + $indexCount++; + $changed = true; + + if(!$mod['search']->indexes() || ($mod['search']->indexes() && in_array($indexCount, $mod['search']->indexes()))) { + + switch($mod['search']->position) { + case 'before': + $offset = ($lineNum - $mod['search']->offset < 0) ? -1 : $lineNum - $mod['search']->offset; + $tmp[$offset] = empty($tmp[$offset]) ? $mod['add']->getContent() : $mod['add']->getContent() . "\n" . $tmp[$offset]; + break; + + case 'after': + $offset = ($lineNum + $mod['search']->offset > $lineMax) ? $lineMax : $lineNum + $mod['search']->offset; + $tmp[$offset] = $tmp[$offset] . "\n" . $mod['add']->getContent(); + break; + + case 'ibefore': + $tmp[$lineNum] = str_replace($mod['search']->getContent(), $mod['add']->getContent() . $mod['search']->getContent(), $line); + break; + + case 'iafter': + $tmp[$lineNum] = str_replace($mod['search']->getContent(), $mod['search']->getContent() . $mod['add']->getContent(), $line); + break; + + default: + if(!empty($mod['search']->offset)) { + if($mod['search']->offset > 0) { + for($i = 1; $i <= $mod['search']->offset; $i++) { + if(isset($tmp[$lineNum + $i])) { + $tmp[$lineNum + $i] = ''; + } + } + } elseif($mod['search']->offset < 0) { + for($i = -1; $i >= $mod['search']->offset; $i--) { + if(isset($tmp[$lineNum + $i])) { + $tmp[$lineNum + $i] = ''; + } + } + } + } + + if($mod['search']->regex == 'true') { + $tmp[$lineNum] = preg_replace($mod['search']->getContent(), $mod['add']->getContent(), $line); + } else { + $tmp[$lineNum] = str_replace($mod['search']->getContent(), $mod['add']->getContent(), $line); + } + break; + } + } + } + } + + if(!$changed) { + $skip = ($mod['error'] == 'skip' || $mod['error'] == 'log') ? ' (SKIPPED)' : ' (ABORTING MOD)'; + + if($mod['error'] == 'log' || $mod['error'] == 'abort') { + VQMod::$log->write('VQModObject::applyMod - SEARCH NOT FOUND' . $skip . ': ' . $mod['search']->getContent(), $this); + } + + if($mod['error'] == 'abort') { + $this->_skip = true; + return; + } + + } + + break; + } + ksort($tmp); + $tmp = $this->_implodeData($tmp); + } + + VQMod::$fileModding = false; + + $data = $tmp; + } + + /** + * VQModObject::_parseMods() + * + * @param DOMNode $node node to be parsed + * @return null + * @description Parses modifications in preparation for the applyMod method to work + */ + private function _parseMods(DOMNode $node){ + $files = $node->getElementsByTagName('file'); + + $replaces = VQMod::$replaces; + + foreach($files as $file) { + $path = $file->getAttribute('path') ? $file->getAttribute('path') : ''; + $filesToMod = explode(',', $file->getAttribute('name')); + + foreach($filesToMod as $filename) { + + $fileToMod = $path . $filename; + if(!empty($replaces)) { + foreach($replaces as $r) { + if(count($r) == 2) { + $fileToMod = preg_replace($r[0], $r[1], $fileToMod); + } + } + } + + $error = ($file->hasAttribute('error')) ? $file->getAttribute('error') : 'log'; + $fullPath = VQMod::path($fileToMod); + + if(!$fullPath || !file_exists($fullPath)){ + if(strpos($fileToMod, '*') !== false) { + $fullPath = VQMod::getCwd() . $fileToMod; + } else { + if ($error == 'log' || $error == 'abort') { + $skip = ($error == 'log') ? ' (SKIPPED)' : ' (ABORTING MOD)'; + VQMod::$log->write('VQModObject::parseMods - Could not resolve path for [' . $fileToMod . ']' . $skip, $this); + } + + if ($error == 'log' || $error == 'skip') { + continue; + } elseif ($error == 'abort') { + return false; + } + } + } + + $operations = $file->getElementsByTagName('operation'); + + foreach($operations as $opIndex => $operation) { + VQMod::$fileModding = $fileToMod . '(' . $opIndex . ')'; + $skipOperation = false; + + $error = ($operation->hasAttribute('error')) ? $operation->getAttribute('error') : 'abort'; + $ignoreif = $operation->getElementsByTagName('ignoreif')->item(0); + + if($ignoreif) { + $ignoreif = new VQSearchNode($ignoreif); + } else { + $ignoreif = false; + } + + $search = $operation->getElementsByTagName('search')->item(0); + $add = $operation->getElementsByTagName('add')->item(0); + + if(!$search) { + VQMod::$log->write('Operation tag missing', $this); + $skipOperation = true; + } + + if(!$add) { + VQMod::$log->write('Operation tag missing', $this); + $skipOperation = true; + } + + if(!$skipOperation) { + $this->mods[$fullPath][] = array( + 'search' => new VQSearchNode($search), + 'add' => new VQAddNode($add), + 'ignoreif' => $ignoreif, + 'error' => $error, + 'fileToMod' => $fileToMod, + 'opIndex' => $opIndex, + ); + } + } + VQMod::$fileModding = false; + } + } + } + + /** + * VQModObject::_explodeData() + * + * @param string $data file contents + * @return string + * @description Splits a file into an array of individual lines + */ + private function _explodeData($data) { + return explode("\n", $data); + } + + /** + * VQModObject::_implodeData() + * + * @param array $data Array of lines + * @return string + * @description Joins an array of lines back into a text file + */ + private function _implodeData($data) { + return implode("\n", $data); + } +} + +/** + * VQNode + * @description Basic node object blueprint + */ +class VQNode { + public $regex = 'false'; + public $trim = 'false'; + private $_content = ''; + + /** + * VQNode::__construct() + * + * @param DOMNode $node Search/add node + * @return null + * @description Parses the node attributes and sets the node property + */ + public function __construct(DOMNode $node) { + $this->_content = $node->nodeValue; + + if($node->hasAttributes()) { + foreach($node->attributes as $attr) { + $name = $attr->nodeName; + if(isset($this->$name)) { + $this->$name = $attr->nodeValue; + } + } + } + } + + /** + * VQNode::getContent() + * + * @return string + * @description Returns the content, trimmed if applicable + */ + public function getContent() { + $content = ($this->trim == 'true') ? trim($this->_content) : $this->_content; + return $content; + } +} + +/** + * VQSearchNode + * @description Object for the xml tags + */ +class VQSearchNode extends VQNode { + public $position = 'replace'; + public $offset = 0; + public $index = 'false'; + public $regex = 'false'; + public $trim = 'true'; + + /** + * VQSearchNode::indexes() + * + * @return bool, array + * @description Returns the index values to use the search on, or false if none + */ + public function indexes() { + if($this->index == 'false') { + return false; + } + $tmp = explode(',', $this->index); + foreach($tmp as $k => $v) { + if(!is_int($v)) { + unset($k); + } + } + $tmp = array_unique($tmp); + return empty($tmp) ? false : $tmp; + } +} + +/** + * VQAddNode + * @description Object for the xml tags + */ +class VQAddNode extends VQNode { + public $position = false; + public $offset = false; + public $index = false; + public $regex = false; + public $trim = 'false'; +} \ No newline at end of file From d8aa31d773d3571d7511f22c1ce164dbea1d8a97 Mon Sep 17 00:00:00 2001 From: GeekoDev Date: Thu, 7 Jul 2022 16:19:58 +0200 Subject: [PATCH 2/6] Update readme --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fb0cc3..95ef8d7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# opencart +# Vqmod for opencart This is a repository for the opencart vqmod core script files. First download and install vQmod from the main repository, then install the script files from this repository for use with opencart @@ -10,4 +10,9 @@ When done, you should see the following files on your server vqmod/xml/vqmod_opencart.xml vqmod/install/index.php -4. Run the installer with https:/yourstore.com/vqmod/install \ No newline at end of file +4. Run the installer with https:/yourstore.com/vqmod/install + + +# Improvment +This fork gives the possibility to include an xml file directly inside oc 4.x extension folder, the file must be into /extension/[module]/vqmod/ in order to work. +This way a package can be fully integrated with its modification into the module package, to avoid extra operation of uploading vqmod file by FTP. From 9c8d5041efbb1af1bcccadf3362680a0d03463d8 Mon Sep 17 00:00:00 2001 From: GeekoDev Date: Thu, 7 Jul 2022 18:30:07 +0200 Subject: [PATCH 3/6] Delete vqmod.php --- vqmod.php | 981 ------------------------------------------------------ 1 file changed, 981 deletions(-) delete mode 100644 vqmod.php diff --git a/vqmod.php b/vqmod.php deleted file mode 100644 index 6cca5cc..0000000 --- a/vqmod.php +++ /dev/null @@ -1,981 +0,0 @@ -= self::$_lastModifiedTime && filemtime($cacheFile) >= $file_last_modified) { - return $cacheFile; - } - - if(isset(self::$_filesModded[$sourcePath])) { - return self::$_filesModded[$sourcePath]['cached'] ? $cacheFile : $sourceFile; - } - - $changed = false; - $fileHash = sha1_file($sourcePath); - $fileData = file_get_contents($sourcePath); - - foreach(self::$_mods as $modObject) { - foreach($modObject->mods as $path => $mods) { - if(self::_checkMatch($path, $modificationsPath)) { - $modObject->applyMod($mods, $fileData); - } - } - } - - if (sha1($fileData) != $fileHash) { - $writePath = $cacheFile; - if(!file_exists($writePath) || is_writable($writePath)) { - file_put_contents($writePath, $fileData, LOCK_EX); - $changed = true; - } - } else { - file_put_contents(self::path(self::$checkedCache, true), $stripped_filename . PHP_EOL, FILE_APPEND | LOCK_EX); - - // Prevent checked.cache file from duplicating lines when checked folder is above vqmod directory - Thanks adrianolmedo - $lines = file(self::path(self::$checkedCache, true)); - $lines = array_unique($lines); - file_put_contents(self::path(self::$checkedCache, true), implode($lines)); - - self::$_doNotMod[] = $sourcePath; - } - - self::$_filesModded[$sourcePath] = array('cached' => $changed); - return $changed ? $writePath : $sourcePath; - } - - /** - * VQMod::path() - * - * @param string $path File path - * @param bool $skip_real If true path is full not relative - * @return bool, string - * @description Returns the full true path of a file if it exists, otherwise false - */ - public static function path($path, $skip_real = false) { - $tmp = self::$_cwd . $path; - $realpath = $skip_real ? $tmp : self::_realpath($tmp); - if(!$realpath) { - return false; - } - return $realpath; - } - - /** - * VQMod::getCwd() - * - * @return string - * @description Returns current working directory - */ - public static function getCwd() { - return self::$_cwd; - } - - /** - * VQMod::dirCheck() - * - * @param string $path - * @return null - * @description Creates $path folder if it doesn't exist - */ - public static function dirCheck($path) { - if(!is_dir($path)) { - if(!mkdir($path)) { - die('VQMod::dirCheck - CANNOT CREATE "' . $path . '" DIRECTORY'); - } - } - } - - /** - * VQMod::handleXMLError() - * - * @description Error handler for bad XML files - */ - public static function handleXMLError($errno, $errstr, $errfile, $errline) { - if ($errno == E_WARNING && (substr_count($errstr, 'DOMDocument::load()') > 0)) { - throw new DOMException(str_replace('DOMDocument::load()', '', $errstr)); - } else { - return false; - } - } - - /** - * VQMod::_getMods() - * - * @return null - * @description Gets list of XML files in vqmod xml folder for processing - */ - private static function _getMods() { - - self::$_modFileList = glob(self::path('vqmod/xml/', true) . '*.xml'); - - // @GeekoDev - Get files from opencart 4.x extension folders - if (defined('DIR_EXTENSION')) { - $opencartExtFileList = glob(DIR_EXTENSION . '*/vqmod/*.xml'); - self::$_modFileList = array_merge(self::$_modFileList, $opencartExtFileList); - } - // END - Get files from opencart 4.x extension folders - - foreach(self::$_modFileList as $file) { - if(file_exists($file)) { - $lastMod = filemtime($file); - if($lastMod > self::$_lastModifiedTime){ - self::$_lastModifiedTime = $lastMod; - } - } - } - - $xml_folder_time = filemtime(self::path('vqmod/xml')); - if($xml_folder_time > self::$_lastModifiedTime){ - self::$_lastModifiedTime = $xml_folder_time; - } - - $modCache = self::path(self::$modCache); - if(self::$_devMode || !file_exists($modCache)) { - self::$_lastModifiedTime = time(); - } elseif(file_exists($modCache) && filemtime($modCache) >= self::$_lastModifiedTime) { - $mods = file_get_contents($modCache); - if(!empty($mods)) - self::$_mods = unserialize($mods); - if(self::$_mods !== false) { - return; - } - } - - // Clear checked cache if rebuilding - file_put_contents(self::path(self::$checkedCache, true), '', LOCK_EX); - - if(self::$_modFileList) { - self::_parseMods(); - } else { - self::$log->write('VQMod::_getMods - NO XML FILES READABLE IN XML FOLDER'); - } - } - - /** - * VQMod::_parseMods() - * - * @return null - * @description Loops through xml files and attempts to load them as VQModObject's - */ - private static function _parseMods() { - - set_error_handler(array('VQMod', 'handleXMLError')); - - $dom = new DOMDocument('1.0', 'UTF-8'); - foreach(self::$_modFileList as $modFileKey => $modFile) { - if(file_exists($modFile)) { - try { - $dom->load($modFile); - $mod = $dom->getElementsByTagName('modification')->item(0); - $vqmver = $mod->getElementsByTagName('vqmver')->item(0); - - if($vqmver) { - $version_check = $vqmver->getAttribute('required'); - if(strtolower($version_check) == 'true') { - if(version_compare(self::$_vqversion, $vqmver->nodeValue, '<')) { - self::$log->write('VQMod::_parseMods - FILE "' . $modFile . '" REQUIRES VQMOD "' . $vqmver->nodeValue . '" OR ABOVE AND HAS BEEN SKIPPED'); - continue; - } - } - } - - self::$_mods[] = new VQModObject($mod, $modFile); - } catch (Exception $e) { - self::$log->write('VQMod::_parseMods - INVALID XML FILE: ' . $e->getMessage()); - } - } else { - self::$log->write('VQMod::_parseMods - FILE NOT FOUND: ' . $modFile); - } - } - - restore_error_handler(); - - $modCache = self::path(self::$modCache, true); - $result = file_put_contents($modCache, serialize(self::$_mods), LOCK_EX); - if(!$result) { - die('VQMod::_parseMods - "/vqmod/mods.cache" FILE NOT WRITEABLE'); - } - } - - /** - * VQMod::_loadProtected() - * - * @return null - * @description Loads protected list and adds them to _doNotMod array - */ - private static function _loadProtected() { - $file = self::path(self::$protectedFilelist); - if($file && is_file($file)) { - $paths = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - if(!empty($paths)) { - foreach($paths as $path) { - $fullPath = self::path($path); - if($fullPath && !in_array($fullPath, self::$_doNotMod)) { - self::$_doNotMod[] = $fullPath; - } - } - } - } - } - - /** - * VQMod::_loadChecked() - * - * @return null - * @description Loads already checked files and adds them to _doNotMod array - */ - private static function _loadChecked() { - $file = self::path(self::$checkedCache); - if($file && is_file($file)) { - $paths = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - if(!empty($paths)) { - foreach($paths as $path) { - $fullPath = self::path($path, true); - if($fullPath) { - self::$_doNotMod[] = $fullPath; - } - } - } - } - } - - /** - * VQMod::_cacheName() - * - * @param string $file Filename to be converted to cache filename - * @return string - * @description Returns cache file name for a path - */ - private static function _cacheName($file) { - return self::$_cachePathFull . 'vq2-' . preg_replace('~[/\\\\]+~', '_', $file); - } - - /** - * VQMod::_setCwd() - * - * @param string $path Path to be used as current working directory - * @return null - * @description Sets the current working directory variable - */ - private static function _setCwd($path) { - self::$_cwd = self::_realpath($path); - } - - /** - * VQMod::_realpath() - * - * @param string $file - * @return string - * @description Returns real path of any path, adding directory slashes if necessary - */ - private static function _realpath($file) { - $path = realpath($file); - if(!$path) { - return false; - } - - if(is_dir($path)) { - $path = rtrim($path, self::$directorySeparator) . self::$directorySeparator; - } - - return $path; - } - - /** - * VQMod::_checkMatch() - * - * @param string $modFilePath Modification path from a node - * @param string $checkFilePath File path - * @return bool - * @description Checks a modification path against a file path - */ - private static function _checkMatch($modFilePath, $checkFilePath) { - $modFilePath = str_replace('\\', '/', $modFilePath); - $checkFilePath = str_replace('\\', '/', $checkFilePath); - - if(self::$windows) { - $modFilePath = strtolower($modFilePath); - $checkFilePath = strtolower($checkFilePath); - } - - if($modFilePath == $checkFilePath) { - $return = true; - } elseif(strpos($modFilePath, '*') !== false) { - $return = true; - $modParts = explode('/', $modFilePath); - $checkParts = explode('/', $checkFilePath); - - if(count($modParts) !== count($checkParts)) { - $return = false; - } else { - - $toCheck = array_diff_assoc($modParts, $checkParts); - - foreach($toCheck as $k => $part) { - if($part === '*') { - continue; - } elseif(strpos($part, '*') !== false) { - $part = preg_replace_callback('~([^*]+)~', array('self', '_quotePath'), $part); - $part = str_replace('*', '[^/]*', $part); - $part = (bool) preg_match('~^' . $part . '$~', $checkParts[$k]); - - if($part) { - continue; - } - } elseif($part === $checkParts[$k]) { - continue; - } - - $return = false; - break; - } - - } - } else { - $return = false; - } - - return $return; - } - - /** - * VQMod::_quotePath() - * - * @param string $matches callback matches - * @return string - * @description apply's preg_quote to string from callback - */ - private static function _quotePath($matches) { - return preg_quote($matches[1], '~'); - } -} - -/** - * VQModLog - * @description Object to log information to a file - */ -class VQModLog { - private $_sep; - private $_defhash = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'; - private $_logs = array(); - - /** - * VQModLog::__construct() - * - * @return null - * @description Object instantiation method - */ - public function __construct() { - $this->_sep = str_repeat('-', 70); - } - - /** - * VQModLog::__destruct() - * - * @return null - * @description Logs any messages to the log file just before object is destroyed - */ - public function __destruct() { - if(empty($this->_logs) || VQMod::$logging == false) { - return; - } - - $logPath = VQMod::path(VQMod::$logFolder . date('w_D') . '.log', true); - - $txt = array(); - - $txt[] = str_repeat('-', 10) . ' Date: ' . date('Y-m-d H:i:s') . ' ~ IP : ' . (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'N/A') . ' ' . str_repeat('-', 10); - $txt[] = 'REQUEST URI : ' . $_SERVER['REQUEST_URI']; - - foreach($this->_logs as $count => $log) { - if($log['obj']) { - $vars = get_object_vars($log['obj']); - $txt[] = 'MOD DETAILS:'; - foreach($vars as $k => $v) { - if(is_string($v)) { - $txt[] = ' ' . str_pad($k, 10, ' ', STR_PAD_RIGHT) . ': ' . $v; - } - } - - } - - foreach($log['log'] as $msg) { - $txt[] = $msg; - } - - if ($count > count($this->_logs)-1) { - $txt[] = ''; - } - } - - $txt[] = $this->_sep; - $txt[] = str_repeat(PHP_EOL, 2); - $append = true; - - if(!file_exists($logPath)) { - $append = false; - } else { - $content = file_get_contents($logPath); - if(!empty($content) && strpos($content, ' Date: ' . date('Y-m-d ')) === false) { - $append = false; - } - } - - $result = file_put_contents($logPath, implode(PHP_EOL, $txt), ($append ? FILE_APPEND | LOCK_EX : LOCK_EX)); - if(!$result) { - die('VQModLog::__destruct - LOG FILE "' . $logPath . '" COULD NOT BE WRITTEN'); - } - } - - /** - * VQModLog::write() - * - * @param string $data Text to be added to log file - * @param VQModObject $obj Modification the error belongs to - * @return null - * @description Adds error to log object ready to be output - */ - public function write($data, VQModObject $obj = NULL) { - if($obj) { - $hash = sha1($obj->id); - } else { - $hash = $this->_defhash; - } - - if(empty($this->_logs[$hash])) { - $this->_logs[$hash] = array( - 'obj' => $obj, - 'log' => array() - ); - } - - if(VQMod::$fileModding) { - $this->_logs[$hash]['log'][] = PHP_EOL . 'File Name : ' . VQMod::$fileModding; - } - - $this->_logs[$hash]['log'][] = $data; - - } -} - -/** - * VQModObject - * @description Object for the that orchestrates each applied modification - */ -class VQModObject { - public $modFile = ''; - public $id = ''; - public $version = ''; - public $vqmver = ''; - public $author = ''; - public $mods = array(); - - private $_skip = false; - - /** - * VQModObject::__construct() - * - * @param DOMNode $node node - * @param string $modFile File modification is from - * @return null - * @description Loads modification meta information - */ - public function __construct(DOMNode $node, $modFile) { - if($node->hasChildNodes()) { - foreach($node->childNodes as $child) { - $name = (string) $child->nodeName; - if(isset($this->$name)) { - $this->$name = (string) $child->nodeValue; - } - } - } - - $this->modFile = $modFile; - $this->_parseMods($node); - } - - /** - * VQModObject::skip() - * - * @return bool - * @description Returns the skip status of a modification - */ - public function skip() { - return $this->_skip; - } - - /** - * VQModObject::applyMod() - * - * @param array $mods Array of search add nodes - * @param string $data File contents to be altered - * @return null - * @description Applies all modifications to the text data - */ - public function applyMod($mods, &$data) { - if($this->_skip) return; - $tmp = $data; - - foreach($mods as $mod) { - VQMod::$fileModding = $mod['fileToMod'] . '(' . $mod['opIndex'] . ')'; - if(!empty($mod['ignoreif'])) { - if($mod['ignoreif']->regex == 'true') { - if (preg_match($mod['ignoreif']->getContent(), $tmp)) { - continue; - } - } else { - if (strpos($tmp, $mod['ignoreif']->getContent()) !== false) { - continue; - } - } - } - - $indexCount = 0; - - $tmp = $this->_explodeData($tmp); - $lineMax = count($tmp) - 1; - - // tag attributes - Override attributes if set - foreach(array_keys((array)$mod['search']) as $key) { - if ($key == "\x0VQNode\x0_content") { continue; } - if ($key == "trim") { continue; } - if (isset($mod['add']->$key) && $mod['add']->$key) { - $mod['search']->$key = $mod['add']->$key; - } - } - - switch($mod['search']->position) { - case 'top': - $tmp[$mod['search']->offset] = $mod['add']->getContent() . $tmp[$mod['search']->offset]; - break; - - case 'bottom': - $offset = $lineMax - $mod['search']->offset; - if($offset < 0){ - $tmp[-1] = $mod['add']->getContent(); - } else { - $tmp[$offset] .= $mod['add']->getContent(); - } - break; - - default: - - $changed = false; - foreach($tmp as $lineNum => $line) { - if(strlen($mod['search']->getContent()) == 0) { - if($mod['error'] == 'log' || $mod['error'] == 'abort') { - VQMod::$log->write('VQModObject::applyMod - EMPTY SEARCH CONTENT ERROR', $this); - } - break; - } - - if($mod['search']->regex == 'true') { - $pos = @preg_match($mod['search']->getContent(), $line); - if($pos === false) { - if($mod['error'] == 'log' || $mod['error'] == 'abort' ) { - VQMod::$log->write('VQModObject::applyMod - INVALID REGEX ERROR - ' . $mod['search']->getContent(), $this); - } - break 2; - } elseif($pos == 0) { - $pos = false; - } - } else { - $pos = strpos($line, $mod['search']->getContent()); - } - - if($pos !== false) { - $indexCount++; - $changed = true; - - if(!$mod['search']->indexes() || ($mod['search']->indexes() && in_array($indexCount, $mod['search']->indexes()))) { - - switch($mod['search']->position) { - case 'before': - $offset = ($lineNum - $mod['search']->offset < 0) ? -1 : $lineNum - $mod['search']->offset; - $tmp[$offset] = empty($tmp[$offset]) ? $mod['add']->getContent() : $mod['add']->getContent() . "\n" . $tmp[$offset]; - break; - - case 'after': - $offset = ($lineNum + $mod['search']->offset > $lineMax) ? $lineMax : $lineNum + $mod['search']->offset; - $tmp[$offset] = $tmp[$offset] . "\n" . $mod['add']->getContent(); - break; - - case 'ibefore': - $tmp[$lineNum] = str_replace($mod['search']->getContent(), $mod['add']->getContent() . $mod['search']->getContent(), $line); - break; - - case 'iafter': - $tmp[$lineNum] = str_replace($mod['search']->getContent(), $mod['search']->getContent() . $mod['add']->getContent(), $line); - break; - - default: - if(!empty($mod['search']->offset)) { - if($mod['search']->offset > 0) { - for($i = 1; $i <= $mod['search']->offset; $i++) { - if(isset($tmp[$lineNum + $i])) { - $tmp[$lineNum + $i] = ''; - } - } - } elseif($mod['search']->offset < 0) { - for($i = -1; $i >= $mod['search']->offset; $i--) { - if(isset($tmp[$lineNum + $i])) { - $tmp[$lineNum + $i] = ''; - } - } - } - } - - if($mod['search']->regex == 'true') { - $tmp[$lineNum] = preg_replace($mod['search']->getContent(), $mod['add']->getContent(), $line); - } else { - $tmp[$lineNum] = str_replace($mod['search']->getContent(), $mod['add']->getContent(), $line); - } - break; - } - } - } - } - - if(!$changed) { - $skip = ($mod['error'] == 'skip' || $mod['error'] == 'log') ? ' (SKIPPED)' : ' (ABORTING MOD)'; - - if($mod['error'] == 'log' || $mod['error'] == 'abort') { - VQMod::$log->write('VQModObject::applyMod - SEARCH NOT FOUND' . $skip . ': ' . $mod['search']->getContent(), $this); - } - - if($mod['error'] == 'abort') { - $this->_skip = true; - return; - } - - } - - break; - } - ksort($tmp); - $tmp = $this->_implodeData($tmp); - } - - VQMod::$fileModding = false; - - $data = $tmp; - } - - /** - * VQModObject::_parseMods() - * - * @param DOMNode $node node to be parsed - * @return null - * @description Parses modifications in preparation for the applyMod method to work - */ - private function _parseMods(DOMNode $node){ - $files = $node->getElementsByTagName('file'); - - $replaces = VQMod::$replaces; - - foreach($files as $file) { - $path = $file->getAttribute('path') ? $file->getAttribute('path') : ''; - $filesToMod = explode(',', $file->getAttribute('name')); - - foreach($filesToMod as $filename) { - - $fileToMod = $path . $filename; - if(!empty($replaces)) { - foreach($replaces as $r) { - if(count($r) == 2) { - $fileToMod = preg_replace($r[0], $r[1], $fileToMod); - } - } - } - - $error = ($file->hasAttribute('error')) ? $file->getAttribute('error') : 'log'; - $fullPath = VQMod::path($fileToMod); - - if(!$fullPath || !file_exists($fullPath)){ - if(strpos($fileToMod, '*') !== false) { - $fullPath = VQMod::getCwd() . $fileToMod; - } else { - if ($error == 'log' || $error == 'abort') { - $skip = ($error == 'log') ? ' (SKIPPED)' : ' (ABORTING MOD)'; - VQMod::$log->write('VQModObject::parseMods - Could not resolve path for [' . $fileToMod . ']' . $skip, $this); - } - - if ($error == 'log' || $error == 'skip') { - continue; - } elseif ($error == 'abort') { - return false; - } - } - } - - $operations = $file->getElementsByTagName('operation'); - - foreach($operations as $opIndex => $operation) { - VQMod::$fileModding = $fileToMod . '(' . $opIndex . ')'; - $skipOperation = false; - - $error = ($operation->hasAttribute('error')) ? $operation->getAttribute('error') : 'abort'; - $ignoreif = $operation->getElementsByTagName('ignoreif')->item(0); - - if($ignoreif) { - $ignoreif = new VQSearchNode($ignoreif); - } else { - $ignoreif = false; - } - - $search = $operation->getElementsByTagName('search')->item(0); - $add = $operation->getElementsByTagName('add')->item(0); - - if(!$search) { - VQMod::$log->write('Operation tag missing', $this); - $skipOperation = true; - } - - if(!$add) { - VQMod::$log->write('Operation tag missing', $this); - $skipOperation = true; - } - - if(!$skipOperation) { - $this->mods[$fullPath][] = array( - 'search' => new VQSearchNode($search), - 'add' => new VQAddNode($add), - 'ignoreif' => $ignoreif, - 'error' => $error, - 'fileToMod' => $fileToMod, - 'opIndex' => $opIndex, - ); - } - } - VQMod::$fileModding = false; - } - } - } - - /** - * VQModObject::_explodeData() - * - * @param string $data file contents - * @return string - * @description Splits a file into an array of individual lines - */ - private function _explodeData($data) { - return explode("\n", $data); - } - - /** - * VQModObject::_implodeData() - * - * @param array $data Array of lines - * @return string - * @description Joins an array of lines back into a text file - */ - private function _implodeData($data) { - return implode("\n", $data); - } -} - -/** - * VQNode - * @description Basic node object blueprint - */ -class VQNode { - public $regex = 'false'; - public $trim = 'false'; - private $_content = ''; - - /** - * VQNode::__construct() - * - * @param DOMNode $node Search/add node - * @return null - * @description Parses the node attributes and sets the node property - */ - public function __construct(DOMNode $node) { - $this->_content = $node->nodeValue; - - if($node->hasAttributes()) { - foreach($node->attributes as $attr) { - $name = $attr->nodeName; - if(isset($this->$name)) { - $this->$name = $attr->nodeValue; - } - } - } - } - - /** - * VQNode::getContent() - * - * @return string - * @description Returns the content, trimmed if applicable - */ - public function getContent() { - $content = ($this->trim == 'true') ? trim($this->_content) : $this->_content; - return $content; - } -} - -/** - * VQSearchNode - * @description Object for the xml tags - */ -class VQSearchNode extends VQNode { - public $position = 'replace'; - public $offset = 0; - public $index = 'false'; - public $regex = 'false'; - public $trim = 'true'; - - /** - * VQSearchNode::indexes() - * - * @return bool, array - * @description Returns the index values to use the search on, or false if none - */ - public function indexes() { - if($this->index == 'false') { - return false; - } - $tmp = explode(',', $this->index); - foreach($tmp as $k => $v) { - if(!is_int($v)) { - unset($k); - } - } - $tmp = array_unique($tmp); - return empty($tmp) ? false : $tmp; - } -} - -/** - * VQAddNode - * @description Object for the xml tags - */ -class VQAddNode extends VQNode { - public $position = false; - public $offset = false; - public $index = false; - public $regex = false; - public $trim = 'false'; -} \ No newline at end of file From 78a821b9bd0aecfeb63d0cefe8bb2c0c4e429b71 Mon Sep 17 00:00:00 2001 From: GeekoDev Date: Thu, 7 Jul 2022 18:33:01 +0200 Subject: [PATCH 4/6] pathReplaces.php file to support OC4 extensions pathReplaces.php file updated for latest vqmod update to support to include xml directly in OC4 module folder structure into extension/[mod]/vqmod/ --- pathReplaces.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pathReplaces.php diff --git a/pathReplaces.php b/pathReplaces.php new file mode 100644 index 0000000..cbac0d0 --- /dev/null +++ b/pathReplaces.php @@ -0,0 +1,18 @@ + Date: Thu, 7 Jul 2022 18:37:13 +0200 Subject: [PATCH 5/6] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 95ef8d7..7691f5a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,3 @@ When done, you should see the following files on your server 4. Run the installer with https:/yourstore.com/vqmod/install - -# Improvment -This fork gives the possibility to include an xml file directly inside oc 4.x extension folder, the file must be into /extension/[module]/vqmod/ in order to work. -This way a package can be fully integrated with its modification into the module package, to avoid extra operation of uploading vqmod file by FTP. From 04bc625db449f4441741e7923ecc6ead380362ab Mon Sep 17 00:00:00 2001 From: GeekoDev Date: Thu, 7 Jul 2022 18:38:52 +0200 Subject: [PATCH 6/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7691f5a..698491d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Vqmod for opencart +# opencart This is a repository for the opencart vqmod core script files. First download and install vQmod from the main repository, then install the script files from this repository for use with opencart