* Copyright (C) 2024 Frédéric France * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see . */ /** * \file htdocs/core/lib/website2.lib.php * \ingroup website * \brief Library for website module (rare functions not required for execution of website) */ /** * Save content of a page on disk * * @param string $filemaster Full path of filename master.inc.php for website to generate * @return boolean True if OK */ function dolSaveMasterFile($filemaster) { // Now generate the master.inc.php page dol_syslog("We regenerate the master.inc.php file"); dol_delete_file($filemaster); $mastercontent = ''."\n"; $result = file_put_contents($filemaster, $mastercontent); dolChmod($filemaster); return $result; } /** * Save an alias page on disk (A page that include the reference page). * It saves file into the root directory but also into language subdirectory. * * @param string $filealias Full path of filename to generate * @param Website $object Object website * @param WebsitePage $objectpage Object websitepage * @return boolean True if OK * @see dolSavePageContent() */ function dolSavePageAlias($filealias, $object, $objectpage) { // Now create the .tpl file dol_syslog("dolSavePageAlias We regenerate the alias page filealias=".$filealias." and a wrapper into all language subdirectories"); $aliascontent = 'id.'.tpl.php\'; '; $aliascontent .= 'else $res=include $dolibarr_main_data_root.\'/website/\'.$website->ref.\'/page'.$objectpage->id.'.tpl.php\';'."\n"; $aliascontent .= 'if ($res === false) { http_response_code(500); print \'Failed to make include\'; }'."\n"; $aliascontent .= '?>'."\n"; $result = file_put_contents($filealias, $aliascontent); if ($result === false) { dol_syslog("Failed to write file ".$filealias, LOG_WARNING); } dolChmod($filealias); // Save also alias into language subdirectory if it is not a main language if ($objectpage->lang && in_array($objectpage->lang, explode(',', $object->otherlang))) { $dirname = dirname($filealias); $filename = basename($filealias); $filealiassub = $dirname.'/'.$objectpage->lang.'/'.$filename; dol_mkdir($dirname.'/'.$objectpage->lang, DOL_DATA_ROOT); $aliascontent = 'id.'.tpl.php\'; '; $aliascontent .= 'else $res=include $dolibarr_main_data_root.\'/website/\'.$website->ref.\'/page'.$objectpage->id.'.tpl.php\';'."\n"; $aliascontent .= 'if ($res === false) { http_response_code(500); print \'Failed to make include\'; }'."\n"; $aliascontent .= '?>'."\n"; $result = file_put_contents($filealiassub, $aliascontent); if ($result === false) { dol_syslog("Failed to write file ".$filealiassub, LOG_WARNING); } dolChmod($filealiassub); } elseif (empty($objectpage->lang) || !in_array($objectpage->lang, explode(',', $object->otherlang))) { // Save also alias into all language subdirectories if it is a main language if (!getDolGlobalString('WEBSITE_DISABLE_MAIN_LANGUAGE_INTO_LANGSUBDIR') && !empty($object->otherlang)) { $dirname = dirname($filealias); $filename = basename($filealias); foreach (explode(',', $object->otherlang) as $sublang) { // Avoid to erase main alias file if $sublang is empty string if (empty(trim($sublang))) { continue; } $filealiassub = $dirname.'/'.$sublang.'/'.$filename; $aliascontent = 'id.'.tpl.php\'; '; $aliascontent .= 'else $res=include $dolibarr_main_data_root.\'/website/\'.$website->ref.\'/page'.$objectpage->id.'.tpl.php\';'."\n"; $aliascontent .= 'if ($res === false) { http_response_code(500); print \'Failed to make include\'; }'."\n"; $aliascontent .= '?>'."\n"; dol_mkdir($dirname.'/'.$sublang); $result = file_put_contents($filealiassub, $aliascontent); if ($result === false) { dol_syslog("Failed to write file ".$filealiassub, LOG_WARNING); } dolChmod($filealiassub); } } } return ($result ? true : false); } /** * Save content of a page on disk (page name is generally ID_of_page.php). * Page contents are always saved into "root" directory. Only aliases pages saved with dolSavePageAlias() can be in root or language subdir. * * @param string $filetpl Full path of filename to generate * @param Website $object Object website * @param WebsitePage $objectpage Object websitepage * @param int $backupold 1=Make a backup of old page * @return boolean True if OK * @see dolSavePageAlias() */ function dolSavePageContent($filetpl, Website $object, WebsitePage $objectpage, $backupold = 0) { global $conf, $db; // Now create the .tpl file (duplicate code with actions updatesource or updatecontent but we need this to save new header) dol_syslog("dolSavePageContent We regenerate the tpl page filetpl=".$filetpl); include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; if (dol_is_file($filetpl)) { if ($backupold) { $result = archiveOrBackupFile($filetpl); if (! $result) { return false; } } else { dol_delete_file($filetpl); } } $shortlangcode = ''; if ($objectpage->lang) { $shortlangcode = substr($objectpage->lang, 0, 2); // en_US or en-US -> en } if (empty($shortlangcode)) { // Take the language of website $shortlangcode = substr($object->lang, 0, 2); // en_US or en-US -> en } if (!empty($objectpage->type_container) && in_array($objectpage->type_container, array('library', 'service'))) { $originalcontentonly = 1; } $tplcontent = ''; if (!isset($originalcontentonly)) { // If we want to generate a page with some code to manage PHP content $tplcontent .= "disable_waf != 'all') { if (strpos($objectpage->disable_waf, 'NOSCANAUDIOFORINJECTION') !== false) { $tplcontent .= ' define(\'NOSCANAUDIOFORINJECTION\', 1);'."\n"; } if (strpos($objectpage->disable_waf, 'NOSCANIFRAMEFORINJECTION') !== false) { $tplcontent .= ' define(\'NOSCANIFRAMEFORINJECTION\', 1);'."\n"; } if (strpos($objectpage->disable_waf, 'NOSCANOBJECTFORINJECTION') !== false) { $tplcontent .= ' define(\'NOSCANOBJECTFORINJECTION\', 1);'."\n"; } $tplcontent .= ' require_once DOL_DOCUMENT_ROOT.\'/waf.inc.php\';'."\n"; } $tplcontent .= "}\n"; $tplcontent .= "require_once DOL_DOCUMENT_ROOT.'/core/lib/website.lib.php';\n"; $tplcontent .= "require_once DOL_DOCUMENT_ROOT.'/core/website.inc.php';\n"; if (in_array($objectpage->type_container, array('page', 'blogpost', 'service'))) { $tplcontent .= 'dol_syslog("--- Prepare content of page '.((int) $objectpage->id).' - '.$objectpage->pageurl.'");'."\n"; } $tplcontent .= "ob_start();\n"; $tplcontent .= "try {\n"; $tplcontent .= "// END PHP ?>\n"; if (getDolGlobalString('WEBSITE_FORCE_DOCTYPE_HTML5')) { $tplcontent .= "\n"; } // If a language was forced on page, we use it, else we use the lang of visitor else the lang of web site $tplcontent .= 'lang ? ' lang="'.substr($objectpage->lang, 0, 2).'"' : 'shortlang ? \' lang="\'.$weblangs->shortlang.\'"\' : \'\' ?>').'>'."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''.dol_string_nohtmltag($objectpage->title, 1, 'UTF-8').''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; // Add favicon if not already done in htmlheader $htmldeaderindestdir = dol_sanitizePathName($conf->website->dir_temp.'/'.$object->ref.'/containers/htmlheader.html'); $htmlheader = file_get_contents($htmldeaderindestdir); if (in_array($objectpage->type_container, array('page', 'blogpost')) && !preg_match('/'.preg_quote('rel="icon"', '/').'/', $htmlheader)) { $tplcontent .= ''."\n"; } $listofaltlang = $object->otherlang; // Note: $object is website, $objectpage is website page if ($object->virtualhost) { // Add the link of the canonical reference $canonicalurladdidlang = ''; if ($objectpage->lang) { // A language is forced on the page, it means we may have other language files with hard links into properties of page $canonicalurl = (($objectpage->id == $object->fk_default_home) ? '/' : (($shortlangcode != substr($object->lang, 0, 2) ? '/'.$shortlangcode : '').'/'.$objectpage->pageurl.'.php')); } else { // No language forced, it means the canonical is the one with params making url unique $canonicalurl = '/'.$objectpage->pageurl.'.php'; if ($object->lang && $listofaltlang) { // Add parameter ID required to be unique/canonical $canonicalurladdidlang = '?__SEO_CANONICAL_URL_PARAMS__'; $canonicalurladdidlang .= '&l=__SEO_CANONICAL_LANG__'; } else { // Add parameter ID required to be unique/canonical $canonicalurladdidlang = '?__SEO_CANONICAL_URL_PARAMS__'; } } $tplcontent .= ''."\n"; // Add the link of alternate translation reference if ($listofaltlang) { // If website has other languages to support if ($objectpage->lang) { // A language is forced on the page, it means we may have other language files with hard links into properties of page // Add page "translation of" $translationof = $objectpage->fk_page; if ($translationof) { $tmppage = new WebsitePage($db); $tmppage->fetch($translationof); if ($tmppage->id > 0) { $tmpshortlangcode = ''; if ($tmppage->lang) { $tmpshortlangcode = preg_replace('/[_-].*$/', '', $tmppage->lang); // en_US or en-US -> en } if (empty($tmpshortlangcode)) { $tmpshortlangcode = preg_replace('/[_-].*$/', '', $object->lang); // en_US or en-US -> en } if ($tmpshortlangcode != $shortlangcode) { $tplcontent .= ''."\n"; } } } // Add "has translation pages" $sql = "SELECT rowid as id, lang, pageurl from ".MAIN_DB_PREFIX.'website_page where fk_page IN ('.$db->sanitize($objectpage->id.($translationof ? ", ".$translationof : '')).")"; $resql = $db->query($sql); if ($resql) { $num_rows = $db->num_rows($resql); if ($num_rows > 0) { while ($obj = $db->fetch_object($resql)) { $tmpshortlangcode = ''; if ($obj->lang) { $tmpshortlangcode = preg_replace('/[_-].*$/', '', $obj->lang); // en_US or en-US -> en } if ($tmpshortlangcode != $shortlangcode) { $tplcontent .= ''."\n"; } } } } else { dol_print_error($db); } // Add myself $tplcontent .= 'fk_default_home == $objectpage->id) ? '/' : (($shortlangcode != substr($object->lang, 0, 2)) ? '/'.$shortlangcode : '')).'/'.$objectpage->pageurl.'.php") { ?>'."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; } else { // No language forced, it means the canonical is the one withparams making url unique $canonicalurl = '/'.$objectpage->pageurl.'.php'; $arrayofaltlang = explode(',', $listofaltlang); foreach ($arrayofaltlang as $altlang) { // Add parameter ID required to be unique/canonical $canonicalurladdidlang = '?__SEO_CANONICAL_URL_PARAMS__'; $canonicalurladdidlang .= '&l='.$altlang; $tplcontent .= ''."\n"; } $tmpshortlangcode = preg_replace('/[_-].*$/', '', $object->lang); // en_US or en-US -> en $canonicalurladdidlang = '?__SEO_CANONICAL_URL_PARAMS__'; $canonicalurladdidlang .= '&l='.$tmpshortlangcode; $tplcontent .= ''."\n"; } } } // Add manifest.json. Do we have to add it only on home page ? $tplcontent .= 'use_manifest) { print \'\'."\n"; } ?>'."\n"; // Add HTML headers (must be before the Add of the common CSS and js). The common js may content javascript using jquery or a framework loaded by the HTML header. $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= preg_replace('/<\/?html>/ims', '', $objectpage->htmlheader)."\n"; // Add css $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; // Add js $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= "\n"; // Page content $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; // Import necessary environment for the config page if ($objectpage->type_container == 'setup') { $content = ''; $content .= ''."\n"; $tplcontent .= $content."\n"; } $tplcontent .= $objectpage->content."\n"; // Add logic to handle view and actions for managing parameters in the special config page if ($objectpage->type_container == 'setup') { $content = '
'."\n"; $content .= 'trans(\'SetupAndProperties\'), \'\', \'title_setup\');' . "\n"; $content .= '' . "\n"; $content .= 'if (!empty($message)) {' . "\n"; $content .= ' print $message;' . "\n"; $content .= '}' . "\n"; $content .= '' . "\n"; $content .= 'if (!empty($formSetup->items)) {' . "\n"; $content .= ' $html = \'\';' . "\n"; $content .= '' . "\n"; $content .= ' $html .= \'
\';' . "\n"; $content .= ' // Generate hidden values from $formSetup->formHiddenInputs' . "\n"; $content .= ' if (!empty($formSetup->formHiddenInputs) && is_array($formSetup->formHiddenInputs)) {' . "\n"; $content .= ' foreach ($formSetup->formHiddenInputs as $hiddenKey => $hiddenValue) {' . "\n"; $content .= ' $html .= \'\';' . "\n"; $content .= ' }' . "\n"; $content .= ' }' . "\n"; $content .= '' . "\n"; $content .= ' // Generate output table' . "\n"; $content .= ' $html .= $formSetup->generateTableOutput(true);' . "\n"; $content .= '' . "\n"; $content .= ' // Submit button' . "\n"; $content .= ' $html .= \'\';' . "\n"; $content .= ' $html .= \'\';' . "\n"; $content .= ' $html .= \'
\';' . "\n"; $content .= ' $html .= \'
\';' . "\n"; $content .= ' $html .= \'\';' . "\n"; $content .= ' $html .= \'
\';' . "\n"; $content .= ' $html .= \'
\';' . "\n"; $content .= '' . "\n"; $content .= ' print $html;' . "\n"; $content .= '}' . "\n"; $content .= '?>' . "\n"; $content .= '
' . "\n"; $tplcontent .= $content."\n"; } $tplcontent .= ''."\n"; $tplcontent .= ''."\n"; $tplcontent .= 'getMessage(); }'."\n"; $tplcontent .= '$tmp = ob_get_contents(); ob_end_clean();'."\n"; // replace with ob_get_clean ? $tplcontent .= "// Now fix the content for SEO or multilanguage\n"; // Old method for custom SEO if (strpos($objectpage->content, '$__PAGE__KEYWORDS__') !== false) { $tplcontent .= '$tmp = preg_replace("/^/ms", "", $tmp);'."\n"; } if (strpos($objectpage->content, '$__PAGE__TITLE__') !== false) { $tplcontent .= '$tmp = preg_replace("/^.*?<\/title>/ms", "<title>" . dolPrintHTMLForAttribute($__PAGE__TITLE__ ?? "", 1) . "", $tmp);'."\n"; $tplcontent .= '$tmp = preg_replace("/^/ms", "", $tmp);'."\n"; } if (strpos($objectpage->content, '$__PAGE__DESC__') !== false) { $tplcontent .= '$tmp = preg_replace("/^/ms", "", $tmp);'."\n"; } // New method for custom SEO if (strpos($objectpage->content, 'define("__SEO_PAGE_LANG__"') !== false) { $tmpshortlangcode = preg_replace('/[_-].*$/', '', $object->lang); // en_US or en-US -> en $tplcontent .= '$tmp = preg_replace("/^shortlang) ? "'.$tmpshortlangcode.'" : $weblangs->shortlang), 1) . "\"", $tmp);'."\n"; } if (strpos($objectpage->content, 'define("__SEO_PAGE_KEYWORDS__"') !== false) { $tplcontent .= '$tmp = preg_replace("/^/ms", "", $tmp);'."\n"; } if (strpos($objectpage->content, 'define("__SEO_PAGE_TITLE__"') !== false) { $tplcontent .= '$tmp = preg_replace("/^.*?<\/title>/ms", "<title>" . dolPrintHTMLForAttribute(defined("__SEO_PAGE_TITLE__") ? constant("__SEO_PAGE_TITLE__") : "", 1) . "", $tmp);'."\n"; $tplcontent .= '$tmp = preg_replace("/^/ms", "", $tmp);'."\n"; } if (strpos($objectpage->content, 'define("__SEO_PAGE_DESC__"') !== false) { $tplcontent .= '$tmp = preg_replace("/^/ms", "", $tmp);'."\n"; } if (empty($objectpage->lang)) { // We may need to use param into the canonical url $tplcontent .= 'defined("__SEO_CANONICAL_URL_PARAMS__") ? ($tmp = preg_replace("/__SEO_CANONICAL_URL_PARAMS__/", dolPrintHTMLForAttributeUrl(constant("__SEO_CANONICAL_URL_PARAMS__")), $tmp)) : ($tmp = preg_replace("/\?__SEO_CANONICAL_URL_PARAMS__\"/", "\"", preg_replace("/\?__SEO_CANONICAL_URL_PARAMS__&/", "?", $tmp)));'."\n"; $tmpshortlangcode = preg_replace('/[_-].*$/', '', $object->lang); // en_US or en-US -> en $tplcontent .= '$tmp = preg_replace("/__SEO_CANONICAL_LANG__/", (defined("__SEO_PAGE_LANG__") ? preg_replace(\'/\[_-\].*$/\', "", constant("__SEO_PAGE_LANG__")) : (empty($weblangs->shortlang) ? "'.$tmpshortlangcode.'" : $weblangs->shortlang)), $tmp);'."\n"; } // Let modules rewrite the final page content (e.g. canonical/hreflang links, when a module // implements its own URL rewriting scheme that core has no knowledge of). Runs on every // request (not only at page compile time), so it applies no matter which code path last // recompiled this page (website editor, inline AJAX edit, a module's own template reset...). $tplcontent .= "// Let modules rewrite the final page content (hook)\n"; $tplcontent .= 'global $hookmanager;'."\n"; $tplcontent .= 'if (is_object($hookmanager)) {'."\n"; $tplcontent .= ' $hookmanager->initHooks(array(\'websitepagecontent\'));'."\n"; $tplcontent .= ' $parameters = array(\'tmp\' => &$tmp);'."\n"; $tplcontent .= ' $hookmanager->executeHooks(\'printWebsitePageContent\', $parameters, $websitepage);'."\n"; $tplcontent .= '}'."\n"; $tplcontent .= "// Now output the generated page content\n"; $tplcontent .= 'dolWebsiteOutput($tmp, "html", '.((int) $objectpage->id).'); dolWebsiteIncrementCounter('.((int) $object->id).', "'.$objectpage->type_container.'", '.((int) $objectpage->id).');'."\n"; $tplcontent .= "// END PHP ?>\n"; } else { $tplcontent .= "disable_waf != 'all') { if (strpos($objectpage->disable_waf, 'NOSCANAUDIOFORINJECTION') !== false) { $tplcontent .= ' define(\'NOSCANAUDIOFORINJECTION\', 1);'."\n"; } if (strpos($objectpage->disable_waf, 'NOSCANIFRAMEFORINJECTION') !== false) { $tplcontent .= ' define(\'NOSCANIFRAMEFORINJECTION\', 1);'."\n"; } if (strpos($objectpage->disable_waf, 'NOSCANOBJECTFORINJECTION') !== false) { $tplcontent .= ' define(\'NOSCANOBJECTFORINJECTION\', 1);'."\n"; } $tplcontent .= ' require_once DOL_DOCUMENT_ROOT.\'/waf.inc.php\';'."\n"; } $tplcontent .= "}\n"; $tplcontent .= "require_once DOL_DOCUMENT_ROOT.'/core/lib/website.lib.php';\n"; $tplcontent .= "require_once DOL_DOCUMENT_ROOT.'/core/website.inc.php';\n"; if (in_array($objectpage->type_container, array('page', 'blogpost', 'service'))) { $tplcontent .= 'dol_syslog("--- Prepare content of page '.((int) $objectpage->id).' - '.$objectpage->pageurl.'");'."\n"; } $tplcontent .= "// END PHP ?>\n"; $tplcontent .= $objectpage->content; } //var_dump($filetpl);exit; $result = file_put_contents($filetpl, $tplcontent); dolChmod($filetpl); return $result; } /** * Save content of the index.php and/or the wrapper.php page * * @param string $pathofwebsite Path of website root * @param string $fileindex Full path of file index.php * @param string $filetpl File tpl the index.php page redirect to (used only if $fileindex is provided) * @param string $filewrapper Full path of file wrapper.php * @param Website $object Object website * @return boolean True if OK */ function dolSaveIndexPage($pathofwebsite, $fileindex, $filetpl, $filewrapper, $object = null) { global $db; $result1 = false; $result2 = false; dol_mkdir($pathofwebsite); if ($fileindex) { dol_delete_file($fileindex); $indexcontent = ''."\n"; $result1 = file_put_contents($fileindex, $indexcontent); dolChmod($fileindex); if (is_object($object) && $object->fk_default_home > 0) { $objectpage = new WebsitePage($db); $objectpage->fetch($object->fk_default_home); // Create a version for sublanguages if (empty($objectpage->lang) || !in_array($objectpage->lang, explode(',', $object->otherlang))) { if (!getDolGlobalString('WEBSITE_DISABLE_MAIN_LANGUAGE_INTO_LANGSUBDIR') && is_object($object) && !empty($object->otherlang)) { $dirname = dirname($fileindex); foreach (explode(',', $object->otherlang) as $sublang) { // Avoid to erase main alias file if $sublang is empty string if (empty(trim($sublang))) { continue; } $fileindexsub = $dirname.'/'.$sublang.'/index.php'; // Same indexcontent than previously but with ../ instead of ./ for master and tpl file include/require_once. $relpath = '..'; $indexcontent = ''."\n"; $result = file_put_contents($fileindexsub, $indexcontent); if ($result === false) { dol_syslog("Failed to write file ".$fileindexsub, LOG_WARNING); } dolChmod($fileindexsub); } } } } } else { $result1 = true; } if ($filewrapper) { dol_delete_file($filewrapper); $wrappercontent = file_get_contents(DOL_DOCUMENT_ROOT.'/website/samples/wrapper.php'); $result2 = file_put_contents($filewrapper, $wrappercontent); dolChmod($filewrapper); } else { $result2 = true; } return ($result1 && $result2); } /** * Save content of a page on disk * * @param string $filehtmlheader Full path of filename to generate * @param string $htmlheadercontent Content of file * @return boolean True if OK */ function dolSaveHtmlHeader($filehtmlheader, $htmlheadercontent) { global $pathofwebsite; dol_syslog("Save html header into ".$filehtmlheader); dol_mkdir($pathofwebsite); $result = file_put_contents($filehtmlheader, $htmlheadercontent); dolChmod($filehtmlheader); return $result; } /** * Save content of a page on disk * * @param string $filecss Full path of filename to generate * @param string $csscontent Content of file * @return boolean True if OK */ function dolSaveCssFile($filecss, $csscontent) { global $pathofwebsite; dol_syslog("Save css file into ".$filecss); dol_mkdir($pathofwebsite); $result = file_put_contents($filecss, $csscontent); dolChmod($filecss); return $result; } /** * Save content of a page on disk. For example into documents/website/mywebsite/javascript.js.php file. * * @param string $filejs Full path of filename to generate * @param string $jscontent Content of file * @return boolean True if OK */ function dolSaveJsFile($filejs, $jscontent) { global $pathofwebsite; dol_syslog("Save js file into ".$filejs); dol_mkdir($pathofwebsite); $result = file_put_contents($filejs, $jscontent); dolChmod($filejs); return $result; } /** * Save content of a page on disk * * @param string $filerobot Full path of filename to generate * @param string $robotcontent Content of file * @return boolean True if OK */ function dolSaveRobotFile($filerobot, $robotcontent) { global $pathofwebsite; dol_syslog("Save robot file into ".$filerobot); dol_mkdir($pathofwebsite); $result = file_put_contents($filerobot, $robotcontent); dolChmod($filerobot); return $result; } /** * Save content of a page on disk * * @param string $filehtaccess Full path of filename to generate * @param string $htaccess Content of file * @return boolean True if OK */ function dolSaveHtaccessFile($filehtaccess, $htaccess) { global $pathofwebsite; dol_syslog("Save htaccess file into ".$filehtaccess); dol_mkdir($pathofwebsite); $result = file_put_contents($filehtaccess, $htaccess); dolChmod($filehtaccess); return $result; } /** * Save content of a page on disk * * @param string $file Full path of filename to generate * @param string $content Content of file * @return boolean True if OK */ function dolSaveManifestJson($file, $content) { global $pathofwebsite; dol_syslog("Save manifest.js.php file into ".$file); dol_mkdir($pathofwebsite); $result = file_put_contents($file, $content); dolChmod($file); return $result; } /** * Save content of a page on disk * * @param string $file Full path of filename to generate * @param string $content Content of file * @return boolean True if OK */ function dolSaveReadme($file, $content) { global $pathofwebsite; dol_syslog("Save README.md file into ".$file); dol_mkdir($pathofwebsite); $result = file_put_contents($file, $content); dolChmod($file); return $result; } /** * Save content of a page on disk * * @param string $file Full path of filename to generate * @param string $content Content of file * @return boolean True if OK */ function dolSaveLicense($file, $content) { global $pathofwebsite; dol_syslog("Save LICENSE file into ".$file); dol_mkdir($pathofwebsite); $result = file_put_contents($file, $content); dolChmod($file); return $result; } /** * Show list of themes. Show all thumbs of themes/skins * * @param Website $website Object website to load the template into * @param int $refresh 1 = Recopy templates into sources not into documents * @return void */ function showWebsiteTemplates(Website $website, int $refresh) { global $conf, $langs, $form, $user; // We want only one directory for dir of website templates. If an external module need to provide a template, the template must be copied into this directory // when module is enabled. $dirthemes = array('/doctemplates/websites'); $warningtoshow = ''; $arrayoftemplatesfound = array(); if (count($dirthemes)) { $i = 0; // Scan dir to get all deployed qualified templates foreach ($dirthemes as $dir) { $dirtheme = DOL_DATA_ROOT.$dir; if (is_dir($dirtheme)) { $handle = opendir($dirtheme); if (is_resource($handle)) { while (($subdir = readdir($handle)) !== false) { // Scan files of directory //var_dump($dirtheme.'/'.$subdir); if (dol_is_file($dirtheme."/".$subdir) && substr($subdir, 0, 1) != '.' && substr($subdir, 0, 3) != 'CVS' && preg_match('/\.zip$/i', $subdir)) { $subdirwithoutzip = preg_replace('/\.zip$/i', '', $subdir); $subdirwithoutzipwithoutver = preg_replace('/(_exp|_dev)$/i', '', $subdirwithoutzip); // Disable not stable themes (dir ends with _exp or _dev) if (getDolGlobalInt('MAIN_FEATURES_LEVEL') < 2 && preg_match('/_dev$/i', $subdirwithoutzip)) { continue; } if (getDolGlobalInt('MAIN_FEATURES_LEVEL') < 1 && preg_match('/_exp$/i', $subdirwithoutzip)) { continue; } $arrayoftemplatesfound[$subdirwithoutzip] = array('id' => $subdirwithoutzip); $i++; } } } } } // Now test if we found template available into source not copied into documents $arrayofsourcetemplates = dol_dir_list(DOL_DOCUMENT_ROOT.'/install/doctemplates/websites', 'directories', 0, 'website_.*$'); $arrayofsourcetemplatesnotdeployed = array(); foreach ($arrayofsourcetemplates as $val) { // Disable not stable themes (dir ends with _exp or _dev) if (getDolGlobalInt('MAIN_FEATURES_LEVEL') < 2 && preg_match('/_dev$/i', $val['relativename'])) { continue; } if (getDolGlobalInt('MAIN_FEATURES_LEVEL') < 1 && preg_match('/_exp$/i', $val['relativename'])) { continue; } if (empty($arrayoftemplatesfound[$val['relativename']])) { // We found a template into sources that is not into documents if ($refresh) { // We copy it $src = DOL_DOCUMENT_ROOT.'/install/doctemplates/websites/'.$val['name']; $dest = DOL_DATA_ROOT.'/doctemplates/websites/'.$val['name']; dol_delete_file($dest.'.zip'); // Compress it global $errormsg; // Used by dol_compress_dir $errormsg = ''; $result = dol_compress_dir($src, $dest.'.zip', 'zip'); if ($result < 0) { dol_syslog("Error in compress of dir ".$src, LOG_ERR); } $srcfile = DOL_DOCUMENT_ROOT.'/install/doctemplates/websites/'.preg_replace('/(_exp|_dev)$/', '', $val['name']).'.jpg'; $destfile = DOL_DATA_ROOT.'/doctemplates/websites/'.preg_replace('/(_exp|_dev)$/', '', $val['name']).'.jpg'; dol_copy($srcfile, $destfile); } else { $arrayofsourcetemplatesnotdeployed[$val['relativename']] = $val; } } } if (count($arrayofsourcetemplatesnotdeployed)) { $warningtoshow = img_picto($langs->trans("WarningTemplatesFoundNotDeployedClickRefresh").': '.implode(', ', array_keys($arrayofsourcetemplatesnotdeployed)).'. '.$langs->trans("WarningTemplatesFoundNotDeployedClickRefresh2"), 'warning', 'class="valignmiddle paddingright"'); } } $colspan = 2; $importButtonIsDisabled = 0; global $dolibarr_website_allow_custom_php; if (!empty($dolibarr_website_allow_custom_php) && $dolibarr_website_allow_custom_php == 1) { $notdisabledsystemfunction = ''; $systemfunctions = array("exec", "passthru", "shell_exec", "system", "popen", "proc_open"); foreach ($systemfunctions as $systemfunction) { // @phpstan-ignore-next-line if (function_exists($systemfunction)) { $notdisabledsystemfunction .= ($notdisabledsystemfunction ? ', ' : '').$systemfunction; } } if ($notdisabledsystemfunction) { print '
'; print $langs->trans("ImportOfWebsiteTemplateIncludingPHPIsAllowedIf", 'warning'); print '
'; $importButtonIsDisabled = 1; } } if (empty($dolibarr_website_allow_custom_php)) { print '
'; print $langs->trans("ImportOfWebsiteTemplateIncludingPHPIsDisabled", 'warning'); print '
'; $importButtonIsDisabled = 1; } print ''."\n"; print ''; // Title print ''; print ''; print ''; print '
'; print $form->textwithpicto($langs->trans("Templates"), $langs->trans("ThemeDir").' : '.implode(", ", $dirthemes)); print ' '; print ''; print $warningtoshow; print img_picto($langs->trans("Refresh"), 'refresh', 'class="valignmiddle"'); print ''; print ''; $url = 'https://www.dolistore.com/index.php?cat=84'; print ''; print img_picto('', 'globe', 'class="pictofixedwidth"').$langs->trans('DownloadMoreSkins'); print ''; print '
'; print '
'; if (count($dirthemes)) { $i = 0; foreach ($dirthemes as $dir) { $dirtheme = DOL_DATA_ROOT.$dir; if (is_dir($dirtheme)) { $handle = opendir($dirtheme); if (is_resource($handle)) { while (($subdir = readdir($handle)) !== false) { // Scan files of directory //var_dump($dirtheme.'/'.$subdir); if (dol_is_file($dirtheme."/".$subdir) && substr($subdir, 0, 1) != '.' && substr($subdir, 0, 3) != 'CVS' && preg_match('/\.zip$/i', $subdir)) { $subdirwithoutzip = preg_replace('/\.zip$/i', '', $subdir); $subdirwithoutzipwithoutver = preg_replace('/(_exp|_dev)$/i', '', $subdirwithoutzip); // Disable not stable themes (dir ends with _exp or _dev) if (getDolGlobalInt('MAIN_FEATURES_LEVEL') < 2 && preg_match('/_dev$/i', $subdirwithoutzip)) { continue; } if (getDolGlobalInt('MAIN_FEATURES_LEVEL') < 1 && preg_match('/_exp$/i', $subdirwithoutzip)) { continue; } $arrayoftemplatesfound[$subdirwithoutzip] = array('id' => $subdirwithoutzip); print '
'; $templatedir = $dirtheme."/".$subdir; $file = $dirtheme."/".$subdirwithoutzipwithoutver.".jpg"; $url = DOL_URL_ROOT.'/viewimage.php?modulepart=doctemplateswebsite&file='.$subdirwithoutzipwithoutver.".jpg"; if (!file_exists($file)) { $url = DOL_URL_ROOT.'/public/theme/common/nophoto.png'; } $originalimgfile = basename($file); $entity = $conf->entity; $modulepart = 'doctemplateswebsite'; $cache = ''; $title = $file; $ret = ''; $urladvanced = getAdvancedPreviewUrl($modulepart, $originalimgfile, 1, '&entity='.$entity); if (!empty($urladvanced)) { $ret .= ''; } else { $ret .= ''; } print $ret; print ''.$title.''; print ''; print '
'; print $subdir; print '
'; print ''.dol_print_size(dol_filesize($dirtheme."/".$subdir), 1, 1).' - '.dol_print_date(dol_filemtime($templatedir), 'dayhour', 'tzuserrel').''; if ($user->hasRight('website', 'delete')) { print ' ref).'&templateuserfile='.urlencode($subdir).'">'.img_picto('', 'delete').''; } print '
ref).'&templateuserfile='.urlencode($subdir).'" class="button'.($importButtonIsDisabled ? ' disabled' : '').'">'.$langs->trans("Load").''; print '
'; $i++; } } print '
'; print '
'; print '
'; print '
'; print '
'; } } } } else { print ''.$langs->trans("None").''; } print '
'; print '
'; } /** * Check that the new string $phpfullcodestring contains only php code (including importWebSite() via dolKeepOnlyPhpCode() or when editing a page. * * @param string $phpfullcodestringold PHP old string (before the change). For example "" * @param string $phpfullcodestring PHP new string. For example "" * @return int Error or not * @see dolKeepOnlyPhpCode(), dol_eval() to see sanitizing rules that should be very close. */ function checkPHPCode(&$phpfullcodestringold, &$phpfullcodestring) { global $langs, $user; $error = 0; if (empty($phpfullcodestringold) && empty($phpfullcodestring)) { return 0; } // First check permission if ($phpfullcodestringold != $phpfullcodestring) { global $dolibarr_website_allow_custom_php; if (empty($dolibarr_website_allow_custom_php)) { // Case of $dolibarr_website_allow_custom_php = 0 $error++; setEventMessages($langs->trans("NotAllowedToAddDynamicContentDisabledGlobaly", 'dolibarr_website_allow_custom_php'), null, 'errors'); } elseif ($dolibarr_website_allow_custom_php == 1) { // Case of $dolibarr_website_allow_custom_php = 1 $notdisabledsystemfunction = ''; $systemfunctions = array("exec", "passthru", "shell_exec", "system", "popen", "proc_open"); foreach ($systemfunctions as $systemfunction) { // @phpstan-ignore-next-line if (function_exists($systemfunction)) { $notdisabledsystemfunction .= ($notdisabledsystemfunction ? ', ' : '').$systemfunction; } } if ($notdisabledsystemfunction) { $error++; $langs->load("errors"); setEventMessages($langs->trans("ErrorDynamicPHPContentNotAllowed", 'dolibarr_website_allow_custom_php'), null, 'errors'); } } if (!$error && !$user->hasRight('website', 'writephp')) { $error++; setEventMessages($langs->trans("NotAllowedToAddDynamicContent"), null, 'errors'); } } $phpfullcodestringnew = $phpfullcodestring; // Then check forbidden commands if (!$error) { if (getDolGlobalString("WEBSITE_DISALLOW_DOLLAR_UNDERSCORE")) { $phpfullcodestring = preg_replace('/\$_COOKIE\[/', '__DOLLARCOOKIE__', $phpfullcodestring); $phpfullcodestring = preg_replace('/\$_FILES\[/', '__DOLLARFILES__', $phpfullcodestring); $phpfullcodestring = preg_replace('/\$_SESSION\[/', '__DOLLARSESSION__', $phpfullcodestring); $forbiddenphpstrings = array('$$', '$_', '}['); } else { $forbiddenphpstrings = array('$$', '}['); } //$forbiddenphpstrings = array_merge($forbiddenphpstrings, array('_ENV', '_FILES', '_SESSION', '_COOKIE', '_GET', '_POST', '_REQUEST', 'ReflectionFunction')); $forbiddenphpstrings = array_merge($forbiddenphpstrings, array('_ENV', 'ReflectionFunction')); $forbiddenphpfunctions = array(); //$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("base64"."_"."decode", "rawurl"."decode", "url"."decode", "str"."_rot13", "hex"."2bin")); // name of forbidden functions are split to avoid false positive $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("override_function", "session_id", "session_create_id", "session_regenerate_id")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("get_defined_functions", "get_defined_vars", "get_defined_constants", "get_declared_classes")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("call_user_func", "call_user_func_array")); //$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("require", "include", "require_once", "include_once")); if (!getDolGlobalString('WEBSITE_PHP_ALLOW_EXEC')) { // If experimental option is not on, we TRY to disallow functions to execute commands // WARNING: This is not reliable. If you don't want user to run code on the server, just NEVER give him permission to execute dynamic PHP in website module (or never enable website module). // The following list shares a lot of content with the whitelist guard in dol_eval() function. $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("ob_start")); // Functions with callable parameters $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("call_user_func", "call_user_func_array")); //$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("array_all", "array_any", "array_diff_ukey", "array_filter", "array_find", "array_find_key", "array_map", "array_reduce", "array_intersect_uassoc", "array_intersect_ukey", "array_walk", "array_walk_recursive")); //$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("usort", "uasort", "uksort")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("preg_replace_callback", "preg_replace_callback_array", "header_register_callback")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("error_log", "set_error_handler", "set_exception_handler", "libxml_set_external_entity_loader", "register_shutdown_function", "register_tick_function", "unregister_tick_function")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("spl_autoload_register", "spl_autoload_unregister", "iterator_apply", "session_set_save_handler")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("forward_static_call", "forward_static_call_array", "register_postsend_function")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("readline_completion_function", "readline_callback_handler_install")); // Exec functions $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("exec", "passthru", "shell_exec", "system", "proc_open", "popen")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("pcntl_alarm", "pcntl_exec", "pcntl_fork", "pcntl_waitpid", "pcntl_wait", "pcntl_wifexited", "pcntl_wifstopped", "pcntl_wifsignaled", "pcntl_wifcontinued", "pcntl_wexitstatus", "pcntl_wtermsig", "pcntl_wstopsig", "pcntl_signal")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("pcntl_signal_get_handler", "pcntl_signal_dispatch", "pcntl_get_last_error", "pcntl_strerror", "pcntl_sigprocmask", "pcntl_sigwaitinfo", "pcntl_sigtimedwait", "pcntl_getpriority", "pcntl_async_signals", "pcntl_unshare", )); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("putenv", "dl", "apache_child_terminate", "apache_setenv")); $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("dol_eval", "dol_eval_new", "dol_eval_standard", "executeCLI", "verifCond", "dolEncrypt", "dolDecrypt")); // native dolibarr functions $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("eval", "create_function", "assert", "mb_ereg_replace")); // function with eval capabilities // Include functions //$forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("require", "include", "require_once", "include_once")); } if (!getDolGlobalString('WEBSITE_PHP_ALLOW_WRITE')) { // If option is not on, we disallow functions to write files $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("dol_compress_dir", "dol_decode", "dol_dir_list", "dol_dir_list_in_database", "dol_delete_file", "dol_delete_dir", "dol_delete_dir_recursive", "dol_copy", "archiveOrBackupFile")); // more dolibarr functions $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("chdir", "dir", "fopen", "file", "file_exists", "file_get_contents", "file_put_contents", "fget", "fgetc", "fgetcsv", "flock", "fputs", "fputscsv", "fpassthru", "fscanf", "fseek", "fwrite", "is_file", "is_dir", "is_link", "mkdir", "opendir", "rmdir", "scandir", "symlink", "touch", "unlink", "umask")); } if (getDolGlobalString('WEBSITE_PHP_DISALLOW_READ')) { // If option is not on, we disallow functions to read files $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("dol_decode")); // more dolibarr functions $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("file", "fopen", "file_get_contents", "fgets", "fgetscsv", "fgetss", "fread")); } $forbiddenphpmethods = array('invoke', 'invokeArgs'); // Method of ReflectionFunction to execute a function foreach ($forbiddenphpstrings as $forbiddenphpstring) { if (preg_match('/'.preg_quote($forbiddenphpstring, '/').'/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", $forbiddenphpstring), null, 'errors'); break; } } /* replaced with next block foreach ($forbiddenphpfunctions as $forbiddenphpfunction) { // Check "function(" but also "'function'(" and "function (" if (preg_match('/'.$forbiddenphpfunction.'[\'\s]*\(/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", $forbiddenphpfunction), null, 'errors'); break; } }*/ foreach ($forbiddenphpfunctions as $forbiddenphpfunction) { // Check "function" whatever is "function(" or "function'(" or "function (" or "function" $reg = array(); if (preg_match('/\b'.$forbiddenphpfunction.'(=| class=|\b)/ims', $phpfullcodestring, $reg)) { if ($reg[1] != '=' && $reg[1] != ' class=') { // So we may accept string ...&file=... even if 'file' is in forbiddenphpfunction. And we accept content with '
trans("DynamicPHPCodeContainsAForbiddenInstruction", $forbiddenphpfunction)." :
\n".$phpfullcodestring, null, 'errors'); setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", $forbiddenphpfunction), null, 'errors'); break; } } } foreach ($forbiddenphpmethods as $forbiddenphpmethod) { if (preg_match('/->'.$forbiddenphpmethod.'/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", $forbiddenphpmethod), null, 'errors'); break; } } } // This char can be used to execute RCE for example by using echo `ls` if (!$error) { $forbiddenphpchars = array(); if (!getDolGlobalString('WEBSITE_PHP_ALLOW_DANGEROUS_CHARS')) { // If option is not on, we disallow functions to execute commands $forbiddenphpchars = array("`"); } foreach ($forbiddenphpchars as $forbiddenphpchar) { if (preg_match('/'.$forbiddenphpchar.'/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", $forbiddenphpchar), null, 'errors'); break; } } } // Deny code to call a function obfuscated with comment, like "exec/*...*/ ('ls')"; if (!$error) { if (preg_match('/\*\/\s*\(/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", "exec/*...*/ ('ls')"), null, 'errors'); } } // Deny dynamic functions '${a}(' or '$a[b](' => So we refuse '}(' and '](' if (!$error) { if (preg_match('/[}\]]\s*\(/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", ']('), null, 'errors'); } } // Deny dynamic functions '$xxx(' or '$xxx (' or '$xxx" (' if (!$error) { if (preg_match('/\$[a-z0-9_\-\/\*\"]+\s*\(/ims', $phpfullcodestring)) { $error++; setEventMessages($langs->trans("DynamicPHPCodeContainsAForbiddenInstruction", '$...('), null, 'errors'); } } // No need to block $conf->global->aaa() because PHP try to run the method aaa of $conf->global and not the function into $conf->global->aaa. // Then check if installmodules.lock does not block dynamic PHP code change. if ($phpfullcodestringold != $phpfullcodestringnew) { if (!$error) { $dolibarrdataroot = preg_replace('/([\\/]+)$/i', '', DOL_DATA_ROOT); $allowimportsite = true; include_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; if (dol_is_file($dolibarrdataroot.'/installmodules.lock')) { $allowimportsite = false; } if (!$allowimportsite) { $error++; // Blocked by installmodules.lock if (getDolGlobalString('MAIN_MESSAGE_INSTALL_MODULES_DISABLED_CONTACT_US')) { // Show clean corporate message $message = $langs->trans('InstallModuleFromWebHasBeenDisabledContactUs'); } else { // Show technical generic message $message = $langs->trans("InstallModuleFromWebHasBeenDisabledByFile", $dolibarrdataroot.'/installmodules.lock'); } setEventMessages($message, null, 'errors'); } } } return $error; }