| File | Folder Link |
|---|---|
| New Snippet : js-script & search-box & search-result | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\snippet |
| search-icon.png | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\public\themes\rckuc\images |
| default.css | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\public\themes\rckuc\css |
| rckuc.js & dataTable | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\public\themes\rckuc\js |
| index-20251120.php | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\wolf\app\views\setting |
| Page.php & SearchResult.php | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\wolf\app\models |
| SettingController-20251120.php | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\wolf\app\controllers |
| Framework.php | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box\rckuc\wolf |
| wolf_search_result | \\SYNAS\Allan\DOCUMENTATION\Component\Search Box |
<!-- DataTables CSS -->
<link rel="stylesheet" href="<?php echo THEME_PATH ?>js/jquery.dataTables-1.13.6.min.css"></link>
<!-- DataTables JS -->
<script src="<?php echo THEME_PATH ?>js/jquery.dataTables-1.13.6.min.js"></script>
<!-- DataTables CSS -->
<link rel="stylesheet" href="<?php echo THEME_PATH ?>js/jquery.dataTables-1.13.6.min.css"></link>
<!-- DataTables JS -->
<script src="<?php echo THEME_PATH ?>js/jquery.dataTables-1.13.6.min.js"></script>
Create New Snippet: search-box
After that in Snippet → header, insert: <?php $this->includeSnippet('search-box'); ?>
<form id="search_form" method="post" action="<?php echo get_url('search-result'); ?>">
<input type="text" id="search_box" name="search" placeholder="Search">
<button type="submit"><img src="<?php echo THEME_PATH ?>images/search-icon.png" alt="search"></button>
</form>
<form id="search_form" method="post" action="<?php echo get_url('search-result'); ?>">
<input type="text" id="search_box" name="search" placeholder="Search">
<button type="submit"><img src="<?php echo THEME_PATH ?>images/search-icon.png" alt="search"></button>
</form>
<?php
if (!empty($_POST['search'])) {
$search = $_POST['search'];
$results = SearchResult::getResult($search);
$keyword = $_POST['search'];
}
function getFullSlugFromUrl($url) {
$path = parse_url($url, PHP_URL_PATH);
$path = trim($path, '/');
if (empty($path)) {
return '';
}
$slug = 'home <span class="breadcrumb-separator">></span> ' . str_replace('/', '<span class="breadcrumb-separator">></span>', $path);
return $slug;
}
function getDescription($text, $keyword, $charLimit) {
// Normalize spaces
$text = preg_replace('/\s+/', ' ', trim($text));
if ($text === '') {
return '';
}
// If text is shorter than limit, just return
if (mb_strlen($text) <= $charLimit) {
return ' - ' . $text;
}
// Detect if keyword is a quoted phrase → exact match
$isPhrase = preg_match('/^".+"$/', $keyword);
if ($isPhrase) {
$searchTerms = [trim($keyword, '"')]; // exact phrase
} else {
// Split like MySQL full-text (basic: split on non-alphanum, incl. hyphens)
$searchTerms = preg_split('/[^[:alnum:]]+/u', mb_strtolower($keyword), -1, PREG_SPLIT_NO_EMPTY);
}
// Try to find first occurrence of any search term
$foundPos = false;
$foundLen = 0;
foreach ($searchTerms as $term) {
$pos = mb_stripos($text, $term);
if ($pos !== false && ($foundPos === false || $pos < $foundPos)) {
$foundPos = $pos;
$foundLen = mb_strlen($term);
}
}
if ($foundPos !== false) {
// Calculate snippet around keyword
$half = floor($charLimit / 2);
$start = max(0, $foundPos - $half);
$excerpt = mb_substr($text, $start, $charLimit);
// Add ellipses if truncated
if ($start > 0) {
$excerpt = '...' . ltrim($excerpt);
}
if ($start + $charLimit < mb_strlen($text)) {
$excerpt = rtrim($excerpt) . '...';
}
return ' - ' . $excerpt;
}
// No keyword found → return first $charLimit characters
$excerpt = mb_substr($text, 0, $charLimit);
return ' - ' . rtrim($excerpt) . '...';
}
?>
<div id="search_result">
<h1>Search Result : "<?php echo $keyword; ?>"</h1>
<p id="search_error">The search keyword is too short. Please enter at least 4 characters.</p>
<input type="hidden" id="keyword" value="<?php echo $keyword; ?>">
<table id="result_table">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($results as $result) :
$description = getDescription($result->description, $keyword, 500);
?>
<tr>
<td>
<a href="<?php echo $result->url ;?>" target="_blank"><h3><?php echo $result->title; ?></h3></a>
<p><?php echo getFullSlugFromUrl($result->url) ;?></p>
<div>
<?php echo $result->image !== null ? '<a href="'.$result->url.'" target="_blank"><img src="'.$result->image.'"></a>' : ''; ?>
<p><?php echo date("d-M-Y", strtotime($result->created_on)) . $description; ?></p>
</div>
</td>
</tr>
<?php endforeach?>
</tbody>
</table>
</div>
<?php
if (!empty($_POST['search'])) {
$search = $_POST['search'];
$results = SearchResult::getResult($search);
$keyword = $_POST['search'];
}
function getFullSlugFromUrl($url) {
$path = parse_url($url, PHP_URL_PATH);
$path = trim($path, '/');
if (empty($path)) {
return '';
}
$slug = 'home <span class="breadcrumb-separator">></span> ' . str_replace('/', '<span class="breadcrumb-separator">></span>', $path);
return $slug;
}
function getDescription($text, $keyword, $charLimit) {
// Normalize spaces
$text = preg_replace('/\s+/', ' ', trim($text));
if ($text === '') {
return '';
}
// If text is shorter than limit, just return
if (mb_strlen($text) <= $charLimit) {
return ' - ' . $text;
}
// Detect if keyword is a quoted phrase → exact match
$isPhrase = preg_match('/^".+"$/', $keyword);
if ($isPhrase) {
$searchTerms = [trim($keyword, '"')]; // exact phrase
} else {
// Split like MySQL full-text (basic: split on non-alphanum, incl. hyphens)
$searchTerms = preg_split('/[^[:alnum:]]+/u', mb_strtolower($keyword), -1, PREG_SPLIT_NO_EMPTY);
}
// Try to find first occurrence of any search term
$foundPos = false;
$foundLen = 0;
foreach ($searchTerms as $term) {
$pos = mb_stripos($text, $term);
if ($pos !== false && ($foundPos === false || $pos < $foundPos)) {
$foundPos = $pos;
$foundLen = mb_strlen($term);
}
}
if ($foundPos !== false) {
// Calculate snippet around keyword
$half = floor($charLimit / 2);
$start = max(0, $foundPos - $half);
$excerpt = mb_substr($text, $start, $charLimit);
// Add ellipses if truncated
if ($start > 0) {
$excerpt = '...' . ltrim($excerpt);
}
if ($start + $charLimit < mb_strlen($text)) {
$excerpt = rtrim($excerpt) . '...';
}
return ' - ' . $excerpt;
}
// No keyword found → return first $charLimit characters
$excerpt = mb_substr($text, 0, $charLimit);
return ' - ' . rtrim($excerpt) . '...';
}
?>
<div id="search_result">
<h1>Search Result : "<?php echo $keyword; ?>"</h1>
<p id="search_error">The search keyword is too short. Please enter at least 4 characters.</p>
<input type="hidden" id="keyword" value="<?php echo $keyword; ?>">
<table id="result_table">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($results as $result) :
$description = getDescription($result->description, $keyword, 500);
?>
<tr>
<td>
<a href="<?php echo $result->url ;?>" target="_blank"><h3><?php echo $result->title; ?></h3></a>
<p><?php echo getFullSlugFromUrl($result->url) ;?></p>
<div>
<?php echo $result->image !== null ? '<a href="'.$result->url.'" target="_blank"><img src="'.$result->image.'"></a>' : ''; ?>
<p><?php echo date("d-M-Y", strtotime($result->created_on)) . $description; ?></p>
</div>
</td>
</tr>
<?php endforeach?>
</tbody>
</table>
</div>
Create New Layout: search-page
Copy from sub-page and keep only the header and footer.
In the <head> section, after <?php $this->includeSnippet('meta-tag'); ?>, insert:
<?php $this->includeSnippet('js_script'); ?>
<?php $this->includeSnippet('js_script'); ?>
Create New Page: "Search Result" under the Page tab
Remember set:
In the body section, insert:
<?php $this->includeSnippet('search-result'); ?>
<?php $this->includeSnippet('search-result'); ?>
CSS: default.css
#search_result {
margin-top: 6rem;
}
#search_form {
position: absolute;
top: 0;
left: 33rem;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
background: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
/* Search box styling */
#search_box {
flex: 1;
padding: 0.625rem 2.5rem 0.625rem 0.625rem;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 6px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
#search_box:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
/* Button styling */
#search_form button {
position: absolute;
right: 1.3rem;
display: none;
border: none;
background-color: unset;
cursor: pointer;
}
#search_form button:hover {
opacity: 0.7;
}
#search_form button img {
height: 1.3rem;
width: 1.3rem;
}
.highlight {
background-color: yellow;
font-weight: bold;
}
table#result_table thead {
display: none;
}
table#result_table td {
padding-bottom: 2rem;
border-top: 1px solid #ccc;
}
table#result_table td > div > p {
font-size: 14px;
color: #8f8f8f;
padding: 0.3rem 0;
text-align: left;
}
table#result_table td > div {
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 1rem;
}
table#result_table td > div > a {
min-width: 5rem;
width: 5rem;
height: 5rem;
padding-top: 0.5rem;
}
table#result_table td > div > a > img {
width: 100%;
height: 100%;
object-fit: cover;
margin: 0;
}
#search_result {
margin-top: 6rem;
}
#search_form {
position: absolute;
top: 0;
left: 33rem;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
background: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
/* Search box styling */
#search_box {
flex: 1;
padding: 0.625rem 2.5rem 0.625rem 0.625rem;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 6px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
#search_box:focus {
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
/* Button styling */
#search_form button {
position: absolute;
right: 1.3rem;
display: none;
border: none;
background-color: unset;
cursor: pointer;
}
#search_form button:hover {
opacity: 0.7;
}
#search_form button img {
height: 1.3rem;
width: 1.3rem;
}
.highlight {
background-color: yellow;
font-weight: bold;
}
table#result_table thead {
display: none;
}
table#result_table td {
padding-bottom: 2rem;
border-top: 1px solid #ccc;
}
table#result_table td > div > p {
font-size: 14px;
color: #8f8f8f;
padding: 0.3rem 0;
text-align: left;
}
table#result_table td > div {
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 1rem;
}
table#result_table td > div > a {
min-width: 5rem;
width: 5rem;
height: 5rem;
padding-top: 0.5rem;
}
table#result_table td > div > a > img {
width: 100%;
height: 100%;
object-fit: cover;
margin: 0;
}
Javascript: rckuc.js
// search box
$(document).ready(function() {
if ($("#search_result table").length) {
let search_table = $("#search_result table").DataTable({
"order": [],
pageLength: 20,
searching: false, // hide Search box
lengthChange: false // hide "Show entries" dropdown
})
if ($("#search_result #keyword").length && $("#search_result #keyword").val() !== "") {
$("#search_box").val($("#search_result #keyword").val());
if (!$("#result_table tbody tr td a").length && $("#search_result #keyword").val().length < 4) {
$("#search_result #search_error").show();
} else {
highlightSearch();
}
}
// function highlightSearch() {
// let keyword = $("#keyword").val().trim();
// let $result = $("#search_result table td");
// // Remove old highlights
// $result.find(".highlight").each(function() {
// $(this).replaceWith($(this).text());
// });
// if (keyword === "") return;
// // Original keyword (to match exact string in text)
// let original = keyword;
// // Cleaned keyword (to mimic SQL tokenization)
// let base = keyword
// .replace(/'s$/i, "") // strip trailing 's
// .replace(/[^a-zA-Z0-9]+/g, " ") // remove symbols
// .trim()
// .split(/\s+/)
// .filter(w => w.length > 2); // ignore short words
// if (base.length === 0) return;
// // Regex for:
// // - exact phrase (original)
// // - base words + prefix (President, Presidents, Presidential)
// let patterns = [];
// patterns.push(original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); // escape regex
// base.forEach(w => patterns.push(w + "[a-z]*"));
// let regex = new RegExp("(" + patterns.join("|") + ")", "gi");
// // Highlight matches
// $result.contents().each(function highlightTextNodes() {
// if (this.nodeType === 3) {
// let text = this.nodeValue;
// let newHtml = text.replace(regex, '<span class="highlight">$1</span>');
// if (newHtml !== text) {
// $(this).replaceWith(newHtml);
// }
// } else {
// $(this).contents().each(highlightTextNodes);
// }
// });
// }
function highlightSearch() {
let keyword = $("#keyword").val().trim();
let $result = $("#search_result table td");
// Remove old highlights
$result.find(".highlight").each(function() {
$(this).replaceWith($(this).text());
});
if (keyword === "") return;
let patterns = [];
// Check if quoted phrase → exact match
if (/^".+"$/.test(keyword)) {
let phrase = keyword.slice(1, -1); // remove quotes
patterns.push(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); // escape regex
} else {
// Tokenize similar to MySQL full-text
let base = keyword
.replace(/'s$/i, "") // strip trailing 's
.replace(/[^a-zA-Z0-9]+/g, " ") // split symbols, incl. hyphens
.trim()
.split(/\s+/)
.filter(w => w.length >= 3); // simulate ft_min_word_len
// Each term → prefix search (word*)
base.forEach(w => {
patterns.push(w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[a-z]*");
});
}
if (patterns.length === 0) return;
let regex = new RegExp("(" + patterns.join("|") + ")", "gi");
// Highlight matches
$result.contents().each(function highlightTextNodes() {
if (this.nodeType === 3) {
let text = this.nodeValue;
let newHtml = text.replace(regex, '<span class="highlight">$1</span>');
if (newHtml !== text) {
$(this).replaceWith(newHtml);
}
} else {
$(this).contents().each(highlightTextNodes);
}
});
}
search_table.on("draw", function() {
highlightSearch();
});
}
})
// search box
$(document).ready(function() {
if ($("#search_result table").length) {
let search_table = $("#search_result table").DataTable({
"order": [],
pageLength: 20,
searching: false, // hide Search box
lengthChange: false // hide "Show entries" dropdown
})
if ($("#search_result #keyword").length && $("#search_result #keyword").val() !== "") {
$("#search_box").val($("#search_result #keyword").val());
if (!$("#result_table tbody tr td a").length && $("#search_result #keyword").val().length < 4) {
$("#search_result #search_error").show();
} else {
highlightSearch();
}
}
// function highlightSearch() {
// let keyword = $("#keyword").val().trim();
// let $result = $("#search_result table td");
// // Remove old highlights
// $result.find(".highlight").each(function() {
// $(this).replaceWith($(this).text());
// });
// if (keyword === "") return;
// // Original keyword (to match exact string in text)
// let original = keyword;
// // Cleaned keyword (to mimic SQL tokenization)
// let base = keyword
// .replace(/'s$/i, "") // strip trailing 's
// .replace(/[^a-zA-Z0-9]+/g, " ") // remove symbols
// .trim()
// .split(/\s+/)
// .filter(w => w.length > 2); // ignore short words
// if (base.length === 0) return;
// // Regex for:
// // - exact phrase (original)
// // - base words + prefix (President, Presidents, Presidential)
// let patterns = [];
// patterns.push(original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); // escape regex
// base.forEach(w => patterns.push(w + "[a-z]*"));
// let regex = new RegExp("(" + patterns.join("|") + ")", "gi");
// // Highlight matches
// $result.contents().each(function highlightTextNodes() {
// if (this.nodeType === 3) {
// let text = this.nodeValue;
// let newHtml = text.replace(regex, '<span class="highlight">$1</span>');
// if (newHtml !== text) {
// $(this).replaceWith(newHtml);
// }
// } else {
// $(this).contents().each(highlightTextNodes);
// }
// });
// }
function highlightSearch() {
let keyword = $("#keyword").val().trim();
let $result = $("#search_result table td");
// Remove old highlights
$result.find(".highlight").each(function() {
$(this).replaceWith($(this).text());
});
if (keyword === "") return;
let patterns = [];
// Check if quoted phrase → exact match
if (/^".+"$/.test(keyword)) {
let phrase = keyword.slice(1, -1); // remove quotes
patterns.push(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); // escape regex
} else {
// Tokenize similar to MySQL full-text
let base = keyword
.replace(/'s$/i, "") // strip trailing 's
.replace(/[^a-zA-Z0-9]+/g, " ") // split symbols, incl. hyphens
.trim()
.split(/\s+/)
.filter(w => w.length >= 3); // simulate ft_min_word_len
// Each term → prefix search (word*)
base.forEach(w => {
patterns.push(w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[a-z]*");
});
}
if (patterns.length === 0) return;
let regex = new RegExp("(" + patterns.join("|") + ")", "gi");
// Highlight matches
$result.contents().each(function highlightTextNodes() {
if (this.nodeType === 3) {
let text = this.nodeValue;
let newHtml = text.replace(regex, '<span class="highlight">$1</span>');
if (newHtml !== text) {
$(this).replaceWith(newHtml);
}
} else {
$(this).contents().each(highlightTextNodes);
}
});
}
search_table.on("draw", function() {
highlightSearch();
});
}
})
Import SQL Table: wolf_search_result.sql
After importing, rename "wolf_" to match your project prefix.
<?php
/**
* Frog CMS - Content Management Simplified. <http://www.madebyfrog.com>
* Copyright (C) 2008 Philippe Archambault <philippe.archambault@gmail.com>
*
* This file is part of Frog CMS.
*
* Frog CMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Frog CMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Frog CMS. If not, see <http://www.gnu.org/licenses/>.
*
* Frog CMS has made an exception to the GNU General Public License for plugins.
* See exception.txt for details and the full text.
*/
/**
* @package frog
* @subpackage models
*
* @author Philippe Archambault <philippe.archambault@gmail.com>
* @version 0.1
* @license http://www.gnu.org/licenses/gpl.html GPL License
* @copyright Philippe Archambault, 2008
*/
/**
* class SearchResult
*
* @author Philippe Archambault <philippe.archambault@gmail.com>
* @since Frog version 0.1
*/
class SearchResult extends Record // All properties and methods of Record are available in SearchResult, unless overridden.
{
const TABLE_NAME = 'search_result';
public static function find($args = null)
{
// Collect attributes...
$where = isset($args['where']) ? trim($args['where']) : '';
$order_by = isset($args['order']) ? trim($args['order']) : '';
$offset = isset($args['offset']) ? (int) $args['offset'] : 0;
$limit = isset($args['limit']) ? (int) $args['limit'] : 0;
// Prepare query parts
$where_string = empty($where) ? '' : "WHERE $where";
$order_by_string = empty($order_by) ? '' : "ORDER BY $order_by";
$limit_string = $limit > 0 ? "LIMIT $offset, $limit" : '';
$tablename = self::tableNameFromClassName('SearchResult');
// Prepare SQL
$sql = "SELECT $tablename.* FROM $tablename".
" $where_string $order_by_string $limit_string";
$stmt = self::$__CONN__->prepare($sql);
$stmt->execute();
// Run!
if ($limit == 1)
{
return $stmt->fetchObject('SearchResult');
}
else
{
$objects = array();
while ($object = $stmt->fetchObject('SearchResult'))
{
$objects[] = $object;
}
return $objects;
}
} // find
public static function findAll($args = null)
{
return self::find($args);
}
public static function findById($id){
return self::find(array(
'where' => self::tableNameFromClassName('SearchResult').'.id='.(int)$id,
'limit' => 1
));
}
public static function findByUrl($url) {
$table = self::tableNameFromClassName('SearchResult');
return self::findAll([
'where' => "$table.url = '" . addslashes($url) . "'"
]);
}
public static function findByRecordId($id, $module) {
$table = self::tableNameFromClassName('SearchResult');
return self::find(array(
'where' => "$table.record_id = " . (int)$id . " AND $table.module = '" . $module . "'",
'limit' => 1
));
}
public static function findByPageId($id) {
return self::find(array(
'where' => self::tableNameFromClassName('SearchResult').'.page_id='.(int)$id,
'limit' => 1
));
}
public static function getResult($keyword) {
$pageTablename = self::tableNameFromClassName('SearchResult');
Setting::init();
$external = Setting::get("external_search_result") ? true : false;
// Implement a search function with the following rules:
// 1. The keyword must match at least one word in either the title or description.
// 2. Filter by URL uniqueness with strict validation:
// - If multiple records share the same URL, and any of them is invalid:
// - module == "Page" && (status != 100 && status != 101)
// - module != "Page" && (status != 1)
// - Then remove all records that share this URL.
// - Otherwise, if multiple records are valid for the same URL, prefer the one where module == "Page".
// 3. Exclude external search result if $external == true
// 4. The results should be ordered as follows:
// - First, by relevance: records where the keyword matches the title should come before those where it matches only the description.
// - Then, by updated_on date (most recent first).
// - Finally, by created_on date (most recent first).
$sql = "
SELECT sr.*
FROM (
SELECT
*,
(MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE) * 3) AS relevance,
(MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE) > 0) AS title_match
FROM `$pageTablename`
WHERE MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE)
" . ($external ? "" : "AND (external IS NULL OR external = 0 OR external <> 1)") . "
) sr
JOIN (
-- Find URLs where all records are valid
SELECT url,
MAX(CASE WHEN module = 'Page' THEN 2 ELSE 1 END) AS page_priority
FROM `$pageTablename`
WHERE MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE)
" . ($external ? "" : "AND (external IS NULL OR external = 0 OR external <> 1)") . "
GROUP BY url
HAVING SUM(
CASE
WHEN (module = 'Page' AND status NOT IN (100, 101)) OR
(module <> 'Page' AND status <> 1)
THEN 1 ELSE 0
END
) = 0 -- drop URL if any invalid
) uniq
ON sr.url = uniq.url
AND ((sr.module = 'Page' AND uniq.page_priority = 2) OR (sr.module <> 'Page' AND uniq.page_priority = 1))
ORDER BY
sr.title_match DESC,
sr.relevance DESC,
sr.updated_on DESC,
sr.created_on DESC;
";
// --- Prepare and clean keyword for boolean search ---
$stmt = self::$__CONN__->prepare($sql);
// Remove trailing 's
$cleanKeyword = preg_replace("/'s$/i", "", $keyword);
$cleanKeyword = trim($cleanKeyword);
$kw = '"' . $cleanKeyword . '"';
// Bind the correct variable
$stmt->execute([
':kw' => $kw,
]);
$objects = [];
while ($object = $stmt->fetchObject('SearchResult')) {
$objects[] = $object;
}
return $objects;
}
} // end SearchResult class
<?php
/**
* Frog CMS - Content Management Simplified. <http://www.madebyfrog.com>
* Copyright (C) 2008 Philippe Archambault <philippe.archambault@gmail.com>
*
* This file is part of Frog CMS.
*
* Frog CMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Frog CMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Frog CMS. If not, see <http://www.gnu.org/licenses/>.
*
* Frog CMS has made an exception to the GNU General Public License for plugins.
* See exception.txt for details and the full text.
*/
/**
* @package frog
* @subpackage models
*
* @author Philippe Archambault <philippe.archambault@gmail.com>
* @version 0.1
* @license http://www.gnu.org/licenses/gpl.html GPL License
* @copyright Philippe Archambault, 2008
*/
/**
* class SearchResult
*
* @author Philippe Archambault <philippe.archambault@gmail.com>
* @since Frog version 0.1
*/
class SearchResult extends Record // All properties and methods of Record are available in SearchResult, unless overridden.
{
const TABLE_NAME = 'search_result';
public static function find($args = null)
{
// Collect attributes...
$where = isset($args['where']) ? trim($args['where']) : '';
$order_by = isset($args['order']) ? trim($args['order']) : '';
$offset = isset($args['offset']) ? (int) $args['offset'] : 0;
$limit = isset($args['limit']) ? (int) $args['limit'] : 0;
// Prepare query parts
$where_string = empty($where) ? '' : "WHERE $where";
$order_by_string = empty($order_by) ? '' : "ORDER BY $order_by";
$limit_string = $limit > 0 ? "LIMIT $offset, $limit" : '';
$tablename = self::tableNameFromClassName('SearchResult');
// Prepare SQL
$sql = "SELECT $tablename.* FROM $tablename".
" $where_string $order_by_string $limit_string";
$stmt = self::$__CONN__->prepare($sql);
$stmt->execute();
// Run!
if ($limit == 1)
{
return $stmt->fetchObject('SearchResult');
}
else
{
$objects = array();
while ($object = $stmt->fetchObject('SearchResult'))
{
$objects[] = $object;
}
return $objects;
}
} // find
public static function findAll($args = null)
{
return self::find($args);
}
public static function findById($id){
return self::find(array(
'where' => self::tableNameFromClassName('SearchResult').'.id='.(int)$id,
'limit' => 1
));
}
public static function findByUrl($url) {
$table = self::tableNameFromClassName('SearchResult');
return self::findAll([
'where' => "$table.url = '" . addslashes($url) . "'"
]);
}
public static function findByRecordId($id, $module) {
$table = self::tableNameFromClassName('SearchResult');
return self::find(array(
'where' => "$table.record_id = " . (int)$id . " AND $table.module = '" . $module . "'",
'limit' => 1
));
}
public static function findByPageId($id) {
return self::find(array(
'where' => self::tableNameFromClassName('SearchResult').'.page_id='.(int)$id,
'limit' => 1
));
}
public static function getResult($keyword) {
$pageTablename = self::tableNameFromClassName('SearchResult');
Setting::init();
$external = Setting::get("external_search_result") ? true : false;
// Implement a search function with the following rules:
// 1. The keyword must match at least one word in either the title or description.
// 2. Filter by URL uniqueness with strict validation:
// - If multiple records share the same URL, and any of them is invalid:
// - module == "Page" && (status != 100 && status != 101)
// - module != "Page" && (status != 1)
// - Then remove all records that share this URL.
// - Otherwise, if multiple records are valid for the same URL, prefer the one where module == "Page".
// 3. Exclude external search result if $external == true
// 4. The results should be ordered as follows:
// - First, by relevance: records where the keyword matches the title should come before those where it matches only the description.
// - Then, by updated_on date (most recent first).
// - Finally, by created_on date (most recent first).
$sql = "
SELECT sr.*
FROM (
SELECT
*,
(MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE) * 3) AS relevance,
(MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE) > 0) AS title_match
FROM `$pageTablename`
WHERE MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE)
" . ($external ? "" : "AND (external IS NULL OR external = 0 OR external <> 1)") . "
) sr
JOIN (
-- Find URLs where all records are valid
SELECT url,
MAX(CASE WHEN module = 'Page' THEN 2 ELSE 1 END) AS page_priority
FROM `$pageTablename`
WHERE MATCH(title, description) AGAINST(:kw IN BOOLEAN MODE)
" . ($external ? "" : "AND (external IS NULL OR external = 0 OR external <> 1)") . "
GROUP BY url
HAVING SUM(
CASE
WHEN (module = 'Page' AND status NOT IN (100, 101)) OR
(module <> 'Page' AND status <> 1)
THEN 1 ELSE 0
END
) = 0 -- drop URL if any invalid
) uniq
ON sr.url = uniq.url
AND ((sr.module = 'Page' AND uniq.page_priority = 2) OR (sr.module <> 'Page' AND uniq.page_priority = 1))
ORDER BY
sr.title_match DESC,
sr.relevance DESC,
sr.updated_on DESC,
sr.created_on DESC;
";
// --- Prepare and clean keyword for boolean search ---
$stmt = self::$__CONN__->prepare($sql);
// Remove trailing 's
$cleanKeyword = preg_replace("/'s$/i", "", $keyword);
$cleanKeyword = trim($cleanKeyword);
$kw = '"' . $cleanKeyword . '"';
// Bind the correct variable
$stmt->execute([
':kw' => $kw,
]);
$objects = [];
while ($object = $stmt->fetchObject('SearchResult')) {
$objects[] = $object;
}
return $objects;
}
} // end SearchResult class
Update: Framework.php → class Record {}:
1. saveSearchResult()
2. getSearchContent()
3. isInternalUrl()
4. getUrl()
5. getOgImgFromPageId() (adjust banner image path if needed)
/**
* 20250919.Allan
* Allows sub-classes do stuff after a Record is saved.
*
* @return boolean True if the actions succeeded.
*/
public function afterSave() {
Setting::init();
$search_module = json_decode(Setting::get('search_module'), true);
$allowedModules = array_keys($search_module);
$allowedModules = array_merge(['Page', 'PagePart'], $allowedModules);
// Get the model name
$module_name = (new \ReflectionClass($this))->getShortName();
if (in_array($module_name, $allowedModules)) {
$this->saveSearchResult($module_name);
}
// To show warning if module status is hidden
if ($module_name == "Page" && ($this->status_id == 100 || $this->status_id == 101)) {
$page_search_result = SearchResult::findByRecordId((int)$this->id, "Page");
$search_results = SearchResult::findByUrl($page_search_result->url);
foreach ($search_results as $result) {
if ($result->module !== "Page" && (string)$result->status === "0") {
Flash::set("error", ("This ".preg_replace('/(?<!^)([A-Z])/', ' $1', $result->module)." status is Hidden!"));
}
}
}
return true;
}
public function saveSearchResult($module_name) {
$page_id = null;
$module_status = null;
$internal_page = false;
$external_page = false;
$page = null;
$page_search_result = null;
$module_search_result = null;
// check if saving Page
if ($module_name == 'Page') {
$page_id = $this->id;
$internal_page = true;
// check if saving PagePart
} else if ($module_name == 'PagePart') {
$page_id = $this->page_id;
$internal_page = true;
// else get module's page_id
} else if (!empty($this->page_id)) {
$page_id = $this->page_id;
$module_status = $this->status;
$internal_page = true;
}
Setting::init();
$search_module = json_decode(Setting::get('search_module'), true);
$search_fields = [];
foreach ($search_module as $name => $fields) {
if ($name == $module_name) {
$search_fields = $fields;
}
}
$image = URL_PUBLIC.'public/default_search_img.webp';
// get image path
if (!empty($search_fields['image_path'])) {
$image = str_replace(
[':module', ':id', ':filename'],
[
strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $module_name)),
$this->id,
$this->{$search_fields['image']}
],
$search_fields['image_path']
);
// remove double slashes and trailing slash
$image = preg_replace('#/+#', '/', $image);
$image = rtrim($image, '/');
$image = URL_PUBLIC . ltrim($image, '/');
}
$search_data = null;
if (!empty($search_fields)) {
$search_data = [
'record_id' => $this->id,
'module' => $module_name,
'title' => $this->{$search_fields['title']},
'url' => $this->{$search_fields['url']},
'external' => "",
'image' => $image,
'description' => $this->{$search_fields['description']},
'created_on' => $this->created_on,
'updated_on' => $this->updated_on,
'status' => $this->status,
];
// If no page_id then get url - check if internal
if (!empty($this->{$search_fields['url']}) && $this->isInternalUrl($this->{$search_fields['url']})) {
$uri = parse_url($this->{$search_fields['url']}, PHP_URL_PATH);
$uri = trim($uri, '/');
if (substr($uri, 0, strlen('rckuc/')) === 'rckuc/') {
$uri = substr($uri, strlen('rckuc/'));
}
$page = Page::findByModuleLink($uri);
if ($page) {
$page_id = $page->id;
$module_status = $this->status;
$internal_page = true;
}
// Check if external
} else if (!empty($this->{$search_fields['url']}) && !$this->isInternalUrl($this->{$search_fields['url']})) {
$external_page = true;
}
}
if ($internal_page) {
$page = Page::findById((int)$page_id);
$url = URL_PUBLIC . (USE_MOD_REWRITE ? '' : '?') . $page->getUri() . ($page->getUri() ? URL_SUFFIX : '');
$search_content = $page->getSearchContent($url, $module_status, $page);
$page_search_result = SearchResult::findByRecordId($page_id, 'Page');
$module_search_result = $search_data ? SearchResult::findByRecordId($this->id, $module_name) : null;
// Handle page
$page_search_data = [
'record_id' => $page_id,
'module' => 'Page',
'title' => $page->title,
'url' => $url,
'image' => $page->getOgImgFromPageId((int)$page_id) ? $page->getOgImgFromPageId((int)$page_id) : $image,
'description' => $search_content,
'created_on' => $page->created_on,
'updated_on' => $page->updated_on,
'status' => $page->status_id,
];
// Handle modules (newsevent, project, etc.)
if ($search_data) {
if (!empty($this->{$map['image']})) {
$page_search_data['image'] = URL_PUBLIC.'public/'.strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $module_name)).'/'.$map['image'].'/'.$this->id.'/'.$this->{$map['image']};
}
$search_data['description'] = $search_content;
$search_data['url'] = $url;
if ($module_search_result) {
$module_search_result->setFromData($search_data);
} else {
$module_search_result = new SearchResult($search_data);
}
}
if ($page_search_result) {
$page_search_result->setFromData($page_search_data);
} else {
$page_search_result = new SearchResult($page_search_data);
}
} else if ($external_page) {
$module_search_result = SearchResult::findByRecordId($this->id, $module_name);
$search_data['external'] = 1;
if ($module_search_result) {
$module_search_result->setFromData($search_data);
} else {
$module_search_result = new SearchResult($search_data);
}
}
if ($page_search_result) {
$page_search_result->save();
}
if ($module_search_result) {
$module_search_result->save();
}
}
public function getSearchContent($url, $module_status = null, $page) {
$html = @file_get_contents($url);
$search_content = '';
if ($html) {
// Parse HTML safely
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING);
libxml_clear_errors();
$xpath = new DOMXPath($doc);
// Remove unwanted tags
$removeQueries = [
'//script',
'//style',
'//body/div[@id="header-holder"]',
'//body/div[@id="banner-zone"]',
'//body/div[@id="banner-content"]',
'//body/div[@id="banner-contant"]',
'//body/div[contains(@class,"breadcrumb")]',
'//body/div[@id="footer"]',
'//body/div[@id="copyright"]'
];
foreach ($removeQueries as $query) {
foreach ($xpath->query($query) as $node) {
$node->parentNode->removeChild($node);
}
}
// Collect visible text nodes, ensure spacing
$nodes = $xpath->query('//body//text()');
$texts = [];
foreach ($nodes as $node) {
$val = trim($node->nodeValue);
if ($val !== '') {
$texts[] = $val;
}
}
$search_content = preg_replace('/\s+/', ' ', implode(' ', $texts));
$search_content = strip_tags($search_content);
}
$blocked = [
Page::STATUS_DRAFT => 'Draft',
Page::STATUS_PREVIEW => 'Preview',
Page::STATUS_ARCHIVED => 'Archived'
];
// if ((string)$module_status === "1") { // if module status show → inform if page status blocked
// if (isset($blocked[$page->status_id])) {
// Flash::set('error', __("Page Status : \"{$blocked[$page->status_id]}\". Change the status to \"Published\" or \"Hidden\" to make it searchable."));
// return $search_content;
// } else {
// return $search_content;
// }
// } else if ((string)$module_status === "0") { // if module status hidden → archive page
// // if ($page->status_id != Page::STATUS_ARCHIVED) {
// // unset($page->updated_by_name, $page->created_by_name, $page->part);
// // $page->status_id = Page::STATUS_ARCHIVED;
// // $page->save();
// // Flash::set('error', __("Page status updated to \"Archived\"."));
// // return $search_content;
// // } else {
// // Flash::set('error', __("Page status remains \"Archived\"."));
// // return $search_content;
// // }
// return $search_content;
// } else {
// // if no $module_status then it is $page
// // if (isset($blocked[$page->status_id])) {
// // $search_results = SearchResult::findByUrl($url);
// // foreach ($search_results as $result) {
// // $result->status = null;
// // $result->save();
// // }
// // }
// return $search_content;
// }
if ((string)$module_status === "1" && isset($blocked[$page->status_id])) {
Flash::set('error', __("This Page Status : \"{$blocked[$page->status_id]}\"!"));
} else {
return $search_content;
}
}
public function isInternalUrl($url) {
$host = parse_url($url, PHP_URL_HOST);
$baseHost = parse_url(URL_PUBLIC, PHP_URL_HOST);
return $host && strcasecmp($host, $baseHost) === 0;
}
public function getUrl() {
return URL_PUBLIC . (USE_MOD_REWRITE ? '' : '?') . $this->getUri() . ($this->getUri() ? URL_SUFFIX : '');
}
public function getOgImgFromPageId($id) {
$bannerImage = '';
if ($id == 1) {
$banner = Banner::getHomeMetaTagImage();
$bannerImage = URL_PUBLIC.'public/banner/banner_image/'.$banner->id.'/'.$banner->banner_image;
} else {
$banner = Banner::findByPageId($id);
if (empty($banner)) {
$banner = Banner::getHomeMetaTagImage();
}
$bannerImage = URL_PUBLIC.'public/banner/banner_image/'.$banner->id.'/'.$banner->banner_image;
}
if (!empty($bannerImage)) {
return $bannerImage;
} else {
return null;
}
}
/**
* Allows sub-classes do stuff after a Record is deleted.
*
* @return boolean True if the actions succeeded.
*/
public function afterDelete() {
Setting::init();
$search_module = json_decode(Setting::get('search_module'), true);
$allowedModules = array_keys($search_module);
$allowedModules = array_merge(['Page', 'PagePart'], $allowedModules);
// Get the model name
$module_name = (new \ReflectionClass($this))->getShortName();
if (in_array($module_name, $allowedModules)) {
$this->deleteSearchResult($module_name);
}
return true;
}
public function deleteSearchResult($module_name) {
$search_result = SearchResult::findByRecordId($this->id, $module_name);
if ($search_result->delete()) {
Flash::set('error', __("This Search Result Deleted."));
}
}
/**
* 20250919.Allan
* Allows sub-classes do stuff after a Record is saved.
*
* @return boolean True if the actions succeeded.
*/
public function afterSave() {
Setting::init();
$search_module = json_decode(Setting::get('search_module'), true);
$allowedModules = array_keys($search_module);
$allowedModules = array_merge(['Page', 'PagePart'], $allowedModules);
// Get the model name
$module_name = (new \ReflectionClass($this))->getShortName();
if (in_array($module_name, $allowedModules)) {
$this->saveSearchResult($module_name);
}
// To show warning if module status is hidden
if ($module_name == "Page" && ($this->status_id == 100 || $this->status_id == 101)) {
$page_search_result = SearchResult::findByRecordId((int)$this->id, "Page");
$search_results = SearchResult::findByUrl($page_search_result->url);
foreach ($search_results as $result) {
if ($result->module !== "Page" && (string)$result->status === "0") {
Flash::set("error", ("This ".preg_replace('/(?<!^)([A-Z])/', ' $1', $result->module)." status is Hidden!"));
}
}
}
return true;
}
public function saveSearchResult($module_name) {
$page_id = null;
$module_status = null;
$internal_page = false;
$external_page = false;
$page = null;
$page_search_result = null;
$module_search_result = null;
// check if saving Page
if ($module_name == 'Page') {
$page_id = $this->id;
$internal_page = true;
// check if saving PagePart
} else if ($module_name == 'PagePart') {
$page_id = $this->page_id;
$internal_page = true;
// else get module's page_id
} else if (!empty($this->page_id)) {
$page_id = $this->page_id;
$module_status = $this->status;
$internal_page = true;
}
Setting::init();
$search_module = json_decode(Setting::get('search_module'), true);
$search_fields = [];
foreach ($search_module as $name => $fields) {
if ($name == $module_name) {
$search_fields = $fields;
}
}
$image = URL_PUBLIC.'public/default_search_img.webp';
// get image path
if (!empty($search_fields['image_path'])) {
$image = str_replace(
[':module', ':id', ':filename'],
[
strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $module_name)),
$this->id,
$this->{$search_fields['image']}
],
$search_fields['image_path']
);
// remove double slashes and trailing slash
$image = preg_replace('#/+#', '/', $image);
$image = rtrim($image, '/');
$image = URL_PUBLIC . ltrim($image, '/');
}
$search_data = null;
if (!empty($search_fields)) {
$search_data = [
'record_id' => $this->id,
'module' => $module_name,
'title' => $this->{$search_fields['title']},
'url' => $this->{$search_fields['url']},
'external' => "",
'image' => $image,
'description' => $this->{$search_fields['description']},
'created_on' => $this->created_on,
'updated_on' => $this->updated_on,
'status' => $this->status,
];
// If no page_id then get url - check if internal
if (!empty($this->{$search_fields['url']}) && $this->isInternalUrl($this->{$search_fields['url']})) {
$uri = parse_url($this->{$search_fields['url']}, PHP_URL_PATH);
$uri = trim($uri, '/');
if (substr($uri, 0, strlen('rckuc/')) === 'rckuc/') {
$uri = substr($uri, strlen('rckuc/'));
}
$page = Page::findByModuleLink($uri);
if ($page) {
$page_id = $page->id;
$module_status = $this->status;
$internal_page = true;
}
// Check if external
} else if (!empty($this->{$search_fields['url']}) && !$this->isInternalUrl($this->{$search_fields['url']})) {
$external_page = true;
}
}
if ($internal_page) {
$page = Page::findById((int)$page_id);
$url = URL_PUBLIC . (USE_MOD_REWRITE ? '' : '?') . $page->getUri() . ($page->getUri() ? URL_SUFFIX : '');
$search_content = $page->getSearchContent($url, $module_status, $page);
$page_search_result = SearchResult::findByRecordId($page_id, 'Page');
$module_search_result = $search_data ? SearchResult::findByRecordId($this->id, $module_name) : null;
// Handle page
$page_search_data = [
'record_id' => $page_id,
'module' => 'Page',
'title' => $page->title,
'url' => $url,
'image' => $page->getOgImgFromPageId((int)$page_id) ? $page->getOgImgFromPageId((int)$page_id) : $image,
'description' => $search_content,
'created_on' => $page->created_on,
'updated_on' => $page->updated_on,
'status' => $page->status_id,
];
// Handle modules (newsevent, project, etc.)
if ($search_data) {
if (!empty($this->{$map['image']})) {
$page_search_data['image'] = URL_PUBLIC.'public/'.strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $module_name)).'/'.$map['image'].'/'.$this->id.'/'.$this->{$map['image']};
}
$search_data['description'] = $search_content;
$search_data['url'] = $url;
if ($module_search_result) {
$module_search_result->setFromData($search_data);
} else {
$module_search_result = new SearchResult($search_data);
}
}
if ($page_search_result) {
$page_search_result->setFromData($page_search_data);
} else {
$page_search_result = new SearchResult($page_search_data);
}
} else if ($external_page) {
$module_search_result = SearchResult::findByRecordId($this->id, $module_name);
$search_data['external'] = 1;
if ($module_search_result) {
$module_search_result->setFromData($search_data);
} else {
$module_search_result = new SearchResult($search_data);
}
}
if ($page_search_result) {
$page_search_result->save();
}
if ($module_search_result) {
$module_search_result->save();
}
}
public function getSearchContent($url, $module_status = null, $page) {
$html = @file_get_contents($url);
$search_content = '';
if ($html) {
// Parse HTML safely
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING);
libxml_clear_errors();
$xpath = new DOMXPath($doc);
// Remove unwanted tags
$removeQueries = [
'//script',
'//style',
'//body/div[@id="header-holder"]',
'//body/div[@id="banner-zone"]',
'//body/div[@id="banner-content"]',
'//body/div[@id="banner-contant"]',
'//body/div[contains(@class,"breadcrumb")]',
'//body/div[@id="footer"]',
'//body/div[@id="copyright"]'
];
foreach ($removeQueries as $query) {
foreach ($xpath->query($query) as $node) {
$node->parentNode->removeChild($node);
}
}
// Collect visible text nodes, ensure spacing
$nodes = $xpath->query('//body//text()');
$texts = [];
foreach ($nodes as $node) {
$val = trim($node->nodeValue);
if ($val !== '') {
$texts[] = $val;
}
}
$search_content = preg_replace('/\s+/', ' ', implode(' ', $texts));
$search_content = strip_tags($search_content);
}
$blocked = [
Page::STATUS_DRAFT => 'Draft',
Page::STATUS_PREVIEW => 'Preview',
Page::STATUS_ARCHIVED => 'Archived'
];
// if ((string)$module_status === "1") { // if module status show → inform if page status blocked
// if (isset($blocked[$page->status_id])) {
// Flash::set('error', __("Page Status : \"{$blocked[$page->status_id]}\". Change the status to \"Published\" or \"Hidden\" to make it searchable."));
// return $search_content;
// } else {
// return $search_content;
// }
// } else if ((string)$module_status === "0") { // if module status hidden → archive page
// // if ($page->status_id != Page::STATUS_ARCHIVED) {
// // unset($page->updated_by_name, $page->created_by_name, $page->part);
// // $page->status_id = Page::STATUS_ARCHIVED;
// // $page->save();
// // Flash::set('error', __("Page status updated to \"Archived\"."));
// // return $search_content;
// // } else {
// // Flash::set('error', __("Page status remains \"Archived\"."));
// // return $search_content;
// // }
// return $search_content;
// } else {
// // if no $module_status then it is $page
// // if (isset($blocked[$page->status_id])) {
// // $search_results = SearchResult::findByUrl($url);
// // foreach ($search_results as $result) {
// // $result->status = null;
// // $result->save();
// // }
// // }
// return $search_content;
// }
if ((string)$module_status === "1" && isset($blocked[$page->status_id])) {
Flash::set('error', __("This Page Status : \"{$blocked[$page->status_id]}\"!"));
} else {
return $search_content;
}
}
public function isInternalUrl($url) {
$host = parse_url($url, PHP_URL_HOST);
$baseHost = parse_url(URL_PUBLIC, PHP_URL_HOST);
return $host && strcasecmp($host, $baseHost) === 0;
}
public function getUrl() {
return URL_PUBLIC . (USE_MOD_REWRITE ? '' : '?') . $this->getUri() . ($this->getUri() ? URL_SUFFIX : '');
}
public function getOgImgFromPageId($id) {
$bannerImage = '';
if ($id == 1) {
$banner = Banner::getHomeMetaTagImage();
$bannerImage = URL_PUBLIC.'public/banner/banner_image/'.$banner->id.'/'.$banner->banner_image;
} else {
$banner = Banner::findByPageId($id);
if (empty($banner)) {
$banner = Banner::getHomeMetaTagImage();
}
$bannerImage = URL_PUBLIC.'public/banner/banner_image/'.$banner->id.'/'.$banner->banner_image;
}
if (!empty($bannerImage)) {
return $bannerImage;
} else {
return null;
}
}
/**
* Allows sub-classes do stuff after a Record is deleted.
*
* @return boolean True if the actions succeeded.
*/
public function afterDelete() {
Setting::init();
$search_module = json_decode(Setting::get('search_module'), true);
$allowedModules = array_keys($search_module);
$allowedModules = array_merge(['Page', 'PagePart'], $allowedModules);
// Get the model name
$module_name = (new \ReflectionClass($this))->getShortName();
if (in_array($module_name, $allowedModules)) {
$this->deleteSearchResult($module_name);
}
return true;
}
public function deleteSearchResult($module_name) {
$search_result = SearchResult::findByRecordId($this->id, $module_name);
if ($search_result->delete()) {
Flash::set('error', __("This Search Result Deleted."));
}
}
Update Model: Page.php
Add 2 new functions:
1. findByModuleLink()
2. findBySlugWithAllStatus()
Update Module: "Setting"
<tr>
<td colspan="3"><h3><?php echo __('Search Result Setting'); ?></h3></td>
</tr>
<tr>
<td class="label"><label for="search_module"><?php echo __('Module To Search'); ?></label></td>
<td class="field">
<textarea class="textbox" id="search_module" name="setting[search_module]" rows="10" cols="10" placeholder="Enter Module Name and saperate by comma ','"><?php echo htmlentities(Setting::get('search_module'), ENT_COMPAT, 'UTF-8'); ?></textarea><br>
<div class="switch_container">
<label class="switch">
<input type="checkbox" name="setting[external_search_result]" <?php echo htmlentities(Setting::get('external_search_result'), ENT_COMPAT, 'UTF-8') == 1 ? 'checked' : ''; ?> value="1">
<span class="switch_slider round"></span>
</label>
<label class="switch_label">Include External Search Result<label>
</div>
<div id="sync_button_list">
<button type="button" onclick="window.location.href='<?php echo get_url('setting/syncSearchResult'); ?>'">Sync All Search Result</button>
<button type="button" onclick="window.location.href='<?php echo get_url('setting/syncSearchResult/Page'); ?>'">Sync 'Page' Search Result</button>
</div>
</td>
<td class="help"><?php echo __("Example:"); ?> <br><br>
<?php echo __('{'); ?> <br>
<?php echo __('"Newsevent": {"title": "title", "description": "description","image": "newsevent_image", "image_path": "public/:module/newsevent_image/:id/:filename"},'); ?> <br>
<?php echo __('"Project": {"title": "title", "description": "description", "image": "image"}, "image_path": "public/:module/images/:id/:filename"'); ?> <br>
<?php echo __('"BusinessProfile": {"title": "company_name", "description": "short_description", "url": "website", "image": "business_logo", "image_path": "public/:module/business_logo/:id/:filename"}'); ?> <br>
<?php echo __('}'); ?> <br><br>
<?php echo __("Your Module should have : id, created_on, updated_on & status."); ?>
</td>
</tr>
<script>
$(document).ready(function() {
function updateSyncButton() {
let json = JSON.parse($("#search_module").val() || '{}');
console.log(json);
let modules = Object.keys(json); // <- NOW it has values
let urlBase = "<?php echo get_url('setting/syncSearchResult/'); ?>"; // PHP runs first
$("#sync_button_list button:gt(1)").remove();
modules.forEach(function(module) {
let url = urlBase + module;
let button = $(`
<button type="button"
onclick="window.location.href='${url}'">
Sync '${module}' Search Result
</button>
`);
$("#sync_button_list").append(button);
});
}
updateSyncButton();
})
</script>
<tr>
<td colspan="3"><h3><?php echo __('Search Result Setting'); ?></h3></td>
</tr>
<tr>
<td class="label"><label for="search_module"><?php echo __('Module To Search'); ?></label></td>
<td class="field">
<textarea class="textbox" id="search_module" name="setting[search_module]" rows="10" cols="10" placeholder="Enter Module Name and saperate by comma ','"><?php echo htmlentities(Setting::get('search_module'), ENT_COMPAT, 'UTF-8'); ?></textarea><br>
<div class="switch_container">
<label class="switch">
<input type="checkbox" name="setting[external_search_result]" <?php echo htmlentities(Setting::get('external_search_result'), ENT_COMPAT, 'UTF-8') == 1 ? 'checked' : ''; ?> value="1">
<span class="switch_slider round"></span>
</label>
<label class="switch_label">Include External Search Result<label>
</div>
<div id="sync_button_list">
<button type="button" onclick="window.location.href='<?php echo get_url('setting/syncSearchResult'); ?>'">Sync All Search Result</button>
<button type="button" onclick="window.location.href='<?php echo get_url('setting/syncSearchResult/Page'); ?>'">Sync 'Page' Search Result</button>
</div>
</td>
<td class="help"><?php echo __("Example:"); ?> <br><br>
<?php echo __('{'); ?> <br>
<?php echo __('"Newsevent": {"title": "title", "description": "description","image": "newsevent_image", "image_path": "public/:module/newsevent_image/:id/:filename"},'); ?> <br>
<?php echo __('"Project": {"title": "title", "description": "description", "image": "image"}, "image_path": "public/:module/images/:id/:filename"'); ?> <br>
<?php echo __('"BusinessProfile": {"title": "company_name", "description": "short_description", "url": "website", "image": "business_logo", "image_path": "public/:module/business_logo/:id/:filename"}'); ?> <br>
<?php echo __('}'); ?> <br><br>
<?php echo __("Your Module should have : id, created_on, updated_on & status."); ?>
</td>
</tr>
<script>
$(document).ready(function() {
function updateSyncButton() {
let json = JSON.parse($("#search_module").val() || '{}');
console.log(json);
let modules = Object.keys(json); // <- NOW it has values
let urlBase = "<?php echo get_url('setting/syncSearchResult/'); ?>"; // PHP runs first
$("#sync_button_list button:gt(1)").remove();
modules.forEach(function(module) {
let url = urlBase + module;
let button = $(`
<button type="button"
onclick="window.location.href='${url}'">
Sync '${module}' Search Result
</button>
`);
$("#sync_button_list").append(button);
});
}
updateSyncButton();
})
</script>
Insert Record in Database: wolf_setting
INSERT INTO `wolf_setting` (`name`, `value`) VALUES ('search_module', ''), ('external_search_result', '1');
INSERT INTO `wolf_setting` (`name`, `value`) VALUES ('search_module', ''), ('external_search_result', '1');
Example JSON:
Modules with external links (Website / Read More URL) must define url in the JSON mapping.
Image path for ":model", ":id", ":filename", wiill be replace
{
"Newsevent": {
"title": "title",
"description": "description",
"url": "url",
"image": "newsevent_image",
"image_path": "public/:module/newsevent_image/:id/:filename"
},
"Project": {
"title": "title",
"description": "description",
"image": "image",
"image_path": "public/:module/images/:id/:filename"
}
}
{
"Newsevent": {
"title": "title",
"description": "description",
"url": "url",
"image": "newsevent_image",
"image_path": "public/:module/newsevent_image/:id/:filename"
},
"Project": {
"title": "title",
"description": "description",
"image": "image",
"image_path": "public/:module/images/:id/:filename"
}
}
Update: SettingController.php
Update _save() function
Add new function: syncSearchResult()
// Add this in your _save() function
if (empty($data['external_search_result'])){
$data['external_search_result'] = 0;
}
public function syncSearchResult($selected_module) {
Setting::init();
$search_modules = json_decode(Setting::get('search_module'), true) ?: [];
$modules = [];
if (!empty($selected_module)) {
$modules[] = $selected_module;
} else {
// Collect module names from $search_modules and ensure 'Page' is included
$modules = array_unique(array_merge(['Page'], array_keys($search_modules)));
}
$chunkSize = 5; // number of records per batch
foreach ($modules as $module) {
if (!is_string($module) || !class_exists($module)) continue;
$offset = 0;
do {
// Fetch a chunk of records
$records = $module::findAll([
'limit' => $chunkSize,
'offset' => $offset
]);
if (empty($records)) break;
foreach ($records as $record) {
if (is_object($record) && method_exists($record, 'saveSearchResult')) {
$record->saveSearchResult($module);
}
}
$offset += $chunkSize;
} while (true);
}
$moduleName = !empty($selected_module) ? '"' . $selected_module . '" ' : '';
Flash::set('success', __('All ' . $moduleName . 'Search Results Have Been Synced!'));
redirect(get_url('setting'));
}
// Add this in your _save() function
if (empty($data['external_search_result'])){
$data['external_search_result'] = 0;
}
public function syncSearchResult($selected_module) {
Setting::init();
$search_modules = json_decode(Setting::get('search_module'), true) ?: [];
$modules = [];
if (!empty($selected_module)) {
$modules[] = $selected_module;
} else {
// Collect module names from $search_modules and ensure 'Page' is included
$modules = array_unique(array_merge(['Page'], array_keys($search_modules)));
}
$chunkSize = 5; // number of records per batch
foreach ($modules as $module) {
if (!is_string($module) || !class_exists($module)) continue;
$offset = 0;
do {
// Fetch a chunk of records
$records = $module::findAll([
'limit' => $chunkSize,
'offset' => $offset
]);
if (empty($records)) break;
foreach ($records as $record) {
if (is_object($record) && method_exists($record, 'saveSearchResult')) {
$record->saveSearchResult($module);
}
}
$offset += $chunkSize;
} while (true);
}
$moduleName = !empty($selected_module) ? '"' . $selected_module . '" ' : '';
Flash::set('success', __('All ' . $moduleName . 'Search Results Have Been Synced!'));
redirect(get_url('setting'));
}