/** * The placeholder keys * * @var array * @since 3.2.0 */ protected array $PKeys = [ 1 => 'REPLACE<>$$$$]', 2 => 'INSERT<>$$$$]', 3 => 'REPLACED<>$$$$]', 4 => 'INSERTED<>$$$$]' ]; /** * Current Joomla Version We are IN * * @var int * @since 3.2.0 **/ protected int $currentVersion; /** * The custom code in local files that already exist in system * * @var array * @since 3.2.0 */ protected array $existing = []; /** * The custom code in local files that are new * * @var array * @since 3.2.0 */ protected array $new = []; /** * The index of code already loaded * * @var array * @since 3.2.0 */ protected array $done = []; /** * The search counter * * @var array * @since 3.2.0 */ protected array $counter = [1 => 0, 2 => 0]; /** * The file types to search * * @var array * @since 3.2.0 */ protected array $fileTypes = ['\.php', '\.js', '\.xml']; /** * The local placeholders * * @var array * @since 3.2.0 */ protected array $placeholders; /** * Today's date in SQL format * * @var string * @since 3.2.0 */ protected string $today; /** * Compiler Config * * @var Config * @since 3.2.0 **/ protected Config $config; /** * Compiler Customcode Gui * * @var Gui * @since 3.2.0 **/ protected Gui $gui; /** * Compiler Customcode Extractor Paths * * @var Paths * @since 3.2.0 **/ protected Paths $paths; /** * Compiler Placeholder Reverse * * @var Reverse * @since 3.2.0 **/ protected Reverse $reverse; /** * Compiler Component Placeholder * * @var Placeholder * @since 3.2.0 **/ protected Placeholder $componentPlaceholder; /** * Compiler Component Pathfix * * @var Pathfix * @since 3.2.0 **/ protected Pathfix $pathfix; /** * Current User Object * * @since 3.2.0 **/ protected $user; /** * Database object to query local DB * * @since 3.2.0 **/ protected $db; /** * Database object to query local DB * * @since 3.2.0 **/ protected $app; /** * Constructor. * * @param Config|null $config The compiler config object. * @param Gui|null $gui The compiler customcode gui object. * @param Paths|null $paths The compiler customcode extractor paths object. * @param Reverse|null $reverse The compiler placeholder reverse object. * @param Placeholder|null $placeholder The compiler component placeholder object. * @param Pathfix|null $pathfix The compiler path fixing object. * * @throws \Exception * @since 3.2.0 */ public function __construct(?Config $config = null, ?Gui $gui = null, ?Paths $paths = null, ?Reverse $reverse = null, ?Placeholder $placeholder = null, ?Pathfix $pathfix = null) { $this->config = $config ?: Compiler::_('Config'); $this->gui = $gui ?: Compiler::_('Customcode.Gui'); $this->paths = $paths ?: Compiler::_('Customcode.Extractor.Paths'); $this->reverse = $reverse ?: Compiler::_('Placeholder.Reverse'); $this->componentPlaceholder = $placeholder ?: Compiler::_('Component.Placeholder'); $this->pathfix = $pathfix ?: Compiler::_('Utilities.Pathfix'); $this->user = Factory::getUser(); $this->db = Factory::getDbo(); $this->app = Factory::getApplication(); // set today's date $this->today = Factory::getDate()->toSql(); // set some local placeholders $placeholders = array_flip( $this->componentPlaceholder->get() ); $placeholders[StringHelper::safe( $this->config->component_code_name, 'F' ) . 'Helper::'] = Placefix::_('Component') . 'Helper::'; $placeholders['COM_' . StringHelper::safe( $this->config->component_code_name, 'U' )] = 'COM_' . Placefix::_('COMPONENT'); $placeholders['com_' . $this->config->component_code_name] = 'com_' . Placefix::_('component'); // set the local placeholders $this->placeholders = array_reverse($placeholders, true); // set the current version $this->currentVersion = (int) Version::MAJOR_VERSION; } /** * get the custom code from the local files * * @return void * @since 3.2.0 */ public function run() { // we must first store the current working directory $joomla = getcwd(); foreach ($this->paths->active as $target => $path) { // we are changing the working directory to the component path chdir($path); foreach ($this->fileTypes as $type) { // get a list of files in the current directory tree (only PHP, JS and XML for now) $files = Folder::files('.', $type, true, true); // check if files found if (ArrayHelper::check($files)) { foreach ($files as $file) { // search the file $this->searchFileContent($file, $target); // insert new code $this->insert(100); // update existing custom code $this->update(30); } } } } // change back to Joomla working directory chdir($joomla); // make sure all code is stored $this->insert(); // update existing custom code $this->update(); } /** * search a file for placeholders and store result * * @param string $file The file path to search * * @return array on success * @since 3.2.0 */ protected function searchFileContent(&$file, &$target) { // we add a new search for the GUI CODE Blocks $this->gui->search($file, $this->placeholders, $this->today, $target); // reset each time per file $loadEndFingerPrint = false; $endFingerPrint = []; $fingerPrint = []; $codeBucket = []; $pointer = []; $reading = []; $reader = 0; // reset found Start type $commentType = 0; // make sure we have the path correct (the script file is not in admin path for example) // there may be more... will nead to keep our eye on this... since files could be moved during install $file = str_replace('./', '', (string) $file); # TODO (windows path issues) $path = $file !== 'script.php' ? $target . '/' . $file : $file; // now we go line by line foreach (new \SplFileObject($file) as $lineNumber => $lineContent) { // we must keep last few lines to dynamic find target entry later $fingerPrint[$lineNumber] = trim($lineContent); // load the end fingerprint if ($loadEndFingerPrint) { $endFingerPrint[$lineNumber] = trim($lineContent); } foreach ($this->PKeys as $type => $search) { $i = (int) ($type == 3 || $type == 4) ? 2 : 1; $_type = (int) ($type == 1 || $type == 3) ? 1 : 2; if ($reader === 0 || $reader === $i) { $targetKey = $type; $start = '/***[' . $search . '***/'; $end = '/***[/' . $search . '***/'; $startHTML = ''; $endHTML = ''; // check if the ending placeholder was found if (isset($reading[$targetKey]) && $reading[$targetKey] && ((trim((string) $lineContent) === $end || strpos((string) $lineContent, $end) !== false) || (trim((string) $lineContent) === $endHTML || strpos((string) $lineContent, $endHTML) !== false))) { // trim the placeholder and if there is still data then load it if (isset($endReplace) && ($_line = $this->addLineChecker($endReplace, 2, $lineContent)) !== false) { $codeBucket[$pointer[$targetKey]][] = $_line; } // deactivate the reader $reading[$targetKey] = false; if ($_type == 2) { // deactivate search $reader = 0; } else { // activate fingerPrint for replacement end target $loadEndFingerPrint = true; $backupTargetKey = $targetKey; $backupI = $i; } // all new records we can do a bulk insert if ($i === 1) { // end the bucket info for this code block $this->new[$pointer[$targetKey]][] = $this->db->quote( (int) $lineNumber ); // 'toline' // first reverse engineer this code block $c0de = $this->reverse->engine( implode('', $codeBucket[$pointer[$targetKey]]), $this->placeholders, $target ); $this->new[$pointer[$targetKey]][] = $this->db->quote( base64_encode((string) $c0de) ); // 'code' if ($_type == 2) { // load the last value $this->new[$pointer[$targetKey]][] = $this->db->quote(0); // 'hashendtarget' } } // the record already exist so we must update instead elseif ($i === 2) { // end the bucket info for this code block $this->existing[$pointer[$targetKey]]['fields'][] = $this->db->quoteName('to_line') . ' = ' . $this->db->quote($lineNumber); // first reverse engineer this code block $c0de = $this->reverse->engine( implode('', $codeBucket[$pointer[$targetKey]]), $this->placeholders, $target, $this->existing[$pointer[$targetKey]]['id'] ); $this->existing[$pointer[$targetKey]]['fields'][] = $this->db->quoteName('code') . ' = ' . $this->db->quote(base64_encode((string) $c0de)); if ($_type == 2) { // load the last value $this->existing[$pointer[$targetKey]]['fields'][] = $this->db->quoteName('hashendtarget') . ' = ' . $this->db->quote(0); } } } // check if the endfingerprint is ready to save if (count((array) $endFingerPrint) === 3) { $hashendtarget = '3__' . md5( implode('', $endFingerPrint) ); // all new records we can do a bulk insert if ($i === 1) { // load the last value $this->new[$pointer[$targetKey]][] = $this->db->quote( $hashendtarget ); // 'hashendtarget' } // the record already exist so we must update elseif ($i === 2) { $this->existing[$pointer[$targetKey]]['fields'][] = $this->db->quoteName('hashendtarget') . ' = ' . $this->db->quote($hashendtarget); } // reset the needed values $endFingerPrint = []; $loadEndFingerPrint = false; // deactivate reader (to allow other search) $reader = 0; } // then read in the code if (isset($reading[$targetKey]) && $reading[$targetKey]) { $codeBucket[$pointer[$targetKey]][] = $lineContent; } // see if the custom code line starts now with PHP/JS comment type if ((!isset($reading[$targetKey]) || !$reading[$targetKey]) && (($i === 1 && trim((string) $lineContent) === $start) || strpos((string) $lineContent, $start) !== false)) { $commentType = 1; // PHP/JS type $startReplace = $start; $endReplace = $end; } // see if the custom code line starts now with HTML comment type elseif ((!isset($reading[$targetKey]) || !$reading[$targetKey]) && (($i === 1 && trim((string) $lineContent) === $startHTML) || strpos((string) $lineContent, $startHTML) !== false)) { $commentType = 2; // HTML type $startReplace = $startHTML; $endReplace = $endHTML; } // check if the starting place holder was found if ($commentType > 0) { // if we have all on one line we have a problem (don't load it TODO) if (strpos((string) $lineContent, (string) $endReplace) !== false) { // reset found comment type $commentType = 0; $this->app->enqueueMessage( Text::_('