From bb277f222f8f644f3b8d2f7e99f9540ab2417114 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Thu, 8 Aug 2024 18:24:07 +0200 Subject: [PATCH 01/19] Update README.md Update broken badges. --- .github/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/README.md b/.github/README.md index 375cc6f..4813d62 100644 --- a/.github/README.md +++ b/.github/README.md @@ -6,12 +6,12 @@ 5 files • 48KB zip - 1 step install -

Docs -Project -Maintained -License -Number of downloads since first release on GitHub -Donate +

+Project +Maintained +License +Number of downloads since first release on GitHub +Donate

WonderCMS is an extremely small flat file CMS. It's fast, responsive and doesn't require any configuration.

From 87be069d52a7f1c0cd7c8e4d7dae01289c160e0a Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sat, 16 Nov 2024 18:11:32 +0100 Subject: [PATCH 02/19] Update README.md Added Sponsor button --- .github/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 4813d62..d47378d 100644 --- a/.github/README.md +++ b/.github/README.md @@ -6,7 +6,11 @@ 5 files • 48KB zip - 1 step install -

+

+ + +
+ Project Maintained License From 10db92feae0e3cf8829cb269b9e036faf34bb468 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Tue, 7 Jan 2025 21:18:52 +0100 Subject: [PATCH 03/19] Update README.md Update maintained to 2025. --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index d47378d..8bdea84 100644 --- a/.github/README.md +++ b/.github/README.md @@ -12,7 +12,7 @@
Project -Maintained +Maintained License Number of downloads since first release on GitHub Donate From 5f5e50bcdd7c665e129841478a3ef45d750aaa8e Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Tue, 7 Jan 2025 21:19:21 +0100 Subject: [PATCH 04/19] Update license year Update license year to 2025 --- license | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/license b/license index 8bc7523..62b3cf1 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Robert Isoski and WonderCMS contributors +Copyright (c) 2025 Robert Isoski and WonderCMS contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c1fae531214aee14db7ff773dd424e1e45e9bb4c Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Mon, 3 Mar 2025 21:41:07 +0100 Subject: [PATCH 05/19] WonderCMS 3.5.0 WonderCMS 3.5.0 1. Search - works for all content and blog plugin by calling search() ?> in theme (has to be styled) - has hook, is modifiable. - https://github.com/WonderCMS/wondercms/issues/237 2. Option to display Blog page as default page. - Available in Settings -> Menu - https://github.com/WonderCMS/wondercms/issues/308 3. Modal window persistence (settings) - user can choose to make settings modal popup persistent (re-opens on last tab that had any changed) - can be activated in settings-security. - https://github.com/WonderCMS/wondercms/issues/320 4. Updated to newest admin.js.min library with included modal persistence functionality. - changes to unminified version: https://github.com/WonderCMS/wondercms-cdn-files/blob/350/wcms-admin.js#L293 5. Pages have now 3 new parameters: - created (time of creation) - modified (time when last modified) - visibility (inherited and synced with menuItems) - https://github.com/WonderCMS/wondercms/issues/307 6. Added header block, which can be called in the theme. - can be called with header() ?> - header includes a hook, is modifiable - https://github.com/WonderCMS/wondercms/issues/320 7. Added hooks on login (supports banning IPs, 2FA implementations) - https://github.com/WonderCMS/wondercms/issues/302 8. renderPageNavMenuItem is now hookable: https://github.com/WonderCMS/wondercms/issues/297 "Nearby future" todo: - fix blog plugin to avoid duplicate entries - include Romanian translation - figure out a way to display files uploaded files in Settings to be displayed in Summernote - contact form data should not be deleted after update --- index.php | 417 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 406 insertions(+), 11 deletions(-) diff --git a/index.php b/index.php index 1fa5736..9ee0719 100644 --- a/index.php +++ b/index.php @@ -7,7 +7,7 @@ */ session_start(); -define('VERSION', '3.4.3'); +define('VERSION', '3.5.0'); mb_internal_encoding('UTF-8'); if (defined('PHPUNIT_TESTING') === false) { @@ -138,6 +138,7 @@ public function init(): void { $this->forceSSL(); $this->loginStatus(); + $this->searchAction(); $this->getSiteLanguage(); $this->pageStatus(); $this->logoutAction(); @@ -154,12 +155,14 @@ public function init(): void $this->backupAction(); $this->forceHttpsAction(); $this->saveChangesPopupAction(); + $this->modalPersistence(); $this->saveLogoutToLoginScreenAction(); $this->deletePageAction(); $this->saveAction(); $this->updateAction(); $this->uploadFileAction(); $this->notifyAction(); + $this->syncPageVisibility(); } } @@ -352,6 +355,20 @@ public function saveChangesPopupAction(): void } } + /** + * Save if modal should persist/reopen after changes are made on a specific tab + * @return void + * @throws Exception + */ + public function modalPersistence(): void + { + if (isset($_POST['modalPersistence']) && $this->verifyFormActions()) { + $this->set('config', 'modalPersistence', $_POST['modalPersistence'] === 'true'); + $this->alert('success', 'Modal settings persitence settings changed.'); + $this->redirect(); + } + } + /** * Save if admin should be redirected to login/last viewed page after logging out. * @return void @@ -469,13 +486,15 @@ public function createDb(): void self::DB_CONFIG => [ 'siteTitle' => 'Website title', 'siteLang' => 'en', - 'adminLang' => 'en', + 'adminLang' => 'en', 'theme' => 'sky', 'defaultPage' => 'home', 'login' => 'loginURL', 'forceLogout' => false, 'forceHttps' => false, 'saveChangesPopup' => false, + 'modalPersistence' => false, + 'logoutToLoginScreen' => true, 'password' => password_hash($password, PASSWORD_DEFAULT), 'lastLogins' => [], 'lastModulesSync' => null, @@ -497,6 +516,9 @@ public function createDb(): void ], 'pages' => [ '404' => [ + 'created' => date('c'), + 'modified' => date('c'), + 'visibility' => 'show', 'title' => '404', 'keywords' => '404', 'description' => '404', @@ -504,6 +526,9 @@ public function createDb(): void self::DB_PAGES_SUBPAGE_KEY => new stdClass() ], 'home' => [ + 'created' => date('c'), + 'modified' => date('c'), + 'visibility' => 'show', 'title' => 'Home', 'keywords' => 'Enter, page, keywords, for, search, engines', 'description' => 'A page description is also good for search engines.', @@ -517,6 +542,9 @@ public function createDb(): void self::DB_PAGES_SUBPAGE_KEY => new stdClass() ], 'how-to' => [ + 'created' => date('c'), + 'modified' => date('c'), + 'visibility' => 'show', 'title' => 'How to', 'keywords' => 'Enter, keywords, for, this page', 'description' => 'A page description is also good for search engines.', @@ -543,6 +571,9 @@ public function createDb(): void

Website description, contact form, mini map or anything else.

This editable area is visible on all pages.

' ], + 'header' => [ + 'content' => '' + ], 'footer' => [ 'content' => '©' . date('Y') . ' Your website' ] @@ -612,9 +643,11 @@ public function createMenuItem( if ($createPage) { $this->createPage($slugTree); + $this->syncPageVisibility(); $_SESSION['redirect_to_name'] = $name; $_SESSION['redirect_to'] = implode('/', $slugTree); } + } /** @@ -760,6 +793,8 @@ public function createPage(array $slugTree = null, bool $createMenuItem = false) $pageTitle = !$slug ? str_replace('-', ' ', $pageSlug) : $pageSlug; $selectedPage->{$slug} = new stdClass; + $selectedPage->{$slug}->created = date('c'); + $selectedPage->{$slug}->modified = date('c'); $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.'; @@ -818,6 +853,7 @@ public function updatePage(array $slugTree, string $fieldname, string $content): } $selectedPage->{$slug}->{$fieldname} = $content; + $selectedPage->{$slug}->modified = date('c'); // Update modification time $this->set(self::DB_PAGES_KEY, $allPages); } @@ -888,6 +924,7 @@ public function updatePageSlug(array $slugTree, string $newSlugName): void } $selectedPage->{$newSlugName} = $selectedPage->{$slug}; + $selectedPage->{$newSlugName}->modified = date('c'); unset($selectedPage->{$slug}); $this->save(); } @@ -907,6 +944,26 @@ public function css(): string return $this->hook('css', '')[0]; } + /** + * Get header content, make it editable and show login link if set to default + * @return string + * @throws Exception + */ + public function header(): string + { + if ($this->loggedIn) { + $output = ''; + } else { + $output = $this->get('blocks', 'header')->content . + (!$this->loggedIn && $this->get('config', 'login') === 'loginURL' + ? ' Login' + : ''); + } + return $this->hook('header', $output)[0]; + } + /** * Get database content * @return stdClass @@ -1259,6 +1316,39 @@ public function checkModulesCache(): void } } + /** + * Synchronize the visibility of pages with their corresponding menu items + * @return void + */ + private function syncPageVisibility(): void + { + $pages = clone $this->get(self::DB_PAGES_KEY); + $menuItems = $this->get(self::DB_CONFIG, self::DB_MENU_ITEMS); + + // Recursive function to sync visibility through hierarchy + $syncVisibility = function($menuNode, $pageNode) use (&$syncVisibility) { + foreach ($menuNode as $menuItem) { + // Find matching page in hierarchy + if (property_exists($pageNode, $menuItem->slug)) { + $pageNode->{$menuItem->slug}->visibility = $menuItem->visibility; + + // Recursively sync subpages + if (property_exists($menuItem, 'subpages') && + property_exists($pageNode->{$menuItem->slug}, 'subpages')) { + $syncVisibility( + $menuItem->subpages, + $pageNode->{$menuItem->slug}->subpages + ); + } + } + } + }; + + $syncVisibility($menuItems, $pages); + $this->set(self::DB_PAGES_KEY, $pages); + $this->save(); + } + /** * Retrieve cached Themes/Plugins data * @param string $type @@ -1663,7 +1753,7 @@ public function js(): string $scripts = << - + EOT; $scripts .= ''; $scripts .= ''; @@ -1717,8 +1807,9 @@ public function loadThemeAndFunctions(): void require_once file_exists($customPageTemplate) ? $customPageTemplate : $location . '/theme.php'; } + /** - * Admin login verification + * Handle admin login verification with success/failure hooks * @return void * @throws Exception */ @@ -1733,17 +1824,37 @@ public function loginAction(): void if ($_SERVER['REQUEST_METHOD'] !== 'POST') { return; } + $password = $_POST['password'] ?? ''; - if (password_verify($password, $this->get('config', 'password'))) { + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + $timestamp = date('c'); + $valid = password_verify($password, $this->get('config', 'password')); + + if ($valid) { + // Success hook before any redirects + $this->hook('login_success', [ + 'ip' => $ip, + 'timestamp' => $timestamp, + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null + ]); + session_regenerate_id(true); $_SESSION['loggedIn'] = true; $_SESSION['rootDir'] = $this->rootDir; $this->set('config', 'forceLogout', false); $this->saveAdminLoginIP(); $this->redirect(); + } else { + // Failure hook before showing error + $this->hook('login_failed', [ + 'ip' => $ip, + 'timestamp' => $timestamp, + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null + ]); + + $this->alert('danger', 'Invalid login credentials'); + $this->redirect($this->get('config', 'login')); } - $this->alert('test', '', 1); - $this->redirect($this->get('config', 'login')); } /** @@ -1952,6 +2063,7 @@ public function updateMenuItemVisibility(string $visibility, string $menu): void $menuSelectionObject->visibility = $visibility; $this->set(self::DB_CONFIG, self::DB_MENU_ITEMS, $menuItems); + $this->syncPageVisibility(); } /** @@ -2053,6 +2165,11 @@ public function getPageData(string $slugTree): ?object } } + if ($pageData && !property_exists($pageData, 'visibility')) { + $pageData->visibility = 'show'; + $this->set(self::DB_PAGES_KEY, $slugTree, $pageData); + } + return $pageData; } @@ -2221,8 +2338,15 @@ public function saveAction(): void if ($target === 'menuItemOrder' && $menu !== null) { $this->orderMenuItem($content, $menu); } - if ($fieldname === 'defaultPage' && $this->getPageData($content) === null) { - return; + if ($target === 'config') { + if ($fieldname === 'defaultPage' && $content === 'blog') { + $this->set('config', $fieldname, $content); + } else { + if ($fieldname === 'defaultPage' && $this->getPageData($content) === null) { + return; + } + $this->set('config', $fieldname, $content); + } } if ($fieldname === 'login' && (empty($content) || $this->getPageData($content) !== null)) { return; @@ -2279,8 +2403,13 @@ public function settings(): string $isHttpsForced = $this->isHttpsForced(); $isSaveChangesPopupEnabled = $this->isSaveChangesPopupEnabled(); $isLogoutToLoginScreenEnabled = $this->isLogoutToLoginScreenEnabled(); + $isModalPersistenceEnabled = $this->isModalPersistenceEnabled(); + + $output = ' + +


Saving


Checking for updates

@@ -2355,6 +2484,10 @@ public function settings(): string foreach ($items as $item) { $output .= $this->renderDefaultPageOptions($item, $defaultPage); } + if ($this->blogPluginInstalled()) { + $isSelected = $this->get('config', 'defaultPage') === 'blog'; + $output .= ''; + } $output .= '
@@ -2431,6 +2564,18 @@ public function settings(): string +

Modal persistence

+

If this is turned "ON", this currently opened modal window will re-open to the same tab you last made changes to.

+
+
+
+
+
+
+ +
+
+

Login redirect

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.

@@ -2535,8 +2680,8 @@ private function renderPageNavMenuItem(object $item, string $parentSlug = ''): s } $output .= ''; - - return $output; + + return $this->hook('renderPageNavMenuItem', $output, $item, $parentSlug, $visibleSubpage)[0]; } /** @@ -2713,6 +2858,22 @@ private function renderModuleTab(string $type = 'themes'): string return $output; } + /** + * Check if blog plugin is installed + * + * Verifies installation through either: + * 1. Presence of "simple-blog" directory in plugins folder + * 2. Existence of "simpleblog.json" in data folder + * @return bool + */ + public function blogPluginInstalled(): bool + { + $pluginDir = $this->rootDir . '/plugins/simple-blog'; + $dataFile = $this->dataPath . '/simpleblog.json'; + + return is_dir($pluginDir) || file_exists($dataFile); + } + /** * Slugify page * @@ -3065,4 +3226,238 @@ private function isLogoutToLoginScreenEnabled(): bool return $value ?? true; } + + /** + * Check if admin will be redirected to the login screen or current page screen after logout. + * @return bool + */ + private function isModalPersistenceEnabled(): bool + { + $value = $this->get('config', 'modalPersistence'); + if (gettype($value) === 'object' && empty(get_object_vars($value))) { + return false; + } + + return $value ?? false; + } + + /** + * Generate search interface HTML/JavaScript. + * @return string HTML/JS markup for search component + */ + public function search(): string + { + $output = << + +
+
+ +EOT; + + return $this->hook('search', $output)[0]; + } + + /** + * Handle search requests and return JSON results. + * Processes POST requests, searches pages/blog posts. + * @return void Outputs JSON response and exits + * @throws Exception If blog data file can't be read/parsed + */ + private function searchAction(): void + { + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['query'], $_POST['token']) + && $this->hashVerify($_POST['token'])) { + + $query = trim($_POST['query']); + $results = []; + + // Search main pages + $pages = $this->get('pages'); + foreach ($pages as $slug => $page) { + if ($slug === '404' || ($page->visibility ?? 'show') === 'hide') { + continue; + } + + if ($this->pageMatchesQuery($page, $query)) { + $results[] = [ + 'title' => $page->title, + 'excerpt' => $this->createExcerpt($page->content, $query, 100), + 'slug' => $slug + ]; + } + } + + // Search blog posts + if ($this->blogPluginInstalled()) { + $blogPath = "{$this->dataPath}/simpleblog.json"; + if (file_exists($blogPath)) { + $blogData = json_decode(file_get_contents($blogPath), true); + if (json_last_error() === JSON_ERROR_NONE && isset($blogData['posts'])) { + foreach ($blogData['posts'] as $slug => $post) { // Get slug and post data + if ($this->postMatchesQuery($post, $query)) { + $results[] = [ + 'title' => $post['title'], + 'excerpt' => $this->createExcerpt($post['body'] ?? '', $query, 100), // Use 'body' field + 'slug' => 'blog/' . $slug // Use slug from array key + ]; + } + } + } + } + } + + if (empty($results)) { + echo json_encode(['error' => 'No matching content found']); + } else { + echo json_encode($results); + } + exit; + } + } + + /** + * Check if page content matches search query. + * @param object $page Page object from database + * @param string $query Search term + * @return bool True if page matches query + */ + private function pageMatchesQuery(object $page, string $query): bool + { + $searchableContent = $page->title . ' ' . strip_tags($page->content); + return stripos($searchableContent, $query) !== false; + } + + /** + * Check if blog post content matches search query. + * @param array $post Blog post data array + * @param string $query Search term + * @return bool True if post matches query + */ + private function postMatchesQuery(array $post, string $query): bool + { + $searchableContent = $post['title'] . ' ' . strip_tags($post['body'] ?? ''); + return stripos($searchableContent, $query) !== false; + } + + /** + * Determine if page should be included in search results. + * @param object $page Page object to check + * @param string $slug Page slug/URL + * @return bool True if page is searchable, false otherwise + */ + private function isSearchablePage(object $page, string $slug): bool + { + return $slug !== '404' + && ($page->visibility ?? 'show') !== 'hide' + && !in_array($slug, ['config', 'blocks']); + } + + /** + * Format page data for search results. + * @param object $page Page object from database + * @param string $slug Page slug/URL + * @return array Formatted result with title/excerpt/slug + */ + private function formatPageResult(object $page, string $slug): array + { + return [ + 'title' => $page->title, + 'excerpt' => $this->createExcerpt($page->content, 100), + 'slug' => $slug + ]; + } + + /** + * Search blog posts from simpleblog.json data. + * @param string $query Search term to match + * @return array Array of matching blog posts with title/excerpt/slug + * @throws Exception If blog data file is missing or invalid JSON + */ + private function searchBlogPosts(string $query): array + { + $blogData = json_decode(file_get_contents("{$this->dataPath}/simpleblog.json"), true); + $results = []; + + foreach ($blogData['posts'] ?? [] as $post) { + if (stripos($post['title'], $query) !== false + || stripos($post['content'], $query) !== false) { + $results[] = [ + 'title' => $post['title'], + 'excerpt' => $this->createExcerpt($post['content'], 100), + 'slug' => 'blog/' . $post['slug'] + ]; + } + } + + return $results; + } + + /** + * Generate a search result excerpt with highlighted query matches. + * @param string $content Full content to create excerpt from + * @param string $query Search term to highlight + * @param int $length Maximum length of excerpt + * @return string HTML-formatted excerpt with highlighted matches + */ + private function createExcerpt(string $content, string $query, int $length): string + { + $stripped = strip_tags($content); + $position = stripos($stripped, $query); + + // Show context around match if found + if ($position !== false) { + $start = max(0, $position - 20); + $excerpt = substr($stripped, $start, $length); + $excerpt = preg_replace('/(' . preg_quote($query, '/') . ')/i', '$1', $excerpt); + return trim($excerpt); + } + + // Fallback to first characters + return substr($stripped, 0, $length) . '...'; + } + } From 173bd9cca9477a2119f4a42e8de826f212763df1 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Mon, 3 Mar 2025 21:42:58 +0100 Subject: [PATCH 06/19] Update version Bump version to 3.5.0 --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 6cb9d3d..1545d96 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.4.3 +3.5.0 From 780a204ba3fd49cf964192121142c3fa922920de Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Tue, 4 Mar 2025 00:06:56 +0100 Subject: [PATCH 07/19] Update index.php Fix PR comment --- index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.php b/index.php index 9ee0719..6ef71f2 100644 --- a/index.php +++ b/index.php @@ -572,7 +572,7 @@ public function createDb(): void

This editable area is visible on all pages.

' ], 'header' => [ - 'content' => '' + 'content' => '' ], 'footer' => [ 'content' => '©' . date('Y') . ' Your website' From d6f56a437ce480f58abf4bc92cda6967e4e9895a Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Tue, 4 Mar 2025 03:02:17 +0100 Subject: [PATCH 08/19] Update index.php Revert alert for incorrect login. --- index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.php b/index.php index 6ef71f2..3ba0ef3 100644 --- a/index.php +++ b/index.php @@ -1852,7 +1852,7 @@ public function loginAction(): void 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null ]); - $this->alert('danger', 'Invalid login credentials'); + $this->alert('test', '', 1); $this->redirect($this->get('config', 'login')); } } From 64e44075025cdd47f6e9b38649c5645a57384e19 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Fri, 7 Mar 2025 00:19:29 +0100 Subject: [PATCH 09/19] Update README.md Update links to docs, Wiki will be deprecated with Wcms 3.5.0 --- .github/README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/README.md b/.github/README.md index 8bdea84..f98f2b5 100644 --- a/.github/README.md +++ b/.github/README.md @@ -10,7 +10,7 @@
- + Project Maintained License @@ -205,15 +205,10 @@ Also listed on the official [WonderCMS website](https://www.wondercms.com/donors - [Twitter](https://twitter.com/wondercms) - [Reddit](https://reddit.com/r/WonderCMS) - -#### Github -- [Docs/Wiki](https://github.com/WonderCMS/wondercms/wiki#wondercms-documentation) - - [Common questions](https://github.com/WonderCMS/wondercms/wiki#common-questions--help) - - [List of common errors](https://github.com/WonderCMS/wondercms/wiki/List-of-common-errors#troubleshooting-common-errors) -- [How to create a theme](https://github.com/WonderCMS/wondercms/wiki/Create-theme-in-8-easy-steps) -- [How to create a plugin](https://github.com/WonderCMS/wondercms/wiki/List-of-hooks) - - #### Hosting and install tutorial - [Hosting with WonderCMS pre-installed](https://www.wondercms.com/hosting) - [Install via cPanel - video tutorial](https://www.youtube.com/watch?v=5tykBmKAUkA&feature=youtu.be&t=25) + + +#### Documentation +- [Docs](https://wondercms.com-docs) From bed93d89b22417799042d364d3519d7100e651e1 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Fri, 14 Mar 2025 22:28:25 +0100 Subject: [PATCH 10/19] Update index.php Update links to new and updated documentation. --- index.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.php b/index.php index 3ba0ef3..11f95ac 100644 --- a/index.php +++ b/index.php @@ -2550,7 +2550,7 @@ public function settings(): string -

How to restore backup

+

How to restore backup

Save confirmation popup

If this is turned "ON", WonderCMS will always ask you to confirm any changes you make.

@@ -2599,7 +2599,7 @@ public function settings(): string -

Read more before enabling

'; +

Read more before enabling

'; $output .= $this->renderAdminLoginIPs(); $output .= ' @@ -2610,7 +2610,7 @@ public function settings(): string WonderCMS ' . VERSION . '   News   Community   - Docs   + Docs   Donate   Shop/Merch

@@ -2853,7 +2853,7 @@ private function renderModuleTab(string $type = 'themes'): string -

Read more about custom modules

+

Read more about custom modules

'; return $output; } From 282164b838535b76d3275f9574c1ba1b307432e6 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sat, 15 Mar 2025 11:34:51 +0100 Subject: [PATCH 11/19] Update index.php Bugfix: fixes the PHP Warning: Undefined property: stdClass::$newlyCreatedPage on line 748 --- index.php | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/index.php b/index.php index 11f95ac..1aa6aef 100644 --- a/index.php +++ b/index.php @@ -745,7 +745,7 @@ public function createPage(array $slugTree = null, bool $createMenuItem = false) $pageData = null; foreach ($slugTree as $parentPage) { if (!$pageData) { - $pageData = $this->get(self::DB_PAGES_KEY)->{$parentPage}; + $pageData = $this->get(self::DB_PAGES_KEY)->{$parentPage} ?? null; continue; } @@ -762,6 +762,7 @@ public function createPage(array $slugTree = null, bool $createMenuItem = false) $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 @@ -770,39 +771,36 @@ public function createPage(array $slugTree = null, bool $createMenuItem = false) } // 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; + if (!isset($selectedPage->{$childSlug})) { + $selectedPage->{$childSlug} = new stdClass(); // Initialize the object + $selectedPage->{$childSlug}->title = mb_convert_case(str_replace('-', ' ', $childSlug), MB_CASE_TITLE); $selectedPage->{$childSlug}->keywords = 'Keywords, are, good, for, search, engines'; $selectedPage->{$childSlug}->description = 'A short description is also good.'; - + $selectedPage->{$childSlug}->subpages = new stdClass(); // Initialize subpages + if ($createMenuItem) { - $this->createMenuItem($parentTitle, $menuKey); + $this->createMenuItem($selectedPage->{$childSlug}->title, $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}; + $selectedPage = $selectedPage->{$childSlug}->subpages; } } - $pageTitle = !$slug ? str_replace('-', ' ', $pageSlug) : $pageSlug; - - $selectedPage->{$slug} = new stdClass; + // Initialize the new page object + $selectedPage->{$slug} = new stdClass(); $selectedPage->{$slug}->created = date('c'); $selectedPage->{$slug}->modified = date('c'); - $selectedPage->{$slug}->title = mb_convert_case($pageTitle, MB_CASE_TITLE); + $selectedPage->{$slug}->title = mb_convert_case(str_replace('-', ' ', $pageSlug), 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; + $selectedPage->{$slug}->subpages = new stdClass(); // Initialize subpages + $this->set(self::DB_PAGES_KEY, $allPages); if ($createMenuItem) { - $this->createMenuItem($pageTitle, $menuKey); + $this->createMenuItem($selectedPage->{$slug}->title, $menuKey); } } From 3c779ec4fd5798b764a2e0b558217bfae648c8ec Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sat, 15 Mar 2025 11:56:20 +0100 Subject: [PATCH 12/19] Update issue templates Create issue temoplates for bugs and feature requests --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From c44c8370b7b1a37eb8cd5205530e9aac3391e1dc Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 16 Mar 2025 12:06:33 +0100 Subject: [PATCH 13/19] Update links to new docs --- .github/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/README.md b/.github/README.md index f98f2b5..c9c2724 100644 --- a/.github/README.md +++ b/.github/README.md @@ -3,7 +3,7 @@ WonderCMS logo
WonderCMS - small flat file CMS
- 5 files • 48KB zip - 1 step install + 5 files • ∼50KB zip - 1 step install

@@ -32,10 +32,10 @@ ## Small and simple flat file CMS - **No configuration needed - unzip and upload.** - - 5 files: [database.js](https://github.com/WonderCMS/wondercms/wiki/Default-database.js#default-databasejs) (JSON format), [index.php](https://github.com/WonderCMS/wondercms/blob/master/index.php), [theme.php](https://github.com/WonderCMS/wondercms/blob/master/themes/sky/theme.php), [style.css](https://github.com/WonderCMS/wondercms/blob/master/themes/sky/css/style.css) and [htaccess](https://github.com/WonderCMS/wondercms/blob/master/.htaccess). + - 5 files: [database.js](https://www.wondercms.com/docs/#default-generated-database) (JSON format), [index.php](https://github.com/WonderCMS/wondercms/blob/master/index.php), [theme.php](https://github.com/WonderCMS/wondercms/blob/master/themes/sky/theme.php), [style.css](https://github.com/WonderCMS/wondercms/blob/master/themes/sky/css/style.css) and [htaccess](https://github.com/WonderCMS/wondercms/blob/master/.htaccess). - Transferring your website to a new host/server is done by only copy/pasting all files (no additional configuration/migration) - Privacy oriented: no cookies, tracking or "powered by" links. - - Includes plugins ([via hooks/listeners](https://github.com/WonderCMS/wondercms/wiki/List-of-hooks)), [themes](https://github.com/WonderCMS/wondercms/wiki/Create-theme-in-8-easy-steps)/plugins installer, [backups](https://github.com/WonderCMS/wondercms/wiki/Backup-all-files), [1 click updates](https://github.com/WonderCMS/wondercms/wiki/One-click-update). + - Includes plugins ([via hooks/listeners](https://www.wondercms.com/docs/#hooks)), [themes](https://www.wondercms.com/docs/#themes)/plugins installer, [backups](https://www.wondercms.com/docs/#backup-and-restore), [1 click updates](https://www.wondercms.com/docs/#do-and-dont). - Supports most server types (Apache, NGINX, IIS, Caddy). - Project goal: keep it simple, tiny, hassle free (infrequent-ish 1 click updates). @@ -62,7 +62,7 @@ - mod_rewrite module - any type of server (Apache, NGINX, IIS, Caddy) -*For setting up WonderCMS on NGINX or IIS servers, there is one additional step required. Read more: [NGINX setup](https://github.com/WonderCMS/wondercms/wiki/NGINX-server-config) or [IIS setup](https://github.com/WonderCMS/wondercms/wiki/IIS-server-config).* +*For setting up WonderCMS on NGINX or IIS servers, there is one additional step required. Read more: [NGINX setup](https://www.wondercms.com/docs/#serverConfigs) or [IIS setup](https://www.wondercms.com/docs/#serverConfigs).* **WonderCMS works on most Apache servers/hosts (even free ones) by default.** @@ -80,9 +80,8 @@ Note: Some plugins also include other libraries such as jQuery, default WonderCM - Track free and transparent - WonderCMS doesn't track users or store any personal cookies, there is only one session state cookie. - Your WonderCMS installation is completely detached from WonderCMS servers. One click updates are pushed through GitHub. - Supports HTTPS out of the box. - - [Check how to further improve security](https://github.com/WonderCMS/wondercms/wiki/Better-security-mode-(HTTPS-and-other-features)). + - [Check how to further improve security](https://www.wondercms.com/docs/#security-settings)). - All CSS and JS libraries include SubResource Integrity (SRI) tags. This prevents any changes to the libraries being loaded. If any changes are made, the libraries won't load for your and your visitors protection. - - [Check how to add SRI tags to your custom theme](https://github.com/WonderCMS/wondercms/wiki/Add-SRI-tags-to-your-theme-libraries#sri-subresource-integrity---3-steps-for-more-security). This step isn't necessary if you're using a theme from the official website. - WonderCMS encourages you to change your default login URL. **Consider your custom login URL as your private username**. - Choosing a good login URL can prevent brute force attacks. - Your login page will always return a 404 header response. Search engines do not (and should not) cache your login URL. @@ -102,8 +101,8 @@ Note: Some plugins also include other libraries such as jQuery, default WonderCM - theme and plugin installer/updater - 1 click updates - 1 click backups - - [easy to theme](https://github.com/WonderCMS/wondercms/wiki/Create-theme-in-8-easy-steps) - - [custom editable blocks](https://github.com/WonderCMS/wondercms/wiki/Create-new-editable-areas-or-editable-blocks#difference-between-editable-blocks-and-editable-areas) + - [easy to theme](https://www.wondercms.com/docs/#themes) + - [custom editable blocks](https://www.wondercms.com/docs/#editable-blocks) - custom theme and plugin repositories - log of last 5 logged in IPs - file uploader From 1599dd63ba8e28521e16878c60422bee330a27c6 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 16 Mar 2025 12:50:29 +0100 Subject: [PATCH 14/19] Fix searchAction errors Ensure search action does not fill up error log when search results are not found --- index.php | 49 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/index.php b/index.php index 1aa6aef..0fa3437 100644 --- a/index.php +++ b/index.php @@ -3295,7 +3295,7 @@ public function search(): string }); EOT; - + return $this->hook('search', $output)[0]; } @@ -3316,16 +3316,21 @@ private function searchAction(): void // Search main pages $pages = $this->get('pages'); foreach ($pages as $slug => $page) { + // Skip 404 page and hidden pages if ($slug === '404' || ($page->visibility ?? 'show') === 'hide') { continue; } - if ($this->pageMatchesQuery($page, $query)) { - $results[] = [ - 'title' => $page->title, - 'excerpt' => $this->createExcerpt($page->content, $query, 100), - 'slug' => $slug - ]; + // Ensure the page object is valid and has the required properties + if (is_object($page) && property_exists($page, 'title') && property_exists($page, 'content')) { + $content = $page->content ?? ''; // Default to empty string if content is missing or null + if ($this->pageMatchesQuery($page, $query)) { + $results[] = [ + 'title' => $page->title, + 'excerpt' => $this->createExcerpt($content, $query, 100), + 'slug' => $slug + ]; + } } } @@ -3335,19 +3340,23 @@ private function searchAction(): void if (file_exists($blogPath)) { $blogData = json_decode(file_get_contents($blogPath), true); if (json_last_error() === JSON_ERROR_NONE && isset($blogData['posts'])) { - foreach ($blogData['posts'] as $slug => $post) { // Get slug and post data - if ($this->postMatchesQuery($post, $query)) { - $results[] = [ - 'title' => $post['title'], - 'excerpt' => $this->createExcerpt($post['body'] ?? '', $query, 100), // Use 'body' field - 'slug' => 'blog/' . $slug // Use slug from array key - ]; + foreach ($blogData['posts'] as $slug => $post) { + // Ensure the post array is valid and has the required keys + if (is_array($post) && isset($post['title']) && isset($post['body'])) { + $postContent = $post['body'] ?? ''; // Default to empty string if body is missing or null + if ($this->postMatchesQuery($post, $query)) { + $results[] = [ + 'title' => $post['title'], + 'excerpt' => $this->createExcerpt($postContent, $query, 100), + 'slug' => 'blog/' . $slug + ]; + } } } } } } - + if (empty($results)) { echo json_encode(['error' => 'No matching content found']); } else { @@ -3365,7 +3374,9 @@ private function searchAction(): void */ private function pageMatchesQuery(object $page, string $query): bool { - $searchableContent = $page->title . ' ' . strip_tags($page->content); + $title = $page->title ?? ''; + $content = $page->content ?? ''; + $searchableContent = $title . ' ' . strip_tags($content); return stripos($searchableContent, $query) !== false; } @@ -3377,7 +3388,9 @@ private function pageMatchesQuery(object $page, string $query): bool */ private function postMatchesQuery(array $post, string $query): bool { - $searchableContent = $post['title'] . ' ' . strip_tags($post['body'] ?? ''); + $title = $post['title'] ?? ''; + $content = $post['body'] ?? ''; + $searchableContent = $title . ' ' . strip_tags($content); return stripos($searchableContent, $query) !== false; } @@ -3443,6 +3456,8 @@ private function searchBlogPosts(string $query): array */ private function createExcerpt(string $content, string $query, int $length): string { + // Ensure content is a string + $content = is_string($content) ? $content : ''; $stripped = strip_tags($content); $position = stripos($stripped, $query); From 4c69f681eee53073024b86dc594258b61fc9adcd Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 16 Mar 2025 13:32:22 +0100 Subject: [PATCH 15/19] Update wcms-modules.json --- themes/sky/wcms-modules.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/sky/wcms-modules.json b/themes/sky/wcms-modules.json index bc6ae6b..0e7510a 100644 --- a/themes/sky/wcms-modules.json +++ b/themes/sky/wcms-modules.json @@ -6,7 +6,7 @@ "repo": "https://github.com/robiso/sky/tree/master", "zip": "https://github.com/robiso/sky/archive/master.zip", "summary": "Default WonderCMS theme (2022). Theme works without Bootstrap and jQuery.", - "version": "3.2.4", + "version": "3.5.0", "image": "https://raw.githubusercontent.com/robiso/sky/master/preview.jpg" } } From 68ad3a6bd54676f29ff9c0518f6d1b8830924d36 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 16 Mar 2025 13:32:44 +0100 Subject: [PATCH 16/19] Update theme.php --- themes/sky/theme.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/sky/theme.php b/themes/sky/theme.php index b670182..1430075 100644 --- a/themes/sky/theme.php +++ b/themes/sky/theme.php @@ -32,7 +32,7 @@ - + settings() ?> From a980f711702ea6d4bf2a88f0c567241f382986ae Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 16 Mar 2025 13:33:17 +0100 Subject: [PATCH 17/19] Update style.css Add search styling. --- themes/sky/css/style.css | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/themes/sky/css/style.css b/themes/sky/css/style.css index 10c8ee2..0f6a875 100644 --- a/themes/sky/css/style.css +++ b/themes/sky/css/style.css @@ -555,3 +555,88 @@ input::-moz-focus-inner { .menu li > a:only-child:after { content: '' } + +/* Search */ + .wondersearch-container { + position: relative; + padding: 2em 0; + max-width: 600px; + margin-left: auto; + margin-right: auto; + } + + .wondersearch-input { + width: 100%; + padding: 10px 15px; + font-size: 1em; + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + outline: none; + transition: border-color 0.2s ease, background-color 0.2s ease; + } + + .wondersearch-input:focus { + border-color: rgba(255, 255, 255, 0.88); + background-color: rgba(255, 255, 255, 0.2); + } + + .wondersearch-results { + position: absolute; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); + border-radius: 5px; + margin-top: 5px; + z-index: 1000; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + padding: 10px; /* Added padding for no results */ + border: 1px solid transparent; /* Hide border by default */ + display: none; /* Hide results container by default */ + } + + .wondersearch-results:not(:empty) { + display: block; /* Show results container when not empty */ + border: 1px solid rgba(255, 255, 255, 0.15); /* Add border when results are present */ + } + + .wondersearch-item { + padding: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + transition: background-color 0.2s ease; + } + + .wondersearch-item:last-child { + border-bottom: none; + } + + .wondersearch-item:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + .wondersearch-item a { + color: #fff; + text-decoration: none; + font-weight: bold; + display: block; + margin-bottom: 5px; + } + + .wondersearch-item a:hover { + text-decoration: underline; + } + + .wondersearch-item p { + color: rgba(255, 255, 255, 0.8); + margin: 0; + font-size: 0.9em; + } + + .wondersearch-item mark { + background-color: rgba(255, 255, 255, 0.2); + color: #fff; + padding: 2px 4px; + border-radius: 3px; + } From 587901440065acc4bcbe449222449021d101cae5 Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 16 Mar 2025 16:29:44 +0100 Subject: [PATCH 18/19] Update README.md Update link to merch. --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index c9c2724..51e9d8e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -190,7 +190,7 @@ Also listed on the official [WonderCMS website](https://www.wondercms.com/donors - [Official website](https://www.wondercms.com) - [News/Changelog](https://www.wondercms.com/news) - [Donate](https://www.wondercms.com/donate) -- [Get merch](https://www.wondercms.com/shop) +- [Get merch](https://swag.wondercms.com) - [Donors Hall of Fame](https://www.wondercms.com/donors) - [List of contributors](https://www.wondercms.com/contributors) - [All WonderCMS related links](https://www.wondercms.com/links) From 49a252beee9d5f57f4cb0fa6e4ff92ab0a8d6f0c Mon Sep 17 00:00:00 2001 From: Robert Isoski Date: Sun, 4 May 2025 17:22:46 +0200 Subject: [PATCH 19/19] Update README.md Fix docs icon --- .github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/README.md b/.github/README.md index 51e9d8e..b2aa87d 100644 --- a/.github/README.md +++ b/.github/README.md @@ -10,7 +10,7 @@
- +Docs Project Maintained License