3069 lines
97 KiB
PHP
3069 lines
97 KiB
PHP
<?php
|
|
/**
|
|
* @package WonderCMS
|
|
* @author Robert Isoski (https://robert.si)
|
|
* @see https://www.wondercms.com
|
|
* @license MIT
|
|
*/
|
|
|
|
session_start();
|
|
define('VERSION', '3.4.3');
|
|
mb_internal_encoding('UTF-8');
|
|
|
|
if (defined('PHPUNIT_TESTING') === false) {
|
|
$Wcms = new Wcms();
|
|
$Wcms->init();
|
|
$Wcms->render();
|
|
}
|
|
|
|
class Wcms
|
|
{
|
|
private const MODULES_JSON_VERSION = 1;
|
|
private const THEMES_DIR = 'themes';
|
|
private const PLUGINS_DIR = 'plugins';
|
|
private const VALID_DIRS = [self::THEMES_DIR, self::PLUGINS_DIR];
|
|
private const THEME_PLUGINS_TYPES = [
|
|
'installs' => 'install',
|
|
'updates' => 'update',
|
|
'exists' => 'exist',
|
|
];
|
|
|
|
/** Database main keys */
|
|
public const DB_CONFIG = 'config';
|
|
public const DB_MENU_ITEMS = 'menuItems';
|
|
public const DB_MENU_ITEMS_SUBPAGE = 'subpages';
|
|
public const DB_PAGES_KEY = 'pages';
|
|
public const DB_PAGES_SUBPAGE_KEY = 'subpages';
|
|
|
|
/** @var int MIN_PASSWORD_LENGTH minimum number of characters */
|
|
public const MIN_PASSWORD_LENGTH = 8;
|
|
|
|
/** @var string WCMS_REPO - repo URL */
|
|
public const WCMS_REPO = 'https://raw.githubusercontent.com/WonderCMS/wondercms/main/';
|
|
|
|
/** @var string WCMS_CDN_REPO - CDN repo URL */
|
|
public const WCMS_CDN_REPO = 'https://raw.githubusercontent.com/WonderCMS/wondercms-cdn-files/main/';
|
|
|
|
/** @var string $currentPage - current page */
|
|
public $currentPage = '';
|
|
|
|
/** @var array $currentPageTree - Tree hierarchy of the current page */
|
|
public $currentPageTree = [];
|
|
|
|
/** @var array $installedPlugins - Currently installed plugins */
|
|
public $installedPlugins = [];
|
|
|
|
/** @var bool $currentPageExists - check if current page exists */
|
|
public $currentPageExists = false;
|
|
|
|
/** @var object $db - content of database.js */
|
|
protected $db;
|
|
|
|
/** @var bool $loggedIn - check if admin is logged in */
|
|
public $loggedIn = false;
|
|
|
|
/** @var array $listeners for hooks */
|
|
public $listeners = [];
|
|
|
|
/** @var string $dataPath path to data folder */
|
|
public $dataPath;
|
|
|
|
/** @var string $modulesCachePath path to cached json file with Themes/Plugins data */
|
|
protected $modulesCachePath;
|
|
|
|
/** @var string $securityCachePath path to security json file with force https caching data */
|
|
protected $securityCachePath;
|
|
|
|
/** @var string $dbPath path to database.js */
|
|
protected $dbPath;
|
|
|
|
/** @var string $filesPath path to uploaded files */
|
|
public $filesPath;
|
|
|
|
/** @var string $rootDir root dir of the install (where index.php is) */
|
|
public $rootDir;
|
|
|
|
/** @var bool $headerResponseDefault read default header response */
|
|
public $headerResponseDefault = true;
|
|
|
|
/** @var string $headerResponse header status */
|
|
public $headerResponse = 'HTTP/1.0 200 OK';
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $dataFolder
|
|
* @param string $filesFolder
|
|
* @param string $dbName
|
|
* @param string $rootDir
|
|
* @throws Exception
|
|
*/
|
|
public function __construct(
|
|
string $dataFolder = 'data',
|
|
string $filesFolder = 'files',
|
|
string $dbName = 'database.js',
|
|
string $rootDir = __DIR__
|
|
) {
|
|
$this->rootDir = $rootDir;
|
|
$this->setPaths($dataFolder, $filesFolder, $dbName);
|
|
$this->db = $this->getDb();
|
|
}
|
|
|
|
/**
|
|
* Setting default paths
|
|
*
|
|
* @param string $dataFolder
|
|
* @param string $filesFolder
|
|
* @param string $dbName
|
|
*/
|
|
public function setPaths(
|
|
string $dataFolder = 'data',
|
|
string $filesFolder = 'files',
|
|
string $dbName = 'database.js'
|
|
): void {
|
|
$this->dataPath = sprintf('%s/%s', $this->rootDir, $dataFolder);
|
|
$this->dbPath = sprintf('%s/%s', $this->dataPath, $dbName);
|
|
$this->filesPath = sprintf('%s/%s', $this->dataPath, $filesFolder);
|
|
$this->modulesCachePath = sprintf('%s/%s', $this->dataPath, 'cache.json');
|
|
$this->securityCachePath = sprintf('%s/%s', $this->dataPath, 'security.json');
|
|
}
|
|
|
|
/**
|
|
* Init function called on each page load
|
|
*
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function init(): void
|
|
{
|
|
$this->forceSSL();
|
|
$this->loginStatus();
|
|
$this->getSiteLanguage();
|
|
$this->pageStatus();
|
|
$this->logoutAction();
|
|
$this->loginAction();
|
|
$this->notFoundResponse();
|
|
$this->loadPlugins();
|
|
if ($this->loggedIn) {
|
|
$this->manuallyRefreshCacheData();
|
|
$this->addCustomModule();
|
|
$this->installUpdateModuleAction();
|
|
$this->changePasswordAction();
|
|
$this->deleteFileModuleAction();
|
|
$this->changePageThemeAction();
|
|
$this->backupAction();
|
|
$this->forceHttpsAction();
|
|
$this->saveChangesPopupAction();
|
|
$this->saveLogoutToLoginScreenAction();
|
|
$this->deletePageAction();
|
|
$this->saveAction();
|
|
$this->updateAction();
|
|
$this->uploadFileAction();
|
|
$this->notifyAction();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set site language based on logged-in user
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function getSiteLanguage(): string
|
|
{
|
|
if ($this->loggedIn) {
|
|
$lang = $this->get('config', 'adminLang');
|
|
} else {
|
|
$lang = $this->get('config', 'siteLang');
|
|
}
|
|
|
|
if (gettype($lang) === 'object' && empty(get_object_vars($lang))) {
|
|
$lang = 'en';
|
|
$this->set('config', 'siteLang', $lang);
|
|
$this->set('config', 'adminLang', $lang);
|
|
}
|
|
|
|
return $lang;
|
|
}
|
|
|
|
/**
|
|
* Display the HTML. Called after init()
|
|
* @return void
|
|
*/
|
|
public function render(): void
|
|
{
|
|
header($this->headerResponse);
|
|
|
|
// Alert admin that page is hidden
|
|
if ($this->loggedIn) {
|
|
$loadingPage = null;
|
|
foreach ($this->get('config', 'menuItems') as $item) {
|
|
if ($this->currentPage === $item->slug) {
|
|
$loadingPage = $item;
|
|
}
|
|
}
|
|
if ($loadingPage && $loadingPage->visibility === 'hide') {
|
|
$this->alert('info',
|
|
'This page (' . $this->currentPage . ') is currently hidden from the menu. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#menu"><b>Open menu visibility settings</b></a>');
|
|
}
|
|
}
|
|
|
|
$this->loadThemeAndFunctions();
|
|
}
|
|
|
|
/**
|
|
* Function used by plugins to add a hook
|
|
*
|
|
* @param string $hook
|
|
* @param callable $functionName
|
|
*/
|
|
public function addListener(string $hook, callable $functionName): void
|
|
{
|
|
$this->listeners[$hook][] = $functionName;
|
|
}
|
|
|
|
/**
|
|
* Add alert message for admin
|
|
*
|
|
* @param string $class see bootstrap alerts classes
|
|
* @param string $message the message to display
|
|
* @param bool $sticky can it be closed?
|
|
* @return void
|
|
*/
|
|
public function alert(string $class, string $message, bool $sticky = false): void
|
|
{
|
|
if (isset($_SESSION['alert'][$class])) {
|
|
foreach ($_SESSION['alert'][$class] as $v) {
|
|
if ($v['message'] === $message) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
$_SESSION['alert'][$class][] = ['class' => $class, 'message' => $this->hook('alert', $message)[0], 'sticky' => $sticky];
|
|
}
|
|
|
|
/**
|
|
* Display alert message to the admin
|
|
* @return string
|
|
*/
|
|
public function alerts(): string
|
|
{
|
|
if (!isset($_SESSION['alert'])) {
|
|
return '';
|
|
}
|
|
$output = '<div id="alertWrapperId" class="alertWrapper" style="">';
|
|
$output .= '<script>
|
|
const displayAlerts = localStorage.getItem("displayAlerts");
|
|
if (displayAlerts === "false") {
|
|
const alertWrapper = document.getElementById("alertWrapperId");
|
|
if (alertWrapper) {
|
|
alertWrapper.style.display = "none";
|
|
}
|
|
}
|
|
</script>';
|
|
foreach ($_SESSION['alert'] as $alertClass) {
|
|
foreach ($alertClass as $alert) {
|
|
$output .= '<div class="alert alert-'
|
|
. $alert['class']
|
|
. (!$alert['sticky'] ? ' alert-dismissible' : '')
|
|
. '">'
|
|
. (!$alert['sticky'] ? '<button type="button" class="close" data-dismiss="alert" onclick="parentNode.remove();">×</button>' : '')
|
|
. $alert['message']
|
|
. $this->hideAlerts();
|
|
}
|
|
}
|
|
$output .= '</div>';
|
|
unset($_SESSION['alert']);
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Allow admin to dismiss alerts
|
|
* @return string
|
|
*/
|
|
public function hideAlerts(): string
|
|
{
|
|
if (!$this->loggedIn) {
|
|
return '';
|
|
}
|
|
$output = '';
|
|
$output .= '<br><a href="" onclick="localStorage.setItem(\'displayAlerts\', \'false\');"><small>Hide all alerts until next login</small></a></div>';
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Get an asset (returns URL of the asset)
|
|
*
|
|
* @param string $location
|
|
* @return string
|
|
*/
|
|
public function asset(string $location): string
|
|
{
|
|
return self::url('themes/' . $this->get('config', 'theme') . '/' . $location);
|
|
}
|
|
|
|
/**
|
|
* Backup whole WonderCMS installation
|
|
*
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function backupAction(): void
|
|
{
|
|
if (!$this->loggedIn) {
|
|
return;
|
|
}
|
|
$backupList = glob($this->filesPath . '/*-backup-*.zip');
|
|
if (!empty($backupList)) {
|
|
$this->alert('danger',
|
|
'Backup files detected. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#files"><b>View and delete unnecessary backup files</b></a>');
|
|
}
|
|
if (isset($_POST['backup']) && $this->verifyFormActions()) {
|
|
$this->zipBackup();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save if WCMS should force https
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function forceHttpsAction(): void
|
|
{
|
|
if (isset($_POST['forceHttps']) && $this->verifyFormActions()) {
|
|
$this->set('config', 'forceHttps', $_POST['forceHttps'] === 'true');
|
|
$this->updateSecurityCache();
|
|
|
|
$this->alert('success', 'Force HTTPs was successfully changed.');
|
|
$this->redirect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save if WCMS should show the popup before saving the page content changes
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function saveChangesPopupAction(): void
|
|
{
|
|
if (isset($_POST['saveChangesPopup']) && $this->verifyFormActions()) {
|
|
$this->set('config', 'saveChangesPopup', $_POST['saveChangesPopup'] === 'true');
|
|
$this->alert('success', 'Saving the confirmation popup settings changed.');
|
|
$this->redirect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save if admin should be redirected to login/last viewed page after logging out.
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function saveLogoutToLoginScreenAction(): void
|
|
{
|
|
if (isset($_POST['logoutToLoginScreen']) && $this->verifyFormActions()) {
|
|
$redirectToLogin = $_POST['logoutToLoginScreen'] === 'true';
|
|
$message = $redirectToLogin
|
|
? 'You will be redirected to login screen after logging out.'
|
|
: 'You will be redirected to last viewed screen after logging out.';
|
|
$this->set('config', 'logoutToLoginScreen', $_POST['logoutToLoginScreen'] === 'true');
|
|
$this->alert('success', $message);
|
|
$this->redirect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update cache for security settings.
|
|
* @return void
|
|
*/
|
|
public function updateSecurityCache(): void
|
|
{
|
|
$content = ['forceHttps' => $this->isHttpsForced()];
|
|
$json = json_encode($content, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
file_put_contents($this->securityCachePath, $json, LOCK_EX);
|
|
}
|
|
|
|
/**
|
|
* Get a static block
|
|
*
|
|
* @param string $key name of the block
|
|
* @return string
|
|
*/
|
|
public function block(string $key): string
|
|
{
|
|
$blocks = $this->get('blocks');
|
|
$content = '';
|
|
|
|
if (isset($blocks->{$key})) {
|
|
$content = $this->loggedIn
|
|
? $this->editable($key, $blocks->{$key}->content, 'blocks')
|
|
: $blocks->{$key}->content;
|
|
}
|
|
return $this->hook('block', $content, $key)[0];
|
|
}
|
|
|
|
/**
|
|
* Change password
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function changePasswordAction(): void
|
|
{
|
|
if (isset($_POST['old_password'], $_POST['new_password'], $_POST['repeat_password'])
|
|
&& $_SESSION['token'] === $_POST['token']
|
|
&& $this->loggedIn
|
|
&& $this->hashVerify($_POST['token'])) {
|
|
if (!password_verify($_POST['old_password'], $this->get('config', 'password'))) {
|
|
$this->alert('danger',
|
|
'Wrong password. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#security"><b>Re-open security settings</b></a>');
|
|
$this->redirect();
|
|
return;
|
|
}
|
|
if (strlen($_POST['new_password']) < self::MIN_PASSWORD_LENGTH) {
|
|
$this->alert('danger',
|
|
sprintf('Password must be longer than %d characters. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#security"><b>Re-open security settings</b></a>',
|
|
self::MIN_PASSWORD_LENGTH));
|
|
$this->redirect();
|
|
return;
|
|
}
|
|
if ($_POST['new_password'] !== $_POST['repeat_password']) {
|
|
$this->alert('danger',
|
|
'New passwords do not match. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#security"><b>Re-open security settings</b></a>');
|
|
$this->redirect();
|
|
return;
|
|
}
|
|
$this->set('config', 'password', password_hash($_POST['new_password'], PASSWORD_DEFAULT));
|
|
$this->set('config', 'forceLogout', true);
|
|
$this->logoutAction(true);
|
|
$this->alert('success', '<center><b>Password changed. Log in again.</b></center>', 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if folders are writable
|
|
* Executed once before creating the database file
|
|
*
|
|
* @param string $folder the relative path of the folder to check/create
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function checkFolder(string $folder): void
|
|
{
|
|
if (!is_dir($folder) && !mkdir($folder, 0755) && !is_dir($folder)) {
|
|
throw new Exception('Could not create data folder.');
|
|
}
|
|
if (!is_writable($folder)) {
|
|
throw new Exception('Could write to data folder.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the JSON database if it doesn't exist
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function createDb(): void
|
|
{
|
|
// Check php requirements
|
|
$this->checkMinimumRequirements();
|
|
$password = $this->generatePassword();
|
|
$this->db = (object)[
|
|
self::DB_CONFIG => [
|
|
'siteTitle' => 'Website title',
|
|
'siteLang' => 'en',
|
|
'adminLang' => 'en',
|
|
'theme' => 'sky',
|
|
'defaultPage' => 'home',
|
|
'login' => 'loginURL',
|
|
'forceLogout' => false,
|
|
'forceHttps' => false,
|
|
'saveChangesPopup' => false,
|
|
'password' => password_hash($password, PASSWORD_DEFAULT),
|
|
'lastLogins' => [],
|
|
'lastModulesSync' => null,
|
|
'customModules' => $this->defaultCustomModules(),
|
|
'menuItems' => [
|
|
'0' => [
|
|
'name' => 'Home',
|
|
'slug' => 'home',
|
|
'visibility' => 'show',
|
|
self::DB_MENU_ITEMS_SUBPAGE => new stdClass()
|
|
],
|
|
'1' => [
|
|
'name' => 'How to',
|
|
'slug' => 'how-to',
|
|
'visibility' => 'show',
|
|
self::DB_MENU_ITEMS_SUBPAGE => new stdClass()
|
|
]
|
|
]
|
|
],
|
|
'pages' => [
|
|
'404' => [
|
|
'title' => '404',
|
|
'keywords' => '404',
|
|
'description' => '404',
|
|
'content' => '<center><h1>404 - Page not found</h1></center>',
|
|
self::DB_PAGES_SUBPAGE_KEY => new stdClass()
|
|
],
|
|
'home' => [
|
|
'title' => 'Home',
|
|
'keywords' => 'Enter, page, keywords, for, search, engines',
|
|
'description' => 'A page description is also good for search engines.',
|
|
'content' => '<h1>Welcome to your website</h1>
|
|
|
|
<p>Your password for editing everything is: <b>' . $password . '</b></p>
|
|
|
|
<p><a href="' . self::url('loginURL') . '" class="button">Click here to login</a></p>
|
|
|
|
<p>To install an awesome editor, open Settings/Plugins and click Install Summernote.</p>',
|
|
self::DB_PAGES_SUBPAGE_KEY => new stdClass()
|
|
],
|
|
'how-to' => [
|
|
'title' => 'How to',
|
|
'keywords' => 'Enter, keywords, for, this page',
|
|
'description' => 'A page description is also good for search engines.',
|
|
'content' => '<h2>Easy editing</h2>
|
|
<p>After logging in, click anywhere to edit and click outside to save. Changes are live and shown immediately.</p>
|
|
|
|
<h2>Create new page</h2>
|
|
<p>Pages can be created in the Settings.</p>
|
|
|
|
<h2>Start a blog or change your theme</h2>
|
|
<p>To install, update or remove themes/plugins, visit the Settings.</p>
|
|
|
|
<h2><b>Support WonderCMS</b></h2>
|
|
<p>WonderCMS is free for over 12 years.<br>
|
|
<a href="https://swag.wondercms.com" target="_blank"><u>Click here to support us by getting a T-shirt</u></a> or <a href="https://www.wondercms.com/donate" target="_blank"><u>with a donation</u></a>.</p>',
|
|
self::DB_PAGES_SUBPAGE_KEY => new stdClass()
|
|
]
|
|
],
|
|
'blocks' => [
|
|
'subside' => [
|
|
'content' => '<h2>About your website</h2>
|
|
|
|
<br>
|
|
<p>Website description, contact form, mini map or anything else.</p>
|
|
<p>This editable area is visible on all pages.</p>'
|
|
],
|
|
'footer' => [
|
|
'content' => '©' . date('Y') . ' Your website'
|
|
]
|
|
]
|
|
];
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Default data for the Custom Modules
|
|
* @return array[]
|
|
*/
|
|
private function defaultCustomModules(): array {
|
|
return [
|
|
'themes' => [],
|
|
'plugins' => []
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create menu item
|
|
*
|
|
* @param string $name
|
|
* @param string|null $menu
|
|
* @param bool $createPage
|
|
* @param string $visibility show or hide
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function createMenuItem(
|
|
string $name,
|
|
string $menu = null,
|
|
string $visibility = 'hide',
|
|
bool $createPage = false
|
|
): void {
|
|
if (!in_array($visibility, ['show', 'hide'], true)) {
|
|
return;
|
|
}
|
|
$name = empty($name) ? 'empty' : str_replace([PHP_EOL, '<br>'], '', $name);
|
|
$slug = $this->createUniqueSlug($name, $menu);
|
|
|
|
$menuItems = $menuSelectionObject = clone $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS);
|
|
$menuTree = !empty($menu) || $menu === '0' ? explode('-', $menu) : [];
|
|
$slugTree = [];
|
|
if (count($menuTree)) {
|
|
foreach ($menuTree as $childMenuKey) {
|
|
$childMenu = $menuSelectionObject->{$childMenuKey};
|
|
|
|
if (!property_exists($childMenu, self::DB_MENU_ITEMS_SUBPAGE)) {
|
|
$childMenu->{self::DB_MENU_ITEMS_SUBPAGE} = new StdClass;
|
|
}
|
|
|
|
$menuSelectionObject = $childMenu->{self::DB_MENU_ITEMS_SUBPAGE};
|
|
$slugTree[] = $childMenu->slug;
|
|
}
|
|
}
|
|
$slugTree[] = $slug;
|
|
|
|
$menuCount = count(get_object_vars($menuSelectionObject));
|
|
|
|
$menuSelectionObject->{$menuCount} = new stdClass;
|
|
$menuSelectionObject->{$menuCount}->name = $name;
|
|
$menuSelectionObject->{$menuCount}->slug = $slug;
|
|
$menuSelectionObject->{$menuCount}->visibility = $visibility;
|
|
$menuSelectionObject->{$menuCount}->{self::DB_MENU_ITEMS_SUBPAGE} = new StdClass;
|
|
$this->set(self::DB_CONFIG, self::DB_MENU_ITEMS, $menuItems);
|
|
|
|
if ($createPage) {
|
|
$this->createPage($slugTree);
|
|
$_SESSION['redirect_to_name'] = $name;
|
|
$_SESSION['redirect_to'] = implode('/', $slugTree);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update menu item
|
|
*
|
|
* @param string $name
|
|
* @param string $menu
|
|
* @param string $visibility show or hide
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function updateMenuItem(string $name, string $menu, string $visibility = 'hide'): void
|
|
{
|
|
if (!in_array($visibility, ['show', 'hide'], true)) {
|
|
return;
|
|
}
|
|
$name = empty($name) ? 'empty' : str_replace([PHP_EOL, '<br>'], '', $name);
|
|
$slug = $this->createUniqueSlug($name, $menu);
|
|
|
|
$menuItems = $menuSelectionObject = clone $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS);
|
|
$menuTree = explode('-', $menu);
|
|
$slugTree = [];
|
|
$menuKey = array_pop($menuTree);
|
|
if (count($menuTree) > 0) {
|
|
foreach ($menuTree as $childMenuKey) {
|
|
$childMenu = $menuSelectionObject->{$childMenuKey};
|
|
|
|
if (!property_exists($childMenu, self::DB_MENU_ITEMS_SUBPAGE)) {
|
|
$childMenu->{self::DB_MENU_ITEMS_SUBPAGE} = new StdClass;
|
|
}
|
|
|
|
$menuSelectionObject = $childMenu->{self::DB_MENU_ITEMS_SUBPAGE};
|
|
$slugTree[] = $childMenu->slug;
|
|
}
|
|
}
|
|
|
|
$slugTree[] = $menuSelectionObject->{$menuKey}->slug;
|
|
$menuSelectionObject->{$menuKey}->name = $name;
|
|
$menuSelectionObject->{$menuKey}->slug = $slug;
|
|
$menuSelectionObject->{$menuKey}->visibility = $visibility;
|
|
$menuSelectionObject->{$menuKey}->{self::DB_MENU_ITEMS_SUBPAGE} = $menuSelectionObject->{$menuKey}->{self::DB_MENU_ITEMS_SUBPAGE} ?? new StdClass;
|
|
$this->set(self::DB_CONFIG, self::DB_MENU_ITEMS, $menuItems);
|
|
|
|
$this->updatePageSlug($slugTree, $slug);
|
|
if ($this->get(self::DB_CONFIG, 'defaultPage') === implode('/', $slugTree)) {
|
|
// Change old slug with new one
|
|
array_pop($slugTree);
|
|
$slugTree[] = $slug;
|
|
$this->set(self::DB_CONFIG, 'defaultPage', implode('/', $slugTree));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if slug already exists and creates unique one
|
|
*
|
|
* @param string $slug
|
|
* @param string|null $menu
|
|
* @return string
|
|
*/
|
|
public function createUniqueSlug(string $slug, string $menu = null): string
|
|
{
|
|
$slug = $this->slugify($slug);
|
|
$allMenuItems = $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS);
|
|
$menuCount = count(get_object_vars($allMenuItems));
|
|
|
|
// Check if it is subpage
|
|
$menuTree = $menu ? explode('-', $menu) : [];
|
|
if (count($menuTree)) {
|
|
foreach ($menuTree as $childMenuKey) {
|
|
$allMenuItems = $allMenuItems->{$childMenuKey}->subpages;
|
|
}
|
|
}
|
|
|
|
foreach ($allMenuItems as $value) {
|
|
if ($value->slug === $slug) {
|
|
$slug .= '-' . $menuCount;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
/**
|
|
* Create new page
|
|
*
|
|
* @param array|null $slugTree
|
|
* @param bool $createMenuItem
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function createPage(array $slugTree = null, bool $createMenuItem = false): void
|
|
{
|
|
$pageExists = false;
|
|
$pageData = null;
|
|
foreach ($slugTree as $parentPage) {
|
|
if (!$pageData) {
|
|
$pageData = $this->get(self::DB_PAGES_KEY)->{$parentPage};
|
|
continue;
|
|
}
|
|
|
|
$pageData = $pageData->subpages->{$parentPage} ?? null;
|
|
$pageExists = !empty($pageData);
|
|
}
|
|
|
|
if ($pageExists) {
|
|
$this->alert('danger', 'Cannot create page with existing slug.');
|
|
return;
|
|
}
|
|
|
|
$slug = array_pop($slugTree);
|
|
$pageSlug = $slug ?: $this->slugify($this->currentPage);
|
|
$allPages = $selectedPage = clone $this->get(self::DB_PAGES_KEY);
|
|
$menuKey = null;
|
|
if (!empty($slugTree)) {
|
|
foreach ($slugTree as $childSlug) {
|
|
// Find menu key tree
|
|
if ($createMenuItem) {
|
|
$menuKey = $this->findAndUpdateMenuKey($menuKey, $childSlug);
|
|
}
|
|
|
|
// Create new parent page if it doesn't exist
|
|
if (!$selectedPage->{$childSlug}) {
|
|
$parentTitle = mb_convert_case(str_replace('-', ' ', $childSlug), MB_CASE_TITLE);
|
|
$selectedPage->{$childSlug}->title = $parentTitle;
|
|
$selectedPage->{$childSlug}->keywords = 'Keywords, are, good, for, search, engines';
|
|
$selectedPage->{$childSlug}->description = 'A short description is also good.';
|
|
|
|
if ($createMenuItem) {
|
|
$this->createMenuItem($parentTitle, $menuKey);
|
|
$menuKey = $this->findAndUpdateMenuKey($menuKey, $childSlug); // Add newly added menu key
|
|
}
|
|
}
|
|
|
|
if (!property_exists($selectedPage->{$childSlug}, self::DB_PAGES_SUBPAGE_KEY)) {
|
|
$selectedPage->{$childSlug}->{self::DB_PAGES_SUBPAGE_KEY} = new StdClass;
|
|
}
|
|
|
|
$selectedPage = $selectedPage->{$childSlug}->{self::DB_PAGES_SUBPAGE_KEY};
|
|
}
|
|
}
|
|
|
|
$pageTitle = !$slug ? str_replace('-', ' ', $pageSlug) : $pageSlug;
|
|
|
|
$selectedPage->{$slug} = new stdClass;
|
|
$selectedPage->{$slug}->title = mb_convert_case($pageTitle, MB_CASE_TITLE);
|
|
$selectedPage->{$slug}->keywords = 'Keywords, are, good, for, search, engines';
|
|
$selectedPage->{$slug}->description = 'A short description is also good.';
|
|
$selectedPage->{$slug}->{self::DB_PAGES_SUBPAGE_KEY} = new StdClass;
|
|
$this->set(self::DB_PAGES_KEY, $allPages);
|
|
|
|
if ($createMenuItem) {
|
|
$this->createMenuItem($pageTitle, $menuKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find and update menu key tree based on newly requested slug
|
|
* @param string|null $menuKey
|
|
* @param string $slug
|
|
* @return string
|
|
*/
|
|
private function findAndUpdateMenuKey(?string $menuKey, string $slug): string
|
|
{
|
|
$menuKeys = $menuKey !== null ? explode('-', $menuKey) : $menuKey;
|
|
$menuItems = json_decode(json_encode($this->get(self::DB_CONFIG, self::DB_MENU_ITEMS)), true);
|
|
foreach ($menuKeys as $key) {
|
|
$menuItems = $menuItems[$key][self::DB_MENU_ITEMS_SUBPAGE] ?? [];
|
|
}
|
|
|
|
if (false !== ($index = array_search($slug, array_column($menuItems, 'slug'), true))) {
|
|
$menuKey = $menuKey === null ? $index : $menuKey . '-' . $index;
|
|
} elseif ($menuKey === null) {
|
|
$menuKey = count($menuItems);
|
|
}
|
|
|
|
return $menuKey;
|
|
}
|
|
|
|
/**
|
|
* Update page data
|
|
*
|
|
* @param array $slugTree
|
|
* @param string $fieldname
|
|
* @param string $content
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function updatePage(array $slugTree, string $fieldname, string $content): void
|
|
{
|
|
$slug = array_pop($slugTree);
|
|
$allPages = $selectedPage = clone $this->get(self::DB_PAGES_KEY);
|
|
if (!empty($slugTree)) {
|
|
foreach ($slugTree as $childSlug) {
|
|
if (!property_exists($selectedPage->{$childSlug}, self::DB_PAGES_SUBPAGE_KEY)) {
|
|
$selectedPage->{$childSlug}->{self::DB_PAGES_SUBPAGE_KEY} = new StdClass;
|
|
}
|
|
|
|
$selectedPage = $selectedPage->{$childSlug}->{self::DB_PAGES_SUBPAGE_KEY};
|
|
}
|
|
}
|
|
|
|
$selectedPage->{$slug}->{$fieldname} = $content;
|
|
$this->set(self::DB_PAGES_KEY, $allPages);
|
|
}
|
|
|
|
/**
|
|
* Delete page key
|
|
*
|
|
* @param array $slugTree
|
|
* @param string $fieldname
|
|
*
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function deletePageKey(array $slugTree, string $fieldname): void
|
|
{
|
|
$slug = array_pop($slugTree);
|
|
$selectedPage = clone $this->get(self::DB_PAGES_KEY);
|
|
if (!empty($slugTree)) {
|
|
foreach ($slugTree as $childSlug) {
|
|
if (!property_exists($selectedPage->{$childSlug}, self::DB_PAGES_SUBPAGE_KEY)) {
|
|
$selectedPage->{$childSlug}->{self::DB_PAGES_SUBPAGE_KEY} = new StdClass;
|
|
}
|
|
|
|
$selectedPage = $selectedPage->{$childSlug}->{self::DB_PAGES_SUBPAGE_KEY};
|
|
}
|
|
}
|
|
|
|
unset($selectedPage->{$slug}->{$fieldname});
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Delete existing page by slug
|
|
*
|
|
* @param array|null $slugTree
|
|
* @throws Exception
|
|
*/
|
|
public function deletePageFromDb(array $slugTree = null): void
|
|
{
|
|
$slug = array_pop($slugTree);
|
|
|
|
$selectedPage = $this->db->{self::DB_PAGES_KEY};
|
|
if (!empty($slugTree)) {
|
|
foreach ($slugTree as $childSlug) {
|
|
$selectedPage = $selectedPage->{$childSlug}->subpages;
|
|
}
|
|
}
|
|
|
|
unset($selectedPage->{$slug});
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Update existing page slug
|
|
*
|
|
* @param array $slugTree
|
|
* @param string $newSlugName
|
|
* @throws Exception
|
|
*/
|
|
public function updatePageSlug(array $slugTree, string $newSlugName): void
|
|
{
|
|
$slug = array_pop($slugTree);
|
|
|
|
$selectedPage = $this->db->{self::DB_PAGES_KEY};
|
|
if (!empty($slugTree)) {
|
|
foreach ($slugTree as $childSlug) {
|
|
$selectedPage = $selectedPage->{$childSlug}->subpages;
|
|
}
|
|
}
|
|
|
|
$selectedPage->{$newSlugName} = $selectedPage->{$slug};
|
|
unset($selectedPage->{$slug});
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Load CSS and enable plugins to load CSS
|
|
* @return string
|
|
*/
|
|
public function css(): string
|
|
{
|
|
if ($this->loggedIn) {
|
|
$styles = <<<'EOT'
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/WonderCMS/wondercms-cdn-files@3.2.25/wcms-admin.min.css" crossorigin="anonymous">
|
|
EOT;
|
|
return $this->hook('css', $styles)[0];
|
|
}
|
|
return $this->hook('css', '')[0];
|
|
}
|
|
|
|
/**
|
|
* Get database content
|
|
* @return stdClass
|
|
* @throws Exception
|
|
*/
|
|
public function getDb(): stdClass
|
|
{
|
|
// initialize database if it doesn't exist
|
|
if (!file_exists($this->dbPath)) {
|
|
// this code only runs one time (on first page load/install)
|
|
$this->checkFolder(dirname($this->dbPath));
|
|
$this->checkFolder($this->filesPath);
|
|
$this->checkFolder($this->rootDir . '/' . self::THEMES_DIR);
|
|
$this->checkFolder($this->rootDir . '/' . self::PLUGINS_DIR);
|
|
$this->createDb();
|
|
}
|
|
return json_decode(file_get_contents($this->dbPath), false);
|
|
}
|
|
|
|
/**
|
|
* Get data from any json file
|
|
* @param string $path
|
|
* @return stdClass|null
|
|
*/
|
|
public function getJsonFileData(string $path): ?array
|
|
{
|
|
if (is_file($path) && file_exists($path)) {
|
|
return json_decode(file_get_contents($path), true);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Delete theme
|
|
* @return void
|
|
*/
|
|
public function deleteFileModuleAction(): void
|
|
{
|
|
if (!$this->loggedIn) {
|
|
return;
|
|
}
|
|
if (isset($_REQUEST['deleteModule'], $_REQUEST['type']) && $this->verifyFormActions(true)) {
|
|
$allowedDeleteTypes = ['files', 'plugins', 'themes'];
|
|
$filename = str_ireplace(
|
|
['/', './', '../', '..', '~', '~/', '\\'],
|
|
null,
|
|
trim($_REQUEST['deleteModule'])
|
|
);
|
|
$type = str_ireplace(
|
|
['/', './', '../', '..', '~', '~/', '\\'],
|
|
null,
|
|
trim($_REQUEST['type'])
|
|
);
|
|
if (!in_array($type, $allowedDeleteTypes, true)) {
|
|
$this->alert('danger',
|
|
'Wrong delete folder path.');
|
|
$this->redirect();
|
|
}
|
|
if ($filename === $this->get('config', 'theme')) {
|
|
$this->alert('danger',
|
|
'Cannot delete currently active theme. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#themes"><b>Re-open theme settings</b></a>');
|
|
$this->redirect();
|
|
}
|
|
$folder = $type === 'files' ? $this->filesPath : sprintf('%s/%s', $this->rootDir, $type);
|
|
$path = realpath("{$folder}/{$filename}");
|
|
if (file_exists($path)) {
|
|
$this->recursiveDelete($path);
|
|
$this->alert('success', "Deleted {$filename}.");
|
|
$this->redirect();
|
|
}
|
|
}
|
|
}
|
|
|
|
public function changePageThemeAction(): void
|
|
{
|
|
if (isset($_REQUEST['selectModule'], $_REQUEST['type']) && $this->verifyFormActions(true)) {
|
|
$theme = $_REQUEST['selectModule'];
|
|
if (!is_dir($this->rootDir . '/' . $_REQUEST['type'] . '/' . $theme)) {
|
|
return;
|
|
}
|
|
|
|
$this->set('config', 'theme', $theme);
|
|
$this->redirect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete page
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function deletePageAction(): void
|
|
{
|
|
if (!isset($_GET['delete']) || !$this->verifyFormActions(true)) {
|
|
return;
|
|
}
|
|
$slugTree = explode('/', $_GET['delete']);
|
|
$this->deletePageFromDb($slugTree);
|
|
|
|
$allMenuItems = $selectedMenuItem = clone $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS);
|
|
if (count(get_object_vars($allMenuItems)) === 1 && count($slugTree) === 1) {
|
|
$this->alert('danger', 'Last page cannot be deleted - at least one page must exist.');
|
|
$this->redirect();
|
|
}
|
|
|
|
$selectedMenuItemParent = $selectedMenuItemKey = null;
|
|
foreach ($slugTree as $slug) {
|
|
$selectedMenuItemParent = $selectedMenuItem->{self::DB_MENU_ITEMS_SUBPAGE} ?? $selectedMenuItem;
|
|
foreach ($selectedMenuItemParent as $menuItemKey => $menuItem) {
|
|
if ($menuItem->slug === $slug) {
|
|
$selectedMenuItem = $menuItem;
|
|
$selectedMenuItemKey = $menuItemKey;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
unset($selectedMenuItemParent->{$selectedMenuItemKey});
|
|
$allMenuItems = $this->reindexObject($allMenuItems);
|
|
|
|
$defaultPage = $this->get(self::DB_CONFIG, 'defaultPage');
|
|
$defaultPageArray = explode('/', $defaultPage);
|
|
$treeIntersect = array_intersect_assoc($defaultPageArray, $slugTree);
|
|
if ($treeIntersect === $slugTree) {
|
|
$this->set(self::DB_CONFIG, 'defaultPage', $allMenuItems->{0}->slug);
|
|
}
|
|
$this->set(self::DB_CONFIG, self::DB_MENU_ITEMS, $allMenuItems);
|
|
|
|
$this->alert('success', 'Page <b>' . $slug . '</b> deleted.');
|
|
$this->redirect();
|
|
}
|
|
|
|
/**
|
|
* Get editable block
|
|
*
|
|
* @param string $id id for the block
|
|
* @param string $content html content
|
|
* @param string $dataTarget
|
|
* @return string
|
|
*/
|
|
public function editable(string $id, string $content, string $dataTarget = ''): string
|
|
{
|
|
return '<div' . ($dataTarget !== '' ? ' data-target="' . $dataTarget . '"' : '') . ' id="' . $id . '" class="editText editable">' . $content . '</div>';
|
|
}
|
|
|
|
/**
|
|
* Get main website title, show edit icon if logged in
|
|
* @return string
|
|
*/
|
|
public function siteTitle(): string
|
|
{
|
|
$output = $this->get('config', 'siteTitle');
|
|
if ($this->loggedIn) {
|
|
$output .= "<a data-toggle='wcms-modal' href='#settingsModal' data-target-tab='#menu'><i class='editIcon'></i></a>";
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Get footer, make it editable and show login link if it's set to default
|
|
* @return string
|
|
*/
|
|
public function footer(): string
|
|
{
|
|
if ($this->loggedIn) {
|
|
$output = '<div data-target="blocks" id="footer" class="editText editable">' . $this->get('blocks',
|
|
'footer')->content . '</div>';
|
|
} else {
|
|
$output = $this->get('blocks', 'footer')->content .
|
|
(!$this->loggedIn && $this->get('config', 'login') === 'loginURL'
|
|
? ' • <a href="' . self::url('loginURL') . '">Login</a>'
|
|
: '');
|
|
}
|
|
return $this->hook('footer', $output)[0];
|
|
}
|
|
|
|
/**
|
|
* Generate random password
|
|
* @return string
|
|
*/
|
|
public function generatePassword(): string
|
|
{
|
|
$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
return substr(str_shuffle($characters), 0, self::MIN_PASSWORD_LENGTH);
|
|
}
|
|
|
|
/**
|
|
* Get CSRF token
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function getToken(): string
|
|
{
|
|
return $_SESSION['token'] ?? $_SESSION['token'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
/**
|
|
* Get something from database
|
|
*/
|
|
public function get()
|
|
{
|
|
$args = func_get_args();
|
|
$object = $this->db;
|
|
|
|
foreach ($args as $key => $arg) {
|
|
if (!property_exists($object, $arg)) {
|
|
$this->set(...array_merge($args, [new stdClass]));
|
|
}
|
|
|
|
$object = $object->{$arg};
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* Download file content from url
|
|
* @param string $fileUrl
|
|
* @return string
|
|
*/
|
|
private function downloadFileFromUrl(string $fileUrl): string
|
|
{
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_URL, $fileUrl);
|
|
$content = curl_exec($ch);
|
|
if (false === $content) {
|
|
$this->alert('danger', 'Cannot get content from url.');
|
|
}
|
|
curl_close($ch);
|
|
|
|
return (string)$content;
|
|
}
|
|
|
|
/**
|
|
* Get content of a file from main branch
|
|
*
|
|
* @param string $file the file we want
|
|
* @param string $repo
|
|
* @return string
|
|
*/
|
|
public function getFileFromRepo(string $file, string $repo = self::WCMS_REPO): string
|
|
{
|
|
$repo = str_replace('https://github.com/', 'https://raw.githubusercontent.com/', $repo);
|
|
return $this->downloadFileFromUrl($repo . $file);
|
|
}
|
|
|
|
/**
|
|
* Get the latest version from main branch
|
|
* @param string $repo
|
|
* @return null|string
|
|
*/
|
|
public function getOfficialVersion(string $repo = self::WCMS_REPO): ?string
|
|
{
|
|
return $this->getCheckFileFromRepo('version', $repo);
|
|
}
|
|
|
|
/**
|
|
* Get the files from main branch
|
|
* @param string $fileName
|
|
* @param string $repo
|
|
* @return null|string
|
|
*/
|
|
public function getCheckFileFromRepo(string $fileName, string $repo = self::WCMS_REPO): ?string
|
|
{
|
|
$version = trim($this->getFileFromRepo($fileName, $repo));
|
|
return $version === '404: Not Found' || $version === '400: Invalid request' ? null : $version;
|
|
}
|
|
|
|
/**
|
|
* Compare token with hash_equals
|
|
*
|
|
* @param string $token
|
|
* @return bool
|
|
*/
|
|
public function hashVerify(string $token): bool
|
|
{
|
|
return hash_equals($token, $this->getToken());
|
|
}
|
|
|
|
/**
|
|
* Return hooks from plugins
|
|
* @return array
|
|
*/
|
|
public function hook(): array
|
|
{
|
|
$numArgs = func_num_args();
|
|
$args = func_get_args();
|
|
if ($numArgs < 2) {
|
|
trigger_error('Insufficient arguments', E_USER_ERROR);
|
|
}
|
|
$hookName = array_shift($args);
|
|
if (!isset($this->listeners[$hookName])) {
|
|
return $args;
|
|
}
|
|
foreach ($this->listeners[$hookName] as $func) {
|
|
$args = $func($args);
|
|
}
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Return array with all themes and their data
|
|
* @param string $type
|
|
* @return array
|
|
* @throws Exception
|
|
*/
|
|
public function listAllModules(string $type = self::THEMES_DIR): array
|
|
{
|
|
$newData = [];
|
|
if ($this->loggedIn) {
|
|
$data = $this->getModulesCachedData($type);
|
|
|
|
foreach ($data as $dirName => $addon) {
|
|
$exists = is_dir($this->rootDir . "/$type/" . $dirName);
|
|
$currentVersion = $exists ? $this->getModuleVersion($type, $dirName) : null;
|
|
$newVersion = $addon['version'];
|
|
$update = $newVersion !== null && $currentVersion !== null && $newVersion > $currentVersion;
|
|
if ($update) {
|
|
$this->alert('info',
|
|
'New ' . $type . ' update available. <b><a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#' . $type . '">Open ' . $type . '</a></b>');
|
|
}
|
|
|
|
$addonType = $exists ? self::THEME_PLUGINS_TYPES['exists'] : self::THEME_PLUGINS_TYPES['installs'];
|
|
$addonType = $update ? self::THEME_PLUGINS_TYPES['updates'] : $addonType;
|
|
|
|
$newData[$addonType][$dirName] = $addon;
|
|
$newData[$addonType][$dirName]['update'] = $update;
|
|
$newData[$addonType][$dirName]['install'] = !$exists;
|
|
$newData[$addonType][$dirName]['currentVersion'] = $currentVersion;
|
|
}
|
|
}
|
|
|
|
return $newData;
|
|
}
|
|
|
|
/**
|
|
* Check modules for cache
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function checkModulesCache(): void
|
|
{
|
|
$db = $this->getDb();
|
|
$data = $this->getJsonFileData($this->modulesCachePath);
|
|
// Recreate cache if lastModulesSync is missing
|
|
$lastSync = $db->config->lastModulesSync ?? strtotime('-2 days');
|
|
if (empty($data) || strtotime($lastSync) < strtotime('-1 days')) {
|
|
$this->updateAndCacheModules();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve cached Themes/Plugins data
|
|
* @param string $type
|
|
* @return array|null
|
|
* @throws Exception
|
|
*/
|
|
public function getModulesCachedData(string $type = self::THEMES_DIR): array
|
|
{
|
|
$this->checkModulesCache();
|
|
$data = $this->getJsonFileData($this->modulesCachePath);
|
|
return $data !== null && array_key_exists($type, $data) ? $data[$type] : [];
|
|
}
|
|
|
|
/**
|
|
* Retrieve cached single Theme/Plugin data
|
|
* @param string $moduleKey
|
|
* @param string $type
|
|
* @return array|null
|
|
* @throws Exception
|
|
*/
|
|
public function getSingleModuleCachedData(string $moduleKey, string $type = self::THEMES_DIR): array
|
|
{
|
|
$data = $this->getModulesCachedData($type);
|
|
return $data !== null && array_key_exists($moduleKey, $data) ? $data[$moduleKey] : [];
|
|
}
|
|
|
|
/**
|
|
* Force cache refresh for updates
|
|
* @throws Exception
|
|
*/
|
|
public function manuallyRefreshCacheData(): void
|
|
{
|
|
if (!isset($_REQUEST['manuallyResetCacheData']) || !$this->verifyFormActions(true)) {
|
|
return;
|
|
}
|
|
$this->updateAndCacheModules();
|
|
$this->checkWcmsCoreUpdate();
|
|
$this->set('config', 'lastModulesSync', date('Y/m/d'));
|
|
$this->redirect();
|
|
}
|
|
|
|
/**
|
|
* Forces http to https
|
|
*/
|
|
private function forceSSL(): void
|
|
{
|
|
if ($this->isHttpsForced() && !Wcms::isCurrentlyOnSSL()) {
|
|
$this->updateSecurityCache();
|
|
$this->redirect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method checks for new modules and caches them
|
|
* @throws Exception
|
|
*/
|
|
private function updateAndCacheModules(): void
|
|
{
|
|
$this->set('config', 'lastModulesSync', date('Y/m/d'));
|
|
$this->cacheModulesData();
|
|
}
|
|
|
|
/**
|
|
* Fetch module config from url
|
|
* @param string $url
|
|
* @param string $type
|
|
* @return object|null
|
|
*/
|
|
private function fetchModuleConfig(string $url, string $type): ?object
|
|
{
|
|
$wcmsModules = json_decode(trim($this->downloadFileFromUrl($url)));
|
|
$wcmsModulesData = $wcmsModules && property_exists($wcmsModules, $type)
|
|
? $wcmsModules->{$type}
|
|
: null;
|
|
if (null === $wcmsModulesData) {
|
|
$this->alert('danger', 'The wcms-modules.json file does not contain all the required information.');
|
|
return null;
|
|
}
|
|
$wcmsModulesData = get_mangled_object_vars($wcmsModulesData);
|
|
$returnData = reset($wcmsModulesData);
|
|
$name = key($wcmsModulesData);
|
|
$returnData->dirName = $name;
|
|
return $returnData;
|
|
}
|
|
|
|
/**
|
|
* Update cache for default themes/plugins modules.
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
private function updateModulesCache(): void
|
|
{
|
|
$wcmsModules = trim($this->getFileFromRepo('wcms-modules.json', self::WCMS_CDN_REPO));
|
|
$jsonObject = json_decode($wcmsModules);
|
|
if (empty($jsonObject)) {
|
|
return;
|
|
}
|
|
|
|
$parsedCache = $this->moduleCacheMapper($jsonObject);
|
|
if (empty($parsedCache)) {
|
|
return;
|
|
}
|
|
|
|
$this->save($this->modulesCachePath, $parsedCache);
|
|
}
|
|
|
|
/**
|
|
* Mapper between wcms-modules.json and applications cache.json
|
|
* @param object $wcmsModule
|
|
* @return object
|
|
*/
|
|
private function moduleCacheMapper(object $wcmsModule): object
|
|
{
|
|
$mappedModules = new stdClass;
|
|
foreach ($wcmsModule as $type => $value) {
|
|
if ($type === 'version') {
|
|
if ($value !== self::MODULES_JSON_VERSION) {
|
|
$this->alert('danger', 'The wcms-modules.json version is incorrect');
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$mappedModules->{$type} = new stdClass();
|
|
foreach ($value as $moduleName => $module) {
|
|
$parsedModule = $this->moduleCacheParser($module, $moduleName);
|
|
if (empty($parsedModule)) {
|
|
continue;
|
|
}
|
|
|
|
$mappedModules->{$type}->{$moduleName} = new stdClass();
|
|
$mappedModules->{$type}->{$moduleName} = $parsedModule;
|
|
}
|
|
}
|
|
|
|
return $mappedModules;
|
|
}
|
|
|
|
/**
|
|
* Parse module cache to
|
|
* @param object $module
|
|
* @param string $moduleName
|
|
* @return object|null
|
|
*/
|
|
private function moduleCacheParser(object $module, string $moduleName): ?object {
|
|
if (!$this->validateWcmsModuleStructure($module)) {
|
|
return null;
|
|
}
|
|
|
|
return (object)[
|
|
"name" => $module->name,
|
|
"dirName" => $moduleName,
|
|
"repo" => $module->repo,
|
|
"zip" => $module->zip,
|
|
"summary" => $module->summary,
|
|
"version" => $module->version,
|
|
"image" => $module->image,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Cache themes and plugins data
|
|
* @throws Exception
|
|
*/
|
|
private function cacheModulesData(): void
|
|
{
|
|
$db = $this->getDb();
|
|
|
|
// Download wcms-modules as cache
|
|
$this->updateModulesCache();
|
|
|
|
// Cache custom modules
|
|
$returnArray = $this->getJsonFileData($this->modulesCachePath);
|
|
|
|
// If custom modules is missing from the DB, we add it
|
|
if (!property_exists($db->config, 'customModules')) {
|
|
$this->set('config', 'customModules', $this->defaultCustomModules());
|
|
$db = $this->getDb();
|
|
}
|
|
|
|
$arrayCustom = (array)$db->config->customModules;
|
|
foreach ($arrayCustom as $type => $modules) {
|
|
foreach ($modules as $url) {
|
|
$wcmsModuleData = $this->fetchModuleConfig($url, $type);
|
|
if (null === $wcmsModuleData) {
|
|
continue;
|
|
}
|
|
|
|
$name = $wcmsModuleData->dirName;
|
|
$wcmsModuleData = $this->moduleCacheParser($wcmsModuleData, $name);
|
|
$returnArray[$type][$name] = $wcmsModuleData;
|
|
}
|
|
}
|
|
|
|
$this->save($this->modulesCachePath, (object)$returnArray);
|
|
}
|
|
|
|
/**
|
|
* Cache single theme or plugin data
|
|
* @param string $url
|
|
* @param string $type
|
|
* @throws Exception
|
|
*/
|
|
private function cacheSingleCacheModuleData(string $url, string $type): void
|
|
{
|
|
$returnArray = $this->getJsonFileData($this->modulesCachePath);
|
|
|
|
$wcmsModuleData = $this->fetchModuleConfig($url, $type);
|
|
if (null === $wcmsModuleData) {
|
|
return;
|
|
}
|
|
$name = $wcmsModuleData->dirName;
|
|
$wcmsModuleData = $this->moduleCacheParser($wcmsModuleData, $name);
|
|
$returnArray[$type][$name] = $wcmsModuleData;
|
|
$this->save($this->modulesCachePath, (object)$returnArray);
|
|
}
|
|
|
|
/**
|
|
* Check if the module url already exists
|
|
* @param string $repo
|
|
* @param string $type
|
|
* @return bool
|
|
* @throws Exception
|
|
*/
|
|
private function checkIfModuleRepoExists(string $repo, string $type): bool
|
|
{
|
|
$data = $this->getModulesCachedData($type);
|
|
return in_array($repo, array_column($data, 'repo'));
|
|
}
|
|
|
|
/**
|
|
* Validate structure of the wcms module json
|
|
* @param object $wcmsModule
|
|
* @return bool
|
|
*/
|
|
private function validateWcmsModuleStructure(object $wcmsModule): bool {
|
|
return property_exists($wcmsModule, 'name')
|
|
&& property_exists($wcmsModule, 'repo')
|
|
&& property_exists($wcmsModule, 'zip')
|
|
&& property_exists($wcmsModule, 'summary')
|
|
&& property_exists($wcmsModule, 'version')
|
|
&& property_exists($wcmsModule, 'image');
|
|
}
|
|
|
|
/**
|
|
* Add custom url links for themes and plugins
|
|
* @throws Exception
|
|
*/
|
|
public function addCustomModule(): void
|
|
{
|
|
if (!isset($_POST['pluginThemeUrl'], $_POST['pluginThemeType'], $_POST['password_recheck']) || !$this->verifyFormActions()) {
|
|
return;
|
|
}
|
|
|
|
if (!password_verify($_POST['password_recheck'], $this->get('config', 'password'))) {
|
|
$this->alert('danger', 'Invalid password.');
|
|
$this->redirect();
|
|
}
|
|
|
|
$type = $_POST['pluginThemeType'];
|
|
$url = rtrim(trim($_POST['pluginThemeUrl']), '/');
|
|
$customModules = (array)$this->get('config', 'customModules', $type);
|
|
$wcmsModuleData = $this->fetchModuleConfig($url, $type);
|
|
$errorMessage = null;
|
|
switch (true) {
|
|
case null === $wcmsModuleData || !$this->isValidModuleURL($url):
|
|
$errorMessage = 'Invalid URL. The module URL needs to contain the full path to the raw wcms-modules.json file.';
|
|
break;
|
|
case !$this->validateWcmsModuleStructure($wcmsModuleData):
|
|
$errorMessage = 'Module not added - the wcms-modules.json file does not contain all the required information.';
|
|
break;
|
|
case $this->checkIfModuleRepoExists($wcmsModuleData->repo, $type):
|
|
$errorMessage = 'Module already exists.';
|
|
break;
|
|
}
|
|
if ($errorMessage !== null) {
|
|
$this->alert('danger', $errorMessage);
|
|
$this->redirect();
|
|
}
|
|
|
|
$customModules[] = $url;
|
|
$this->set('config', 'customModules', $type, $customModules);
|
|
$this->cacheSingleCacheModuleData($url, $type);
|
|
$this->alert('success',
|
|
'Module successfully added to <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#' . $type . '">' . ucfirst($type) . '</b></a>.');
|
|
$this->redirect();
|
|
}
|
|
|
|
/**
|
|
* Read plugin version
|
|
* @param string $type
|
|
* @param string $name
|
|
* @return string|null
|
|
*/
|
|
public function getModuleVersion(string $type, string $name): ?string
|
|
{
|
|
$version = null;
|
|
$path = sprintf('%s/%s/%s', $this->rootDir, $type, $name);
|
|
$wcmsModulesPath = $path . '/wcms-modules.json';
|
|
$versionPath = $path . '/version';
|
|
if (is_dir($path) && (is_file($wcmsModulesPath) || is_file($versionPath))) {
|
|
if (is_file($wcmsModulesPath)) {
|
|
$wcmsModules = json_decode(trim(file_get_contents($wcmsModulesPath)));
|
|
$version = $wcmsModules->{$type}->{$name}->version;
|
|
} else {
|
|
$version = trim(file_get_contents($versionPath));
|
|
}
|
|
}
|
|
|
|
return $version;
|
|
}
|
|
|
|
/**
|
|
* Install and update theme
|
|
* @throws Exception
|
|
*/
|
|
public function installUpdateModuleAction(): void
|
|
{
|
|
if (!isset($_REQUEST['installModule'], $_REQUEST['type']) || !$this->verifyFormActions(true)) {
|
|
return;
|
|
}
|
|
|
|
$folderName = trim(htmlspecialchars($_REQUEST['installModule']));
|
|
$type = $_REQUEST['type'];
|
|
$cached = $this->getSingleModuleCachedData($folderName, $type);
|
|
$url = !empty($cached) ? $cached['zip'] : null;
|
|
|
|
if (empty($url)) {
|
|
$this->alert('danger', 'Unable to find theme or plugin.');
|
|
return;
|
|
}
|
|
|
|
$path = sprintf('%s/%s/', $this->rootDir, $type);
|
|
|
|
if (in_array($type, self::VALID_DIRS, true)) {
|
|
$zipFile = $this->filesPath . '/ZIPFromURL.zip';
|
|
$zipResource = fopen($zipFile, 'w');
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
curl_setopt($ch, CURLOPT_FILE, $zipResource);
|
|
curl_exec($ch);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
$zip = new \ZipArchive;
|
|
if ($curlError || $zip->open($zipFile) !== true || (stripos($url, '.zip') === false)) {
|
|
$this->recursiveDelete($this->rootDir . '/data/files/ZIPFromURL.zip');
|
|
$this->alert('danger',
|
|
'Error opening ZIP file.' . ($curlError ? ' Error description: ' . $curlError : ''));
|
|
$this->redirect();
|
|
}
|
|
// First delete old plugin folder
|
|
$this->recursiveDelete($path . $folderName);
|
|
|
|
// Then extract new one
|
|
$zip->extractTo($path);
|
|
$zip->close();
|
|
$this->recursiveDelete($this->rootDir . '/data/files/ZIPFromURL.zip');
|
|
$moduleFolder = $path . $folderName . '-master';
|
|
if (!is_dir($moduleFolder)) {
|
|
$moduleFolder = $path . $folderName . '-main';
|
|
}
|
|
if (is_dir($moduleFolder) && !rename($moduleFolder, $path . $folderName)) {
|
|
throw new Exception('Theme or plugin not installed. Possible cause: themes or plugins folder is not writable.');
|
|
}
|
|
$this->alert('success', 'Successfully installed/updated ' . $folderName . '.');
|
|
$this->redirect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate if custom module url has wcms-modules.json
|
|
* @param string $url
|
|
* @return boolean
|
|
*/
|
|
private function isValidModuleURL(string $url): bool
|
|
{
|
|
return strpos($url, 'wcms-modules.json') !== false;
|
|
}
|
|
|
|
/**
|
|
* Verify if admin is logged in and has verified token for POST calls
|
|
* @param bool $isRequest
|
|
* @return bool
|
|
*/
|
|
public function verifyFormActions(bool $isRequest = false): bool
|
|
{
|
|
return ($isRequest ? isset($_REQUEST['token']) : isset($_POST['token']))
|
|
&& $this->loggedIn
|
|
&& $this->hashVerify($isRequest ? $_REQUEST['token'] : $_POST['token']);
|
|
}
|
|
|
|
/**
|
|
* Load JS and enable plugins to load JS
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function js(): string
|
|
{
|
|
if ($this->loggedIn) {
|
|
$scripts = <<<EOT
|
|
<script src="https://cdn.jsdelivr.net/npm/autosize@4.0.2/dist/autosize.min.js" integrity="sha384-gqYjRLBp7SeF6PCEz2XeqqNyvtxuzI3DuEepcrNHbrO+KG3woVNa/ISn/i8gGtW8" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/taboverride@4.0.3/build/output/taboverride.min.js" integrity="sha384-fYHyZra+saKYZN+7O59tPxgkgfujmYExoI6zUvvvrKVT1b7krdcdEpTLVJoF/ap1" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/gh/WonderCMS/wondercms-cdn-files@3.2.26/wcms-admin.min.js" integrity="sha384-lwdbkm/17hWy+Y4iBnY0iEp0FlaKvjdeTBZaRYM1DGPshGgxKoPaB87Xue26Wv1W" crossorigin="anonymous"></script>
|
|
EOT;
|
|
$scripts .= '<script>const token = "' . $this->getToken() . '";</script>';
|
|
$scripts .= '<script>const rootURL = "' . $this->url() . '";</script>';
|
|
$scripts .= '<script>function recheckPasswordPrompt(e) {
|
|
e.target.elements["password_recheck"].value = prompt("Please confirm your admin password.");
|
|
return true;
|
|
}</script>';
|
|
|
|
return $this->hook('js', $scripts)[0];
|
|
}
|
|
return $this->hook('js', '')[0];
|
|
}
|
|
|
|
/**
|
|
* Load plugins (if any exist)
|
|
* @return void
|
|
*/
|
|
public function loadPlugins(): void
|
|
{
|
|
$this->installedPlugins = [];
|
|
$plugins = $this->rootDir . '/plugins';
|
|
if (!is_dir($plugins) && !mkdir($plugins) && !is_dir($plugins)) {
|
|
return;
|
|
}
|
|
if (!is_dir($this->filesPath) && !mkdir($this->filesPath) && !is_dir($this->filesPath)) {
|
|
return;
|
|
}
|
|
foreach (glob($plugins . '/*', GLOB_ONLYDIR) as $dir) {
|
|
$pluginName = basename($dir);
|
|
if (file_exists($dir . '/' . $pluginName . '.php')) {
|
|
include $dir . '/' . $pluginName . '.php';
|
|
$this->installedPlugins[] = $pluginName;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads theme files and functions.php file (if they exist)
|
|
* @return void
|
|
*/
|
|
public function loadThemeAndFunctions(): void
|
|
{
|
|
$location = $this->rootDir . '/themes/' . $this->get('config', 'theme');
|
|
if (file_exists($location . '/functions.php')) {
|
|
require_once $location . '/functions.php';
|
|
}
|
|
|
|
# If page does not exist for non-logged-in users, then show 404 theme if it exists.
|
|
$is404 = !$this->loggedIn && !$this->currentPageExists && $this->currentPage !== $this->get('config', 'login');
|
|
$customPageTemplate = sprintf('%s/%s.php', $location, $is404 ? '404' : $this->currentPage);
|
|
require_once file_exists($customPageTemplate) ? $customPageTemplate : $location . '/theme.php';
|
|
}
|
|
|
|
/**
|
|
* Admin login verification
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function loginAction(): void
|
|
{
|
|
if ($this->currentPage !== $this->get('config', 'login')) {
|
|
return;
|
|
}
|
|
if ($this->loggedIn) {
|
|
$this->redirect();
|
|
}
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
return;
|
|
}
|
|
$password = $_POST['password'] ?? '';
|
|
if (password_verify($password, $this->get('config', 'password'))) {
|
|
session_regenerate_id(true);
|
|
$_SESSION['loggedIn'] = true;
|
|
$_SESSION['rootDir'] = $this->rootDir;
|
|
$this->set('config', 'forceLogout', false);
|
|
$this->saveAdminLoginIP();
|
|
$this->redirect();
|
|
}
|
|
$this->alert('test', '<script>alert("Wrong password")</script>', 1);
|
|
$this->redirect($this->get('config', 'login'));
|
|
}
|
|
|
|
/**
|
|
* Save admins last 5 IPs
|
|
*/
|
|
private function saveAdminLoginIP(): void
|
|
{
|
|
$getAdminIP = $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
|
|
if ($getAdminIP === null) {
|
|
return;
|
|
}
|
|
|
|
if (!$savedIPs = $this->get('config', 'lastLogins')) {
|
|
$this->set('config', 'lastLogins', []);
|
|
$savedIPs = [];
|
|
}
|
|
$savedIPs = (array)$savedIPs;
|
|
$savedIPs[date('Y/m/d H:i:s')] = $getAdminIP;
|
|
krsort($savedIPs);
|
|
$this->set('config', 'lastLogins', array_slice($savedIPs, 0, 5));
|
|
}
|
|
|
|
/**
|
|
* Check if admin is logged in
|
|
* @return void
|
|
*/
|
|
public function loginStatus(): void
|
|
{
|
|
$this->loggedIn = $this->get('config', 'forceLogout')
|
|
? false
|
|
: isset($_SESSION['loggedIn'], $_SESSION['rootDir']) && $_SESSION['rootDir'] === $this->rootDir;
|
|
}
|
|
|
|
/**
|
|
* Login form view
|
|
* @return array
|
|
*/
|
|
public function loginView(): array
|
|
{
|
|
return [
|
|
'title' => $this->hook('loginView', 'Login')[0],
|
|
'description' => '',
|
|
'keywords' => '',
|
|
'content' => $this->hook('loginView', '
|
|
<style>.showUpdate{display: block !important}</style>
|
|
<div class="wUpdate" style="display:none;color:#ccc;left:0;top:0;width:100%;height:100%;position:fixed;text-align:center;padding-top:100px;background:rgba(51,51,51,.8);z-index:2448"><h2>Logging in and checking for updates</h2><p>This might take a moment.</p></div>
|
|
<form action="' . self::url($this->get('config', 'login')) . '" method="post">
|
|
<div class="winput-group text-center">
|
|
<h1>Login to your website</h1>
|
|
<input type="password" class="wform-control" id="password" name="password" placeholder="Password" autofocus><br><br>
|
|
<span class="winput-group-btn">
|
|
<button type="submit" class="wbtn wbtn-info" onclick="document.getElementsByClassName(\'wUpdate\')[0].classList.toggle(\'showUpdate\'); localStorage.clear();">Login</button>
|
|
</span>
|
|
</div>
|
|
</form>')[0]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Logout action
|
|
* @param bool $forceLogout
|
|
* @return void
|
|
*/
|
|
public function logoutAction(bool $forceLogout = false): void
|
|
{
|
|
if ($forceLogout
|
|
|| ($this->currentPage === 'logout'
|
|
&& isset($_REQUEST['token'])
|
|
&& $this->hashVerify($_REQUEST['token']))) {
|
|
$to = isset($_GET['to']) && !empty($_GET['to']) && !$this->isLogoutToLoginScreenEnabled()
|
|
? $_GET['to']
|
|
: $this->get('config', 'login');
|
|
unset($_SESSION['loggedIn'], $_SESSION['rootDir'], $_SESSION['token'], $_SESSION['alert']);
|
|
$this->redirect($to);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If admin is logged in and on existing page, this will save previous page and push it to logout action
|
|
* @return string|null
|
|
*/
|
|
private function logoutToUrl(): ?string
|
|
{
|
|
if (!$this->loggedIn || !$this->currentPageExists) {
|
|
return null;
|
|
}
|
|
|
|
return $this->getCurrentPagePath();
|
|
}
|
|
|
|
/**
|
|
* Return menu items, if they are set to be visible
|
|
* @return string
|
|
*/
|
|
public function menu(): string
|
|
{
|
|
$output = '';
|
|
foreach ($this->get('config', 'menuItems') as $item) {
|
|
if ($item->visibility === 'hide') {
|
|
continue;
|
|
}
|
|
$output .= $this->renderPageNavMenuItem($item);
|
|
}
|
|
return $this->hook('menu', $output)[0];
|
|
}
|
|
|
|
/**
|
|
* 404 header response
|
|
* @return void
|
|
*/
|
|
public function notFoundResponse(): void
|
|
{
|
|
if (!$this->loggedIn && !$this->currentPageExists && $this->headerResponseDefault) {
|
|
$this->headerResponse = 'HTTP/1.1 404 Not Found';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return 404 page to visitors
|
|
* Admin can create a page that doesn't exist yet
|
|
*/
|
|
public function notFoundView()
|
|
{
|
|
if ($this->loggedIn) {
|
|
return [
|
|
'title' => str_replace('-', ' ', $this->currentPage),
|
|
'description' => '',
|
|
'keywords' => '',
|
|
'content' => '<h2>Click to create content</h2>'
|
|
];
|
|
}
|
|
return $this->get('pages', '404');
|
|
}
|
|
|
|
/**
|
|
* Admin notifications
|
|
* Alerts for non-existent pages, changing default settings, new version/update
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function notifyAction(): void
|
|
{
|
|
if (!$this->loggedIn) {
|
|
return;
|
|
}
|
|
if (!$this->currentPageExists) {
|
|
$this->alert(
|
|
'info',
|
|
'<b>This page (' . $this->currentPage . ') doesn\'t exist.</b> Editing the content below will create it.'
|
|
);
|
|
}
|
|
if ($this->get('config', 'login') === 'loginURL') {
|
|
$this->alert('danger',
|
|
'Change your login URL and save it for later use. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#security"><b>Open security settings</b></a>');
|
|
}
|
|
|
|
$this->checkModulesCache();
|
|
}
|
|
|
|
/**
|
|
* Checks if there is new Wcms version
|
|
*/
|
|
private function checkWcmsCoreUpdate(): void
|
|
{
|
|
$onlineVersion = $this->getOfficialVersion();
|
|
if ($onlineVersion > VERSION) {
|
|
$this->alert(
|
|
'info',
|
|
'<h3>New WonderCMS update available</h3>
|
|
<a href="https://wondercms.com/news" target="_blank"><u><b>Check what\'s new</b></u></a>
|
|
and <b>backup your website</b> before updating.
|
|
<form action="' . $this->getCurrentPageUrl() . '" method="post" class="marginTop5">
|
|
<button type="submit" class="wbtn wbtn-info marginTop20" name="backup"><i class="installIcon"></i>Download backup</button>
|
|
<div class="clear"></div>
|
|
<button class="wbtn wbtn-info marginTop5" name="update"><i class="refreshIcon"></i>Update WonderCMS ' . VERSION . ' to ' . $onlineVersion . '</button>
|
|
<input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</form>'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update menu visibility state
|
|
*
|
|
* @param string $visibility - "show" for visible, "hide" for invisible
|
|
* @param string $menu
|
|
* @throws Exception
|
|
*/
|
|
public function updateMenuItemVisibility(string $visibility, string $menu): void
|
|
{
|
|
if (!in_array($visibility, ['show', 'hide'], true)) {
|
|
return;
|
|
}
|
|
|
|
$menuTree = explode('-', $menu);
|
|
$menuItems = $menuSelectionObject = clone $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS);
|
|
|
|
// Find sub menu item
|
|
if ($menuTree) {
|
|
$mainParentMenu = array_shift($menuTree);
|
|
$menuSelectionObject = $menuItems->{$mainParentMenu};
|
|
foreach ($menuTree as $childMenuKey) {
|
|
$menuSelectionObject = $menuSelectionObject->subpages->{$childMenuKey};
|
|
}
|
|
}
|
|
|
|
$menuSelectionObject->visibility = $visibility;
|
|
$this->set(self::DB_CONFIG, self::DB_MENU_ITEMS, $menuItems);
|
|
}
|
|
|
|
/**
|
|
* Reorder the pages
|
|
*
|
|
* @param int $content 1 for down arrow or -1 for up arrow
|
|
* @param string $menu
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function orderMenuItem(int $content, string $menu): void
|
|
{
|
|
// check if content is 1 or -1 as only those values are acceptable
|
|
if (!in_array($content, [1, -1], true)) {
|
|
return;
|
|
}
|
|
$menuTree = explode('-', $menu);
|
|
$mainParentMenu = $selectedMenuKey = array_shift($menuTree);
|
|
$menuItems = $menuSelectionObject = clone $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS);
|
|
|
|
// Sorting of subpages in menu
|
|
if ($menuTree) {
|
|
$selectedMenuKey = array_pop($menuTree);
|
|
$menuSelectionObject = $menuItems->{$mainParentMenu}->subpages;
|
|
foreach ($menuTree as $childMenuKey) {
|
|
$menuSelectionObject = $menuSelectionObject->{$childMenuKey}->subpages;
|
|
}
|
|
}
|
|
|
|
$targetPosition = $selectedMenuKey + $content;
|
|
|
|
// Find and switch target and selected menu position in DB
|
|
$selectedMenu = $menuSelectionObject->{$selectedMenuKey};
|
|
$targetMenu = $menuSelectionObject->{$targetPosition};
|
|
$menuSelectionObject->{$selectedMenuKey} = $targetMenu;
|
|
$menuSelectionObject->{$targetPosition} = $selectedMenu;
|
|
|
|
$this->set(self::DB_CONFIG, self::DB_MENU_ITEMS, $menuItems);
|
|
}
|
|
|
|
/**
|
|
* Return pages and display correct view (actual page or 404)
|
|
* Display different content and editable areas for admin
|
|
*
|
|
* @param string $key
|
|
* @return string
|
|
*/
|
|
public function page(string $key): string
|
|
{
|
|
$segments = $this->getCurrentPageData();
|
|
if (!$this->currentPageExists || !$segments) {
|
|
$segments = $this->get('config', 'login') === $this->currentPage
|
|
? (object)$this->loginView()
|
|
: (object)$this->notFoundView();
|
|
}
|
|
|
|
$segments->content = $segments->content ?? '<h2>Click here add content</h2>';
|
|
$keys = [
|
|
'title' => $segments->title,
|
|
'description' => $segments->description,
|
|
'keywords' => $segments->keywords,
|
|
'content' => $this->loggedIn
|
|
? $this->editable('content', $segments->content, 'pages')
|
|
: $segments->content
|
|
];
|
|
$content = $keys[$key] ?? '';
|
|
return $this->hook('page', $content, $key)[0];
|
|
}
|
|
|
|
/**
|
|
* Return database data of current page
|
|
*
|
|
* @return object|null
|
|
*/
|
|
public function getCurrentPageData(): ?object
|
|
{
|
|
return $this->getPageData(implode('/', $this->currentPageTree));
|
|
}
|
|
|
|
/**
|
|
* Return database data of any page
|
|
*
|
|
* @param string $slugTree
|
|
* @return object|null
|
|
*/
|
|
public function getPageData(string $slugTree): ?object
|
|
{
|
|
$arraySlugTree = explode('/', $slugTree);
|
|
$pageData = null;
|
|
foreach ($arraySlugTree as $slug) {
|
|
if ($pageData === null) {
|
|
$pageData = $this->get(self::DB_PAGES_KEY)->{$slug} ?? null;
|
|
continue;
|
|
}
|
|
|
|
$pageData = $pageData->{self::DB_PAGES_SUBPAGE_KEY}->{$slug} ?? null;
|
|
if (!$pageData) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return $pageData;
|
|
}
|
|
|
|
/**
|
|
* Get current page url
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCurrentPageUrl(): string
|
|
{
|
|
return self::url($this->getCurrentPagePath());
|
|
}
|
|
|
|
/**
|
|
* Get current page path
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCurrentPagePath(): string
|
|
{
|
|
$path = '';
|
|
foreach ($this->currentPageTree as $parentPage) {
|
|
$path .= $parentPage . '/';
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Page status (exists or doesn't exist)
|
|
* @return void
|
|
*/
|
|
public function pageStatus(): void
|
|
{
|
|
$this->currentPage = $this->parseUrl() ?: $this->get('config', 'defaultPage');
|
|
$this->currentPageExists = !empty($this->getCurrentPageData());
|
|
}
|
|
|
|
/**
|
|
* URL parser
|
|
* @return string
|
|
*/
|
|
public function parseUrl(): string
|
|
{
|
|
$page = $_GET['page'] ?? null;
|
|
$page = !empty($page) ? trim(htmlspecialchars($page, ENT_QUOTES)) : null;
|
|
|
|
if (!isset($page) || !$page) {
|
|
$defaultPage = $this->get('config', 'defaultPage');
|
|
$this->currentPageTree = explode('/', $defaultPage);
|
|
return $defaultPage;
|
|
}
|
|
|
|
$this->currentPageTree = explode('/', rtrim($page, '/'));
|
|
if ($page === $this->get('config', 'login')) {
|
|
return $page;
|
|
}
|
|
|
|
$currentPage = end($this->currentPageTree);
|
|
return $this->slugify($currentPage);
|
|
}
|
|
|
|
/**
|
|
* Recursive delete - used for deleting files, themes, plugins
|
|
*
|
|
* @param string $file
|
|
* @return void
|
|
*/
|
|
public function recursiveDelete(string $file): void
|
|
{
|
|
if (is_dir($file)) {
|
|
$files = new DirectoryIterator($file);
|
|
foreach ($files as $dirFile) {
|
|
if (!$dirFile->isDot()) {
|
|
$dirFile->isDir() ? $this->recursiveDelete($dirFile->getPathname()) : unlink($dirFile->getPathname());
|
|
}
|
|
}
|
|
rmdir($file);
|
|
} elseif (is_file($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redirect to any URL
|
|
*
|
|
* @param string $location
|
|
* @return void
|
|
*/
|
|
public function redirect(string $location = ''): void
|
|
{
|
|
header('Location: ' . self::url($location));
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* Save object to disk (default is set for DB)
|
|
* @param string|null $path
|
|
* @param object|null $content
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function save(string $path = null, object $content = null): void
|
|
{
|
|
$path = $path ?? $this->dbPath;
|
|
$content = $content ?? $this->db;
|
|
$json = json_encode($content, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
if (empty($content) || empty($json) || json_last_error() !== JSON_ERROR_NONE) {
|
|
$errorMessage = sprintf(
|
|
'%s - Error while trying to save in %s: %s',
|
|
time(),
|
|
$path,
|
|
print_r($content, true)
|
|
);
|
|
try {
|
|
$randomNumber = random_bytes(8);
|
|
} catch (Exception $e) {
|
|
$randomNumber = microtime(false);
|
|
}
|
|
$logName = date('Y-m-d H:i:s') . '-error-' . bin2hex($randomNumber) . '.log';
|
|
$logsPath = sprintf('%s/data/logs', $this->rootDir);
|
|
$this->checkFolder($logsPath);
|
|
error_log(
|
|
$errorMessage,
|
|
3,
|
|
sprintf('%s/%s', $logsPath, $logName)
|
|
);
|
|
return;
|
|
}
|
|
file_put_contents($path, $json, LOCK_EX);
|
|
}
|
|
|
|
/**
|
|
* Saving menu items, default page, login URL, theme, editable content
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function saveAction(): void
|
|
{
|
|
if (!$this->loggedIn) {
|
|
return;
|
|
}
|
|
if (isset($_SESSION['redirect_to'])) {
|
|
$newUrl = $_SESSION['redirect_to'];
|
|
$newPageName = $_SESSION['redirect_to_name'];
|
|
unset($_SESSION['redirect_to'], $_SESSION['redirect_to_name']);
|
|
$this->alert('success',
|
|
"Page <b>$newPageName</b> created. Click <a href=" . $newUrl . ">here</a> to open it.");
|
|
$this->redirect($newUrl);
|
|
}
|
|
if (isset($_POST['fieldname'], $_POST['content'], $_POST['target'], $_POST['token'])
|
|
&& $this->hashVerify($_POST['token'])) {
|
|
[$fieldname, $content, $target, $menu, $visibility] = $this->hook('save', $_POST['fieldname'],
|
|
$_POST['content'], $_POST['target'], $_POST['menu'] ?? null, ($_POST['visibility'] ?? 'hide'));
|
|
if ($target === 'menuItemUpdate' && $menu !== null) {
|
|
$this->updateMenuItem($content, $menu, $visibility);
|
|
$_SESSION['redirect_to_name'] = $content;
|
|
$_SESSION['redirect_to'] = $this->slugify($content);
|
|
}
|
|
if ($target === 'menuItemCreate' && $menu !== null) {
|
|
$this->createMenuItem($content, $menu, $visibility, true);
|
|
}
|
|
if ($target === 'menuItemVsbl' && $menu !== null) {
|
|
$this->updateMenuItemVisibility($visibility, $menu);
|
|
}
|
|
if ($target === 'menuItemOrder' && $menu !== null) {
|
|
$this->orderMenuItem($content, $menu);
|
|
}
|
|
if ($fieldname === 'defaultPage' && $this->getPageData($content) === null) {
|
|
return;
|
|
}
|
|
if ($fieldname === 'login' && (empty($content) || $this->getPageData($content) !== null)) {
|
|
return;
|
|
}
|
|
if ($fieldname === 'theme' && !is_dir($this->rootDir . '/themes/' . $content)) {
|
|
return;
|
|
}
|
|
if ($target === 'config') {
|
|
$this->set('config', $fieldname, $content);
|
|
} elseif ($target === 'blocks') {
|
|
$this->set('blocks', $fieldname, 'content', $content);
|
|
} elseif ($target === 'pages') {
|
|
if (!$this->currentPageExists) {
|
|
$this->createPage($this->currentPageTree, true);
|
|
}
|
|
$this->updatePage($this->currentPageTree, $fieldname, $content);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set something to database
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function set(): void
|
|
{
|
|
$args = func_get_args();
|
|
|
|
$value = array_pop($args);
|
|
$lastKey = array_pop($args);
|
|
$data = $this->db;
|
|
foreach ($args as $arg) {
|
|
$data = $data->{$arg};
|
|
}
|
|
$data->{$lastKey} = $value;
|
|
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Display admin settings panel
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function settings(): string
|
|
{
|
|
if (!$this->loggedIn) {
|
|
return '';
|
|
}
|
|
$currentPageData = $this->getCurrentPageData();
|
|
$fileList = array_slice(scandir($this->filesPath), 2);
|
|
$logoutTo = $this->logoutToUrl();
|
|
$isHttpsForced = $this->isHttpsForced();
|
|
$isSaveChangesPopupEnabled = $this->isSaveChangesPopupEnabled();
|
|
$isLogoutToLoginScreenEnabled = $this->isLogoutToLoginScreenEnabled();
|
|
$output = '
|
|
<script>var saveChangesPopup = ' . ($isSaveChangesPopupEnabled ? "true" : "false") . '</script>
|
|
<div id="save" class="loader-overlay"><h2><i class="animationLoader"></i><br />Saving</h2></div>
|
|
<div id="cache" class="loader-overlay"><h2><i class="animationLoader"></i><br />Checking for updates</h2></div>
|
|
<div id="adminPanel">
|
|
<a data-toggle="wcms-modal" class="wbtn wbtn-secondary wbtn-sm settings button" href="#settingsModal"><i class="settingsIcon"></i> Settings </a> <a href="' . self::url('logout?token=' . $this->getToken()) . '&to=' . $logoutTo . '" class="wbtn wbtn-danger wbtn-sm button logout" title="Logout" onclick="return confirm(\'Log out?\')"><i class="logoutIcon"></i></a>
|
|
<div class="wcms-modal modal" id="settingsModal">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header"><button type="button" class="close" data-dismiss="wcms-modal" aria-hidden="true">×</button></div>
|
|
<div class="modal-body coll-xs-12 coll-12">
|
|
<ul class="nav nav-tabs justify-content-center text-center" role="tablist">
|
|
<li role="presentation" class="nav-item"><a href="#currentPage" aria-controls="currentPage" role="tab" data-toggle="tab" class="nav-link active">Current page</a></li>
|
|
<li role="presentation" class="nav-item"><a href="#menu" aria-controls="menu" role="tab" data-toggle="tab" class="nav-link">Menu</a></li>
|
|
<li role="presentation" class="nav-item"><a href="#files" aria-controls="files" role="tab" data-toggle="tab" class="nav-link">Files</a></li>
|
|
<li role="presentation" class="nav-item"><a href="#themes" aria-controls="themes" role="tab" data-toggle="tab" class="nav-link">Themes</a></li>
|
|
<li role="presentation" class="nav-item"><a href="#plugins" aria-controls="plugins" role="tab" data-toggle="tab" class="nav-link">Plugins</a></li>
|
|
<li role="presentation" class="nav-item"><a href="#security" aria-controls="security" role="tab" data-toggle="tab" class="nav-link">Security</a></li>
|
|
</ul>
|
|
<div class="tab-content coll-md-8 coll-md-offset-2 offset-md-2">
|
|
<div role="tabpanel" class="tab-pane active" id="currentPage">';
|
|
if ($this->currentPageExists && $currentPageData) {
|
|
$output .= '
|
|
<p class="subTitle">Page title</p>
|
|
<div class="change">
|
|
<div data-target="pages" id="title" class="editText">' . ($currentPageData->title ?: '') . '</div>
|
|
</div>
|
|
<p class="subTitle">Page keywords</p>
|
|
<div class="change">
|
|
<div data-target="pages" id="keywords" class="editText">' . ($currentPageData->keywords ?: '') . '</div>
|
|
</div>
|
|
<p class="subTitle">Page description</p>
|
|
<div class="change">
|
|
<div data-target="pages" id="description" class="editText">' . ($currentPageData->description ?: '') . '</div>
|
|
</div>
|
|
<a href="' . self::url('?delete=' . implode('/',
|
|
$this->currentPageTree) . '&token=' . $this->getToken()) . '" class="wbtn wbtn-danger pull-right marginTop40" title="Delete page" onclick="return confirm(\'Delete ' . $this->currentPage . '?\')"><i class="deleteIconInButton"></i> Delete page (' . $this->currentPage . ')</a>';
|
|
} else {
|
|
$output .= 'This page doesn\'t exist. More settings will be displayed here after this page is created.';
|
|
}
|
|
$output .= '
|
|
</div>
|
|
<div role="tabpanel" class="tab-pane" id="menu">';
|
|
$items = get_mangled_object_vars($this->get('config', 'menuItems'));
|
|
reset($items);
|
|
$first = key($items);
|
|
end($items);
|
|
$end = key($items);
|
|
$output .= '
|
|
<p class="subTitle">Website title</p>
|
|
<div class="change">
|
|
<div data-target="config" id="siteTitle" class="editText">' . $this->get('config',
|
|
'siteTitle') . '</div>
|
|
</div>
|
|
<p class="subTitle">Menu</p>
|
|
<div>
|
|
<div id="menuSettings" class="container-fluid">
|
|
<a class="menu-item-add wbtn wbtn-info cursorPointer" data-toggle="tooltip" id="menuItemAdd" title="Add new page"><i class="addNewIcon"></i> Add page</a><br><br>';
|
|
foreach ($items as $key => $value) {
|
|
$output .= '<div class="row">';
|
|
$output .= $this->renderSettingsMenuItem($key, $value, ($key === $first), ($key === $end), $value->slug);
|
|
if (property_exists($value, 'subpages')) {
|
|
$output .= $this->renderSettingsSubMenuItem($value->subpages, $key, $value->slug);
|
|
}
|
|
$output .= '</div>';
|
|
}
|
|
$output .= ' </div>
|
|
</div>
|
|
<p class="subTitle">Page to display on homepage</p>
|
|
<div class="change">
|
|
<select id="changeDefaultPage" class="wform-control" name="defaultPage">';
|
|
$items = $this->get('config', 'menuItems');
|
|
$defaultPage = $this->get('config', 'defaultPage');
|
|
foreach ($items as $item) {
|
|
$output .= $this->renderDefaultPageOptions($item, $defaultPage);
|
|
}
|
|
$output .= '
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div role="tabpanel" class="tab-pane" id="files">
|
|
<p class="subTitle">Upload</p>
|
|
<div class="change">
|
|
<form action="' . $this->getCurrentPageUrl() . '" method="post" enctype="multipart/form-data">
|
|
<div class="winput-group"><input type="file" name="uploadFile" class="wform-control">
|
|
<span class="winput-group-btn"><button type="submit" class="wbtn wbtn-info"><i class="uploadIcon"></i>Upload</button></span>
|
|
<input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<p class="subTitle marginTop20">Delete files</p>
|
|
<div class="change">';
|
|
foreach ($fileList as $file) {
|
|
$output .= '
|
|
<a href="' . self::url('?deleteModule=' . $file . '&type=files&token=' . $this->getToken()) . '" class="wbtn wbtn-sm wbtn-danger" onclick="return confirm(\'Delete ' . $file . '?\')" title="Delete file"><i class="deleteIcon"></i></a>
|
|
<span class="marginLeft5">
|
|
<a href="' . self::url('data/files/') . $file . '" class="normalFont" target="_blank">' . self::url('data/files/') . '<b class="fontSize21">' . $file . '</b></a>
|
|
</span>
|
|
<p></p>';
|
|
}
|
|
$output .= '
|
|
</div>
|
|
</div>';
|
|
$output .= $this->renderModuleTab();
|
|
$output .= $this->renderModuleTab('plugins');
|
|
$output .= ' <div role="tabpanel" class="tab-pane" id="security">
|
|
<p class="subTitle">Admin login URL</p>
|
|
<p class="change marginTop5 small danger">Important: save your login URL to log in to your website next time:<br/><b><span class="normalFont">' . self::url($this->get('config', 'login')) . '</b></span></p>
|
|
<div class="change">
|
|
<div data-target="config" id="login" class="editText">' . $this->get('config', 'login') . '</div>
|
|
</div>
|
|
<p class="subTitle">Site language config</p>
|
|
<p class="change marginTop5 small">HTML lang value for admin.</p>
|
|
<div class="change">
|
|
<div data-target="config" id="adminLang" class="editText">' . $this->get('config', 'adminLang') . '</div>
|
|
</div>
|
|
<p class="change marginTop5 small">HTML lang value for visitors.</p>
|
|
<div class="change">
|
|
<div data-target="config" id="siteLang" class="editText">' . $this->get('config', 'siteLang') . '</div>
|
|
</div>
|
|
<p class="subTitle">Password</p>
|
|
<div class="change">
|
|
<form action="' . $this->getCurrentPageUrl() . '" method="post">
|
|
<input type="password" name="old_password" class="wform-control normalFont" placeholder="Old password"><br>
|
|
<div class="winput-group">
|
|
<input type="password" name="new_password" class="wform-control normalFont" placeholder="New password"><span class="winput-group-btn"></span>
|
|
<input type="password" name="repeat_password" class="wform-control normalFont" placeholder="Repeat new password">
|
|
<span class="winput-group-btn"><button type="submit" class="wbtn wbtn-info"><i class="lockIcon"></i> Change password</button></span>
|
|
</div>
|
|
<input type="hidden" name="fieldname" value="password"><input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</form>
|
|
</div>
|
|
<p class="subTitle">Backup</p>
|
|
<div class="change">
|
|
<form action="' . $this->getCurrentPageUrl() . '" method="post">
|
|
<button type="submit" class="wbtn wbtn-block wbtn-info" name="backup"><i class="installIcon"></i> Backup website</button><input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</form>
|
|
</div>
|
|
<p class="text-right marginTop5"><a href="https://github.com/WonderCMS/wondercms/wiki/Restore-backup#how-to-restore-a-backup-in-3-steps" target="_blank"><i class="linkIcon"></i> How to restore backup</a></p>
|
|
|
|
<p class="subTitle">Save confirmation popup</p>
|
|
<p class="change small">If this is turned "ON", WonderCMS will always ask you to confirm any changes you make.</p>
|
|
<div class="change">
|
|
<form method="post">
|
|
<div class="wbtn-group wbtn-group-justified w-100">
|
|
<div class="wbtn-group w-50"><button type="submit" class="wbtn wbtn-info" name="saveChangesPopup" value="true">ON' . ($isSaveChangesPopupEnabled ? " (Selected)" : "") . '</button></div>
|
|
<div class="wbtn-group w-50"><button type="submit" class="wbtn wbtn-danger" name="saveChangesPopup" value="false">OFF' . (!$isSaveChangesPopupEnabled ? " (Selected)" : "") . '</button></div>
|
|
</div>
|
|
<input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</form>
|
|
</div>
|
|
|
|
<p class="subTitle">Login redirect</p>
|
|
<p class="change small">If this is set to "ON", when logging out, you will be redirected to the login page. If set to "OFF", you will be redirected to the last viewed page.</p>
|
|
<div class="change">
|
|
<form method="post">
|
|
<div class="wbtn-group wbtn-group-justified w-100">
|
|
<div class="wbtn-group w-50"><button type="submit" class="wbtn wbtn-info" name="logoutToLoginScreen" value="true">ON' . ($isLogoutToLoginScreenEnabled ? " (Selected)" : "") . '</button></div>
|
|
<div class="wbtn-group w-50"><button type="submit" class="wbtn wbtn-danger" name="logoutToLoginScreen" value="false">OFF' . (!$isLogoutToLoginScreenEnabled ? " (Selected)" : "") . '</button></div>
|
|
</div>
|
|
<input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</form>
|
|
</div>
|
|
|
|
<p class="subTitle">Force HTTPS</p>
|
|
<p class="change small">WonderCMS automatically checks for SSL, this will force to always use HTTPS.</p>
|
|
<div class="change">
|
|
<form method="post">
|
|
<div class="wbtn-group wbtn-group-justified w-100">
|
|
<div class="wbtn-group w-50"><button type="submit" class="wbtn wbtn-info" name="forceHttps" value="true" onclick="return confirm(\'Are you sure? This might break your website if you do not have SSL configured correctly.\')">ON' . ($isHttpsForced ? " (Selected)" : "") . '</button></div>
|
|
<div class="wbtn-group w-50"><button type="submit" class="wbtn wbtn-danger" name="forceHttps" value="false">OFF' . (!$isHttpsForced ? " (Selected)" : "") . '</button></div>
|
|
</div>
|
|
<input type="hidden" name="token" value="' . $this->getToken() . '">
|
|
</form>
|
|
</div>
|
|
<p class="text-right marginTop5"><a href="https://github.com/WonderCMS/wondercms/wiki/Better-security-mode-(HTTPS-and-other-features)#important-read-before-turning-this-feature-on" target="_blank"><i class="linkIcon"></i> Read more before enabling</a></p>';
|
|
$output .= $this->renderAdminLoginIPs();
|
|
$output .= '
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer clear">
|
|
<p class="small">
|
|
<a href="https://wondercms.com" target="_blank">WonderCMS ' . VERSION . '</a>
|
|
<b><a href="https://wondercms.com/news" target="_blank">News</a>
|
|
<a href="https://wondercms.com/community" target="_blank">Community</a>
|
|
<a href="https://github.com/WonderCMS/wondercms/wiki#wondercms-documentation" target="_blank">Docs</a>
|
|
<a href="https://wondercms.com/donate" target="_blank">Donate</a>
|
|
<a href="https://swag.wondercms.com" target="_blank">Shop/Merch</a></b>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
return $this->hook('settings', $output)[0];
|
|
}
|
|
|
|
/**
|
|
* Render options for default page selection
|
|
*
|
|
* @param object $menuItem
|
|
* @param string $defaultPage
|
|
* @param string $parentSlug
|
|
* @param string $parentName
|
|
* @return string
|
|
*/
|
|
private function renderDefaultPageOptions(
|
|
object $menuItem,
|
|
string $defaultPage,
|
|
string $parentSlug = '',
|
|
string $parentName = ''
|
|
): string {
|
|
$slug = $parentSlug ? sprintf('%s/%s', $parentSlug, $menuItem->slug) : $menuItem->slug;
|
|
$name = $parentName ? sprintf('%s | %s', $parentName, $menuItem->name) : $menuItem->name;
|
|
$output = '<option value="' . $slug . '" ' . ($slug === $defaultPage ? 'selected' : '') . '>' . $name . '</option>';
|
|
|
|
foreach ($menuItem->subpages ?? [] as $subpage) {
|
|
$output .= $this->renderDefaultPageOptions($subpage, $defaultPage, $slug, $name);
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Render page navigation items
|
|
*
|
|
* @param object $item
|
|
* @param string $parentSlug
|
|
* @return string
|
|
*/
|
|
private function renderPageNavMenuItem(object $item, string $parentSlug = ''): string
|
|
{
|
|
$subpages = $visibleSubpage = false;
|
|
if (property_exists($item, 'subpages') && !empty((array)$item->subpages)) {
|
|
$subpages = $item->subpages;
|
|
$visibleSubpage = $subpages && in_array('show', array_column((array)$subpages, 'visibility'));
|
|
}
|
|
|
|
$parentSlug .= $subpages ? $item->slug . '/' : $item->slug;
|
|
$output = '<li class="nav-item ' . ($this->currentPage === $item->slug ? 'active ' : '') . ($visibleSubpage ? 'subpage-nav' : '') . '">
|
|
<a class="nav-link" href="' . self::url($parentSlug) . '">' . $item->name . '</a>';
|
|
|
|
// Recursive method for rendering infinite subpages
|
|
if ($visibleSubpage) {
|
|
$output .= '<ul class="subPageDropdown">';
|
|
foreach ($subpages as $subpage) {
|
|
if ($subpage->visibility === 'hide') {
|
|
continue;
|
|
}
|
|
$output .= $this->renderPageNavMenuItem($subpage, $parentSlug);
|
|
}
|
|
$output .= '</ul>';
|
|
}
|
|
|
|
$output .= '</li>';
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Render menu item for settings
|
|
*
|
|
* @param string $menuKeyTree
|
|
* @param object $value
|
|
* @param bool $isFirstEl
|
|
* @param bool $isLastEl
|
|
* @param string $slugTree
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
private function renderSettingsMenuItem(
|
|
string $menuKeyTree,
|
|
object $value,
|
|
bool $isFirstEl,
|
|
bool $isLastEl,
|
|
string $slugTree
|
|
): string {
|
|
$arraySlugTree = explode('/', $slugTree);
|
|
array_shift($arraySlugTree);
|
|
$subMenuLevel = count($arraySlugTree);
|
|
$output = '<div class="coll-xs-2 coll-sm-1">
|
|
<i class="menu-toggle eyeIcon' . ($value->visibility === 'show' ? ' eyeShowIcon menu-item-hide' : ' eyeHideIcon menu-item-show') . '" data-toggle="tooltip" title="' . ($value->visibility === 'show' ? 'Hide page from menu' : 'Show page in menu') . '" data-menu="' . $menuKeyTree . '"></i>
|
|
</div>
|
|
<div class="coll-xs-4 coll-md-7">
|
|
<div data-target="menuItemUpdate" data-menu="' . $menuKeyTree . '" data-visibility="' . $value->visibility . '" id="menuItems-' . $menuKeyTree . '" class="editText" style="margin-right: ' . (13.1 * $subMenuLevel) . 'px;">' . $value->name . '</div>
|
|
</div>
|
|
<div class="coll-xs-6 coll-md-4 text-right">';
|
|
|
|
if (!$isFirstEl) {
|
|
$output .= '<a class="arrowIcon upArrowIcon toolbar menu-item-up cursorPointer" data-toggle="tooltip" data-menu="' . $menuKeyTree . '" data-menu-slug="' . $value->slug . '" title="Move up"></a>';
|
|
}
|
|
if (!$isLastEl) {
|
|
$output .= '<a class="arrowIcon downArrowIcon toolbar menu-item-down cursorPointer" data-toggle="tooltip" data-menu="' . $menuKeyTree . '" data-menu-slug="' . $value->slug . '" title="Move down"></a>';
|
|
}
|
|
$output .= ' <a class="linkIcon" href="' . self::url($slugTree) . '" title="Visit page" style="display: inline;">visit</a>
|
|
</div>
|
|
<div class="coll-xs-12 text-right marginTop5 marginBottom20">
|
|
<a class="menu-item-add wbtn wbtn-sm wbtn-info cursorPointer" data-toggle="tooltip" data-menu="' . $menuKeyTree . '" title="Add new sub-page"><i class="addNewIcon"></i> Add subpage</a>
|
|
<a href="' . self::url('?delete=' . urlencode($slugTree) . '&token=' . $this->getToken()) . '" title="Delete page" class="wbtn wbtn-sm wbtn-danger" data-menu="' . $menuKeyTree . '" onclick="return confirm(\'Delete ' . $value->slug . '?\')"><i class="deleteIcon"></i></a>
|
|
</div>';
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Render sub menu item for settings
|
|
*
|
|
* @param object $subpages
|
|
* @param string $parentKeyTree
|
|
* @param string $parentSlugTree
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
private function renderSettingsSubMenuItem(object $subpages, string $parentKeyTree, string $parentSlugTree): string
|
|
{
|
|
$subpages = get_mangled_object_vars($subpages);
|
|
reset($subpages);
|
|
$firstSubpage = key($subpages);
|
|
end($subpages);
|
|
$endSubpage = key($subpages);
|
|
$output = '';
|
|
|
|
foreach ($subpages as $subpageKey => $subpage) {
|
|
$keyTree = $parentKeyTree . '-' . $subpageKey;
|
|
$slugTree = $parentSlugTree . '/' . $subpage->slug;
|
|
$output .= '<div class="coll-xs-offset-1 coll-xs-11">
|
|
<div class="row marginTop5">';
|
|
$firstElement = ($subpageKey === $firstSubpage);
|
|
$lastElement = ($subpageKey === $endSubpage);
|
|
$output .= $this->renderSettingsMenuItem($keyTree, $subpage, $firstElement, $lastElement, $slugTree);
|
|
|
|
// Recursive method for rendering infinite subpages
|
|
if (property_exists($subpage, 'subpages')) {
|
|
$output .= $this->renderSettingsSubMenuItem($subpage->subpages, $keyTree, $slugTree);
|
|
}
|
|
$output .= ' </div>
|
|
</div>';
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Render last login IPs
|
|
* @return string
|
|
*/
|
|
private function renderAdminLoginIPs(): string
|
|
{
|
|
$getIPs = $this->get('config', 'lastLogins') ?? [];
|
|
$renderIPs = '';
|
|
foreach ($getIPs as $time => $adminIP) {
|
|
$renderIPs .= sprintf('%s - %s<br />', date('M d, Y H:i:s', strtotime($time)), $adminIP);
|
|
}
|
|
return '<p class="subTitle">Last 5 logins</p>
|
|
<div class="change">
|
|
' . $renderIPs . '
|
|
</div>';
|
|
}
|
|
|
|
/**
|
|
* Render Plugins/Themes cards
|
|
* @param string $type
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
private function renderModuleTab(string $type = 'themes'): string
|
|
{
|
|
$output = '<div role="tabpanel" class="tab-pane" id="' . $type . '">
|
|
<a class="wbtn wbtn-info wbtn-sm pull-right float-right marginTop20 marginBottom20" data-loader-id="cache" href="' . self::url('?manuallyResetCacheData=true&token=' . $this->getToken()) . '" title="Check updates" onclick="localStorage.clear();"><i class="refreshIcon" aria-hidden="true"></i> Check for updates</a>
|
|
<div class="clear"></div>
|
|
<div class="change row custom-cards">';
|
|
$defaultImage = '<svg style="max-width: 100%;" xmlns="http://www.w3.org/2000/svg" width="100%" height="140"><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="#ddd">No preview</text></svg>';
|
|
$updates = $exists = $installs = '';
|
|
foreach ($this->listAllModules($type) as $addonType => $addonModules) {
|
|
foreach ($addonModules as $directoryName => $addon) {
|
|
$name = $addon['name'];
|
|
$info = $addon['summary'];
|
|
$infoUrl = $addon['repo'];
|
|
$currentVersion = $addon['currentVersion'] ? sprintf('Installed version: %s',
|
|
$addon['currentVersion']) : '';
|
|
$isThemeSelected = $this->get('config', 'theme') === $directoryName;
|
|
|
|
$image = $addon['image'] !== null ? '<a class="text-center center-block" href="' . $addon['image'] . '" target="_blank"><img style="max-width: 100%; max-height: 250px;" src="' . $addon['image'] . '" alt="' . $name . '" /></a>' : $defaultImage;
|
|
$installButton = $addon['install'] ? '<a class="wbtn wbtn-success wbtn-block wbtn-sm" href="' . self::url('?installModule=' . $directoryName . '&type=' . $type . '&token=' . $this->getToken()) . '" title="Install"><i class="installIcon"></i> Install</a>' : '';
|
|
$updateButton = !$addon['install'] && $addon['update'] ? '<a class="wbtn wbtn-info wbtn-sm wbtn-block marginTop5" href="' . self::url('?installModule=' . $directoryName . '&type=' . $type . '&token=' . $this->getToken()) . '" title="Update"><i class="refreshIcon"></i> Update to ' . $addon['version'] . '</a>' : '';
|
|
$removeButton = !$addon['install'] ? '<a class="wbtn wbtn-danger wbtn-sm marginTop5" href="' . self::url('?deleteModule=' . $directoryName . '&type=' . $type . '&token=' . $this->getToken()) . '" onclick="return confirm(\'Remove ' . $name . '?\')" title="Remove"><i class="deleteIcon"></i></a>' : '';
|
|
$inactiveThemeButton = $type === 'themes' && !$addon['install'] && !$isThemeSelected ? '<a class="wbtn wbtn-primary wbtn-sm wbtn-block" href="' . self::url('?selectModule=' . $directoryName . '&type=' . $type . '&token=' . $this->getToken()) . '" onclick="return confirm(\'Activate ' . $name . ' theme?\')"><i class="checkmarkIcon"></i> Activate</a>' : '';
|
|
$activeThemeButton = $type === 'themes' && !$addon['install'] && $isThemeSelected ? '<a class="wbtn wbtn-primary wbtn-sm wbtn-block" disabled>Active</a>' : '';
|
|
|
|
$html = "<div class='coll-sm-4'>
|
|
<div>
|
|
$image
|
|
<h4>$name</h4>
|
|
<p class='normalFont'>$info</p>
|
|
<p class='text-right small normalFont marginTop20'>$currentVersion<br /><a href='$infoUrl' target='_blank'><i class='linkIcon'></i> More info</a></p>
|
|
<div class='text-right'>$inactiveThemeButton $activeThemeButton</div>
|
|
<div class='text-left'>$installButton</div>
|
|
<div class='text-right'><span class='text-left bold'>$updateButton</span> <span class='text-right'>$removeButton</span></div>
|
|
</div>
|
|
</div>";
|
|
|
|
switch ($addonType) {
|
|
case self::THEME_PLUGINS_TYPES['updates']:
|
|
$updates .= $html;
|
|
break;
|
|
case self::THEME_PLUGINS_TYPES['exists']:
|
|
$exists .= $html;
|
|
break;
|
|
case self::THEME_PLUGINS_TYPES['installs']:
|
|
default:
|
|
$installs .= $html;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$output .= $updates;
|
|
$output .= $exists;
|
|
$output .= $installs;
|
|
$output .= '</div>
|
|
<p class="subTitle">Custom module</p>
|
|
<form action="' . $this->getCurrentPageUrl() . '" method="post" onsubmit="recheckPasswordPrompt(event)">
|
|
<div class="wform-group">
|
|
<div class="change winput-group marginTop5"><input type="text" name="pluginThemeUrl" class="wform-control normalFont" placeholder="Enter full URL to wcms-modules.json file">
|
|
<span class="winput-group-btn"><button type="submit" class="wbtn wbtn-info" onclick="return confirm(\'Adding unknown modules can be VERY dangerous, are you sure you want to continue?\')"><i class="addNewIcon"></i> Add</button></span>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="token" value="' . $this->getToken() . '" /><input type="hidden" name="pluginThemeType" value="' . $type . '" /><input type="hidden" name="password_recheck" />
|
|
</form>
|
|
<p class="text-right"><a href="https://github.com/WonderCMS/wondercms/wiki/Custom-modules" target="_blank"><i class="linkIcon"></i> Read more about custom modules</a></p>
|
|
</div>';
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Slugify page
|
|
*
|
|
* @param string $text for slugifying
|
|
* @return string
|
|
*/
|
|
public function slugify(string $text): string
|
|
{
|
|
$text = preg_replace('~[^\\pL\d]+~u', '-', $text);
|
|
$text = trim(htmlspecialchars(mb_strtolower($text), ENT_QUOTES), '/');
|
|
$text = trim($text, '-');
|
|
return empty($text) ? '-' : $text;
|
|
}
|
|
|
|
/**
|
|
* Delete something from database
|
|
* Has variadic arguments
|
|
* @return void
|
|
*/
|
|
public function unset(): void
|
|
{
|
|
$numArgs = func_num_args();
|
|
$args = func_get_args();
|
|
switch ($numArgs) {
|
|
case 1:
|
|
unset($this->db->{$args[0]});
|
|
break;
|
|
case 2:
|
|
unset($this->db->{$args[0]}->{$args[1]});
|
|
break;
|
|
case 3:
|
|
unset($this->db->{$args[0]}->{$args[1]}->{$args[2]});
|
|
break;
|
|
case 4:
|
|
unset($this->db->{$args[0]}->{$args[1]}->{$args[2]}->{$args[3]});
|
|
break;
|
|
}
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Update WonderCMS
|
|
* Overwrites index.php with latest version from GitHub
|
|
* @return void
|
|
*/
|
|
public function updateAction(): void
|
|
{
|
|
if (!isset($_POST['update']) || !$this->verifyFormActions()) {
|
|
return;
|
|
}
|
|
$contents = $this->getFileFromRepo('index.php');
|
|
if ($contents) {
|
|
file_put_contents(__FILE__, $contents);
|
|
$this->alert('success', 'WonderCMS successfully updated. Wohoo!');
|
|
$this->redirect();
|
|
}
|
|
$this->alert('danger', 'Something went wrong. Could not update WonderCMS.');
|
|
$this->redirect();
|
|
}
|
|
|
|
/**
|
|
* Upload file to files folder
|
|
* List of allowed extensions
|
|
* @return void
|
|
*/
|
|
public function uploadFileAction(): void
|
|
{
|
|
if (!isset($_FILES['uploadFile']) || !$this->verifyFormActions()) {
|
|
return;
|
|
}
|
|
$allowedMimeTypes = [
|
|
'video/avi',
|
|
'text/css',
|
|
'text/x-asm',
|
|
'application/msword',
|
|
'application/vnd.ms-word',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'video/x-flv',
|
|
'image/gif',
|
|
'text/html',
|
|
'image/x-icon',
|
|
'image/jpeg',
|
|
'application/octet-stream',
|
|
'audio/mp4',
|
|
'video/x-matroska',
|
|
'video/quicktime',
|
|
'audio/mpeg',
|
|
'video/mp4',
|
|
'video/mpeg',
|
|
'application/vnd.oasis.opendocument.spreadsheet',
|
|
'application/vnd.oasis.opendocument.text',
|
|
'application/ogg',
|
|
'video/ogg',
|
|
'application/pdf',
|
|
'image/png',
|
|
'application/vnd.ms-powerpoint',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
'application/photoshop',
|
|
'application/rar',
|
|
'image/svg',
|
|
'image/svg+xml',
|
|
'image/avif',
|
|
'image/webp',
|
|
'application/svg+xm',
|
|
'text/plain',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'video/webm',
|
|
'video/x-ms-wmv',
|
|
'application/zip',
|
|
];
|
|
|
|
$allowedExtensions = [
|
|
'avi',
|
|
'avif',
|
|
'css',
|
|
'doc',
|
|
'docx',
|
|
'flv',
|
|
'gif',
|
|
'htm',
|
|
'html',
|
|
'ico',
|
|
'jpeg',
|
|
'jpg',
|
|
'kdbx',
|
|
'm4a',
|
|
'mkv',
|
|
'mov',
|
|
'mp3',
|
|
'mp4',
|
|
'mpg',
|
|
'ods',
|
|
'odt',
|
|
'ogg',
|
|
'ogv',
|
|
'pdf',
|
|
'png',
|
|
'ppt',
|
|
'pptx',
|
|
'psd',
|
|
'rar',
|
|
'svg',
|
|
'txt',
|
|
'xls',
|
|
'xlsx',
|
|
'webm',
|
|
'webp',
|
|
'wmv',
|
|
'zip',
|
|
];
|
|
if (!isset($_FILES['uploadFile']['error']) || is_array($_FILES['uploadFile']['error'])) {
|
|
$this->alert('danger', 'Invalid parameters.');
|
|
$this->redirect();
|
|
}
|
|
switch ($_FILES['uploadFile']['error']) {
|
|
case UPLOAD_ERR_OK:
|
|
break;
|
|
case UPLOAD_ERR_NO_FILE:
|
|
$this->alert('danger',
|
|
'No file selected. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#files"><b>Re-open file options</b></a>');
|
|
$this->redirect();
|
|
break;
|
|
case UPLOAD_ERR_INI_SIZE:
|
|
case UPLOAD_ERR_FORM_SIZE:
|
|
$this->alert('danger',
|
|
'File too large. Change maximum upload size limit or contact your host. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#files"><b>Re-open file options</b></a>');
|
|
$this->redirect();
|
|
break;
|
|
default:
|
|
$this->alert('danger', 'Unknown error.');
|
|
$this->redirect();
|
|
}
|
|
$mimeType = '';
|
|
$fileName = basename(str_replace(
|
|
['"', "'", '*', '<', '>', '%22', ''', '%', ';', '#', '&', './', '../', '/', '+'],
|
|
'',
|
|
htmlspecialchars(strip_tags($_FILES['uploadFile']['name']))
|
|
));
|
|
$nameExploded = explode('.', $fileName);
|
|
$ext = strtolower(array_pop($nameExploded));
|
|
|
|
if (class_exists('finfo')) {
|
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
|
$mimeType = $finfo->file($_FILES['uploadFile']['tmp_name']);
|
|
} elseif (function_exists('mime_content_type')) {
|
|
$mimeType = mime_content_type($_FILES['uploadFile']['tmp_name']);
|
|
} elseif (array_key_exists($ext, $allowedExtensions)) {
|
|
$mimeType = $allowedExtensions[$ext];
|
|
}
|
|
if (!in_array($mimeType, $allowedMimeTypes, true) || !in_array($ext, $allowedExtensions)) {
|
|
$this->alert('danger',
|
|
'File format is not allowed. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#files"><b>Re-open file options</b></a>');
|
|
$this->redirect();
|
|
}
|
|
if (!move_uploaded_file($_FILES['uploadFile']['tmp_name'], $this->filesPath . '/' . $fileName)) {
|
|
$this->alert('danger', 'Failed to move uploaded file.');
|
|
}
|
|
$this->alert('success',
|
|
'File uploaded. <a data-toggle="wcms-modal" href="#settingsModal" data-target-tab="#files"><b>Open file options to see your uploaded file</b></a>');
|
|
$this->redirect();
|
|
}
|
|
|
|
/**
|
|
* Get canonical URL
|
|
*
|
|
* @param string $location
|
|
* @return string
|
|
*/
|
|
public static function url(string $location = ''): string
|
|
{
|
|
$showHttps = Wcms::isCurrentlyOnSSL();
|
|
$dataPath = sprintf('%s/%s', __DIR__, 'data');
|
|
$securityCachePath = sprintf('%s/%s', $dataPath, 'security.json');
|
|
|
|
if (is_file($securityCachePath) && file_exists($securityCachePath)) {
|
|
$securityCache = json_decode(file_get_contents($securityCachePath), true);
|
|
$showHttps = $securityCache['forceHttps'] ?? false;
|
|
}
|
|
|
|
$serverPort = ((($_SERVER['SERVER_PORT'] == '80') || ($_SERVER['SERVER_PORT'] == '443')) ? '' : ':' . $_SERVER['SERVER_PORT']);
|
|
return ($showHttps ? 'https' : 'http')
|
|
. '://' . ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'])
|
|
. ($_SERVER['HTTP_HOST'] ? '' : $serverPort)
|
|
. ((dirname($_SERVER['SCRIPT_NAME']) === '/') ? '' : dirname($_SERVER['SCRIPT_NAME']))
|
|
. '/' . $location;
|
|
}
|
|
|
|
/**
|
|
* Create a ZIP backup of whole WonderCMS installation (all files)
|
|
*
|
|
* @return void
|
|
*/
|
|
public function zipBackup(): void
|
|
{
|
|
try {
|
|
$randomNumber = random_bytes(8);
|
|
} catch (Exception $e) {
|
|
$randomNumber = microtime(false);
|
|
}
|
|
$zipName = date('Y-m-d') . '-backup-' . bin2hex($randomNumber) . '.zip';
|
|
$zipPath = $this->rootDir . '/data/files/' . $zipName;
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
|
|
$this->alert('danger', 'Cannot create ZIP archive.');
|
|
}
|
|
$iterator = new RecursiveDirectoryIterator($this->rootDir);
|
|
$iterator->setFlags(RecursiveDirectoryIterator::SKIP_DOTS);
|
|
$files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
|
|
foreach ($files as $file) {
|
|
$file = realpath($file);
|
|
$source = realpath($this->rootDir);
|
|
if (is_dir($file)) {
|
|
$zip->addEmptyDir(str_replace($source . '/', '', $file . '/'));
|
|
} elseif (is_file($file)) {
|
|
$zip->addFromString(str_replace($source . '/', '', $file), file_get_contents($file));
|
|
}
|
|
}
|
|
$zip->close();
|
|
$this->redirect('data/files/' . $zipName);
|
|
}
|
|
|
|
/**
|
|
* Check if currently user is on https
|
|
* @return bool
|
|
*/
|
|
public static function isCurrentlyOnSSL(): bool
|
|
{
|
|
return (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on')
|
|
|| (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) === 'on')
|
|
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https');
|
|
}
|
|
|
|
/**
|
|
* Check compatibility
|
|
*/
|
|
private function checkMinimumRequirements(): void
|
|
{
|
|
if (PHP_VERSION_ID <= 70200) {
|
|
die('<p>To run WonderCMS, PHP version 7.2 or greater is required.</p>');
|
|
}
|
|
$extensions = ['curl', 'zip', 'mbstring'];
|
|
$missingExtensions = [];
|
|
foreach ($extensions as $ext) {
|
|
if (!extension_loaded($ext)) {
|
|
$missingExtensions[] = $ext;
|
|
}
|
|
}
|
|
if (!empty($missingExtensions)) {
|
|
die('<p>The following extensions are required: '
|
|
. implode(', ', $missingExtensions)
|
|
. '. Contact your host or configure your server to enable them with correct permissions.</p>');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper for reseting the index key of the object
|
|
* @param stdClass $object
|
|
* @return stdClass
|
|
*/
|
|
private function reindexObject(stdClass $object): stdClass
|
|
{
|
|
$reindexObject = new stdClass;
|
|
$index = 0;
|
|
foreach ($object as $value) {
|
|
$reindexObject->{$index} = $value;
|
|
$index++;
|
|
}
|
|
return $reindexObject;
|
|
}
|
|
|
|
/**
|
|
* Check if user has forced https
|
|
* @return bool
|
|
*/
|
|
private function isHttpsForced(): bool
|
|
{
|
|
$value = $this->get('config', 'forceHttps');
|
|
if (gettype($value) === 'object' && empty(get_object_vars($value))) {
|
|
return false;
|
|
}
|
|
|
|
return $value ?? false;
|
|
}
|
|
|
|
/**
|
|
* Check if user has confirmation dialog enabled
|
|
* @return bool
|
|
*/
|
|
private function isSaveChangesPopupEnabled(): bool
|
|
{
|
|
$value = $this->get('config', 'saveChangesPopup');
|
|
if (gettype($value) === 'object' && empty(get_object_vars($value))) {
|
|
return false;
|
|
}
|
|
|
|
return $value ?? false;
|
|
}
|
|
|
|
/**
|
|
* Check if admin will be redirected to the login screen or current page screen after logout.
|
|
* @return bool
|
|
*/
|
|
private function isLogoutToLoginScreenEnabled(): bool
|
|
{
|
|
$value = $this->get('config', 'logoutToLoginScreen');
|
|
if (gettype($value) === 'object' && empty(get_object_vars($value))) {
|
|
return true;
|
|
}
|
|
|
|
return $value ?? true;
|
|
}
|
|
}
|