Optional SNGINE Security Fix for Ajax Endpoints
How We Added Smarter Guest Privacy Controls to Vibeforge Without Making the Whole Site Private
** If you find this helpful? You can send a $10.00 donation to $vibeforge on Cash App. **
* BACKUP FOR FILES BEFORE TRYING THIS PATCH *
One of the privacy challenges we have been working through on Vibeforge is how to protect user content better without shutting off the entire platform to the public.
Like a lot of social platforms, Vibeforge has some areas that benefit from public visibility and some that really should not be fully exposed to non-logged-in visitors. Blogs, market listings, and the outer shell of profiles and groups can help visitors understand the platform. On the other hand, timelines, deeper group activity, member interactions, and other social content should be treated more carefully.
The built-in site privacy toggle in Sngine was too broad for what we needed. It blocks guest access across the site, including sections like blogs. That meant we needed a more selective solution.
What we ended up building was a layered approach:
- Template changes for profile.tpl and group.tpl so guests see a limited public shell instead of the full content
- A global server-side guest-access guard inside bootstrap.php for normal page requests
- Separate protection inside AJAX endpoints so background requests do not bypass the visual lock
This post outlines what we changed, why we changed it, and how other Sngine site owners can do something similar.
Step 1: Decide what should stay public and what should be protected
Before touching code, the first thing to do is define your privacy model clearly.
For Vibeforge, the goal was not to make the whole site private. The goal was to keep a few strategic sections public while protecting the deeper social layer.
We decided to leave these areas public:
- Blogs
- Groups at the surface level
- Market
- Base profile URLs such as
/username
We wanted to protect deeper content like:
- Profile activity and social details
- Group posts and member activity
- Deeper member-only tabs and content loads
- Protected AJAX content requests
If you are doing this on your own site, define your public and protected areas first. Everything else becomes much easier once that part is clear.
Step 2: Update profile.tpl to show a public shell instead of full content
The first visible layer of this work happened in profile.tpl.
The idea here is simple. Do not force guests away from the profile page entirely. Instead, let them see a limited outer shell and replace the protected parts with a login prompt.
That lets visitors see basic public-facing profile information like the name, username, avatar, and cover image, while keeping the deeper profile content protected.
Here is the basic pattern to add inside your profile template:
{if !$user->_logged_in}
<div class="card">
<div class="card-body text-center">
<h4>Log in to see more</h4>
<p>Posts, friends, followers, and activity are visible to members only.</p>
<a href="{$system['system_url']}/signin?next={$smarty.server.REQUEST_URI|escape:'url'}" class="btn btn-primary">Log In</a>
<a href="{$system['system_url']}/signup" class="btn btn-outline-primary">Sign Up</a>
</div>
</div>
{else}
{* full logged-in profile content goes here *}
{/if}
What this does:
- Checks whether the visitor is logged in
- If not, shows a guest-only message and call to action
- If yes, shows the normal full profile content
When applying this in a real template, the goal is not to wrap the entire page. Usually you will want to leave the profile header visible and wrap the deeper content area only. That gives you the “public shell, protected body” behavior.
In practical terms, that means leaving things like the cover, avatar, and basic intro visible, then wrapping the timeline and other protected blocks in the conditional.
Step 3: Update group.tpl the same way
Next, we applied the same privacy philosophy to group.tpl.
Groups are a little different from profiles because they often expose even more about people through community participation, interests, and discussion. That makes a public shell useful, but it makes deeper protection even more important.
Here is the same general idea for groups:
{if !$user->_logged_in}
<div class="card">
<div class="card-body">
<h4>About this group</h4>
<p>This group's discussions and member activity are visible to logged-in members only.</p>
<a href="{$system['system_url']}/signin?next={$smarty.server.REQUEST_URI|escape:'url'}" class="btn btn-primary">Log In to Continue</a>
</div>
</div>
{else}
{* full group content goes here *}
{/if}
What to do when applying it:
- Leave the group header visible
- Leave any basic group description or public-facing summary visible
- Wrap discussion areas, posts, and member-only content in the login check
This makes the group page feel present and discoverable while still protecting the actual community activity inside it.
Step 4: Move real page enforcement into bootstrap.php
The template changes are important, but they are only the user-facing layer. They make the experience look right, but they do not truly secure anything by themselves.
For real protection, the access decision needs to happen on the server.
We looked at bootloader.php first, but the better place turned out to be bootstrap.php. That is because bootstrap.php is where the session is initialized, the system is loaded, and the user object is created. By that point, the application already knows whether a visitor is logged in.
That makes it the best central place to run a guest-access guard for normal page requests.
The first step is to include your helper file inside bootstrap.php. Place it after the core user checks and before the rest of the application continues loading.
Example:
/* check if the viewer is banned */
if ($user->_is_banned) {
_error(__("System Message"), $user->_data['user_banned_message']);
}
require_once ABSPATH . 'includes/vf_global_guest_guard.php';
vf_global_guest_guard($user, $system);
// 🚀 Starting the web app ...
What this does:
- Lets the normal Sngine session and user boot process complete first
- Runs your custom guest-access rules before the rest of the application starts
- Stops guests early instead of letting the app fully load protected page areas
Important: this global guard should be used for normal page routing, not for redirecting AJAX or API requests. That distinction matters, because login forms and other background actions often run through AJAX.
Step 5: Build the global guard so it skips AJAX requests
This turned out to be one of the most important parts of the implementation.
If you add a global guest redirect in bootstrap.php and it catches AJAX requests too, you can accidentally break login, sign-up, and other background actions. Instead of getting the JSON or success response they expect, those requests get redirected HTML back from the sign-in page.
That is why the global guard must explicitly skip AJAX and API-style requests.
Create a file such as /includes/vf_global_guest_guard.php and use a pattern like this:
<?php
function vf_global_guest_guard($user, $system)
{
global $db;
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$requestPath = parse_url($requestUri, PHP_URL_PATH) ?: '/';
$requestPath = '/' . ltrim($requestPath, '/');
$isLoggedIn = isset($user) && !empty($user->_logged_in);
if ($isLoggedIn) {
return;
}
/*
|--------------------------------------------------------------------------
| Skip AJAX and API requests here
|--------------------------------------------------------------------------
|
| The global guard should not redirect background requests.
| Those should be handled inside the endpoint files themselves.
|
*/
$isAjaxRequest =
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
|| strpos($requestPath, '/includes/ajax/') === 0
|| strpos($requestPath, '/api/') === 0;
if ($isAjaxRequest) {
return;
}
$publicExact = [
'/',
'/signin',
'/signup',
'/forgot',
'/reset',
'/activation',
'/blogs',
'/groups',
'/market',
'/marketplace',
];
$publicPrefixes = [
'/blogs/',
'/blog/',
'/groups/',
'/group/',
'/market/',
'/marketplace/',
'/connect/',
'/oauth/',
'/api/webhooks/',
'/content/uploads/',
'/content/themes/',
'/includes/assets/',
'/assets/',
];
$reservedTopLevel = [
'admincp', 'modcp', 'ads', 'affiliates', 'api', 'apps', 'articles', 'auctions',
'blogs', 'blog', 'bookmarks', 'boosted', 'coinpayments', 'connect', 'contact',
'courses', 'directory', 'events', 'explore', 'forums', 'games', 'group', 'groups',
'hashtag', 'help', 'home', 'index', 'jobs', 'live', 'login', 'logout', 'market',
'marketplace', 'messages', 'movies', 'notifications', 'offers', 'order', 'orders',
'page', 'pages', 'pay', 'payments', 'people', 'photos', 'places', 'post', 'posts',
'privacy', 'products', 'profile', 'profile.php', 'reels', 'register', 'reset',
'saved', 'search', 'settings', 'share', 'signin', 'signout', 'signup', 'static',
'terms', 'videos', 'wallet', 'watch', 'webhooks'
];
$isAsset = (bool) preg_match(
'~\\.(css|js|mjs|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|eot|map|mp4|webm|mp3|json|txt|xml)$~i',
$requestPath
);
if ($isAsset) {
return;
}
if (in_array($requestPath, $publicExact, true)) {
return;
}
foreach ($publicPrefixes as $prefix) {
if (strpos($requestPath, $prefix) === 0) {
return;
}
}
// Allow ONLY base profile URLs like /username for guests
$trimmed = trim($requestPath, '/');
if ($trimmed !== '' && strpos($trimmed, '/') === false) {
$segment = strtolower($trimmed);
if (!in_array($segment, $reservedTopLevel, true)) {
$stmt = $db->prepare('SELECT user_id FROM users WHERE user_name = ? LIMIT 1');
if ($stmt) {
$stmt->bind_param('s', $trimmed);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$stmt->close();
return;
}
$stmt->close();
}
}
}
$next = urlencode($requestUri);
header("Location: /signin?next={$next}");
exit;
}
What this helper does:
- Immediately returns if the visitor is already logged in
- Skips AJAX and API requests so login and other background actions do not break
- Allows selected public routes like blogs, groups, and market
- Allows public assets like CSS, JS, images, fonts, and media
- Checks whether a one-segment URL like
/usernamematches a real profile - Redirects everything else to sign-in
Step 6: Understand why /username needed special handling
Profiles were the trickiest part because Vibeforge uses clean profile URLs like /username.
That means a single top-level route might be a profile, or it might be a normal system path. The system cannot safely assume every one-part URL is a profile. That is why the helper needs both a reserved route list and a username lookup.
This is the important part:
if ($trimmed !== '' && strpos($trimmed, '/') === false) {
$segment = strtolower($trimmed);
if (!in_array($segment, $reservedTopLevel, true)) {
$stmt = $db->prepare('SELECT user_id FROM users WHERE user_name = ? LIMIT 1');
if ($stmt) {
$stmt->bind_param('s', $trimmed);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$stmt->close();
return;
}
$stmt->close();
}
}
}
When adapting this for your own site, make sure you update the $reservedTopLevel array with any custom routes or plugin slugs your install uses. Otherwise, a non-profile route could accidentally be treated like a username.
Step 7: Protect AJAX endpoints separately
This is the part that often gets missed.
Even if your templates look locked to guests, protected content may still be accessible through AJAX requests if you do not block those endpoints server-side too.
The important correction here is this: do not use the global guard in bootstrap.php to redirect AJAX requests. That can break login and other background actions.
Instead, protect sensitive AJAX endpoints inside the endpoint files themselves.
The simplest pattern is to add a login check at the top of protected AJAX files:
<?php
if (!$user->_logged_in) {
_error(403);
}
If you want to target only certain AJAX request types, you can check the request more selectively:
<?php
$get = $_GET['get'] ?? $_POST['get'] ?? '';
$lockedGuestEndpoints = [
'posts_profile',
'friends',
'followers',
'followings',
'photos',
'groups',
'pages',
'events'
];
if (!$user->_logged_in && in_array($get, $lockedGuestEndpoints, true)) {
_error(403);
}
What this does:
- Prevents guests from loading protected background content directly
- Keeps the backend consistent with the visual privacy model
- Avoids breaking the login flow by leaving general AJAX alone unless it is a protected endpoint
If you are implementing this on your own site, search through your includes/ajax/ files and identify anything that loads profile posts, member lists, group activity, photos, or similar content. Those are the places to add server-side checks.
Step 8: Test the login flow before going live
Because the global guard lives in bootstrap.php, it is especially important to test guest behavior carefully before going live.
At minimum, check these scenarios:
- Guest visits a blog page
- Guest visits a market page
- Guest visits a group page
- Guest visits a profile at
/username - Guest tries to access protected deeper content
- Guest can still submit the login form successfully
- Guest can still submit sign-up if that is public
- Logged-in user visits the same pages
- Assets still load correctly
- OAuth or external callbacks still work if your site uses them
Always back up the original files first. This kind of change affects routing and access control, so even a small mistake in the allowlist can lock down something you meant to leave public or interfere with an important request type.
Why this approach worked for us
What we liked about this approach is that it did not force us into an all-public or all-private model.
It gave us a more intentional structure:
- profile.tpl creates a clean public profile shell
- group.tpl creates a clean public group shell
- bootstrap.php becomes the enforcement point for global page routing
- Protected AJAX endpoints get their own server-side checks
That balance matters. It lets Vibeforge stay discoverable while respecting the privacy of the people actually using it.
Closing thoughts
This was not about hiding Vibeforge from the public. It was about being more deliberate about what should be public and what should not.
If you are running a Sngine-based platform and want a more privacy-conscious setup without blocking your entire site, this is a practical way to do it. Start with your public-versus-protected route plan, build a public shell in the templates, enforce page rules in bootstrap.php, and protect sensitive AJAX endpoints separately.
That combination is what turns a visual privacy idea into a real privacy system.
Want more behind-the-scenes writeups like this? Follow the Vibeforge blog as we keep refining the platform, improving privacy controls, and building a social space that feels more thoughtful, more human, and more respectful of the people using it.
** If you find this helpful? You can send a $10.00 donation to $vibeforge on Cash App. **
- Art
- Business
- Causes
- Crafts
- Community
- Dance
- Drinks
- Education
- Fashion
- Editorial
- Film
- Fitness
- Food
- Games
- Gardening
- Health
- Home
- LGBTQ+ News
- Literature
- Music
- News
- Nature
- Networking
- Oddities
- Other
- Opinion
- Party
- Politics
- Religion
- Science
- Press Releases
- Shopping
- Sports
- Theater
- Technology
- Wellness
