Components Guide
Learn the DAO, Service, Controller, and Model patterns for building features in Scriptlog.
Architecture Pattern
Scriptlog uses a layered architecture for clean separation of concerns:
Request
HTTP request from user
Controller
Handles HTTP logic
Service
Business logic & validation
DAO
Database operations
Database
MySQL/MariaDB
DAO
Data Access Layer - handles all database operations
Service
Business logic, validation, and orchestration
Controller
HTTP request handling, calls services
Model
Data entities and transformation
DAO (Data Access Object)
DAO Pattern Guidelines
| Guideline | Description |
|---|---|
| Single Responsibility | Each DAO handles one database table |
| Prepared Statements | Use for all queries to prevent SQL injection |
| Return Format | Return associative arrays or objects |
| Error Handling | Handle exceptions gracefully |
Example: PostDao
PHP
lib/dao/PostDao.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");
class PostDao extends Dao
{
public function __construct()
{
parent::__construct();
}
public function findPosts($orderBy = 'ID', $author = null, $onlyPublished = true)
{
$allowedColumns = ['ID', 'post_date', 'post_title', 'post_modified'];
$sortColumn = in_array($orderBy, $allowedColumns) ? $orderBy : 'ID';
$sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date,
p.post_modified, p.post_title, p.post_slug,
p.post_content, p.post_status, p.post_visibility,
p.post_password, p.post_tags, p.post_headlines,
p.post_type, p.post_locale, p.passphrase, u.user_login
FROM tbl_posts AS p
INNER JOIN tbl_users AS u ON p.post_author = u.ID
WHERE p.post_type = 'blog'";
$data = [];
if (!is_null($author)) {
$sql .= " AND p.post_author = ?";
$data[] = (int)$author;
}
if ($onlyPublished) {
$sql .= " AND p.post_status = 'publish' AND p.post_visibility = 'public'";
}
$sql .= " ORDER BY p.$sortColumn DESC";
$this->setSQL($sql);
$posts = $this->findAll($data);
return (empty($posts)) ? [] : $posts;
}
public function findPost($id, $sanitize, $author = null, $onlyPublished = true)
{
$idsanitized = $this->filteringId($sanitize, $id, 'sql');
$sql = "SELECT ID, media_id, post_author, post_date, post_modified,
post_title, post_slug, post_content, post_summary,
post_status, post_visibility, post_password, post_tags,
post_headlines, post_locale, comment_status, passphrase
FROM tbl_posts
WHERE ID = ? AND post_type = 'blog'";
$data = [$idsanitized];
if (!is_null($author)) {
$sql .= " AND post_author = ?";
$data[] = (int)$author;
}
if ($onlyPublished) {
$sql .= " AND post_status = 'publish' AND post_visibility = 'public'";
}
$this->setSQL($sql);
$postDetail = $this->findRow($data);
return (empty($postDetail)) ? false : $postDetail;
}
public function createPost($bind, $topicId)
{
$this->setSQL("SET SQL_MODE='ALLOW_INVALID_DATE'");
$this->create("tbl_posts", [
'media_id' => $bind['media_id'] ?? null,
'post_author' => $bind['post_author'],
'post_date' => $bind['post_date'],
'post_title' => $bind['post_title'],
'post_slug' => $bind['post_slug'],
'post_content' => $bind['post_content'],
'post_summary' => $bind['post_summary'],
'post_status' => $bind['post_status'],
'post_visibility' => $bind['post_visibility'],
'post_password' => $bind['post_password'],
'post_tags' => $bind['post_tags'],
'post_headlines' => $bind['post_headlines'],
'post_locale' => $bind['post_locale'] ?? 'en',
'comment_status' => $bind['comment_status'],
'passphrase' => $bind['passphrase']
]);
$postId = $this->lastId();
if ((is_array($topicId)) && (!empty($postId))) {
foreach ($_POST['catID'] as $topicId) {
$this->create("tbl_post_topic", [
'post_id' => $postId,
'topic_id' => $topicId
]);
}
} else {
$this->create("tbl_post_topic", [
'post_id' => $postId,
'topic_id' => $topicId
]);
}
return $postId;
}
public function updatePost($sanitize, $bind, $ID, $topicId)
{
$cleanId = $this->filteringId($sanitize, $ID, 'sql');
try {
$this->callTransaction();
$this->modify("tbl_posts", [
'post_author' => $bind['post_author'],
'post_modified' => $bind['post_modified'],
'post_title' => $bind['post_title'],
'post_slug' => $bind['post_slug'],
'post_content' => $bind['post_content'],
'post_summary' => $bind['post_summary'],
'post_status' => $bind['post_status'],
'post_visibility' => $bind['post_visibility'],
'post_password' => $bind['post_password'],
'post_tags' => $bind['post_tags'],
'post_headlines' => $bind['post_headlines'],
'post_locale' => $bind['post_locale'] ?? 'en',
'comment_status' => $bind['comment_status'],
'passphrase' => $bind['passphrase']
], ['ID' => (int)$cleanId]);
$this->deleteRecord("tbl_post_topic", ['post_id' => $cleanId]);
if ((is_array($topicId)) && (isset($_POST['catID']))) {
foreach ($_POST['catID'] as $topicId) {
$this->create("tbl_post_topic", [
'post_id' => $cleanId,
'topic_id' => $topicId
]);
}
}
$this->callCommit();
} catch (\Throwable $th) {
$this->callRollBack();
$this->error = LogError::exceptionHandler($th);
}
}
public function deletePost($id, $sanitize)
{
$cleanId = $this->filteringId($sanitize, $id, 'sql');
$this->deleteRecord("tbl_posts", ['ID' => $cleanId]);
}
public function anonymizePostAuthor($authorId)
{
$sql = "UPDATE tbl_posts SET post_author = ? WHERE post_author = ?";
$this->setSQL($sql);
$this->dbc->dbQuery($sql, [1, (int)$authorId]);
return true;
}
public function checkPostId($id, $sanitizing)
{
$sql = "SELECT ID FROM tbl_posts WHERE ID = ? AND post_type = 'blog'";
$idsanitized = $this->filteringId($sanitizing, $id, 'sql');
$this->setSQL($sql);
return $this->checkCountValue([$idsanitized]) > 0;
}
public function dropDownPostStatus($selected = "")
{
$posts_status = ['publish' => 'Publish', 'draft' => 'Draft'];
$this->selected = $selected;
return dropdown('post_status', $posts_status, $this->selected);
}
public function dropDownCommentStatus($selected = "")
{
$comment_status = ['open' => 'Open', 'closed' => 'Closed'];
$this->selected = $selected;
return dropdown('comment_status', $comment_status, $this->selected);
}
public function dropDownVisibility($selected = null, $postId = null)
{
// Returns HTML select with public/private/protected options
// Includes password field for protected posts
}
public function dropDownLocale($selected = "")
{
$locales = [
'en' => 'English', 'es' => 'Spanish', 'fr' => 'French',
'de' => 'German', 'zh' => 'Chinese', 'ar' => 'Arabic', ...
];
$this->selected = $selected;
return dropdown('post_locale', $locales, $this->selected);
}
public function totalPostRecords(array $data = []): ?int
{
if (!empty($data)) {
$sql = "SELECT ID FROM tbl_posts WHERE post_author = ? AND post_type = 'blog'";
} else {
$sql = "SELECT ID FROM tbl_posts WHERE post_type = 'blog'";
}
$this->setSQL($sql);
return $this->checkCountValue($data) ?? 0;
}
}
Service Layer
Service Layer Guidelines
| Principle | Description |
|---|---|
| Business Logic | Services contain business logic |
| Validation | Services validate input |
| Data Access | Services call DAOs |
| Composition | Services can call other services |
Example: PostService
PHP
lib/service/PostService.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");
class PostService
{
private $postId;
private $post_image;
private $author;
private $post_date;
private $post_modified;
private $title;
private $slug;
private $content;
private $meta_desc;
private $post_status;
private $post_visibility;
private $post_password;
private $post_headlines;
private $comment_status;
private $passphrase;
private $topics;
private $tags;
private $post_locale;
private $postDao;
private $validator;
private $sanitizer;
public function __construct(PostDao $postDao, FormValidator $validator, Sanitize $sanitizer)
{
$this->postDao = $postDao;
$this->validator = $validator;
$this->sanitizer = $sanitizer;
}
// Setter methods for post properties
public function setPostId($postId) { $this->postId = $postId; }
public function setPostImage($post_image) { $this->post_image = $post_image; }
public function setPostAuthor($author) { $this->author = $author; }
public function setPostDate($date_created) { $this->post_date = $date_created; }
public function setPostModified($date_modified) { $this->post_modified = $date_modified; }
public function setPostTitle($title) { $this->title = prevent_injection($title); }
public function setPostSlug($slug) { $this->slug = make_slug($slug); }
public function setPostContent($content) { $this->content = purify_dirty_html($content); }
public function setMetaDesc($meta_desc) { $this->meta_desc = prevent_injection($meta_desc); }
public function setPublish($post_status) { $this->post_status = $post_status; }
public function setVisibility($post_visibility) { $this->post_visibility = $post_visibility; }
public function setProtected($post_password) { $this->post_password = $post_password; }
public function setHeadlines($post_headlines) { $this->post_headlines = $post_headlines; }
public function setComment($comment_status) { $this->comment_status = $comment_status; }
public function setPassPhrase($passphrase) { $this->passphrase = md5(app_key() . $passphrase); }
public function setTopics($topics) { $this->topics = $topics; }
public function setPostTags($tags) { $this->tags = $tags; }
public function setPostLocale($post_locale) { $this->post_locale = sanitize_locale($post_locale); }
// Retrieve posts
public function grabPosts($orderBy = 'ID', $author = null)
{
return $this->postDao->findPosts($orderBy, $author, false);
}
public function grabPost($postId)
{
return $this->postDao->findPost($postId, $this->sanitizer, null, false);
}
// Create new post
public function addPost()
{
$category = new TopicDao();
$this->validator->sanitize($this->author, 'int');
$this->validator->sanitize($this->post_image, 'int');
$this->validator->sanitize($this->title, 'string');
// Create "Uncategorized" topic if no topic selected
if ($this->topics == 0) {
$categoryId = $category->createTopic([
'topic_title' => 'Uncategorized',
'topic_slug' => 'uncategorized'
]);
$getCategory = $category->findTopicById($categoryId, $this->sanitizer, PDO::FETCH_ASSOC);
$topic_id = isset($getCategory['ID']) ? abs((int)$getCategory['ID']) : 0;
} else {
$topic_id = $this->topics;
}
$new_post = [
'media_id' => $this->post_image,
'post_author' => $this->author,
'post_date' => $this->post_date,
'post_title' => $this->title,
'post_slug' => $this->slug,
'post_content' => $this->content,
'post_summary' => $this->meta_desc,
'post_status' => $this->post_status,
'post_visibility' => $this->post_visibility,
'post_password' => $this->post_password,
'post_tags' => $this->tags,
'post_headlines' => $this->post_headlines,
'post_locale' => $this->post_locale ?? 'en',
'comment_status' => $this->comment_status,
'passphrase' => $this->passphrase
];
return $this->postDao->createPost($new_post, $topic_id);
}
// Update existing post
public function modifyPost()
{
$this->validator->sanitize($this->postId, 'int');
$this->validator->sanitize($this->author, 'int');
$this->validator->sanitize($this->post_image, 'int');
$this->validator->sanitize($this->title, 'string');
$post_data = [
'post_author' => $this->author,
'post_modified' => $this->post_modified,
'post_title' => $this->title,
'post_slug' => $this->slug,
'post_content' => $this->content,
'post_summary' => $this->meta_desc,
'post_status' => $this->post_status,
'post_visibility' => $this->post_visibility,
'post_password' => $this->post_password,
'post_tags' => $this->tags,
'post_headlines' => $this->post_headlines,
'post_locale' => $this->post_locale ?? 'en',
'comment_status' => $this->comment_status,
'passphrase' => $this->passphrase
];
if (!empty($this->post_image)) {
$post_data['media_id'] = $this->post_image;
}
return $this->postDao->updatePost($this->sanitizer, $post_data, $this->postId, $this->topics);
}
// Delete post with media cleanup
public function removePost()
{
$this->validator->sanitize($this->postId, 'int');
if (!$data_post = $this->postDao->findPost($this->postId, $this->sanitizer)) {
$_SESSION['error'] = "postNotFound";
direct_page('index.php?load=posts&error=postNotFound', 404);
return false;
}
// Delete associated media files
if ($media_id = $data_post['media_id'] ?? 0) {
$medialib = new MediaDao();
$media_data = $medialib->findMediaBlog((int)$media_id);
// ... delete media files (large_, medium_, small_ variants)
$medialib->deleteMedia((int)$media_id, $this->sanitizer);
}
return $this->postDao->deletePost($this->postId, $this->sanitizer);
}
// Dropdown helpers
public function postStatusDropDown($selected = "") { return $this->postDao->dropDownPostStatus($selected); }
public function commentStatusDropDown($selected = "") { return $this->postDao->dropDownCommentStatus($selected); }
public function visibilityDropDown($selected = "") { return $this->postDao->dropDownVisibility($selected); }
public function localeDropDown($selected = "") { return $this->postDao->dropDownLocale($selected); }
// Author utilities
public function postAuthorId() { return Session::getInstance()->scriptlog_session_id; }
public function postAuthorLevel() { return user_privilege(); }
public function totalPosts(array $data = []): ?int { return $this->postDao->totalPostRecords($data); }
}
Controller
Controller Guidelines
| Guideline | Description |
|---|---|
| HTTP Handling | Controllers handle HTTP requests |
| Service Calls | Controllers call services |
| Response Format | Controllers return views or JSON |
| Thin Design | Keep controllers thin, move logic to services |
Example: PostController
PHP
lib/controller/PostController.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");
class PostController extends BaseApp
{
private $view;
private $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
// List all posts
public function listItems()
{
$this->setView('all-posts');
$this->setPageTitle('Posts');
if ($this->postService->postAuthorLevel() == 'administrator') {
$this->view->set('postsTotal', $this->postService->totalPosts());
$this->view->set('posts', $this->postService->grabPosts());
} else {
$this->view->set('postsTotal',
$this->postService->totalPosts([$this->postService->postAuthorId()]));
$this->view->set('posts',
$this->postService->grabPosts('ID', $this->postService->postAuthorId()));
}
return $this->view->render();
}
// Create new post form and handler
public function insert()
{
$topics = new TopicDao();
$medialib = new MediaDao();
$user_level = $this->postService->postAuthorLevel();
if (isset($_POST['postFormSubmit'])) {
// CSRF validation
if (!csrf_check_token('csrfToken', $_POST, 60 * 10)) {
throw new AppException(MESSAGE_UNPLEASANT_ATTEMPT);
}
// Form validation and sanitization
$filters = [
'post_title' => isset($_POST['post_title']) ? Sanitize::strictSanitizer($_POST['post_title']) : "",
'post_content' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'post_date' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'image_id' => FILTER_SANITIZE_NUMBER_INT,
'catID' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY],
'post_status' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'visibility' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'post_password' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'comment_status' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'post_locale' => FILTER_SANITIZE_FULL_SPECIAL_CHARS
];
// Handle media upload
if (!empty($_FILES['media']['tmp_name'])) {
upload_media($_FILES['media']['tmp_name'], $_FILES['media']['type'],
$_FILES['media']['size'], $new_filename);
}
// Set post properties via service
$this->postService->setPostAuthor((int)$this->postService->postAuthorId());
$this->postService->setPostTitle(distill_post_request($filters)['post_title']);
$this->postService->setPostSlug(distill_post_request($filters)['post_title']);
$this->postService->setPostContent(distill_post_request($filters)['post_content']);
$this->postService->setPublish(distill_post_request($filters)['post_status']);
$this->postService->setVisibility(distill_post_request($filters)['visibility']);
// Handle password-protected posts
if (isset($_POST['visibility']) && $_POST['visibility'] == 'protected') {
$protected = protect_post($content, 'protected', $_POST['post_password']);
$this->postService->setProtected($protected['post_password']);
$this->postService->setPassPhrase($_POST['post_password']);
}
$this->postService->addPost();
$_SESSION['status'] = "postAdded";
direct_page('index.php?load=posts&status=postAdded', 200);
} else {
// Display empty form
$this->setView('edit-post');
$this->setPageTitle('Add new post');
$this->setFormAction(ActionConst::NEWPOST);
$this->view->set('topics', $topics->setCheckBoxTopic());
$this->view->set('medialibs', $medialib->imageUploadHandler());
$this->view->set('postStatus', $this->postService->postStatusDropDown());
$this->view->set('postVisibility', $this->postService->visibilityDropDown());
$this->view->set('postLocale', $this->postService->localeDropDown());
$this->view->set('csrfToken', csrf_generate_token('csrfToken'));
}
return $this->view->render();
}
// Update existing post
public function update($id)
{
$topics = new TopicDao();
$medialib = new MediaDao();
$user_level = $this->postService->postAuthorLevel();
if (!$getPost = $this->postService->grabPost($id)) {
$_SESSION['error'] = "postNotFound";
direct_page('index.php?load=posts&error=postNotFound', 404);
}
if (isset($_POST['postFormSubmit'])) {
// CSRF and validation...
$this->postService->setPostId((int)$id);
$this->postService->setPostAuthor($this->postService->postAuthorId());
$this->postService->setPostTitle($title);
$this->postService->setPostSlug($title);
// ... set other properties
$this->postService->modifyPost();
$_SESSION['status'] = "postUpdated";
direct_page('index.php?load=posts&status=postUpdated', 200);
} else {
$this->setView('edit-post');
$this->setPageTitle('Edit Post');
$this->setFormAction(ActionConst::EDITPOST);
$this->view->set('postData', $getPost);
$this->view->set('topics', $topics->setCheckBoxTopic($getPost['ID']));
// Decrypt protected posts for editing
if ($getPost['post_visibility'] == 'protected') {
$decrypted = decrypt_post_admin($getPost['ID']);
$this->view->set('postContent', $decrypted['post_content']);
}
}
return $this->view->render();
}
// Delete post
public function remove($id)
{
$id = abs((int)$id);
if (!$this->postService->grabPost($id)) {
$_SESSION['error'] = "postNotFound";
direct_page('index.php?load=posts&error=postNotFound', 404);
}
$this->postService->setPostId($id);
$this->postService->removePost();
$_SESSION['status'] = "postDeleted";
direct_page('index.php?load=posts&status=postDeleted', 200);
}
protected function setView($viewName)
{
$this->view = new View('admin', 'ui', 'posts', $viewName);
}
}
Model
Model Guidelines
| Principle | Description |
|---|---|
| Data Entities | Models represent data entities |
| Transformation | Models can contain data transformation logic |
| View Preparation | Models are used for view data preparation |
Example: PostModel
PHP
lib/model/PostModel.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");
class PostModel extends BaseModel
{
private $linkPosts;
// Get posts for sharing feeds
public function getPostFeeds($limit)
{
$sql = "SELECT p.ID, p.media_id, p.post_author,
p.post_date, p.post_modified, p.post_title,
p.post_slug, p.post_content, p.post_type,
p.post_status, p.post_tags, p.post_sticky,
u.user_fullname, u.user_login
FROM tbl_posts AS p
INNER JOIN tbl_users AS u ON p.post_author = u.ID
WHERE p.post_type = 'blog' AND p.post_status = 'publish'
AND p.post_visibility = 'public'
ORDER BY p.ID DESC LIMIT :limit";
$this->setSQL($sql);
return $this->findAll([':limit' => $limit]) ?: [];
}
// Get latest posts for homepage
public function getLatestPosts($limit)
{
$sql = "SELECT p.ID, p.media_id, p.post_author,
p.post_date AS created_at, p.post_modified AS modified_at,
p.post_title, p.post_slug, p.post_content, p.post_summary,
p.post_keyword, p.post_status, p.post_tags,
m.media_filename, m.media_caption, m.media_access,
u.user_fullname, u.user_login,
(SELECT COUNT(c.ID) FROM " . $this->table('tbl_comments') . " c
WHERE c.comment_post_id = p.ID AND c.comment_status = 'approved') AS total_comments,
(SELECT GROUP_CONCAT(CONCAT(t.ID, ':', t.topic_title, ':', t.topic_slug) SEPARATOR '|')
FROM " . $this->table('tbl_post_topic') . " pt
JOIN " . $this->table('tbl_topics') . " t ON pt.topic_id = t.ID
WHERE pt.post_id = p.ID AND t.topic_status = 'Y') AS topics_data
FROM " . $this->table('tbl_posts') . " AS p
INNER JOIN " . $this->table('tbl_media') . " AS m ON p.media_id = m.ID
INNER JOIN " . $this->table('tbl_users') . " AS u ON p.post_author = u.ID
WHERE p.post_status = 'publish' AND p.post_type = 'blog'
AND m.media_target = 'blog' AND m.media_access = 'public'
AND m.media_status = '1' AND u.user_banned = '0'
ORDER BY p.post_date DESC LIMIT :limit";
$this->setSQL($sql);
return $this->findAll([':limit' => $limit]) ?: [];
}
// Get all blog posts with pagination
public function getAllBlogPosts($sanitize, Paginator $perPage)
{
$this->linkPosts = $perPage;
$stmt = $this->dbc->dbQuery("SELECT ID FROM tbl_posts WHERE post_type = 'blog'");
$this->linkPosts->set_total($stmt->rowCount());
$sql = "SELECT p.ID, p.media_id, p.post_author,
p.post_date AS created_at, p.post_modified AS modified_at,
p.post_title, p.post_slug, p.post_content, p.post_summary,
m.media_filename, m.media_caption,
(SELECT COUNT(c.ID) FROM " . $this->table('tbl_comments') . " c
WHERE c.comment_post_id = p.ID AND c.comment_status = 'approved') AS total_comments,
...topics_data...
FROM " . $this->table('tbl_posts') . " AS p
INNER JOIN " . $this->table('tbl_users') . " AS u ON p.post_author = u.ID
INNER JOIN " . $this->table('tbl_media') . " AS m ON p.media_id = m.ID
WHERE p.post_type = 'blog' AND p.post_status = 'publish'
AND m.media_target = 'blog' AND m.media_status = '1'
ORDER BY p.ID DESC " . $this->linkPosts->get_limit($sanitize);
$this->setSQL($sql);
$entries = $this->findAll([]);
$this->pagination = $this->linkPosts->page_links($sanitize);
return ['blogPosts' => $entries, 'paginationLink' => $this->pagination];
}
// Get single post by ID
public function getPostById($id)
{
$sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date, p.post_modified,
p.post_title, p.post_slug, p.post_content, p.post_summary,
p.post_keyword, p.post_status, p.post_sticky, p.post_type,
p.post_visibility, p.post_password, p.comment_status AS comment_permit,
m.media_filename, m.media_caption, m.media_target, m.media_access,
u.user_login, u.user_fullname
FROM tbl_posts p
INNER JOIN tbl_media m ON p.media_id = m.ID
INNER JOIN tbl_users u ON p.post_author = u.ID
WHERE p.ID = :ID AND p.post_status = 'publish'
AND p.post_type = 'blog' AND m.media_target = 'blog'
AND m.media_access = 'public' AND m.media_status = '1'";
$this->setSQL($sql);
return $this->findRow([':ID' => Sanitize::severeSanitizer($id)]) ?: [];
}
// Get single post by slug
public function getPostBySlug($slug, $fetchMode = null)
{
$sql = "SELECT p.ID, p.media_id, p.post_author,
p.post_date, p.post_modified, p.post_title,
p.post_slug, p.post_content, p.post_summary,
p.post_keyword, p.post_status, p.post_sticky,
m.media_filename, m.media_caption, m.media_access,
u.user_login, u.user_fullname
FROM tbl_posts AS p
INNER JOIN tbl_media AS m ON p.media_id = m.ID
INNER JOIN tbl_users AS u ON p.post_author = u.ID
WHERE p.post_slug = :slug AND p.post_status = 'publish'
AND p.post_type = 'blog' AND m.media_target = 'blog'
AND m.media_access = 'public' AND m.media_status = '1'";
$this->setSQL($sql);
return $this->findRow([':slug' => Sanitize::severeSanitizer($slug)]) ?: [];
}
// Get random headline posts
public function getRandomHeadlines()
{
$sql = "SELECT p.ID, p.media_id, p.post_author,
p.post_date, p.post_modified, p.post_title,
p.post_slug, p.post_content, p.post_summary,
p.post_tags, u.user_login, u.user_fullname,
m.media_filename, m.media_caption
FROM tbl_posts AS p
INNER JOIN (SELECT ID FROM tbl_posts ORDER BY RAND() LIMIT 5) AS p2 ON p.ID = p2.ID
INNER JOIN tbl_users AS u ON p.post_author = u.ID
INNER JOIN tbl_media AS m ON p.media_id = m.ID
WHERE p.post_type = 'blog' AND p.post_status = 'publish'
AND m.media_target = 'blog' AND p.post_headlines = '1'";
$this->setSQL($sql);
return $this->findAll([]) ?: [];
}
// Get related posts using FULLTEXT search
public function getRelatedPosts($post_title)
{
$sql = "SELECT ID, media_id, post_author, post_date, post_modified,
post_title, post_slug, post_content,
MATCH(post_title, post_content, post_tags)
AGAINST(? IN BOOLEAN MODE) AS score
FROM tbl_posts
WHERE MATCH(post_title, post_content) AGAINST(? IN BOOLEAN MODE)
ORDER BY score ASC LIMIT 3";
$this->setSQL($sql);
return $this->findAll([$post_title, $post_title]) ?: [];
}
// Get random posts for homepage
public function getRandomPosts($start, $end)
{
$sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date,
p.post_title, p.post_slug, p.post_content,
m.media_filename, m.media_caption,
u.user_login, u.user_fullname,
(SELECT COUNT(c.ID) FROM " . $this->table('tbl_comments') . " c
WHERE c.comment_post_id = p.ID) AS total_comments,
...topics_data...
FROM " . $this->table('tbl_posts') . " AS p
INNER JOIN (SELECT ID FROM " . $this->table('tbl_posts') . "
ORDER BY RAND() LIMIT 3) AS p2 ON p.ID = p2.ID
INNER JOIN " . $this->table('tbl_users') . " AS u ON p.post_author = u.ID
INNER JOIN " . $this->table('tbl_media') . " AS m ON p.media_id = m.ID
WHERE p.post_type = 'blog' AND p.post_status = 'publish'
AND m.media_target = 'blog'
LIMIT :position, :end";
$this->setSQL($sql);
return $this->findAll([':position' => $start, ':end' => $end]) ?: [];
}
// Get posts for sidebar
public function getPostsOnSidebar($limit)
{
$sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date,
p.post_title, p.post_slug, p.post_summary,
u.user_login, u.user_fullname
FROM tbl_posts AS p
INNER JOIN tbl_users AS u ON p.post_author = u.ID
WHERE p.post_type = 'blog' AND p.post_status = 'publish'
ORDER BY p.post_date DESC LIMIT :limit";
$this->setSQL($sql);
return $this->findAll([':limit' => $limit]) ?: [];
}
}
Utility Functions
Utility functions are loaded via lib/utility-loader.php:
| Category | Functions |
|---|---|
| Security | csrf-defender.php, remove-xss.php, form-security.php |
| Validation | email-validation.php, url-validation.php |
| Plugins | plugin-helper.php, plugin-validator.php, invoke-plugin.php |
| Formatting | escape-html.php, limit-word.php |
| Media | invoke-frontimg.php, upload-video.php |
| Session | turn-on-session.php, regenerate-session.php |
Image Handling Functions
| Function | Description | Location |
|---|---|---|
invoke_webp_image() |
Returns WebP URL if available, else original | lib/utility/invoke-webp-image.php |
invoke_frontimg() |
Primary function for displaying featured images | lib/utility/invoke-frontimg.php |
invoke_responsive_image() |
Generates <picture> element with WebP | lib/utility/invoke-responsive-image.php |
invoke_hero_image() |
Hero images with fetchpriority="high" | lib/utility/invoke-responsive-image.php |
invoke_gallery_image() |
Gallery images with lazy loading | lib/utility/invoke-responsive-image.php |
Access Control
All admin pages must implement proper authorization checks:
PHP
Authorization Check
// In admin pages, check authorization before processing
if (false === $authenticator->userAccessControl(ActionConst::PRIVACY)) {
direct_page('index.php?load=403&forbidden=' . forbidden_id(), 403);
}
Action Constants & Required Levels
| Action | Required Level |
|---|---|
ActionConst::PRIVACY |
administrator |
ActionConst::USERS |
administrator |
ActionConst::IMPORT |
administrator |
ActionConst::PLUGINS, THEMES, CONFIGURATION |
administrator, manager |
ActionConst::PAGES, NAVIGATION |
administrator, manager |
ActionConst::TOPICS |
administrator, manager, editor |
ActionConst::COMMENTS, MEDIALIB, REPLY |
administrator, manager, author |
ActionConst::POSTS |
administrator, manager, editor, author, contributor |
Security Considerations
Always use prepared statements
Sanitize all user input
Validate data before processing
Check authorization before actions
Use CSRF tokens on all forms
Log errors securely (no secrets)