big commit

This commit is contained in:
flash 2016-04-01 23:44:31 +02:00
parent f69f2742cd
commit e77ffc6849
47 changed files with 2544 additions and 1374 deletions

1
.gitignore vendored
View file

@ -7,4 +7,3 @@ google*.html
[Tt]humbs.db [Tt]humbs.db
Desktop.ini Desktop.ini
$RECYCLE.BIN/ $RECYCLE.BIN/
composer.lock

View file

@ -19,5 +19,13 @@
"phroute/phroute": "^2.1", "phroute/phroute": "^2.1",
"illuminate/database": "5.2.*", "illuminate/database": "5.2.*",
"doctrine/dbal": "~2.4" "doctrine/dbal": "~2.4"
},
"autoload": {
"psr-4": {
"Sakura\\": "libraries/"
},
"files": [
"utility.php"
]
} }
} }

1604
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,6 @@ use JBBCode\CodeDefinition;
use JBBCode\ElementNode; use JBBCode\ElementNode;
use Sakura\Router; use Sakura\Router;
use Sakura\User as SakuraUser; use Sakura\User as SakuraUser;
use Sakura\Utils;
/** /**
* Username BBcode for JBBCode. * Username BBcode for JBBCode.
@ -44,7 +43,7 @@ class User extends CodeDefinition
$content = ""; $content = "";
foreach ($el->getChildren() as $child) { foreach ($el->getChildren() as $child) {
$content .= Utils::cleanString($child->getAsText(), true); $content .= clean_string($child->getAsText(), true);
} }
$user = SakuraUser::construct($content); $user = SakuraUser::construct($content);

View file

@ -104,7 +104,7 @@ class Comment
public function parsed() public function parsed()
{ {
if (!$this->parsedCache) { if (!$this->parsedCache) {
$this->parsedCache = BBcode::parseEmoticons(Utils::cleanString($this->text)); $this->parsedCache = BBcode::parseEmoticons(clean_string($this->text));
} }
return $this->parsedCache; return $this->parsedCache;

View file

@ -18,8 +18,6 @@ use Sakura\Router;
use Sakura\Session; use Sakura\Session;
use Sakura\Template; use Sakura\Template;
use Sakura\User; use Sakura\User;
use Sakura\Users;
use Sakura\Utils;
/** /**
* Authentication controllers. * Authentication controllers.
@ -125,7 +123,7 @@ class AuthController extends Controller
} }
// Get account data // Get account data
$user = User::construct(Utils::cleanString($username, true, true)); $user = User::construct(clean_string($username, true, true));
// Check if the user that's trying to log in actually exists // Check if the user that's trying to log in actually exists
if ($user->id === 0) { if ($user->id === 0) {
@ -304,11 +302,12 @@ class AuthController extends Controller
} }
// Attempt to get account data // Attempt to get account data
$user = User::construct(Utils::cleanString($username, true, true)); $user = User::construct(clean_string($username, true, true));
// Check if the username already exists // Check if the username already exists
if ($user && $user->id !== 0) { if ($user && $user->id !== 0) {
$message = "{$user->username} is already a member here! If this is you please use the password reset form instead of making a new account."; $message = "{$user->username} is already a member here!"
. " If this is you please use the password reset form instead of making a new account.";
Template::vars(['page' => compact('success', 'redirect', 'message')]); Template::vars(['page' => compact('success', 'redirect', 'message')]);
@ -343,7 +342,7 @@ class AuthController extends Controller
} }
// Check the MX record of the email // Check the MX record of the email
if (!Utils::checkMXRecord($email)) { if (!check_mx_record($email)) {
$message = 'No valid MX-Record found on the e-mail address you supplied.'; $message = 'No valid MX-Record found on the e-mail address you supplied.';
Template::vars(['page' => compact('success', 'redirect', 'message')]); Template::vars(['page' => compact('success', 'redirect', 'message')]);
@ -364,7 +363,7 @@ class AuthController extends Controller
} }
// Check password entropy // Check password entropy
if (Utils::pwdEntropy($password) < Config::get('min_entropy')) { if (password_entropy($password) < Config::get('min_entropy')) {
$message = 'Your password is too weak, try adding some special characters.'; $message = 'Your password is too weak, try adding some special characters.';
Template::vars(['page' => compact('success', 'redirect', 'message')]); Template::vars(['page' => compact('success', 'redirect', 'message')]);
@ -382,7 +381,7 @@ class AuthController extends Controller
// Check if we require e-mail activation // Check if we require e-mail activation
if ($requireActive) { if ($requireActive) {
// Send activation e-mail to user // Send activation e-mail to user
Users::sendActivationMail($user->id); $this->sendActivationMail($user);
} }
// Return true with a specific message if needed // Return true with a specific message if needed
@ -502,8 +501,8 @@ class AuthController extends Controller
} }
// Grab forms // Grab forms
$username = isset($_POST['username']) ? Utils::cleanString($_POST['username'], true) : null; $username = isset($_POST['username']) ? clean_string($_POST['username'], true) : null;
$email = isset($_POST['email']) ? Utils::cleanString($_POST['email'], true) : null; $email = isset($_POST['email']) ? clean_string($_POST['email'], true) : null;
// Do database request // Do database request
$getUser = DB::table('users') $getUser = DB::table('users')
@ -533,7 +532,7 @@ class AuthController extends Controller
} }
// Send activation e-mail to user // Send activation e-mail to user
Users::sendActivationMail($user->id); $this->sendActivationMail($user);
$success = 1; $success = 1;
$redirect = Router::route('auth.login'); $redirect = Router::route('auth.login');
@ -587,8 +586,8 @@ class AuthController extends Controller
$userId = isset($_POST['user']) ? $_POST['user'] : 0; $userId = isset($_POST['user']) ? $_POST['user'] : 0;
$key = isset($_POST['key']) ? $_POST['key'] : ""; $key = isset($_POST['key']) ? $_POST['key'] : "";
$password = isset($_POST['password']) ? $_POST['password'] : ""; $password = isset($_POST['password']) ? $_POST['password'] : "";
$userName = isset($_POST['username']) ? Utils::cleanString($_POST['username'], true) : ""; $userName = isset($_POST['username']) ? clean_string($_POST['username'], true) : "";
$email = isset($_POST['email']) ? Utils::cleanString($_POST['email'], true) : null; $email = isset($_POST['email']) ? clean_string($_POST['email'], true) : null;
// Create user object // Create user object
$user = User::construct($userId ? $userId : $userName); $user = User::construct($userId ? $userId : $userName);
@ -613,7 +612,7 @@ class AuthController extends Controller
if ($key && $password) { if ($key && $password) {
// Check password entropy // Check password entropy
if (Utils::pwdEntropy($password) < Config::get('min_entropy')) { if (password_entropy($password) < Config::get('min_entropy')) {
$message = "Your password doesn't meet the strength requirements!"; $message = "Your password doesn't meet the strength requirements!";
Template::vars(['page' => compact('success', 'redirect', 'message')]); Template::vars(['page' => compact('success', 'redirect', 'message')]);
@ -650,8 +649,8 @@ class AuthController extends Controller
$message = "Changed your password! You may now log in."; $message = "Changed your password! You may now log in.";
$redirect = Router::route('auth.login'); $redirect = Router::route('auth.login');
} else { } else {
// Send e-mail // Send the e-mail
Users::sendPasswordForgot($user->id, $user->email); $this->sendPasswordMail($user);
$success = 1; $success = 1;
$message = "Sent the e-mail, keep an eye on your spam folder as well!"; $message = "Sent the e-mail, keep an eye on your spam folder as well!";
@ -662,4 +661,72 @@ class AuthController extends Controller
return Template::render('global/information'); return Template::render('global/information');
} }
/**
* Send the activation e-mail
*
* @param User $user
*/
private function sendActivationMail($user)
{
// Generate activation key
$activate = ActionCode::generate('ACTIVATE', $user->id);
$siteName = Config::get('sitename');
$baseUrl = "http://" . Config::get('url_main');
$activateLink = Router::route('auth.activate') . "?u={$user->id}&k={$activate}";
$profileLink = Router::route('user.profile', $user->id);
$signature = Config::get('mail_signature');
// Build the e-mail
$message = "Welcome to {$siteName}!\r\n\r\n"
. "Please keep this e-mail for your records. Your account intormation is as follows:\r\n\r\n"
. "----------------------------\r\n\r\n"
. "Username: {$user->username}\r\n\r\n"
. "Your profile: {$baseUrl}{$profileLink}\r\n\r\n"
. "----------------------------\r\n\r\n"
. "Please visit the following link in order to activate your account:\r\n\r\n"
. "{$baseUrl}{$activateLink}\r\n\r\n"
. "Your password has been securely stored in our database and cannot be retrieved. "
. "In the event that it is forgotten,"
. " you will be able to reset it using the email address associated with your account.\r\n\r\n"
. "Thank you for registering.\r\n\r\n"
. "--\r\n\r\nThanks\r\n\r\n{$signature}";
// Send the message
send_mail([$user->email => $user->username], "{$siteName} activation mail", $message);
}
/**
* Send the activation e-mail
*
* @param User $user
*/
private function sendPasswordMail($user)
{
// Generate the verification key
$verk = ActionCode::generate('LOST_PASS', $user->id);
$siteName = Config::get('sitename');
$baseUrl = "http://" . Config::get('url_main');
$reactivateLink = Router::route('auth.resetpassword') . "?u={$user->id}&k={$verk}";
$signature = Config::get('mail_signature');
// Build the e-mail
$message = "Hello {$user->username},\r\n\r\n"
. "You are receiving this notification because you have (or someone pretending to be you has)"
. " requested a password reset link to be sent for your account on \"{$siteName}\"."
. " If you did not request this notification then please ignore it,"
. " if you keep receiving it please contact the site administrator.\r\n\r\n"
. "To use this password reset key you need to go to a special page."
. " To do this click the link provided below.\r\n\r\n"
. "{$baseUrl}{$reactivateLink}\r\n\r\n"
. "If successful you should be able to change your password here.\r\n\r\n"
. "You can of course change this password yourself via the settings page."
. " If you have any difficulties please contact the site administrator.\r\n\r\n"
. "--\r\n\r\nThanks\r\n\r\n{$signature}";
// Send the message
send_mail([$user->email => $user->username], "{$siteName} password restoration", $message);
}
} }

View file

@ -7,6 +7,10 @@
namespace Sakura\Controllers\Settings; namespace Sakura\Controllers\Settings;
use Sakura\ActiveUser;
use Sakura\DB;
use Sakura\Template;
/** /**
* Advanced settings. * Advanced settings.
* *
@ -17,7 +21,13 @@ class AdvancedController extends Controller
{ {
public function sessions() public function sessions()
{ {
return $this->go('advanced.sessions'); $sessions = DB::table('sessions')
->where('user_id', ActiveUser::$user->id)
->get();
Template::vars(compact('sessions'));
return Template::render('settings/advanced/sessions');
} }
public function deactivate() public function deactivate()

View file

@ -11,6 +11,7 @@ use Sakura\ActiveUser;
use Sakura\Controllers\Controller as BaseController; use Sakura\Controllers\Controller as BaseController;
use Sakura\Perms\Site; use Sakura\Perms\Site;
use Sakura\Router; use Sakura\Router;
use Sakura\Template;
use Sakura\Urls; use Sakura\Urls;
/** /**
@ -26,6 +27,10 @@ class Controller extends BaseController
public function __construct() public function __construct()
{ {
$this->urls = new Urls(); $this->urls = new Urls();
$navigation = $this->navigation();
Template::vars(compact('navigation'));
} }
public function go($location) public function go($location)

View file

@ -7,6 +7,8 @@
namespace Sakura\Controllers\Settings; namespace Sakura\Controllers\Settings;
use Sakura\Template;
/** /**
* Friends settings. * Friends settings.
* *
@ -17,11 +19,11 @@ class FriendsController extends Controller
{ {
public function listing() public function listing()
{ {
return $this->go('friends.listing'); return Template::render('settings/friends/listing');
} }
public function requests() public function requests()
{ {
return $this->go('friends.requests'); return Template::render('settings/friends/requests');
} }
} }

View file

@ -8,8 +8,11 @@
namespace Sakura\Controllers\Settings; namespace Sakura\Controllers\Settings;
use Sakura\ActiveUser; use Sakura\ActiveUser;
use Sakura\DB;
use Sakura\Perms\Site; use Sakura\Perms\Site;
use Sakura\Router;
use Sakura\Template; use Sakura\Template;
use stdClass;
/** /**
* General settings. * General settings.
@ -21,23 +24,177 @@ class GeneralController extends Controller
{ {
public function home() public function home()
{ {
ActiveUser::class;
Site::class;
$navigation = $this->navigation();
Template::vars(compact('navigation'));
return Template::render('settings/general/home'); return Template::render('settings/general/home');
} }
public function profile() public function profile()
{ {
return $this->go('general.profile'); // Check permission
if (!ActiveUser::$user->permission(Site::ALTER_PROFILE)) {
$message = "You aren't allowed to edit your profile!";
$redirect = Router::route('settings.general.home');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
// Get profile fields
$rawFields = DB::table('profilefields')
->get();
// Create output array
$fields = [];
// Iterate over the fields and clean them up
foreach ($rawFields as $fieldData) {
$field = new stdClass;
$field->id = clean_string($fieldData->field_name, true, true);
$field->name = $fieldData->field_name;
$field->type = $fieldData->field_type;
$field->link = $fieldData->field_link;
$field->format = $fieldData->field_linkformat;
$field->description = $fieldData->field_description;
$field->additional = json_decode($fieldData->field_additional, true);
$fields[$fieldData->field_id] = $field;
}
// Attempt to get the session value
$session = $_POST['session'] ?? null;
if ($session) {
$redirect = Router::route('settings.general.profile');
// Go over each field
foreach ($fields as $field) {
// Add to the store table
if (isset($_POST["profile_{$field->id}"])) {
DB::table('user_profilefields')
->insert([
'user_id' => ActiveUser::$user->id,
'field_name' => $field->id,
'field_value' => $_POST["profile_{$field->id}"],
]);
}
// Check if there's additional values we should keep in mind
if (!empty($field->additional)) {
// Go over each additional value
foreach ($field->additional as $addKey => $addVal) {
// Add to the array
$store = (isset($_POST["profile_additional_{$addKey}"]))
? $_POST["profile_additional_{$addKey}"]
: false;
DB::table('user_profilefields')
->insert([
'user_id' => ActiveUser::$user->id,
'field_name' => $addKey,
'field_value' => $store,
]);
}
}
}
// Birthdays
if (isset($_POST['birthday_day'])
&& isset($_POST['birthday_month'])
&& isset($_POST['birthday_year'])) {
$day = intval($_POST['birthday_day']);
$month = intval($_POST['birthday_month']);
$year = intval($_POST['birthday_year']);
// Check the values
if (!checkdate($month, $day, $year ? $year : 1)
|| $year > date("Y")
|| ($year != 0 && $year < (date("Y") - 100))) {
$message = "Your birthdate was considered invalid, everything else was saved though.";
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
// Combine it into a YYYY-MM-DD format
$birthdate = implode(
'-',
[$_POST['birthday_year'], $_POST['birthday_month'], $_POST['birthday_day']]
);
DB::table('users')
->where('user_id', ActiveUser::$user->id)
->update([
'user_birthday' => $birthdate,
]);
}
$message = "Updated your profile!";
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
Template::vars(compact('fields'));
return Template::render('settings/general/profile');
} }
public function options() public function options()
{ {
return $this->go('general.options'); // Get profile fields
$rawFields = DB::table('optionfields')
->get();
// Create output array
$fields = [];
// Iterate over the fields and clean them up
foreach ($rawFields as $fieldData) {
if (!ActiveUser::$user->permission(constant("Sakura\Perms\Site::{$fieldData->option_permission}"))) {
continue;
}
$field = new stdClass;
$field->id = $fieldData->option_id;
$field->name = $fieldData->option_name;
$field->description = $fieldData->option_description;
$field->type = $fieldData->option_type;
$field->permission = $fieldData->option_permission;
$fields[$fieldData->option_id] = $field;
}
// Attempt to get the session value
$session = $_POST['session'] ?? null;
if ($session) {
// Delete all option fields for this user
DB::table('user_optionfields')
->where('user_id', ActiveUser::$user->id)
->delete();
// Go over each field
foreach ($fields as $field) {
if (isset($_POST["option_{$field->id}"])) {
DB::table('user_optionfields')
->insert([
'user_id' => ActiveUser::$user->id,
'field_name' => $field->id,
'field_value' => $_POST["option_{$field->id}"],
]);
}
}
$message = "Updated your options!";
$redirect = Router::route('settings.general.options');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
Template::vars(compact('fields'));
return Template::render('settings/general/options');
} }
} }

View file

@ -7,6 +7,8 @@
namespace Sakura\Controllers\Settings; namespace Sakura\Controllers\Settings;
use Sakura\Template;
/** /**
* Notification settings. * Notification settings.
* *
@ -17,6 +19,6 @@ class NotificationsController extends Controller
{ {
public function history() public function history()
{ {
return $this->go('notifications.history'); return Template::render('settings/notifications/history');
} }
} }

View file

@ -15,7 +15,6 @@ use Sakura\Rank;
use Sakura\Router; use Sakura\Router;
use Sakura\Template; use Sakura\Template;
use Sakura\User; use Sakura\User;
use Sakura\Utils;
/** /**
* Everything that is just for serving user data. * Everything that is just for serving user data.
@ -41,7 +40,7 @@ class UserController extends Controller
if ($profile->id == 0) { if ($profile->id == 0) {
// Fetch from username_history // Fetch from username_history
$check = DB::table('username_history') $check = DB::table('username_history')
->where('username_old_clean', Utils::cleanString($id, true, true)) ->where('username_old_clean', clean_string($id, true, true))
->orderBy('change_id', 'desc') ->orderBy('change_id', 'desc')
->get(); ->get();
@ -94,7 +93,7 @@ class UserController extends Controller
} }
// Get the active rank // Get the active rank
$rank = array_key_exists($rank, $ranks) ? $rank : ($rank ? 0 : 2); $rank = array_key_exists($rank, $ranks) ? $rank : ($rank ? 0 : intval(Config::get('default_rank_id')));
// Get members per page // Get members per page
$membersPerPage = Config::get('members_per_page'); $membersPerPage = Config::get('members_per_page');
@ -105,4 +104,9 @@ class UserController extends Controller
// Render the template // Render the template
return Template::render('user/members'); return Template::render('user/members');
} }
public function report($id = 0)
{
return Template::render('user/report');
}
} }

View file

@ -98,7 +98,7 @@ class Session
->insert([ ->insert([
'user_id' => $this->userId, 'user_id' => $this->userId,
'user_ip' => Net::pton(Net::ip()), 'user_ip' => Net::pton(Net::ip()),
'user_agent' => Utils::cleanString(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'No user agent header.'), 'user_agent' => clean_string(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'No user agent header.'),
'session_key' => $session, 'session_key' => $session,
'session_start' => time(), 'session_start' => time(),
'session_expire' => time() + 604800, 'session_expire' => time() + 604800,

View file

@ -8,325 +8,33 @@
namespace Sakura; namespace Sakura;
/** /**
* Rewrite URL generator. * URL generator.
* *
* @package Sakura * @package Sakura
* @author Julian van de Groep <me@flash.moe> * @author Julian van de Groep <me@flash.moe>
*/ */
class Urls class Urls
{ {
/**
* Unformatted links
* 0 - Plain
* 1 - mod_rewrite
*
* @var array
*/
protected $urls = [
// General site sections
'SITE_HOME' => [
'/index.php',
'/',
],
'SITE_NEWS' => [
'/news.php',
'/news',
],
'SITE_NEWS_POST' => [
'/news.php?id=%u',
'/news/%u',
],
'SITE_NEWS_CAT' => [
'/news.php?cat=$s',
'/news/%s',
],
'SITE_NEWS_CAT_POST' => [
'/news.php?cat=%s&id=%u',
'/news/%s/%u',
],
'SITE_SEARCH' => [
'/search.php',
'/search',
],
'SITE_PREMIUM' => [
'/support.php',
'/support',
],
'SITE_DONATE_TRACK' => [
'/support.php?tracker=true',
'/support/tracker',
],
'SITE_FAQ' => [
'/faq.php',
'/faq',
],
'SITE_LOGIN' => [
'/authenticate.php?ref=login',
'/login',
],
'SITE_LOGOUT' => [
'/authenticate.php?ref=logout',
'/logout',
],
'SITE_REGISTER' => [
'/authenticate.php?ref=register',
'/register',
],
'SITE_FORGOT_PASSWORD' => [
'/authenticate.php?ref=forgotpassword',
'/forgotpassword',
],
'SITE_ACTIVATE' => [
'/authenticate.php?ref=activate',
'/activate',
],
'INFO_PAGE' => [
'/index.php?p=%s',
'/p/%s',
],
'AUTH_ACTION' => [
'/authenticate.php',
'/authenticate',
],
// Memberlist
'MEMBERLIST_INDEX' => [
'/members.php',
'/members',
],
'MEMBERLIST_SORT' => [
'/members.php?sort=%s',
'/members/%s',
],
'MEMBERLIST_RANK' => [
'/members.php?rank=%u',
'/members/%u',
],
'MEMBERLIST_PAGE' => [
'/members.php?page=%u',
'/members?page=%u',
],
'MEMBERLIST_ALL' => [
'/members.php?sort=%s&rank=%u',
'/members/%s/%u',
],
// Forums
'FORUM_INDEX' => [
'/index.php?forum=true',
'/forum',
],
'FORUM_SUB' => [
'/viewforum.php?f=%u',
'/forum/%u',
],
'FORUM_MARK_READ' => [
'/viewforum.php?f=%u&read=true&session=%s',
'/forum/%u?read=true&session=%s',
],
'FORUM_THREAD' => [
'/viewtopic.php?t=%u',
'/forum/thread/%u',
],
'FORUM_POST' => [
'/viewtopic.php?p=%u',
'/forum/post/%u',
],
'FORUM_REPLY' => [
'/posting.php?t=%u',
'/forum/thread/%u/reply',
],
'FORUM_NEW_THREAD' => [
'/posting.php?f=%u',
'/forum/%u/new',
],
'FORUM_EDIT_POST' => [
'/posting.php?p=%1$u&edit=%1$u',
'/forum/post/%u/edit',
],
'FORUM_DELETE_POST' => [
'/posting.php?p=%1$u&delete=%1$u',
'/forum/post/%u/delete',
],
'FORUM_QUOTE_POST' => [
'/posting.php?p=%1$u&quote=%1$u',
'/forum/post/%u/quote',
],
'FORUM_LOCK' => [
'/viewtopic.php?t=%u&lock=%s',
'/forum/thread/%u?lock=%s',
],
'FORUM_STICKY' => [
'/viewtopic.php?t=%u&sticky=%s',
'/forum/thread/%u?sticky=%s',
],
'FORUM_ANNOUNCE' => [
'/viewtopic.php?t=%u&announce=%s',
'/forum/thread/%u?announce=%s',
],
'FORUM_RESTORE' => [
'/viewtopic.php?t=%u&restore=%s',
'/forum/thread/%u?restore=%s',
],
'FORUM_TRASH' => [
'/viewtopic.php?t=%u&trash=%s',
'/forum/thread/%u?trash=%s',
],
'FORUM_PRUNE' => [
'/viewtopic.php?t=%u&prune=%s',
'/forum/thread/%u?prune=%s',
],
// Image serve references
'IMAGE_AVATAR' => [
'/imageserve.php?m=avatar&u=%u',
'/a/%u',
],
'IMAGE_BACKGROUND' => [
'/imageserve.php?m=background&u=%u',
'/bg/%u',
],
'IMAGE_HEADER' => [
'/imageserve.php?m=header&u=%u',
'/u/%u/header',
],
// User actions
'USER_LOGOUT' => [
'/authenticate.php?mode=logout&time=%u&session=%s&redirect=%s',
'/logout?mode=logout&time=%u&session=%s&redirect=%s',
],
'USER_REPORT' => [
'/report.php?mode=user&u=%u',
'/u/%u/report',
],
'USER_PROFILE' => [
'/profile.php?u=%s',
'/u/%s',
],
'USER_COMMENTS' => [
'/profile.php?u=%u&view=comments',
'/u/%u/comments',
],
'USER_FRIENDS' => [
'/profile.php?u=%u&view=friends',
'/u/%u/friends',
],
'USER_GROUPS' => [
'/profile.php?u=%u&view=groups',
'/u/%u/groups',
],
'USER_THREADS' => [
'/profile.php?u=%u&view=threads',
'/u/%u/threads',
],
'USER_POSTS' => [
'/profile.php?u=%u&view=posts',
'/u/%u/posts',
],
'USER_GROUP' => [
'/group.php?g=%u',
'/g/%u',
],
// Settings urls
'SETTINGS_INDEX' => [
'/settings.php',
'/settings',
],
'SETTING_CAT' => [
'/settings.php?cat=%s',
'/settings/%s',
],
'SETTING_MODE' => [
'/settings.php?cat=%s&mode=%s',
'/settings/%s/%s',
],
'REQUEST_NOTIFICATIONS' => [
'/settings.php?request-notifications=true',
'/notifications',
],
// Friend Actions
'FRIEND_ACTION' => [
'/settings.php?friend-action=true',
'/friends',
],
'FRIEND_ADD' => [
'/settings.php?friend-action=true&add=%u&session=%s&time=%u&redirect=%s',
'/friends?add=%u&session=%s&time=%u&redirect=%s',
],
'FRIEND_REMOVE' => [
'/settings.php?friend-action=true&remove=%u&session=%s&time=%u&redirect=%s',
'/friends?remove=%u&session=%s&time=%u&redirect=%s',
],
// Manage urls
'MANAGE_INDEX' => [
'/manage.php',
'/manage',
],
'MANAGE_CAT' => [
'/manage.php?cat=%s',
'/manage/%s',
],
'MANAGE_MODE' => [
'/manage.php?cat=%s&mode=%s',
'/manage/%s/%s',
],
// Comments urls
'COMMENT_POST' => [
'/settings.php?comment-action=true',
'/comments',
],
'COMMENT_VOTE' => [
'/settings.php?comment-action=true&id=%u&mode=vote&state=%u&category=%s&session=%s',
'/comments?id=%u&mode=vote&state=%u&category=%s&session=%s',
],
'COMMENT_DELETE' => [
'/settings.php?comment-action=true&id=%u&category=%s&mode=delete&session=%s',
'/comments?id=%u&mode=delete&category=%s&session=%s',
],
];
/** /**
* Format a URL. * Format a URL.
* *
* @param string $lid The ID of a URL. * @param string $lid doesn't do anything
* @param array $args Additional arguments. * @param array $args [category, mode]
* @param bool $rewrite Toggle mod_rewrite. * @param bool $rewrite doesn't do anything either
* @param bool $b hackjob for the settings panel
* *
* @return null|string The formatted URL. * @return null|string url
*/ */
public function format($lid, $args = [], $rewrite = null, $b = true) public function format($lid, $args = [], $rewrite = null, $b = true)
{ {
if ($b) {
// Check if the requested url exists $a = implode('.', $args);
if (!array_key_exists($lid, $this->urls)) { $a = str_replace("usertitle", "title", $a);
return null; return Router::route("settings.{$a}");
}
if ($b && ($lid === 'SETTING_CAT' || $lid === 'SETTING_MODE')) {
if (in_array('messages', $args)) {
return null;
}
if ($lid === 'SETTING_CAT') {
return Router::route("settings.{$args[0]}");
}
if ($lid === 'SETTING_MODE') {
$a = implode('.', $args);
$a = str_replace("usertitle", "title", $a);
return Router::route("settings.{$a}");
}
} }
// Format urls // Format urls
$formatted = vsprintf($this->urls[$lid][0], $args); $formatted = vsprintf('/settings.php?cat=%s&mode=%s', $args);
// Return the formatted url // Return the formatted url
return $formatted; return $formatted;

View file

@ -254,8 +254,8 @@ class User
public static function create($username, $password, $email, $ranks = [2]) public static function create($username, $password, $email, $ranks = [2])
{ {
// Set a few variables // Set a few variables
$usernameClean = Utils::cleanString($username, true); $usernameClean = clean_string($username, true);
$emailClean = Utils::cleanString($email, true); $emailClean = clean_string($email, true);
$password = Hashing::createHash($password); $password = Hashing::createHash($password);
// Insert the user into the database and get the id // Insert the user into the database and get the id
@ -273,7 +273,7 @@ class User
'last_ip' => Net::pton(Net::ip()), 'last_ip' => Net::pton(Net::ip()),
'user_registered' => time(), 'user_registered' => time(),
'user_last_online' => 0, 'user_last_online' => 0,
'user_country' => Utils::getCountryCode(), 'user_country' => get_country_code(),
]); ]);
// Create a user object // Create a user object
@ -299,7 +299,7 @@ class User
// Get the user database row // Get the user database row
$userRow = DB::table('users') $userRow = DB::table('users')
->where('user_id', $userId) ->where('user_id', $userId)
->orWhere('username_clean', Utils::cleanString($userId, true, true)) ->orWhere('username_clean', clean_string($userId, true, true))
->get(); ->get();
// Populate the variables // Populate the variables
@ -401,7 +401,7 @@ class User
*/ */
public function country($long = false) public function country($long = false)
{ {
return $long ? Utils::getCountryName($this->country) : $this->country; return $long ? get_country_name($this->country) : $this->country;
} }
/** /**
@ -793,7 +793,7 @@ class User
// Check if profile fields aren't fake // Check if profile fields aren't fake
foreach ($profileFields as $field) { foreach ($profileFields as $field) {
// Completely strip all special characters from the field name // Completely strip all special characters from the field name
$fieldName = Utils::cleanString($field->field_name, true, true); $fieldName = clean_string($field->field_name, true, true);
// Check if the user has the current field set otherwise continue // Check if the user has the current field set otherwise continue
if (!array_key_exists($fieldName, $profileValues)) { if (!array_key_exists($fieldName, $profileValues)) {
@ -1087,7 +1087,7 @@ class User
public function setUsername($username) public function setUsername($username)
{ {
// Create a cleaned version // Create a cleaned version
$username_clean = Utils::cleanString($username, true); $username_clean = clean_string($username, true);
// Check if the username is too short // Check if the username is too short
if (strlen($username_clean) < Config::get('username_min_length')) { if (strlen($username_clean) < Config::get('username_min_length')) {
@ -1210,7 +1210,7 @@ class User
} }
// Check password entropy // Check password entropy
if (Utils::pwdEntropy($new) < Config::get('min_entropy')) { if (password_entropy($new) < Config::get('min_entropy')) {
return [0, 'PASS_TOO_SHIT']; return [0, 'PASS_TOO_SHIT'];
} }

View file

@ -1,169 +0,0 @@
<?php
/**
* Holds various functions to interface with users.
*
* @package Sakura
*/
namespace Sakura;
use Sakura\Perms\Site;
/**
* User management
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class Users
{
/**
* Send password forgot e-mail
*
* @param string $userId The user id.
* @param string $email The e-mail.
*/
public static function sendPasswordForgot($userId, $email)
{
$user = User::construct($userId);
if (!$user->id || $user->permission(Site::DEACTIVATED)) {
return;
}
// Generate the verification key
$verk = ActionCode::generate('LOST_PASS', $user->id);
$siteName = Config::get('sitename');
$baseUrl = "http://" . Config::get('url_main');
$reactivateLink = Router::route('auth.resetpassword') . "?u={$user->id}&k={$verk}";
$signature = Config::get('mail_signature');
// Build the e-mail
$message = "Hello {$user->username},\r\n\r\n"
. "You are receiving this notification because you have (or someone pretending to be you has)"
. " requested a password reset link to be sent for your account on \"{$siteName}\"."
. " If you did not request this notification then please ignore it,"
. " if you keep receiving it please contact the site administrator.\r\n\r\n"
. "To use this password reset key you need to go to a special page."
. " To do this click the link provided below.\r\n\r\n"
. "{$baseUrl}{$reactivateLink}\r\n\r\n"
. "If successful you should be able to change your password here.\r\n\r\n"
. "You can of course change this password yourself via the settings page."
. " If you have any difficulties please contact the site administrator.\r\n\r\n"
. "--\r\n\r\nThanks\r\n\r\n{$signature}";
// Send the message
Utils::sendMail([$user->email => $user->username], "{$siteName} password restoration", $message);
}
/**
* Send activation e-mail.
*
* @param mixed $userId User ID.
* @param mixed $customKey Key.
*/
public static function sendActivationMail($userId, $customKey = null)
{
// Get the user data
$user = User::construct($userId);
// User is already activated or doesn't even exist
if (!$user->id || !$user->permission(Site::DEACTIVATED)) {
return;
}
// Generate activation key
$activate = ActionCode::generate('ACTIVATE', $user->id);
$siteName = Config::get('sitename');
$baseUrl = "http://" . Config::get('url_main');
$activateLink = Router::route('auth.activate') . "?u={$user->id}&k={$activate}";
$profileLink = Router::route('user.profile', $user->id);
$signature = Config::get('mail_signature');
// Build the e-mail
$message = "Welcome to {$siteName}!\r\n\r\n"
. "Please keep this e-mail for your records. Your account intormation is as follows:\r\n\r\n"
. "----------------------------\r\n\r\n"
. "Username: {$user->username}\r\n\r\n"
. "Your profile: {$baseUrl}{$profileLink}\r\n\r\n"
. "----------------------------\r\n\r\n"
. "Please visit the following link in order to activate your account:\r\n\r\n"
. "{$baseUrl}{$activateLink}\r\n\r\n"
. "Your password has been securely stored in our database and cannot be retrieved. "
. "In the event that it is forgotten,"
. " you will be able to reset it using the email address associated with your account.\r\n\r\n"
. "Thank you for registering.\r\n\r\n"
. "--\r\n\r\nThanks\r\n\r\n{$signature}";
// Send the message
Utils::sendMail([$user->email => $user->username], "{$siteName} activation mail", $message);
}
/**
* Get all available profile fields.
*
* @return array|null The fields.
*/
public static function getProfileFields()
{
// Get profile fields
$profileFields = DB::table('profilefields')
->get();
// If there's nothing just return null
if (!count($profileFields)) {
return null;
}
// Create output array
$fields = [];
// Iterate over the fields and clean them up
foreach ($profileFields as $field) {
$field = get_object_vars($field);
$fields[$field['field_id']] = $field;
$fields[$field['field_id']]['field_identity'] = Utils::cleanString($field['field_name'], true, true);
$fields[$field['field_id']]['field_additional'] = json_decode($field['field_additional'], true);
}
// Return the yeahs
return $fields;
}
/**
* Get all available option fields.
*
* @return array|null The fields.
*/
public static function getOptionFields()
{
// Get option fields
$optionFields = DB::table('optionfields')
->get();
// If there's nothing just return null
if (!count($optionFields)) {
return null;
}
// Create output array
$fields = [];
// Iterate over the fields and clean them up
foreach ($optionFields as $field) {
$field = get_object_vars($field);
if (!ActiveUser::$user->permission(constant('Sakura\Perms\Site::' . $field['option_permission']))) {
continue;
}
$fields[$field['option_id']] = $field;
}
// Return the yeahs
return $fields;
}
}

View file

@ -1,349 +0,0 @@
<?php
/**
* Holds various utility functions.
*
* @package Sakura
*/
namespace Sakura;
use PHPMailer;
/**
* Meta utility functions.
*
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class Utils
{
/**
* The error handler.
*
* @param int $errno The error ID.
* @param string $errstr Quick description of the event.
* @param string $errfile File the error occurred in.
* @param int $errline Line the error occurred on.
*/
public static function errorHandler($errno, $errstr, $errfile, $errline)
{
// Remove ROOT path from the error string and file location
$errstr = str_replace(ROOT, '', $errstr);
$errfile = str_replace(ROOT, '', $errfile);
switch ($errno) {
case E_ERROR:
case E_USER_ERROR:
$error = '<b>FATAL ERROR</b>: ' . $errstr . ' on line ' . $errline . ' in ' . $errfile;
break;
case E_WARNING:
case E_USER_WARNING:
$error = '<b>WARNING</b>: ' . $errstr . ' on line ' . $errline . ' in ' . $errfile;
break;
case E_NOTICE:
case E_USER_NOTICE:
$error = '<b>NOTICE</b>: ' . $errstr . ' on line ' . $errline . ' in ' . $errfile;
break;
default:
$error = '<b>Unknown error type</b> [' . $errno . ']: ' . $errstr . ' on line ' . $errline
. ' in ' . $errfile;
}
// Truncate all previous outputs
ob_clean();
ob_end_clean();
// Check for dev mode
$detailed = Config::local('dev', 'show_errors');
// Build page
$errorPage = '<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Sakura Internal Error</title>
<style type="text/css">
body { margin: 0; padding: 0; background: #EEE; color: #000;
font: 12px/20px Verdana, Arial, Helvetica, sans-serif; }
h1, h2 { font-weight: 100; background: #CAA; padding: 8px 5px 10px;
margin: 0; font-style: italic; font-family: serif; }
h1 { border-radius: 8px 8px 0 0; }
h2 { margin: 0 -10px; }
.container { border: 1px solid #CAA; margin: 10px auto; background: #FFF;
box-shadow: 2px 2px 1em #888; max-width: 1024px; border-radius: 10px; }
.container .inner { padding: 0 10px; }
.container .inner .error { background: #555; color: #EEE; border-left: 5px solid #C22;
padding: 4px 6px; text-shadow: 0 1px 1px #888; white-space: pre-wrap;
word-wrap: break-word; margin: 12px 0; border-radius: 5px; box-shadow: inset 0 0 1em #333; }
.container .footer { border-top: 1px solid #CAA; font-size: x-small; padding: 0 5px 1px; }
a { color: #77E; text-decoration: none; }
a:hover { text-decoration: underline; }
a:active { color: #E77; }
</style>
</head>
<body>
<div class="container">
<h1>An error occurred while executing the script.</h1>
<div class="inner">
<p>To prevent potential security risks or data loss Sakura has stopped execution of the script.</p>';
if (isset($errid)) {
$errorPage .= '<p>The error and surrounding data has been logged.</p>
<h2>' . (!$detailed ? 'Report the following text to a staff member' : 'Logged as') . '</h2>
<pre class="error">' . $errid . '</pre>';
} else {
$errorPage .= '<p>Sakura was not able to log this error which could mean that there was an error
with the database connection. If you\'re the system administrator check the database credentials
and make sure the server is running and if you\'re not please let the system administrator
know about this error if it occurs again.</p>';
}
if ($detailed) {
$errorPage .= ' <h2>Summary</h2>
<pre class="error">' . $error . '</pre>
<h2>Backtraces</h2>';
foreach (debug_backtrace() as $num => $trace) {
$errorPage .= '<h3>#' . $num . '</h3><pre class="error">';
foreach ($trace as $key => $val) {
$errorPage .=
str_pad(
'[' . $key . ']',
12
) . '=> ' . (
is_array($val) || is_object($val) ?
json_encode($val) :
$val
) . "\r\n";
}
$errorPage .= '</pre>';
}
}
$errorPage .= '</div>
<div class="footer">
Sakura r' . SAKURA_VERSION . '.
</div>
</div>
</body>
</html>';
// Die and display error message
die($errorPage);
}
/**
* Send an e-mail.
*
* @param string $to Destination e-mail.
* @param string $subject E-mail subject.
* @param string $body Contents of the message.
* @return bool|string Return whatever PHPMailer returns.
*/
public static function sendMail($to, $subject, $body)
{
// Initialise PHPMailer
$mail = new PHPMailer();
// Set to SMTP
$mail->isSMTP();
// Set the SMTP server host
$mail->Host = Config::get('smtp_server');
// Do we require authentication?
$mail->SMTPAuth = Config::get('smtp_auth');
// Do we encrypt as well?
$mail->SMTPSecure = Config::get('smtp_secure');
// Set the port to the SMTP server
$mail->Port = Config::get('smtp_port');
// If authentication is required log in as well
if (Config::get('smtp_auth')) {
$mail->Username = Config::get('smtp_username');
$mail->Password = base64_decode(Config::get('smtp_password'));
}
// Add a reply-to header
$mail->addReplyTo(Config::get('smtp_replyto_mail'), Config::get('smtp_replyto_name'));
// Set a from address as well
$mail->setFrom(Config::get('smtp_from_email'), Config::get('smtp_from_name'));
// Set the addressee
foreach ($to as $email => $name) {
$mail->addBCC($email, $name);
}
// Subject line
$mail->Subject = $subject;
// Set body
$mail->Body = $body;
// Send the message
$send = $mail->send();
// Clear the addressee list
$mail->clearAddresses();
// If we got an error return the error
if (!$send) {
return $mail->ErrorInfo;
}
// Else just return whatever
return $send;
}
/**
* Clean a string
*
* @param string $string Dirty string.
* @param bool $lower Make the string lowercase.
* @param bool $noSpecial String all special characters.
* @param bool $replaceSpecial Thing to replace special characters with.
*
* @return string Clean string.
*/
public static function cleanString($string, $lower = false, $noSpecial = false, $replaceSpecial = '')
{
// Run common sanitisation function over string
$string = htmlentities($string, ENT_NOQUOTES | ENT_HTML401, Config::get('charset'));
$string = stripslashes($string);
$string = strip_tags($string);
// If set also make the string lowercase
if ($lower) {
$string = strtolower($string);
}
// If set remove all characters that aren't a-z or 0-9
if ($noSpecial) {
$string = preg_replace('/[^a-z0-9]/', $replaceSpecial, $string);
}
// Return clean string
return $string;
}
/**
* Validate MX records.
*
* @param string $email E-mail address.
*
* @return bool Success.
*/
public static function checkMXRecord($email)
{
// Get the domain from the e-mail address
$domain = substr(strstr($email, '@'), 1);
// Check the MX record
$record = checkdnsrr($domain, 'MX');
// Return the record data
return $record;
}
/**
* Get the country code of a visitor.
*
* @return string 2 character country code.
*/
public static function getCountryCode()
{
// Attempt to get country code using PHP's built in geo thing
if (function_exists("geoip_country_code_by_name")) {
try {
$code = geoip_country_code_by_name(Net::ip());
// Check if $code is anything
if ($code) {
return $code;
}
} catch (\Exception $e) {
}
}
// Check if the required header is set and return it
if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
return $_SERVER['HTTP_CF_IPCOUNTRY'];
}
// Return XX as a fallback
return 'XX';
}
/**
* Check the entropy of a password.
*
* @param string $pw Password.
*
* @return double|int Entropy.
*/
public static function pwdEntropy($pw)
{
// Decode utf-8 chars
$pw = utf8_decode($pw);
// Count the amount of unique characters in the password string and calculate the entropy
return count(count_chars($pw, 1)) * log(256, 2);
}
/**
* Get the country name from a 2 character code.
*
* @param string $code The country code.
*
* @return string The country name.
*/
public static function getCountryName($code)
{
// Catch XX
if (strtolower($code) === 'xx') {
return 'Unknown';
}
// Catch proxy
if (strtolower($code) === 'a1') {
return 'Anonymous Proxy';
}
return locale_get_display_region("-{$code}", 'en');
}
/**
* Get the byte symbol for a unit from bytes.
*
* @param int $bytes The amount of bytes.
*
* @return string The converted amount with the symbol.
*/
public static function getByteSymbol($bytes)
{
// Return nothing if the input was 0
if (!$bytes) {
return;
}
// Array with byte symbols
$symbols = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
// Calculate byte entity
$exp = floor(log($bytes) / log(1024));
// Format the things
$bytes = sprintf("%.2f " . $symbols[$exp], ($bytes / pow(1024, floor($exp))));
// Return the formatted string
return $bytes;
}
}

View file

@ -1277,12 +1277,12 @@ a.default:active {
} }
.content .content-right { .content .content-right {
width: 100%; width: 100% !important;
min-height: 0; min-height: 0;
} }
.content .content-left { .content .content-left {
width: 100%; width: 100% !important;
min-height: 0; min-height: 0;
border-top: 1px solid #9475B2; border-top: 1px solid #9475B2;
} }
@ -1558,6 +1558,13 @@ a.default:active {
margin-left: 2em; margin-left: 2em;
list-style: square; list-style: square;
} }
.settings .content-left {
width: 850px;
}
.settings .content-right {
width: 174px;
text-align: right;
}
.settings .right-menu-nav > div { .settings .right-menu-nav > div {
background: #C2AFFE; background: #C2AFFE;
padding: 4px; padding: 4px;
@ -1573,7 +1580,7 @@ a.default:active {
line-height: 25px; line-height: 25px;
color: #22E; color: #22E;
text-decoration: none; text-decoration: none;
padding-left: 10px; padding: 0 10px;
} }
.settings .right-menu-nav > a:hover { .settings .right-menu-nav > a:hover {

View file

@ -244,145 +244,6 @@ if (isset($_POST['submit']) && isset($_POST['submit'])) {
]; ];
break; break;
// Profile
case 'profile':
// Get profile fields and create storage var
$fields = Users::getProfileFields();
// Delete all profile fields
DB::table('user_profilefields')
->where('user_id', $currentUser->id)
->delete();
// Go over each field
foreach ($fields as $field) {
// Add to the store array
if (isset($_POST['profile_' . $field['field_identity']]) && !empty($_POST['profile_' . $field['field_identity']])) {
DB::table('user_profilefields')
->insert([
'user_id' => $currentUser->id,
'field_name' => $field['field_identity'],
'field_value' => $_POST['profile_' . $field['field_identity']],
]);
}
// Check if there's additional values we should keep in mind
if (isset($field['field_additional']) && !empty($field['field_additional'])) {
// Go over each additional value
foreach ($field['field_additional'] as $addKey => $addVal) {
// Add to the array
$store = (isset($_POST['profile_additional_' . $addKey]) || !empty($_POST['profile_additional_' . $addKey])) ? $_POST['profile_additional_' . $addKey] : false;
DB::table('user_profilefields')
->insert([
'user_id' => $currentUser->id,
'field_name' => $addKey,
'field_value' => $store,
]);
}
}
}
// Set render data
$renderData['page'] = [
'redirect' => $redirect,
'message' => 'Your profile has been updated!',
'success' => 1,
];
// Birthdays
if (isset($_POST['birthday_day'])
&& isset($_POST['birthday_month'])
&& isset($_POST['birthday_year'])) {
// Check if the values aren't fucked with
if ($_POST['birthday_day'] < 0
|| $_POST['birthday_day'] > 31
|| $_POST['birthday_month'] < 0
|| $_POST['birthday_month'] > 12
|| (
$_POST['birthday_year'] != 0
&& $_POST['birthday_year'] < (date("Y") - 100)
)
|| $_POST['birthday_year'] > date("Y")) {
$renderData['page']['message'] = 'Your birthdate is invalid.';
$renderData['page']['success'] = 0;
break;
}
// Check if the values aren't fucked with
if ((
$_POST['birthday_day'] < 1
&& $_POST['birthday_month'] > 0
)
|| (
$_POST['birthday_day'] > 0
&& $_POST['birthday_month'] < 1)
) {
$renderData['page']['message'] = 'Only setting a day or month is disallowed.';
$renderData['page']['success'] = 0;
break;
}
// Check if the values aren't fucked with
if ($_POST['birthday_year'] > 0
&& (
$_POST['birthday_day'] < 1
|| $_POST['birthday_month'] < 1
)
) {
$renderData['page']['message'] = 'Only setting a year is disallowed.';
$renderData['page']['success'] = 0;
break;
}
$birthdate = implode(
'-',
[$_POST['birthday_year'], $_POST['birthday_month'], $_POST['birthday_day']]
);
DB::table('users')
->where('user_id', $currentUser->id)
->update([
'user_birthday' => $birthdate,
]);
}
break;
// Site Options
case 'options':
// Get profile fields and create storage var
$fields = Users::getOptionFields();
// Delete all option fields for this user
DB::table('user_optionfields')
->where('user_id', $currentUser->id)
->delete();
// Go over each field
foreach ($fields as $field) {
// Make sure the user has sufficient permissions to complete this action
if (!$currentUser->permission(constant('Sakura\Perms\Site::' . $field['option_permission']))) {
continue;
}
if (isset($_POST['option_' . $field['option_id']])
&& !empty($_POST['option_' . $field['option_id']])) {
DB::table('user_optionfields')
->insert([
'user_id' => $currentUser->id,
'field_name' => $field['option_id'],
'field_value' => $_POST['option_' . $field['option_id']],
]);
}
}
// Set render data
$renderData['page'] = [
'redirect' => $redirect,
'message' => 'Changed your options!',
'success' => 1,
];
break;
// Usertitle // Usertitle
case 'usertitle': case 'usertitle':
// Check permissions // Check permissions
@ -799,7 +660,7 @@ if (ActiveUser::$user->id) {
'menu' => true, 'menu' => true,
], ],
'profile' => [ 'profile' => [
'title' => 'Edit Profile', 'title' => 'Profile',
'description' => [ 'description' => [
'These are the external account links etc. 'These are the external account links etc.
on your profile, shouldn\'t need any additional explanation for this one.', on your profile, shouldn\'t need any additional explanation for this one.',
@ -808,7 +669,7 @@ if (ActiveUser::$user->id) {
'menu' => true, 'menu' => true,
], ],
'options' => [ 'options' => [
'title' => 'Site Options', 'title' => 'Options',
'description' => [ 'description' => [
'These are a few personalisation options for the site while you\'re logged in.', 'These are a few personalisation options for the site while you\'re logged in.',
], ],
@ -837,44 +698,7 @@ if (ActiveUser::$user->id) {
'menu' => true, 'menu' => true,
], ],
], ],
]/*,
'messages' => [
'title' => 'Messages',
'modes' => [
'inbox' => [
'title' => 'Inbox',
'description' => [
'The list of messages you\'ve received.',
], ],
'access' => $currentUser->permission(Site::USE_MESSAGES),
'menu' => true,
],
'sent' => [
'title' => 'Sent',
'description' => [
'The list of messages you\'ve sent to other users.',
],
'access' => $currentUser->permission(Site::USE_MESSAGES),
'menu' => true,
],
'compose' => [
'title' => 'Compose',
'description' => [
'Write a new message.',
],
'access' => $currentUser->permission(Site::SEND_MESSAGES),
'menu' => true,
],
'read' => [
'title' => 'Read',
'description' => [
'Read a message.',
],
'access' => $currentUser->permission(Site::USE_MESSAGES),
'menu' => false,
],
],
]*/,
'notifications' => [ 'notifications' => [
'title' => 'Notifications', 'title' => 'Notifications',
'modes' => [ 'modes' => [
@ -949,7 +773,7 @@ if (ActiveUser::$user->id) {
'title' => 'Account', 'title' => 'Account',
'modes' => [ 'modes' => [
'email' => [ 'email' => [
'title' => 'E-mail Address', 'title' => 'E-mail address',
'description' => [ 'description' => [
'You e-mail address is used for password recovery and stuff like that, we won\'t spam you ;).', 'You e-mail address is used for password recovery and stuff like that, we won\'t spam you ;).',
], ],
@ -966,7 +790,7 @@ if (ActiveUser::$user->id) {
'menu' => true, 'menu' => true,
], ],
'usertitle' => [ 'usertitle' => [
'title' => 'Usertitle', 'title' => 'Title',
'description' => [ 'description' => [
'That little piece of text displayed under your username on your profile.', 'That little piece of text displayed under your username on your profile.',
], ],
@ -1011,7 +835,7 @@ if (ActiveUser::$user->id) {
'menu' => true, 'menu' => true,
], ],
'deactivate' => [ 'deactivate' => [
'title' => 'Deactivate Account', 'title' => 'Deactivate',
'description' => [ 'description' => [
'You can deactivate your account here if you want to leave :(.', 'You can deactivate your account here if you want to leave :(.',
], ],
@ -1069,39 +893,6 @@ if (ActiveUser::$user->id) {
// Section specific // Section specific
switch ($category . '.' . $mode) { switch ($category . '.' . $mode) {
// Profile
case 'general.profile':
$renderData['profile'] = [
'fields' => Users::getProfileFields(),
'months' => [
1 => 'January',
2 => 'February',
3 => 'March',
4 => 'April',
5 => 'May',
6 => 'June',
7 => 'July',
8 => 'August',
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December',
],
];
break;
// Options
case 'general.options':
$renderData['options'] = [
'fields' => Users::getOptionFields(),
];
break;
// PM inbox
case 'messages.inbox':
$renderData['messages'] = [];
break;
// Avatar and background sizes // Avatar and background sizes
case 'appearance.avatar': case 'appearance.avatar':
case 'appearance.background': case 'appearance.background':
@ -1112,7 +903,7 @@ if (ActiveUser::$user->id) {
'min_width' => Config::get($mode . '_min_width'), 'min_width' => Config::get($mode . '_min_width'),
'min_height' => Config::get($mode . '_min_height'), 'min_height' => Config::get($mode . '_min_height'),
'max_size' => Config::get($mode . '_max_fsize'), 'max_size' => Config::get($mode . '_max_fsize'),
'max_size_view' => Utils::getByteSymbol(Config::get($mode . '_max_fsize')), 'max_size_view' => byte_symbol(Config::get($mode . '_max_fsize')),
]; ];
break; break;

View file

@ -94,8 +94,11 @@ Router::group(['prefix' => 'members', 'before' => 'loginCheck'], function () {
}); });
// User // User
Router::get('/u/{id}', 'UserController@profile', 'user.profile'); Router::group(['prefix' => 'u'], function () {
Router::get('/u/{id}/header', 'FileController@header', 'user.header'); Router::get('/{id}', 'UserController@profile', 'user.profile');
Router::get('/{id}/report', 'UserController@report', 'user.report');
Router::get('/{id}/header', 'FileController@header', 'user.header');
});
// Notifications // Notifications
Router::group(['prefix' => 'notifications'], function () { Router::group(['prefix' => 'notifications'], function () {
@ -152,7 +155,9 @@ Router::group(['prefix' => 'settings', 'before' => 'loginCheck'], function () {
Router::get('/home', 'Settings.GeneralController@home', 'settings.general.home'); Router::get('/home', 'Settings.GeneralController@home', 'settings.general.home');
Router::get('/profile', 'Settings.GeneralController@profile', 'settings.general.profile'); Router::get('/profile', 'Settings.GeneralController@profile', 'settings.general.profile');
Router::post('/profile', 'Settings.GeneralController@profile', 'settings.general.profile');
Router::get('/options', 'Settings.GeneralController@options', 'settings.general.options'); Router::get('/options', 'Settings.GeneralController@options', 'settings.general.options');
Router::post('/options', 'Settings.GeneralController@options', 'settings.general.options');
}); });
// Friends section // Friends section

View file

@ -8,7 +8,7 @@
namespace Sakura; namespace Sakura;
// Define Sakura version // Define Sakura version
define('SAKURA_VERSION', 20160331); define('SAKURA_VERSION', 20160401);
// Define Sakura Path // Define Sakura Path
define('ROOT', __DIR__ . '/'); define('ROOT', __DIR__ . '/');
@ -30,33 +30,18 @@ if (version_compare(phpversion(), '7.0.0', '<')) {
// Check if the composer autoloader exists // Check if the composer autoloader exists
if (!file_exists(ROOT . 'vendor/autoload.php')) { if (!file_exists(ROOT . 'vendor/autoload.php')) {
throw new \Exception('Autoloader not found, did you run composer?'); throw new \Exception('Autoloader not found, did you run composer install?');
} }
// Require composer libraries // Require composer libraries
require_once ROOT . 'vendor/autoload.php'; require_once ROOT . 'vendor/autoload.php';
// Setup the autoloader
spl_autoload_register(function ($className) {
// Replace \ with /
$className = str_replace('\\', '/', $className);
// Create a throwaway count variable
$i = 1;
// Replace the sakura namespace with the libraries directory
$className = str_replace('Sakura/', 'libraries/', $className, $i);
// Require the file
require_once ROOT . $className . '.php';
});
// Set Error handler
set_error_handler(['Sakura\Utils', 'errorHandler']);
// Load the local configuration // Load the local configuration
Config::init(ROOT . 'config/config.ini'); Config::init(ROOT . 'config/config.ini');
// Set Error handler
set_error_handler('error_handler');
// Change error reporting according to the dev configuration // Change error reporting according to the dev configuration
error_reporting(Config::local('dev', 'show_errors') ? -1 : 0); error_reporting(Config::local('dev', 'show_errors') ? -1 : 0);
@ -110,12 +95,9 @@ ActiveUser::init(
// Create the Urls object // Create the Urls object
$urls = new Urls(); $urls = new Urls();
// Prepare the name of the template to load
$templateName = Config::get('site_style');
if (!defined('SAKURA_NO_TPL')) { if (!defined('SAKURA_NO_TPL')) {
// Start templating engine // Start templating engine
Template::set($templateName); Template::set(Config::get('site_style'));
// Set base page rendering data // Set base page rendering data
Template::vars([ Template::vars([
@ -128,8 +110,6 @@ if (!defined('SAKURA_NO_TPL')) {
'showChangelog' => Config::local('dev', 'show_changelog'), 'showChangelog' => Config::local('dev', 'show_changelog'),
], ],
'resources' => Config::get('content_path') . '/data/' . $templateName,
'currentPage' => $_SERVER['REQUEST_URI'] ?? null, 'currentPage' => $_SERVER['REQUEST_URI'] ?? null,
'referrer' => $_SERVER['HTTP_REFERER'] ?? null, 'referrer' => $_SERVER['HTTP_REFERER'] ?? null,
], ],

View file

@ -96,7 +96,7 @@
<li class="settings"><a title="Change your settings" href="{{ urls.format('SETTINGS_INDEX') }}">Settings</a></li> <li class="settings"><a title="Change your settings" href="{{ urls.format('SETTINGS_INDEX') }}">Settings</a></li>
{% else %} {% else %}
<li class="{% if user.checkFriends(profile.id) == 2 %}mutualFriend{% elseif user.checkFriends(profile.id) == 1 %}pendingFriend{% else %}addFriend{% endif %}"><a href="{% if user.checkFriends(profile.id) == 0 %}{{ urls.format('FRIEND_ADD', [profile.id, session_id(), date().timestamp, sakura.currentPage]) }}{% else %}{{ urls.format('FRIEND_REMOVE', [profile.id, session_id(), date().timestamp, sakura.currentPage]) }}{% endif %}">{% if user.checkFriends(profile.id) == 0 %}Add friend{% else %}Friends{% endif %}</a></li> <li class="{% if user.checkFriends(profile.id) == 2 %}mutualFriend{% elseif user.checkFriends(profile.id) == 1 %}pendingFriend{% else %}addFriend{% endif %}"><a href="{% if user.checkFriends(profile.id) == 0 %}{{ urls.format('FRIEND_ADD', [profile.id, session_id(), date().timestamp, sakura.currentPage]) }}{% else %}{{ urls.format('FRIEND_REMOVE', [profile.id, session_id(), date().timestamp, sakura.currentPage]) }}{% endif %}">{% if user.checkFriends(profile.id) == 0 %}Add friend{% else %}Friends{% endif %}</a></li>
<li class="report"><a href="{{ urls.format('USER_REPORT', [profile.id]) }}">Report</a></li> <li class="report"><a href="{{ route('user.report', profile.id) }}">Report</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View file

@ -8,7 +8,7 @@
{% if comment.userData.id == user.id %} {% if comment.userData.id == user.id %}
<li><a href="javascript:void(0);" onclick="commentDelete({{ comment.id }});" class="clean fa fa-trash-o" title="Delete"></a></li> <li><a href="javascript:void(0);" onclick="commentDelete({{ comment.id }});" class="clean fa fa-trash-o" title="Delete"></a></li>
{% else %} {% else %}
<li><a href="{{ urls.format('USER_REPORT', [comment.userData.id]) }}" class="clean fa fa-exclamation-circle" title="Report"></a></li> <li><a href="{{ route('user.report', comment.userData.id) }}" class="clean fa fa-exclamation-circle" title="Report"></a></li>
{% endif %} {% endif %}
<li><a href="javascript:void(0);" onclick="commentReply({{ comment.id }}, '{{ session_id() }}', '{{ route('file.avatar', user.id) }}');" class="clean fa fa-reply" title="Reply" id="comment-action-reply-{{ comment.id }}"></a></li> <li><a href="javascript:void(0);" onclick="commentReply({{ comment.id }}, '{{ session_id() }}', '{{ route('file.avatar', user.id) }}');" class="clean fa fa-reply" title="Reply" id="comment-action-reply-{{ comment.id }}"></a></li>
<li class="shown voting like"><a href="javascript:void(0);" onclick="commentVote({{ comment.id }}, 1);" class="clean"><span class="fa fa-thumbs-up"></span> <span id="comment-{{ comment.id }}-likes">{{ comment.upvotes }}</span></a></li> <li class="shown voting like"><a href="javascript:void(0);" onclick="commentVote({{ comment.id }}, 1);" class="clean"><span class="fa fa-thumbs-up"></span> <span id="comment-{{ comment.id }}-likes">{{ comment.upvotes }}</span></a></li>

View file

@ -98,7 +98,7 @@
{% if not (post.poster.permission(constant('Sakura\\Perms\\Site::DEACTIVATED')) or post.poster.permission(constant('Sakura\\Perms\\Site::RESTRICTED')) or user.id == post.poster.id) %} {% if not (post.poster.permission(constant('Sakura\\Perms\\Site::DEACTIVATED')) or post.poster.permission(constant('Sakura\\Perms\\Site::RESTRICTED')) or user.id == post.poster.id) %}
<a class="fa fa-{% if user.isFriends(post.poster.id) == 2 %}heart{% else %}star{% endif %} friend-{{ post.poster.id }}-level" title="You are friends" {% if user.isFriends(post.poster.id) == 0 %}style="display: none;"{% endif %}></a> <a class="fa fa-{% if user.isFriends(post.poster.id) == 2 %}heart{% else %}star{% endif %} friend-{{ post.poster.id }}-level" title="You are friends" {% if user.isFriends(post.poster.id) == 0 %}style="display: none;"{% endif %}></a>
<a class="fa fa-user-{% if user.isFriends(post.poster.id) == 0 %}plus{% else %}times{% endif %} forum-friend-toggle friend-{{ post.poster.id }}-toggle" title="{% if user.isFriends(post.poster.id) == 0 %}Add {{ post.poster.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="{% if user.isFriends(post.poster.id) == 0 %}addFriend({{ post.poster.id }}){% else %}removeFriend({{ post.poster.id }}){% endif %}"></a> <a class="fa fa-user-{% if user.isFriends(post.poster.id) == 0 %}plus{% else %}times{% endif %} forum-friend-toggle friend-{{ post.poster.id }}-toggle" title="{% if user.isFriends(post.poster.id) == 0 %}Add {{ post.poster.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="{% if user.isFriends(post.poster.id) == 0 %}addFriend({{ post.poster.id }}){% else %}removeFriend({{ post.poster.id }}){% endif %}"></a>
<a class="fa fa-flag" title="Report {{ post.poster.username }}" href="{{ urls.format('USER_REPORT', [post.poster.id]) }}"></a> <a class="fa fa-flag" title="Report {{ post.poster.username }}" href="{{ route('user.report', post.poster.id) }}"></a>
{% endif %} {% endif %}
<a class="fa fa-reply" title="Quote this post" href="javascript:void(0);" onclick="quotePost({{ post.id }});"></a> <a class="fa fa-reply" title="Quote this post" href="javascript:void(0);" onclick="quotePost({{ post.id }});"></a>
</div> </div>

View file

@ -50,7 +50,6 @@
"siteName": "{{ config('sitename') }}", "siteName": "{{ config('sitename') }}",
"content": "{{ config('content_path') }}", "content": "{{ config('content_path') }}",
"resources": "{{ sakura.resources }}",
"recaptchaEnabled": "{{ config('recaptcha') }}", "recaptchaEnabled": "{{ config('recaptcha') }}",
"minUserLen": {{ config('username_min_length') }}, "minUserLen": {{ config('username_min_length') }},

View file

@ -1,37 +0,0 @@
{% set friends = user.friends(1)|batch(12) %}
{% set paginationPages = friends %}
{% set paginationUrl %}{{ urls.format('SETTING_MODE', ['friends', 'listing']) }}{% endset %}
{% block css %}
<style type="text/css">
.pagination {
float: right;
}
</style>
{% endblock %}
{% if friends|length %}
<div class="friends-list">
{% for friend in friends[get.page|default(1) - 1] %}
<div class="friend-container" id="friendslist-friend-{{ friend.id }}">
<a class="friends-list-data clean" href="{{ route('user.profile', friend.id) }}">
<img src="{{ route('file.avatar', friend.id) }}" alt="{{ friend.username }}" class="friends-list-avatar default-avatar-setting" style="width: 150px; height: 150px;" />
<div class="friends-list-name" style="color: {{ friend.colour }};">{{ friend.username }}</div>
</a>
<div class="friends-list-actions">
<a class="remove fill fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="removeFriend({{ friend.id }});"></a>
<div class="clear"></div>
</div>
</div>
{% endfor %}
<div class="clear"></div>
</div>
{% if friends|length > 1 %}
<div>
{% include 'elements/pagination.twig' %}
</div>
{% endif %}
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">You don't have any friends yet!</h1>
{% endif %}

View file

@ -1,38 +0,0 @@
{% set friends = user.friends(-1)|batch(12) %}
{% set paginationPages = friends %}
{% set paginationUrl %}{{ urls.format('SETTING_MODE', ['friends', 'requests']) }}{% endset %}
{% block css %}
<style type="text/css">
.pagination {
float: right;
}
</style>
{% endblock %}
{% if friends|length %}
<div class="friends-list">
{% for friend in friends[get.page|default(1) - 1] %}
<div class="friend-container" id="friend-{{ friend.id }}">
<a class="friends-list-data clean" href="{{ route('user.profile', friend.id) }}">
<img src="{{ route('file.avatar', friend.id) }}" alt="{{ friend.username }}" class="friends-list-avatar default-avatar-setting" style="width: 150px; height: 150px;" />
<div class="friends-list-name" style="color: {{ friend.colour }};">{{ friend.username }}</div>
</a>
<div class="friends-list-actions">
<a class="add fa fa-check" title="Add friend" href="javascript:void(0);" onclick="addFriend({{ friend.id }});"></a>
<a class="remove fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="removeFriend({{ friend.id }});"></a>
<div class="clear"></div>
</div>
</div>
{% endfor %}
<div class="clear"></div>
</div>
{% if friends|length > 1 %}
<div>
{% include 'elements/pagination.twig' %}
</div>
{% endif %}
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">You don't have any pending requests!</h1>
{% endif %}

View file

@ -1,20 +0,0 @@
<div style="margin: 5px;">
<h1 class="stylised">Common Tasks</h1>
<h2>Profile</h2>
<ul>
<li><a href="{{ urls.format('SETTING_MODE', ['appearance', 'avatar']) }}" class="default">Change Avatar</a></li>
<li><a href="{{ urls.format('SETTING_MODE', ['appearance', 'userpage']) }}" class="default">Change Userpage</a></li>
<li><a href="{{ urls.format('SETTING_MODE', ['appearance', 'signature']) }}" class="default">Change Signature</a></li>
<li><a href="{{ urls.format('SETTING_MODE', ['general', 'profile']) }}" class="default">Change Profile Details</a></li>
</ul>
<h2>Messaging</h2>
<ul>
<li><a href="{{ urls.format('SETTING_MODE', ['messages', 'inbox']) }}" class="default">View Inbox</a></li>
<li><a href="{{ urls.format('SETTING_MODE', ['messages', 'compose']) }}" class="default">Send PM</a></li>
</ul>
<h2>Account</h2>
<ul>
<li><a href="{{ urls.format('SETTING_MODE', ['advanced', 'sessions']) }}" class="default">Manage Active Sessions</a></li>
<li><a href="{{ urls.format('SETTING_MODE', ['account', 'password']) }}" class="default">Change Password</a></li>
</ul>
</div>

View file

@ -1,31 +0,0 @@
{% if options.fields %}
<form enctype="multipart/form-data" method="post" action="{{ sakura.currentPage }}" id="optionsForm">
<input type="hidden" name="sessid" value="{{ session_id() }}" />
<input type="hidden" name="timestamp" value="{{ date().timestamp }}" />
<input type="hidden" name="mode" value="options" />
{% for field in options.fields %}
<div class="profile-field">
<div>
<h2>{{ field.option_name }}</h2>
<div style="font-size: .8em; line-height: 110%;">
{{ field.option_description }}
</div>
</div>
<div style="padding: 8px 0;">
<input type="{{ field.option_type }}" name="option_{{ field.option_id }}" class="inputStyling"{% if user.optionFields[field.option_id] %}{% if field.option_type == 'checkbox' and user.optionFields[field.option_id] %} checked="checked" value="option_{{ field.option_id }}"{% else %} value="{{ user.optionFields[field.option_id] }}"{% endif %}{% endif %} />
</div>
</div>
{% endfor %}
<div class="profile-save">
<input type="submit" value="Save" name="submit" class="inputStyling" />
<input type="reset" value="Reset" name="reset" class="inputStyling" />
</div>
</form>
<script type="text/javascript">
window.addEventListener("load", function() {
prepareAjaxForm('optionsForm', 'Changing Options...');
});
</script>
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">There are currently no changeable options.</h1>
{% endif %}

View file

@ -1,59 +0,0 @@
{% set birthday = user.birthday|split('-') %}
<form enctype="multipart/form-data" method="post" action="{{ sakura.currentPage }}" id="editProfileForm">
<input type="hidden" name="sessid" value="{{ session_id() }}" />
<input type="hidden" name="timestamp" value="{{ date().timestamp }}" />
<input type="hidden" name="mode" value="profile" />
{% for field in profile.fields %}
<div class="profile-field">
<div>
<h2>{{ field.field_name }}</h2>
</div>
<div>
<input type="{{ field.field_type }}" name="profile_{{ field.field_identity }}" class="inputStyling" placeholder="{{ field.field_description }}"{% if user.profileFields[field.field_identity].value %}{% if field.field_type == 'checkbox' and user.profileFields[field.field_identity].value == 'true' %} checked="checked" value="profile_{{ field.field_identity }}"{% else %} value="{{ user.profileFields[field.field_identity].value }}"{% endif %}{% endif %} />
</div>
{% if field.field_additional %}
{% for id,addit in field.field_additional %}
<div>
<input type="{{ addit[0] }}" id="{{ id }}" name="profile_additional_{{ id }}"{% if user.profileFields[field.field_identity][id] %}{% if addit[0] == 'checkbox' and user.profileFields[field.field_identity][id] == true %} checked="checked"{% else %} value="{{ user.profileFields[field.field_identity][id] }}"{% endif %}{% endif %} />
<label for="{{ id }}" style="font-size: 10px;">{{ addit[1]|raw }}</label>
</div>
{% endfor %}
{% endif %}
</div>
{% endfor %}
<div class="profile-field birthday">
<div>
<h2>Birthday</h2>
</div>
<div style="text-align: center;">
Day: <select name="birthday_day">
<option value="0"{% if not birthday[2] %} selected="selected"{% endif %}>--</option>
{% for i in 1..31 %}
<option value="{{ i }}"{% if birthday[2] == i %} selected="selected"{% endif %}>{{ i }}</option>
{% endfor %}
</select>
Month: <select name="birthday_month">
<option value="0"{% if not birthday[1] %} selected="selected"{% endif %}>--</option>
{% for i in 1..12 %}
<option value="{{ i }}"{% if birthday[1] == i %} selected="selected"{% endif %}>{{ profile.months[i] }}</option>
{% endfor %}
</select>
Year: <select name="birthday_year">
<option value="0"{% if not birthday[0] %} selected="selected"{% endif %}>----</option>
{% for i in "now"|date('Y')..("now"|date('Y') - 100) %}
<option value="{{ i }}"{% if birthday[0] == i %} selected="selected"{% endif %}>{{ i }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="profile-save">
<input type="submit" value="Save" name="submit" class="inputStyling" />
<input type="reset" value="Reset" name="reset" class="inputStyling" />
</div>
</form>
<script type="text/javascript">
window.addEventListener("load", function() {
prepareAjaxForm('editProfileForm', 'Updating Profile...');
});
</script>

View file

@ -1,23 +0,0 @@
{% if messages|length %}
<table class="msgTable">
<thead>
<tr>
<th>From</th>
<th>Subject</th>
<th>Sent on</th>
</tr>
</thead>
<tbody>
{% for message in messages %}
<tr>
<td><a href="/u/{{ message.data.from.user.id }}" class="default" style="font-weight: 700; color: {{ message.data.from.user.colour }};">{{ message.data.from.user.username }}</a></td>
<td><a href="/messages/read/{{ message.id }}" class="default">{{ message.subject }}</a></td>
<td>{{ message.time|date(config('date_format')) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h1 class="stylised"style="line-height: 1.8em; text-align: center;">Nothing to view!</h1>
{% endif %}
<h3 style="text-align: center;">Click Compose in the menu on the right side to write a new message!</h3>

View file

@ -1,49 +0,0 @@
{% set alerts = user.notifications(0, false)|batch(10) %}
{% set paginationPages = alerts %}
{% set paginationUrl %}{{ urls.format('SETTING_MODE', ['notifications', 'history']) }}{% endset %}
{% block css %}
<style type="text/css">
.pagination {
float: right;
}
</style>
{% endblock %}
{% if alerts %}
<div class="notification-history">
{% for alert in alerts[get.page|default(1) - 1] %}
<a id="notif-hist-{{ alert.id }}" class="clean {% if alert.read %}read{% endif %}"{% if alert.link %} href="{{ alert.link }}"{% endif %}>
<div class="notif-hist-icon">
{% if 'FONT:' in alert.image %}
<div class="font-icon fa {{ alert.image|replace({'FONT:': ''}) }} fa-4x"></div>
{% else %}
<img src="{{ alert.image }}" alt="Notification" />
{% endif %}
</div>
<div class="notif-hist-content">
<div class="notif-hist-inside">
<div class="notif-hist-title">
{{ alert.title }}
</div>
<div class="notif-hist-text">
{{ alert.title }}
</div>
</div>
<div class="notif-hist-time">
<time datetime="{{ alert.time|date('r') }}">{{ alert.time|date(config('date_format')) }}</time>
</div>
</div>
<div class="clear"></div>
</a>
{% endfor %}
</div>
{% if alerts|length > 1 %}
<div>
{% include 'elements/pagination.twig' %}
</div>
{% endif %}
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">You don't have any notifications in your history!</h1>
{% endif %}

View file

@ -0,0 +1,3 @@
{% extends 'settings/master.twig' %}
{% set category = 'Advanced' %}

View file

@ -0,0 +1,51 @@
{% extends 'settings/advanced/master.twig' %}
{% set mode = 'Home' %}
{% block description %}
<p>Welcome to the Settings Panel! From here you can monitor, view and update your profile and preferences.</p>
{% endblock %}
{% block settingsContent %}
<table class="settings-table">
<thead>
<tr><th style="width: 100px;">IP</th><th>Useragent</th><th style="width: 120px;">Login time</th><th></th></tr>
</thead>
<tfoot>
<tr><th>IP</th><th>Useragent</th><th>Login time</th><th></th></tr>
</tfoot>
<tbody>
{% for s in sessions %}
<tr {% if s.session_key == session.sessionId %} class="current-session"{% endif %}>
<td>
{{ s.user_ip }}
</td>
<td>
{{ s.user_agent }}
</td>
<td>
<time datetime="{{ s.session_start|date('r') }}">{{ s.session_start|date(config('date_format')) }}</time>
</td>
<td style="width: 90px;">
<form method="post" action="/settings.php?cat=advanced&amp;mode=sessions">
<input type="hidden" name="sessid" value="{{ session_id() }}" />
<input type="hidden" name="timestamp" value="{{ date().timestamp }}" />
<input type="hidden" name="sessionid" value="{{ s.session_id }}" />
<input type="hidden" name="mode" value="sessions" />
<button class="inputStyling small" name="submit">Kill</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="profile-save">
<form method="post" action="/settings.php?cat=advanced&amp;mode=sessions">
<input type="hidden" name="sessid" value="{{ session_id() }}" />
<input type="hidden" name="timestamp" value="{{ date().timestamp }}" />
<input type="hidden" name="sessionid" value="all" />
<input type="hidden" name="mode" value="sessions" />
<button class="inputStyling" name="submit">Kill all active sessions</button>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends 'settings/general/master.twig' %}
{% set friends = user.friends(1)|batch(12) %}
{% set paginationPages = friends %}
{% set paginationUrl %}{{ route('settings.friends.listing') }}{% endset %}
{% set mode = 'Listing' %}
{% block description %}
<p>Manage your friends.</p>
{% endblock %}
{% block settingsContent %}
{% if friends|length %}
<div class="friends-list">
{% for friend in friends[get.page|default(1) - 1] %}
<div class="friend-container" id="friendslist-friend-{{ friend.id }}">
<a class="friends-list-data clean" href="{{ route('user.profile', friend.id) }}">
<img src="{{ route('file.avatar', friend.id) }}" alt="{{ friend.username }}" class="friends-list-avatar default-avatar-setting" style="width: 150px; height: 150px;" />
<div class="friends-list-name" style="color: {{ friend.colour }};">{{ friend.username }}</div>
</a>
<div class="friends-list-actions">
<a class="remove fill fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="removeFriend({{ friend.id }});"></a>
<div class="clear"></div>
</div>
</div>
{% endfor %}
<div class="clear"></div>
</div>
{% if friends|length > 1 %}
<div>
{% include 'elements/pagination.twig' %}
</div>
{% endif %}
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">You don't have any friends yet!</h1>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends 'settings/master.twig' %}
{% set category = 'Friends' %}
{% block css %}
<style type="text/css">
.pagination {
float: right;
}
</style>
{% endblock %}

View file

@ -0,0 +1,40 @@
{% extends 'settings/general/master.twig' %}
{% set friends = user.friends(-1)|batch(12) %}
{% set paginationPages = friends %}
{% set paginationUrl %}{{ route('settings.friends.requests') }}{% endset %}
{% set mode = 'Requests' %}
{% block description %}
<p>Handle friend requests.</p>
{% endblock %}
{% block settingsContent %}
{% if friends|length %}
<div class="friends-list">
{% for friend in friends[get.page|default(1) - 1] %}
<div class="friend-container" id="friend-{{ friend.id }}">
<a class="friends-list-data clean" href="{{ route('user.profile', friend.id) }}">
<img src="{{ route('file.avatar', friend.id) }}" alt="{{ friend.username }}" class="friends-list-avatar default-avatar-setting" style="width: 150px; height: 150px;" />
<div class="friends-list-name" style="color: {{ friend.colour }};">{{ friend.username }}</div>
</a>
<div class="friends-list-actions">
<a class="add fa fa-check" title="Add friend" href="javascript:void(0);" onclick="addFriend({{ friend.id }});"></a>
<a class="remove fa fa-remove" title="Remove friend" href="javascript:void(0);" onclick="removeFriend({{ friend.id }});"></a>
<div class="clear"></div>
</div>
</div>
{% endfor %}
<div class="clear"></div>
</div>
{% if friends|length > 1 %}
<div>
{% include 'elements/pagination.twig' %}
</div>
{% endif %}
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">You don't have any pending requests!</h1>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,33 @@
{% extends 'settings/general/master.twig' %}
{% set mode = 'Options' %}
{% block description %}
<p>These are a few personalisation options for the site while you're logged in.</p>
{% endblock %}
{% block settingsContent %}
{% if fields %}
<form enctype="multipart/form-data" method="post" action="{{ route('settings.general.options') }}">
{% for field in fields %}
<div class="profile-field">
<div>
<h2>{{ field.name }}</h2>
<div style="font-size: .8em; line-height: 110%;">
{{ field.description }}
</div>
</div>
<div style="padding: 8px 0;">
<input type="{{ field.type }}" name="option_{{ field.id }}" class="inputStyling"{% if user.optionFields[field.id] %}{% if field.type == 'checkbox' and user.optionFields[field.id] %} checked="checked" value="option_{{ field.id }}"{% else %} value="{{ user.optionFields[field.id] }}"{% endif %}{% endif %} />
</div>
</div>
{% endfor %}
<div class="profile-save">
<button name="session" value="{{ session_id() }}" class="inputStyling">Save</button>
<button type="reset" class="inputStyling">Reset</button>
</div>
</form>
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">There are currently no changeable options.</h1>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends 'settings/general/master.twig' %}
{% set mode = 'Profile' %}
{% block description %}
<p>These are the external account links etc. on your profile, shouldn't need any additional explanation for this one.</p>
{% endblock %}
{% set months = {
1: "January",
2: "February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December",
} %}
{% set birthday = user.birthday|split('-') %}
{% block settingsContent %}
<form enctype="multipart/form-data" method="post" action="{{ route('settings.general.profile') }}">
{% for field in fields %}
<div class="profile-field" id="{{ field.id }}">
<div>
<h2>{{ field.name }}</h2>
</div>
<div>
<input type="{{ field.type }}" name="profile_{{ field.id }}" class="inputStyling" placeholder="{{ field.description }}"{% if user.profileFields[field.id].value %}{% if field.type == 'checkbox' and user.profileFields[field.id].value == 'true' %} checked="checked" value="profile_{{ field.id }}"{% else %} value="{{ user.profileFields[field.id].value }}"{% endif %}{% endif %} />
</div>
{% if field.additional %}
{% for id,addit in field.additional %}
<div>
<input type="{{ addit[0] }}" id="{{ id }}" name="profile_additional_{{ id }}"{% if user.profileFields[field.id][id] %}{% if addit[0] == 'checkbox' and user.profileFields[field.id][id] == true %} checked="checked"{% else %} value="{{ user.profileFields[field.id][id] }}"{% endif %}{% endif %} />
<label for="{{ id }}" style="font-size: 10px;">{{ addit[1]|raw }}</label>
</div>
{% endfor %}
{% endif %}
</div>
{% endfor %}
<div class="profile-field birthday">
<div>
<h2>Birthday</h2>
</div>
<div style="text-align: center;">
Day: <select name="birthday_day">
<option value="0"{% if not birthday[2] %} selected="selected"{% endif %}>--</option>
{% for i in 1..31 %}
<option value="{{ i }}"{% if birthday[2] == i %} selected="selected"{% endif %}>{{ i }}</option>
{% endfor %}
</select>
Month: <select name="birthday_month">
<option value="0"{% if not birthday[1] %} selected="selected"{% endif %}>--</option>
{% for i in 1..12 %}
<option value="{{ i }}"{% if birthday[1] == i %} selected="selected"{% endif %}>{{ months[i] }}</option>
{% endfor %}
</select>
Year: <select name="birthday_year">
<option value="0"{% if not birthday[0] %} selected="selected"{% endif %}>----</option>
{% for i in "now"|date('Y')..("now"|date('Y') - 100) %}
<option value="{{ i }}"{% if birthday[0] == i %} selected="selected"{% endif %}>{{ i }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="profile-save">
<button name="session" value="{{ session_id() }}" class="inputStyling">Save</button>
<button type="reset" class="inputStyling">Reset</button>
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,59 @@
{% extends 'settings/notifications/master.twig' %}
{% set mode = 'History' %}
{% block description %}
<p>The history of notifications that have been sent to you.</p>
{% endblock %}
{% set alerts = user.notifications(0, false)|batch(10) %}
{% set paginationPages = alerts %}
{% set paginationUrl %}{{ route('settings.notifications.history') }}{% endset %}
{% block css %}
<style type="text/css">
.pagination {
float: right;
}
</style>
{% endblock %}
{% block settingsContent %}
{% if alerts %}
<div class="notification-history">
{% for alert in alerts[get.page|default(1) - 1] %}
<a id="notif-hist-{{ alert.id }}" class="clean {% if alert.read %}read{% endif %}"{% if alert.link %} href="{{ alert.link }}"{% endif %}>
<div class="notif-hist-icon">
{% if 'FONT:' in alert.image %}
<div class="font-icon fa {{ alert.image|replace({'FONT:': ''}) }} fa-4x"></div>
{% else %}
<img src="{{ alert.image }}" alt="Notification" />
{% endif %}
</div>
<div class="notif-hist-content">
<div class="notif-hist-inside">
<div class="notif-hist-title">
{{ alert.title }}
</div>
<div class="notif-hist-text">
{{ alert.title }}
</div>
</div>
<div class="notif-hist-time">
<time datetime="{{ alert.time|date('r') }}">{{ alert.time|date(config('date_format')) }}</time>
</div>
</div>
<div class="clear"></div>
</a>
{% endfor %}
</div>
{% if alerts|length > 1 %}
<div>
{% include 'elements/pagination.twig' %}
</div>
{% endif %}
{% else %}
<h1 class="stylised" style="margin: 2em auto; text-align: center;">You don't have any notifications in your history!</h1>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,3 @@
{% extends 'settings/master.twig' %}
{% set category = 'Notifications' %}

View file

@ -115,7 +115,7 @@
{% else %} {% else %}
{% if user.isFriends(profile.id) != 0 %}<a class="fa fa-{% if user.isFriends(profile.id) == 2 %}heart{% else %}star{% endif %}" title="You are friends"></a>{% endif %} {% if user.isFriends(profile.id) != 0 %}<a class="fa fa-{% if user.isFriends(profile.id) == 2 %}heart{% else %}star{% endif %}" title="You are friends"></a>{% endif %}
<a class="fa fa-user-{% if user.isFriends(profile.id) == 0 %}plus{% else %}times{% endif %}" title="{% if user.isFriends(profile.id) == 0 %}Add {{ profile.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="{% if user.isFriends(profile.id) == 0 %}addFriend({{ profile.id }}){% else %}removeFriend({{ profile.id }}){% endif %}"></a> <a class="fa fa-user-{% if user.isFriends(profile.id) == 0 %}plus{% else %}times{% endif %}" title="{% if user.isFriends(profile.id) == 0 %}Add {{ profile.username }} as a friend{% else %}Remove friend{% endif %}" href="javascript:void(0);" onclick="{% if user.isFriends(profile.id) == 0 %}addFriend({{ profile.id }}){% else %}removeFriend({{ profile.id }}){% endif %}"></a>
{#<a class="fa fa-exclamation-circle" title="Report {{ profile.username }}" href="{{ urls.format('USER_REPORT', [profile.id]) }}"></a>#} <a class="fa fa-exclamation-circle" title="Report {{ profile.username }}" href="{{ route('user.report', profile.id) }}"></a>
{% endif %} {% endif %}
{% if user.permission(constant('Sakura\\Perms\\Manage::CAN_RESTRICT_USERS'), constant('Sakura\\Perms::MANAGE')) %} {% if user.permission(constant('Sakura\\Perms\\Manage::CAN_RESTRICT_USERS'), constant('Sakura\\Perms::MANAGE')) %}
<a class="fa fa-trash" title="Restrict {{ profile.username }}" href="?restrict={{ session_id() }}"></a> <a class="fa fa-trash" title="Restrict {{ profile.username }}" href="?restrict={{ session_id() }}"></a>

View file

@ -0,0 +1,5 @@
{% extends 'global/master.twig' %}
{% block content %}
<h1 class="stylised" style="text-align: center; margin: 2em auto;">I'll actually make reporting a thing, someday...</h1>
{% endblock %}

278
utility.php Normal file
View file

@ -0,0 +1,278 @@
<?php
/*
* A set of utility helper functions
*/
use Sakura\Config;
use Sakura\Net;
function clean_string($string, $lower = false, $noSpecial = false, $replaceSpecial = '')
{
// Run common sanitisation function over string
$string = htmlentities($string, ENT_NOQUOTES | ENT_HTML401, Config::get('charset'));
$string = stripslashes($string);
$string = strip_tags($string);
// If set also make the string lowercase
if ($lower) {
$string = strtolower($string);
}
// If set remove all characters that aren't a-z or 0-9
if ($noSpecial) {
$string = preg_replace('/[^a-z0-9]/', $replaceSpecial, $string);
}
// Return clean string
return $string;
}
function check_mx_record($email)
{
// Get the domain from the e-mail address
$domain = substr(strstr($email, '@'), 1);
// Check the MX record
$record = checkdnsrr($domain, 'MX');
// Return the record data
return $record;
}
function get_country_code()
{
// Attempt to get country code using PHP's built in geo thing
if (function_exists("geoip_country_code_by_name")) {
try {
$code = geoip_country_code_by_name(Net::ip());
// Check if $code is anything
if ($code) {
return $code;
}
} catch (\Exception $e) {
}
}
// Check if the required header is set and return it
if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
return $_SERVER['HTTP_CF_IPCOUNTRY'];
}
// Return XX as a fallback
return 'XX';
}
function get_country_name($code)
{
// Catch XX
if (strtolower($code) === 'xx') {
return 'Unknown';
}
// Catch proxy
if (strtolower($code) === 'a1') {
return 'Anonymous Proxy';
}
return locale_get_display_region("-{$code}", 'en');
}
function password_entropy($password)
{
// Decode utf-8 chars
$password = utf8_decode($password);
// Count the amount of unique characters in the password string and calculate the entropy
return count(count_chars($password, 1)) * log(256, 2);
}
function byte_symbol($bytes)
{
// Return nothing if the input was 0
if (!$bytes) {
return "0 B";
}
// Array with byte symbols
$symbols = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
// Calculate byte entity
$exp = floor(log($bytes) / log(1024));
// Format the things
$bytes = sprintf("%.2f " . $symbols[$exp], ($bytes / pow(1024, floor($exp))));
// Return the formatted string
return $bytes;
}
function send_mail($to, $subject, $body)
{
// Initialise PHPMailer
$mail = new PHPMailer;
// Set to SMTP
$mail->isSMTP();
// Set the SMTP server host
$mail->Host = Config::get('smtp_server');
// Do we require authentication?
$mail->SMTPAuth = Config::get('smtp_auth');
// Do we encrypt as well?
$mail->SMTPSecure = Config::get('smtp_secure');
// Set the port to the SMTP server
$mail->Port = Config::get('smtp_port');
// If authentication is required log in as well
if (Config::get('smtp_auth')) {
$mail->Username = Config::get('smtp_username');
$mail->Password = base64_decode(Config::get('smtp_password'));
}
// Add a reply-to header
$mail->addReplyTo(Config::get('smtp_replyto_mail'), Config::get('smtp_replyto_name'));
// Set a from address as well
$mail->setFrom(Config::get('smtp_from_email'), Config::get('smtp_from_name'));
// Set the addressee
foreach ($to as $email => $name) {
$mail->addBCC($email, $name);
}
// Subject line
$mail->Subject = $subject;
// Set body
$mail->Body = $body;
// Send the message
$send = $mail->send();
// Clear the addressee list
$mail->clearAddresses();
// If we got an error return the error
if (!$send) {
return $mail->ErrorInfo;
}
// Else just return whatever
return $send;
}
function error_handler($errno, $errstr, $errfile, $errline)
{
// Remove ROOT path from the error string and file location
$errstr = str_replace(ROOT, '', $errstr);
$errfile = str_replace(ROOT, '', $errfile);
switch ($errno) {
case E_ERROR:
case E_USER_ERROR:
$error = '<b>FATAL ERROR</b>: ' . $errstr . ' on line ' . $errline . ' in ' . $errfile;
break;
case E_WARNING:
case E_USER_WARNING:
$error = '<b>WARNING</b>: ' . $errstr . ' on line ' . $errline . ' in ' . $errfile;
break;
case E_NOTICE:
case E_USER_NOTICE:
$error = '<b>NOTICE</b>: ' . $errstr . ' on line ' . $errline . ' in ' . $errfile;
break;
default:
$error = '<b>Unknown error type</b> [' . $errno . ']: ' . $errstr . ' on line ' . $errline
. ' in ' . $errfile;
}
// Truncate all previous outputs
ob_clean();
ob_end_clean();
// Check for dev mode
$detailed = Config::local('dev', 'show_errors');
// Build page
$errorPage = '<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Sakura Internal Error</title>
<style type="text/css">
body { margin: 0; padding: 0; background: #EEE; color: #000;
font: 12px/20px Verdana, Arial, Helvetica, sans-serif; }
h1, h2 { font-weight: 100; background: #CAA; padding: 8px 5px 10px;
margin: 0; font-style: italic; font-family: serif; }
h1 { border-radius: 8px 8px 0 0; }
h2 { margin: 0 -10px; }
.container { border: 1px solid #CAA; margin: 10px auto; background: #FFF;
box-shadow: 2px 2px 1em #888; max-width: 1024px; border-radius: 10px; }
.container .inner { padding: 0 10px; }
.container .inner .error { background: #555; color: #EEE; border-left: 5px solid #C22;
padding: 4px 6px; text-shadow: 0 1px 1px #888; white-space: pre-wrap;
word-wrap: break-word; margin: 12px 0; border-radius: 5px; box-shadow: inset 0 0 1em #333; }
.container .footer { border-top: 1px solid #CAA; font-size: x-small; padding: 0 5px 1px; }
a { color: #77E; text-decoration: none; }
a:hover { text-decoration: underline; }
a:active { color: #E77; }
</style>
</head>
<body>
<div class="container">
<h1>An error occurred while executing the script.</h1>
<div class="inner">
<p>To prevent potential security risks or data loss Sakura has stopped execution of the script.</p>';
if (isset($errid)) {
$errorPage .= '<p>The error and surrounding data has been logged.</p>
<h2>' . (!$detailed ? 'Report the following text to a staff member' : 'Logged as') . '</h2>
<pre class="error">' . $errid . '</pre>';
} else {
$errorPage .= '<p>Sakura was not able to log this error which could mean that there was an error
with the database connection. If you\'re the system administrator check the database credentials
and make sure the server is running and if you\'re not please let the system administrator
know about this error if it occurs again.</p>';
}
if ($detailed) {
$errorPage .= ' <h2>Summary</h2>
<pre class="error">' . $error . '</pre>
<h2>Backtraces</h2>';
foreach (debug_backtrace() as $num => $trace) {
$errorPage .= '<h3>#' . $num . '</h3><pre class="error">';
foreach ($trace as $key => $val) {
$errorPage .=
str_pad(
'[' . $key . ']',
12
) . '=> ' . (
is_array($val) || is_object($val) ?
json_encode($val) :
$val
) . "\r\n";
}
$errorPage .= '</pre>';
}
}
$errorPage .= '</div>
<div class="footer">
Sakura r' . SAKURA_VERSION . '.
</div>
</div>
</body>
</html>';
// Die and display error message
die($errorPage);
}