premium stuff

This commit is contained in:
flash 2016-03-27 23:18:57 +02:00
parent 9280706a0a
commit 6da8eb931d
24 changed files with 541 additions and 505 deletions

View file

@ -6,14 +6,6 @@
// Declare Namespace
namespace Sakura;
// Check if the script isn't executed by root
if (function_exists('posix_getuid')) {
if (posix_getuid() === 0) {
trigger_error('Running cron as root is disallowed for security reasons.', E_USER_ERROR);
exit;
}
}
// Define that this page won't require templating
define('SAKURA_NO_TPL', true);
@ -35,9 +27,13 @@ DB::table('notifications')
// Get expired premium accounts
$expiredPremium = DB::table('premium')
->where('premium_expire', '<', time())
->get();
->get(['user_id']);
// Process expired premium accounts, make this not stupid in the future
foreach ($expiredPremium as $expired) {
Users::updatePremiumMeta($expired->user_id);
foreach ($expiredPremium as $premium) {
DB::table('premium')
->where('user_id', $premium->user_id)
->delete();
User::construct($premium->user_id)
->isPremium();
}

View file

@ -139,7 +139,7 @@ class BBcode
$parsed = nl2br(self::$bbcode->getAsHtml());
$parsed = Utils::fixCodeTags($parsed);
$parsed = self::fixCodeTags($parsed);
$parsed = self::parseEmoticons($parsed);
return $parsed;
@ -178,4 +178,37 @@ class BBcode
return self::$bbcode->getAsText();
}
/**
* Clean up the contents of <code> tags.
*
* @param string $text Dirty
*
* @return string Clean
*/
public static function fixCodeTags($text)
{
$parts = explode('<code>', $text);
$newStr = '';
if (count($parts) > 1) {
foreach ($parts as $p) {
$parts2 = explode('</code>', $p);
if (count($parts2) > 1) {
$code = str_replace('<br />', '', $parts2[0]);
$code = str_replace('<br/>', '', $code);
$code = str_replace('<br>', '', $code);
$code = str_replace('<', '&lt;', $code);
$newStr .= '<code>' . $code . '</code>';
$newStr .= $parts2[1];
} else {
$newStr .= $p;
}
}
} else {
$newStr = $text;
}
return $newStr;
}
}

View file

@ -28,17 +28,28 @@ use Sakura\Utils;
*/
class AuthController extends Controller
{
protected function touchRateLimit($user, $mode = 0)
/**
* Touch the login rate limit.
*
* @param $user int The ID of the user that attempted to log in.
* @param $sucess bool Whether the login attempt was successful.
*/
protected function touchRateLimit($user, $success = false)
{
DB::table('login_attempts')
->insert([
'attempt_success' => $mode,
'attempt_success' => $success ? 1 : 0,
'attempt_timestamp' => time(),
'attempt_ip' => Net::pton(Net::IP()),
'attempt_ip' => Net::pton(Net::ip()),
'user_id' => $user,
]);
}
/**
* End the current session.
*
* @return string
*/
public function logout()
{
// Check if user is logged in
@ -65,11 +76,21 @@ class AuthController extends Controller
return Template::render('global/information');
}
/**
* Get the login page.
*
* @return string
*/
public function loginGet()
{
return Template::render('auth/login');
}
/**
* Do a login attempt.
*
* @return string
*/
public function loginPost()
{
// Preliminarily set login to failed
@ -91,7 +112,7 @@ class AuthController extends Controller
// Check if we haven't hit the rate limit
$rates = DB::table('login_attempts')
->where('attempt_ip', Net::pton(Net::IP()))
->where('attempt_ip', Net::pton(Net::ip()))
->where('attempt_timestamp', '>', time() - 1800)
->where('attempt_success', '0')
->count();
@ -172,7 +193,7 @@ class AuthController extends Controller
Config::get('cookie_path')
);
$this->touchRateLimit($user->id, 1);
$this->touchRateLimit($user->id, true);
$success = 1;
@ -189,12 +210,17 @@ class AuthController extends Controller
return Template::render('global/information');
}
/**
* Get the registration page.
*
* @return string
*/
public function registerGet()
{
// Attempt to check if a user has already registered from the current IP
$getUserIP = DB::table('users')
->where('register_ip', Net::pton(Net::IP()))
->orWhere('last_ip', Net::pton(Net::IP()))
->where('register_ip', Net::pton(Net::ip()))
->orWhere('last_ip', Net::pton(Net::ip()))
->get();
if ($getUserIP) {
@ -207,6 +233,11 @@ class AuthController extends Controller
return Template::render('auth/register');
}
/**
* Do a registration attempt.
*
* @return string
*/
public function registerPost()
{
// Preliminarily set registration to failed
@ -366,6 +397,11 @@ class AuthController extends Controller
return Template::render('global/information');
}
/**
* Do a activation attempt.
*
* @return string
*/
public function activate()
{
// Preliminarily set activation to failed
@ -426,11 +462,21 @@ class AuthController extends Controller
return Template::render('global/information');
}
/**
* Get the reactivation request form.
*
* @return string
*/
public function reactivateGet()
{
return Template::render('auth/reactivate');
}
/**
* Do a reactivation preparation attempt.
*
* @return string
*/
public function reactivatePost()
{
// Preliminarily set registration to failed
@ -498,11 +544,21 @@ class AuthController extends Controller
return Template::render('global/information');
}
/**
* Get the password reset forum.
*
* @return string
*/
public function resetPasswordGet()
{
return Template::render('auth/resetpassword');
}
/**
* Do a password reset attempt.
*
* @return string
*/
public function resetPasswordPost()
{
// Preliminarily set action to failed

View file

@ -8,9 +8,9 @@
namespace Sakura\Controllers;
use Sakura\Config;
use Sakura\User;
use Sakura\File;
use Sakura\Perms\Site;
use Sakura\User;
/**
* File controller, handles user uploads like avatars.
@ -20,18 +20,28 @@ use Sakura\Perms\Site;
*/
class FileController extends Controller
{
private function serveImage($data, $mime, $name)
/**
* The base for serving a file.
*
* @return string
*/
private function serve($data, $mime, $name)
{
// Add original filename
header('Content-Disposition: inline; filename="' . $name . '"');
header("Content-Disposition: inline; filename={$name}");
// Set content type
header('Content-Type: ' . $mime);
header("Content-Type: {$mime}");
// Return image data
return $data;
}
/**
* Attempt to get an avatar.
*
* @return string
*/
public function avatar($id = 0)
{
global $templateName;
@ -72,26 +82,32 @@ class FileController extends Controller
$user = User::construct($id);
if ($user->permission(Site::DEACTIVATED)) {
return $this->serveImage($deactive['data'], $deactive['mime'], $deactive['name']);
return $this->serve($deactive['data'], $deactive['mime'], $deactive['name']);
}
if ($user->checkBan() || $user->permission(Site::RESTRICTED)) {
return $this->serveImage($banned['data'], $banned['mime'], $banned['name']);
if ($user->checkBan()
|| $user->permission(Site::RESTRICTED)) {
return $this->serve($banned['data'], $banned['mime'], $banned['name']);
}
if (!$user->avatar) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
return $this->serve($none['data'], $none['mime'], $none['name']);
}
$serve = new File($user->avatar);
if (!$serve->id) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
return $this->serve($none['data'], $none['mime'], $none['name']);
}
return $this->serveImage($serve->data, $serve->mime, $serve->name);
return $this->serve($serve->data, $serve->mime, $serve->name);
}
/**
* Attempt to get a background.
*
* @return string
*/
public function background($id = 0)
{
global $templateName;
@ -104,24 +120,32 @@ class FileController extends Controller
];
if (!$id) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
return $this->serve($none['data'], $none['mime'], $none['name']);
}
$user = User::construct($id);
if ($user->permission(Site::DEACTIVATED) || $user->checkBan() || $user->permission(Site::RESTRICTED) || !$user->background) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
if ($user->permission(Site::DEACTIVATED)
|| $user->checkBan()
|| $user->permission(Site::RESTRICTED)
|| !$user->background) {
return $this->serve($none['data'], $none['mime'], $none['name']);
}
$serve = new File($user->background);
if (!$serve->id) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
return $this->serve($none['data'], $none['mime'], $none['name']);
}
return $this->serveImage($serve->data, $serve->mime, $serve->name);
return $this->serve($serve->data, $serve->mime, $serve->name);
}
/**
* Attempt to get a profile header.
*
* @return string
*/
public function header($id = 0)
{
global $templateName;
@ -134,21 +158,24 @@ class FileController extends Controller
];
if (!$id) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
return $this->serve($none['data'], $none['mime'], $none['name']);
}
$user = User::construct($id);
if ($user->permission(Site::DEACTIVATED) || $user->checkBan() || $user->permission(Site::RESTRICTED) || !$user->header) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
if ($user->permission(Site::DEACTIVATED)
|| $user->checkBan()
|| $user->permission(Site::RESTRICTED)
|| !$user->header) {
return $this->serve($none['data'], $none['mime'], $none['name']);
}
$serve = new File($user->header);
if (!$serve->id) {
return $this->serveImage($none['data'], $none['mime'], $none['name']);
return $this->serve($none['data'], $none['mime'], $none['name']);
}
return $this->serveImage($serve->data, $serve->mime, $serve->name);
return $this->serve($serve->data, $serve->mime, $serve->name);
}
}

View file

@ -29,7 +29,7 @@ class ForumController extends Controller
/**
* Serves the forum index.
*
* @return mixed HTML for the forum index.
* @return string HTML for the forum index.
*/
public function index()
{
@ -119,6 +119,11 @@ class ForumController extends Controller
return Template::render('forum/index');
}
/**
* Get a forum page.
*
* @return string
*/
public function forum($id = 0)
{
global $currentUser;
@ -182,6 +187,11 @@ class ForumController extends Controller
return Template::render('forum/viewforum');
}
/**
* Mark a forum as read.
*
* @return string
*/
public function markForumRead($id = 0)
{
global $currentUser;
@ -246,6 +256,11 @@ class ForumController extends Controller
return Template::render('global/information');
}
/**
* View a thread.
*
* @return string
*/
public function thread($id = 0)
{
global $currentUser;
@ -283,6 +298,11 @@ class ForumController extends Controller
return Template::render('forum/viewtopic');
}
/**
* Moderate a thread.
*
* @return string
*/
public function threadModerate($id = 0)
{
global $currentUser;
@ -409,14 +429,17 @@ class ForumController extends Controller
}
// Set the variables
Template::vars([
'page' => compact('message', 'redirect'),
]);
Template::vars(compact('message', 'redirect'));
// Print page contents
return Template::render('global/information');
}
/**
* Redirect to the position of a post in a thread.
*
* @return mixed
*/
public function post($id = 0)
{
global $currentUser;
@ -462,6 +485,11 @@ class ForumController extends Controller
return header("Location: {$threadLink}#p{$post->id}");
}
/**
* Get the raw text of a post.
*
* @return string
*/
public function postRaw($id = 0)
{
global $currentUser;
@ -485,6 +513,11 @@ class ForumController extends Controller
return $post->text;
}
/**
* Reply to a thread.
*
* @return string
*/
public function threadReply($id = 0)
{
global $currentUser;
@ -571,6 +604,11 @@ class ForumController extends Controller
return header("Location: {$postLink}");
}
/**
* Create a thread.
*
* @return string
*/
public function createThread($id = 0)
{
global $currentUser;
@ -664,6 +702,11 @@ class ForumController extends Controller
return Template::render('forum/viewtopic');
}
/**
* Edit a post.
*
* @return string
*/
public function editPost($id = 0)
{
global $currentUser;
@ -781,6 +824,11 @@ class ForumController extends Controller
return header("Location: {$postLink}");
}
/**
* Delete a post.
*
* @return string
*/
public function deletePost($id = 0)
{
global $currentUser;

View file

@ -17,9 +17,14 @@ use Sakura\BBcode;
*/
class HelperController extends Controller
{
/**
* Parsed BBcode from a post request
*
* @return string The parsed BBcode
*/
public function bbcodeParse()
{
$text = isset($_POST['text']) ? $_POST['text'] : null;
$text = isset($_POST['text']) ? $_POST['text'] : '';
$text = BBcode::toHTML($text);

View file

@ -12,7 +12,6 @@ use Sakura\DB;
use Sakura\News;
use Sakura\Template;
use Sakura\User;
use Sakura\Users;
/**
* Meta page controllers (sections that aren't big enough to warrant a dedicated controller).
@ -29,6 +28,38 @@ class MetaController extends Controller
*/
public function index()
{
// Get the newest user
$newestUserId = DB::table('users')
->where('rank_main', '!=', Config::get('restricted_rank_id'))
->where('rank_main', '!=', Config::get('deactive_rank_id'))
->orderBy('user_id', 'desc')
->limit(1)
->get(['user_id']);
$newestUser = User::construct($newestUserId ? $newestUserId[0]->user_id : 0);
// Get all the currently online users
$timeRange = time() - Config::get('max_online_time');
// Create a storage variable
$onlineUsers = [];
// Get all online users
$getOnline = DB::table('users')
->where('user_last_online', '>', $timeRange)
->get(['user_id']);
$getOnline = array_column($getOnline, 'user_id');
foreach ($getOnline as $user) {
$user = User::construct($user);
// Do a second check
if (!$user->isOnline()) {
continue;
}
$onlineUsers[$user->id] = $user;
}
// Merge index specific stuff with the global render data
Template::vars([
'news' => new News(Config::get('site_news_category')),
@ -38,14 +69,14 @@ class MetaController extends Controller
->where('password_algo', '!=', 'disabled')
->whereNotIn('rank_main', [1, 10])
->count(),
'newestUser' => User::construct(Users::getNewestUserId()),
'newestUser' => $newestUser,
'lastRegDate' => date_diff(
date_create(date('Y-m-d', User::construct(Users::getNewestUserId())->registered)),
date_create(date('Y-m-d', $newestUser->registered)),
date_create(date('Y-m-d'))
)->format('%a'),
'topicCount' => DB::table('topics')->count(),
'postCount' => DB::table('posts')->count(),
'onlineUsers' => Users::checkAllOnline(),
'onlineUsers' => $onlineUsers,
],
]);
@ -153,14 +184,6 @@ class MetaController extends Controller
*/
public function search()
{
// Set parse variables
Template::vars([
'page' => [
'title' => 'Search',
],
]);
// Print page contents
return Template::render('main/search');
}
}

View file

@ -9,12 +9,10 @@ namespace Sakura\Controllers;
use Exception;
use Sakura\Config;
use Sakura\Template;
use Sakura\User;
use Sakura\Users;
use Sakura\Utils;
use Sakura\Payments;
use Sakura\Perms\Site;
use Sakura\Router;
use Sakura\Template;
/**
* Premium pages controller.
@ -24,144 +22,164 @@ use Sakura\Perms\Site;
*/
class PremiumController extends Controller
{
/**
* The amount of premium a user received per period.
*/
const PERIOD_PER_PAYMENT = 2628000;
/**
* Constructor.
*/
public function __construct()
{
Payments::init();
}
/**
* Returns the premium purchase index.
*
* @return mixed
*/
public function index()
{
global $currentUser, $urls;
global $currentUser;
// Switch between modes (we only allow this to be used by logged in user)
if (isset($_REQUEST['mode'])
&& Users::checkLogin()
&& $currentUser->permission(Site::OBTAIN_PREMIUM)) {
// Initialise Payments class
if (!Payments::init()) {
header('Location: ' . $urls->format('SITE_PREMIUM') . '?fail=true');
} else {
switch ($_REQUEST['mode']) {
// Create the purchase
case 'purchase':
// Compare time and session so we know the link isn't forged
if (!isset($_REQUEST['time'])
|| $_REQUEST['time'] < time() - 1000) {
return header('Location: ' . $urls->format('SITE_PREMIUM') . '?fail=true');
}
$price = Config::get('premium_price_per_month');
$amountLimit = Config::get('premium_amount_max');
// Match session ids for the same reason
if (!isset($_REQUEST['session'])
|| $_REQUEST['session'] != session_id()) {
return header('Location: ' . $urls->format('SITE_PREMIUM') . '?fail=true');
}
Template::vars(compact('price', 'amountLimit'));
// Half if shit isn't gucci
if (!isset($_POST['months'])
|| !is_numeric($_POST['months'])
|| (int) $_POST['months'] < 1
|| (int) $_POST['months'] > Config::get('premium_amount_max')) {
return header('Location: ' . $urls->format('SITE_PREMIUM') . '?fail=true');
} else {
// Calculate the total
$total = (float) Config::get('premium_price_per_month') * (int) $_POST['months'];
$total = number_format($total, 2, '.', '');
// Generate item name
$itemName = Config::get('sitename')
. ' Premium - '
. (string) $_POST['months']
. ' month'
. ((int) $_POST['months'] == 1 ? '' : 's');
// Attempt to create a transaction
if ($transaction = Payments::createTransaction(
$total,
$itemName,
Config::get('sitename') . ' Premium Purchase',
'http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . Config::get('url_main') . $urls->format('SITE_PREMIUM')
)) {
// Store the amount of months in the global session array
$_SESSION['premiumMonths'] = (int) $_POST['months'];
return header('Location: ' . $transaction);
} else {
return header('Location: ' . $urls->format('SITE_PREMIUM') . '?fail=true');
}
}
// Finalising the purchase
case 'finish':
// Check if the success GET request is set and is true
if (isset($_GET['success'])
&& isset($_GET['paymentId'])
&& isset($_GET['PayerID'])
&& isset($_SESSION['premiumMonths'])) {
// Attempt to complete the transaction
try {
$finalise = Payments::completeTransaction($_GET['paymentId'], $_GET['PayerID']);
} catch (Exception $e) {
return trigger_error('Something went horribly wrong.', E_USER_ERROR);
}
// Attempt to complete the transaction
if ($finalise) {
// Make the user premium
Users::addUserPremium($currentUser->id, (2628000 * $_SESSION['premiumMonths']));
Users::updatePremiumMeta($currentUser->id);
Utils::updatePremiumTracker(
$currentUser->id,
((float) Config::get('premium_price_per_month') * $_SESSION['premiumMonths']),
$currentUser->username
. ' bought premium for '
. $_SESSION['premiumMonths']
. ' month'
. ($_SESSION['premiumMonths'] == 1 ? '' : 's')
. '.'
);
// Redirect to the complete
return header('Location: ' . $urls->format('SITE_PREMIUM') . '?mode=complete');
}
}
return header('Location: ' . $urls->format('SITE_PREMIUM') . '?fail=true');
case 'complete':
// Set parse variables
Template::vars([
'page' => [
'expiration' => ($prem = $currentUser->isPremium()[2]) !== null ? $prem : 0,
],
]);
// Print page contents
return Template::render('main/premiumcomplete');
default:
return header('Location: ' . $urls->format('SITE_PREMIUM'));
}
}
}
// Set parse variables
Template::vars([
'page' => [
'fail' => isset($_GET['fail']),
'price' => Config::get('premium_price_per_month'),
'current' => $currentUser->isPremium(),
'amount_max' => Config::get('premium_amount_max'),
],
]);
// Print page contents
return Template::render('main/support');
}
public function tracker()
/**
* Handles a purchase request.
*
* @return mixed
*/
public function purchase()
{
// Set parse variables
Template::vars([
'tracker' => Utils::getPremiumTrackerData(),
]);
global $currentUser;
// Print page contents
return Template::render('main/supporttracker');
// Get values from post
$session = isset($_POST['session']) ? $_POST['session'] : '';
$months = isset($_POST['months']) ? $_POST['months'] : 0;
// Check if the session is valid
if ($session !== session_id()
|| $currentUser->permission(Site::DEACTIVATED)
|| !$currentUser->permission(Site::OBTAIN_PREMIUM)) {
$message = "You are not allowed to get premium!";
$redirect = Router::route('premium.index');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
// Fetch the limit
$amountLimit = Config::get('premium_amount_max');
// Check months
if ($months < 1
|| $months > $amountLimit) {
$message = "An incorrect amount of months was specified, stop messing with the source.";
$redirect = Router::route('premium.index');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
$pricePerMonth = Config::get('premium_price_per_month');
$total = number_format($pricePerMonth * $months, 2, '.', '');
$siteName = Config::get('sitename');
$multiMonths = $months !== 1 ? 's' : '';
$siteUrl = 'http'
. (isset($_SERVER['HTTPS']) ? 's' : '')
. "://{$_SERVER['SERVER_NAME']}"
. ($_SERVER['SERVER_PORT'] != 80 ? ":{$_SERVER['SERVER_PORT']}" : '');
$handlerRoute = Router::route('premium.handle');
$itemName = "{$siteName} Premium - {$months} month{$multiMonths}";
$transactionName = "{$siteName} premium purchase";
$handlerUrl = "{$siteUrl}{$handlerRoute}";
// Create the transaction
$transaction = Payments::createTransaction(
$total,
$itemName,
$transactionName,
$handlerUrl
);
// Attempt to create a transaction
if (!$transaction) {
$message = "Something went wrong while preparing the transaction.";
$redirect = Router::route('premium.index');
Template::vars(compact('message', 'redirect'));
return Template::render('global/information');
}
// Store the amount of months in the global session array
$_SESSION['premiumMonths'] = (int) $months;
return header("Location: {$transaction}");
}
/**
* Handles the data returned by PayPal.
*
* @return mixed
*/
public function handle()
{
global $currentUser;
$success = isset($_GET['success']);
$payment = isset($_GET['paymentId']) ? $_GET['paymentId'] : null;
$payer = isset($_GET['PayerID']) ? $_GET['PayerID'] : null;
$months = isset($_SESSION['premiumMonths']) ? $_SESSION['premiumMonths'] : null;
$successRoute = Router::route('premium.complete');
$failRoute = Router::route('premium.index') . "?fail=true";
if (!$success
|| !$payment
|| !$payer
|| !$months) {
return header("Location: {$failRoute}");
}
// Attempt to complete the transaction
try {
$finalise = Payments::completeTransaction($_GET['paymentId'], $_GET['PayerID']);
} catch (Exception $e) {
$finalise = false;
}
if (!$finalise) {
return header("Location: {$failRoute}");
}
$pricePerMonth = Config::get('premium_price_per_month');
$currentUser->addPremium(self::PERIOD_PER_PAYMENT * $months);
return header("Location: {$successRoute}");
}
/**
* Presents the user with a thank you <3.
*
* @return mixed
*/
public function complete()
{
return Template::render('main/premiumcomplete');
}
}

View file

@ -49,46 +49,18 @@ class UserController extends Controller
// Redirect if so
if ($check) {
Template::vars([
'page' => [
'message' => 'The user this profile belongs to changed their username, you are being redirected.',
'redirect' => Router::route('user.profile', $check[0]->user_id),
],
]);
$message = "This user changed their username! Redirecting you to their new profile.";
$redirect = Router::route('user.profile', $check[0]->user_id);
Template::vars(compact('message', 'redirect'));
// Print page contents
return Template::render('global/information');
}
}
// Check if we're trying to restrict
if (isset($_GET['restrict']) && $_GET['restrict'] == session_id() && $currentUser->permission(\Sakura\Perms\Manage::CAN_RESTRICT_USERS, \Sakura\Perms::MANAGE)) {
// Check restricted status
$restricted = $profile->permission(\Sakura\Perms\Site::RESTRICTED);
if ($restricted) {
$profile->removeRanks([Config::get('restricted_rank_id')]);
$profile->addRanks([2]);
} else {
$profile->addRanks([Config::get('restricted_rank_id')]);
$profile->removeRanks(array_keys($profile->ranks));
}
Template::vars([
'page' => [
'message' => 'Toggled the restricted status of the user.',
'redirect' => Router::route('user.profile', $profile->id),
],
]);
// Print page contents
return Template::render('global/information');
}
// Set parse variables
Template::vars([
'profile' => $profile,
]);
Template::vars(compact('profile'));
// Print page contents
return Template::render('main/profile');
@ -128,17 +100,21 @@ class UserController extends Controller
// Get the active rank
$rank = array_key_exists($rank, $ranks) ? $rank : ($rank ? 0 : 2);
// Get members per page
$membersPerPage = Config::get('members_per_page');
// Set parse variables
Template::vars([
'ranks' => $ranks,
'rank' => $rank,
'membersPerPage' => Config::get('members_per_page'),
]);
Template::vars(compact('ranks', 'rank', 'membersPerPage'));
// Render the template
return Template::render('main/memberlist');
}
/**
* Get the notification JSON object for the currently authenticated user.
*
* @return string The JSON object.
*/
public function notifications()
{
// TODO: add friend on/offline messages
@ -153,6 +129,13 @@ class UserController extends Controller
);
}
/**
* Mark a notification as read.
*
* @param int The ID of the notification.
*
* @return string Not entirely set on this one yet but 1 for success and 0 for fail.
*/
public function markNotification($id = 0)
{
global $currentUser;

View file

@ -168,7 +168,7 @@ class Post
'topic_id' => $thread->id,
'forum_id' => $thread->forum,
'poster_id' => $poster->id,
'poster_ip' => Net::IP(),
'poster_ip' => Net::ip(),
'post_time' => time(),
'post_subject' => $subject,
'post_text' => $text,
@ -206,7 +206,7 @@ class Post
'topic_id' => $thread->id,
'forum_id' => $thread->forum,
'poster_id' => $this->poster->id,
'poster_ip' => Net::pton(Net::IP()),
'poster_ip' => Net::pton(Net::ip()),
'post_time' => $this->time,
'post_subject' => $this->subject,
'post_text' => $this->text,

View file

@ -20,7 +20,7 @@ class Net
*
* @return string The IP.
*/
public static function IP()
public static function ip()
{
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '::1';
}

View file

@ -98,7 +98,7 @@ class Session
DB::table('sessions')
->insert([
'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.'),
'session_key' => $session,
'session_start' => time(),
@ -141,7 +141,7 @@ class Session
if ($ipCheck) {
// Split both IPs up
$sessionIP = explode('.', $session[0]->user_ip);
$userIP = explode('.', Net::IP());
$userIP = explode('.', Net::ip());
// Take 1 off the ipCheck variable so it's equal to the array keys
$ipCheck = $ipCheck - 1;

View file

@ -9,6 +9,7 @@ namespace Sakura;
use Sakura\Perms;
use Sakura\Perms\Site;
use stdClass;
/**
* Everything you'd ever need from a specific user.
@ -268,8 +269,8 @@ class User
'password_iter' => $password[1],
'email' => $emailClean,
'rank_main' => 0,
'register_ip' => Net::pton(Net::IP()),
'last_ip' => Net::pton(Net::IP()),
'register_ip' => Net::pton(Net::ip()),
'last_ip' => Net::pton(Net::ip()),
'user_registered' => time(),
'user_last_online' => 0,
'user_country' => Utils::getCountryCode(),
@ -913,37 +914,99 @@ class User
}
/**
* Does this user have premium?
* Add premium in seconds.
*
* @return array Premium status information.
* @param int $seconds The amount of seconds.
*
* @return int The new expiry date.
*/
public function isPremium()
public function addPremium($seconds)
{
// Check if the user has static premium
if ($this->permission(Site::STATIC_PREMIUM)) {
return [2, 0, time() + 1];
}
// Attempt to retrieve the premium record from the database
$getRecord = DB::table('premium')
// Check if there's already a record of premium for this user in the database
$getUser = DB::table('premium')
->where('user_id', $this->id)
->get();
// If nothing was returned just return false
if (empty($getRecord)) {
return [0];
// Calculate the (new) start and expiration timestamp
$start = $getUser ? $getUser[0]->premium_start : time();
$expire = $getUser ? $getUser[0]->premium_expire + $seconds : time() + $seconds;
// If the user already exists do an update call, otherwise an insert call
if ($getUser) {
DB::table('premium')
->where('user_id', $this->id)
->update([
'premium_expire' => $expire,
]);
} else {
DB::table('premium')
->insert([
'user_id' => $this->id,
'premium_start' => $start,
'premium_expire' => $expire,
]);
}
$getRecord = $getRecord[0];
// Return the expiration timestamp
return $expire;
}
// Check if the Tenshi hasn't expired
if ($getRecord->premium_expire < time()) {
return [0, $getRecord->premium_start, $getRecord->premium_expire];
/**
* Does this user have premium?
*
* @return int Returns the premium expiration date.
*/
public function isPremium()
{
// Get rank IDs from the db
$premiumRank = (int) Config::get('premium_rank_id');
$defaultRank = (int) Config::get('default_rank_id');
// Fetch expiration date
$expire = $this->premiumInfo()->expire;
// Check if the user has static premium
if (!$expire
&& $this->permission(Site::STATIC_PREMIUM)) {
$expire = time() + 1;
}
// Else return the start and expiration date
return [1, $getRecord->premium_start, $getRecord->premium_expire];
// Check if the user has premium and isn't in the premium rank
if ($expire
&& !$this->hasRanks([$premiumRank])) {
// Add the premium rank
$this->addRanks([$premiumRank]);
// Set it as default
if ($this->mainRankId == $defaultRank) {
$this->setMainRank($premiumRank);
}
} elseif (!$expire
&& $this->hasRanks([$premiumRank])) {
$this->removeRanks([$premiumRank]);
if ($this->mainRankId == $premiumRank) {
$this->setMainRank($defaultRank);
}
}
return $expire;
}
public function premiumInfo()
{
// Attempt to retrieve the premium record from the database
$check = DB::table('premium')
->where('user_id', $this->id)
->where('premium_expire', '>', time())
->get();
$return = new stdClass;
$return->start = $check ? $check[0]->premium_start : 0;
$return->expire = $check ? $check[0]->premium_expire : 0;
return $return;
}
/**

View file

@ -47,22 +47,6 @@ class Users
// Check if the session exists and check if the user is activated
if ($sessionValid == 0 || $user->permission(Site::DEACTIVATED)) {
// Unset User ID
setcookie(
Config::get('cookie_prefix') . 'id',
0,
time() - 60,
Config::get('cookie_path')
);
// Unset Session ID
setcookie(
Config::get('cookie_prefix') . 'session',
'',
time() - 60,
Config::get('cookie_path')
);
return false;
}
@ -92,9 +76,6 @@ class Users
'user_last_online' => time(),
]);
// Update the premium meta
self::updatePremiumMeta($uid);
// If everything went through return true
return [$uid, $sid];
}
@ -250,120 +231,4 @@ class Users
// Return the yeahs
return $fields;
}
/**
* Get all online users.
*
* @return array Array containing User instances.
*/
public static function checkAllOnline()
{
// Assign time - 500 to a variable
$time = time() - Config::get('max_online_time');
$return = [];
// Get all online users in the past 5 minutes
$getAll = DB::table('users')
->where('user_last_online', '>', $time)
->get();
foreach ($getAll as $user) {
$return[] = User::construct($user->user_id);
}
// Return all the online users
return $return;
}
/**
* Add premium time to a user.
*
* @param int $id The user ID.
* @param int $seconds The amount of extra seconds.
*
* @return array|double|int The new expiry date.
*/
public static function addUserPremium($id, $seconds)
{
// Check if there's already a record of premium for this user in the database
$getUser = DB::table('premium')
->where('user_id', $id)
->count();
// Calculate the (new) start and expiration timestamp
$start = isset($getUser['premium_start']) ? $getUser['premium_start'] : time();
$expire = isset($getUser['premium_expire']) ? $getUser['premium_expire'] + $seconds : time() + $seconds;
// If the user already exists do an update call, otherwise an insert call
if (empty($getUser)) {
DB::table('premium')
->insert([
'user_id' => $id,
'premium_start' => $start,
'premium_expire' => $expire,
]);
} else {
DB::table('premium')
->where('user_id', $id)
->update('premium_expire', $expire);
}
// Return the expiration timestamp
return $expire;
}
/**
* Process premium meta data.
*
* @param int $id The user ID.
*/
public static function updatePremiumMeta($id)
{
// Get the ID for the premium user rank from the database
$premiumRank = Config::get('premium_rank_id');
$excepted = Config::get('restricted_rank_id');
// Create user object
$user = User::construct($id);
// Run the check
$check = $user->isPremium();
// Check if the user has premium
if ($check[0] && !array_key_exists($excepted, $user->ranks)) {
// If so add the rank to them
$user->addRanks([$premiumRank]);
// Check if the user's default rank is standard user and update it to premium
if ($user->mainRankId == 2) {
$user->setMainRank($premiumRank);
}
} elseif (!$check[0]) {
// Remove the expired entry
DB::table('premium')
->where('user_id', $user->id)
->delete();
// Else remove the rank from them
$user->removeRanks([$premiumRank]);
}
}
/**
* Get the newest member's ID.
*
* @return int The user ID.
*/
public static function getNewestUserId()
{
$get = DB::table('users')
->where('rank_main', '!=', Config::get('restricted_rank_id'))
->where('rank_main', '!=', Config::get('deactive_rank_id'))
->orderBy('user_id', 'desc')
->limit(1)
->get(['user_id']);
return $get ? $get[0]->user_id : 0;
}
}

View file

@ -263,7 +263,7 @@ class Utils
// 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());
$code = geoip_country_code_by_name(Net::ip());
// Check if $code is anything
if ($code) {
@ -346,94 +346,4 @@ class Utils
// Return the formatted string
return $bytes;
}
/**
* Get the premium tracker data.
*
* @return array The premium tracker data.
*/
public static function getPremiumTrackerData()
{
// Create data array
$data = [];
// Get database stuff
$table = DB::table('premium_log')
->orderBy('transaction_id', 'desc')
->get();
// Add raw table data to data array
$data['table'] = $table;
// Create balance entry
$data['balance'] = 0.0;
// users
$data['users'] = [];
// Calculate the thing
foreach ($table as $row) {
// Calculate balance
$data['balance'] = $data['balance'] + $row->transaction_amount;
// Add userdata to table
if (!array_key_exists($row->user_id, $data['users'])) {
$data['users'][$row->user_id] = User::construct($row->user_id);
}
}
// Return the data
return $data;
}
/**
* Add a new entry to the tracker.
*
* @param int $id The user ID.
* @param float $amount The amount of money.
* @param string $comment A little information.
*/
public static function updatePremiumTracker($id, $amount, $comment)
{
DB::table('premium_log')
->insert([
'user_id' => $id,
'transaction_amount' => $amount,
'transaction_date' => time(),
'transaction_comment' => $comment,
]);
}
/**
* Clean up the contents of <code> tags.
*
* @param string $text Dirty
*
* @return string Clean
*/
public static function fixCodeTags($text)
{
$parts = explode('<code>', $text);
$newStr = '';
if (count($parts) > 1) {
foreach ($parts as $p) {
$parts2 = explode('</code>', $p);
if (count($parts2) > 1) {
$code = str_replace('<br />', '', $parts2[0]);
$code = str_replace('<br/>', '', $code);
$code = str_replace('<br>', '', $code);
$code = str_replace('<', '&lt;', $code);
$newStr .= '<code>' . $code . '</code>';
$newStr .= $parts2[1];
} else {
$newStr .= $p;
}
}
} else {
$newStr = $text;
}
return $newStr;
}
}

View file

@ -106,9 +106,11 @@ Router::get('/a/{id}', 'FileController@avatar', 'file.avatar');
Router::get('/bg/{id}', 'FileController@background', 'file.background');
// Premium
Router::group(['prefix' => 'support'], function () {
Router::group(['prefix' => 'support', 'before' => 'loginCheck'], function () {
Router::get('/', 'PremiumController@index', 'premium.index');
Router::get('/tracker', 'PremiumController@tracker', 'premium.tracker');
Router::get('/handle', 'PremiumController@handle', 'premium.handle');
Router::get('/complete', 'PremiumController@complete', 'premium.complete');
Router::post('/purchase', 'PremiumController@purchase', 'premium.purchase');
});
// Helpers

View file

@ -8,7 +8,7 @@
namespace Sakura;
// Define Sakura version
define('SAKURA_VERSION', 20160326);
define('SAKURA_VERSION', 20160327);
// Define Sakura Path
define('ROOT', __DIR__ . '/');

View file

@ -51,7 +51,7 @@
<div class="default-avatar-setting user-container-avatar" style="background-image: url({{ route('file.avatar', activePoster.id) }}); box-shadow: 0 0 5px #{% if activePoster.isOnline %}484{% else %}844{% endif %};"></div>
<div class="user-container-info">
<h1 style="color: {{ activePoster.colour }}; text-shadow: 0 0 7px {% if activePoster.colour != 'inherit' %}{{ activePoster.colour }}{% else %}#222{% endif %}; padding: 0 0 2px;" {% if activePoster.getUsernameHistory %} title="Known as {{ activePoster.getUsernameHistory[0].username_old }} before {{ activePoster.getUsernameHistory[0].change_time|date(sakura.dateFormat) }}." {% endif %}>{{ activePoster.username }}</h1>
{% if activePoster.isPremium[0] %}<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi" style="vertical-align: middle;" /> {% endif %}<img src="{{ sakura.contentPath }}/images/flags/{{ activePoster.country|lower }}.png" alt="{{ activePoster.country }}" style="vertical-align: middle;" title="{{ activePoster.country(true) }}" /> <span style="font-size: .8em;">{{ activePoster.title }}</span>
{% if activePoster.isPremium %}<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi" style="vertical-align: middle;" /> {% endif %}<img src="{{ sakura.contentPath }}/images/flags/{{ activePoster.country|lower }}.png" alt="{{ activePoster.country }}" style="vertical-align: middle;" title="{{ activePoster.country(true) }}" /> <span style="font-size: .8em;">{{ activePoster.title }}</span>
</div>
</div>
</a>

View file

@ -86,7 +86,7 @@
{% endif %}
<div class="userdata">
<div class="usertitle">{{ post.poster.title }}</div>
<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi"{% if not post.poster.isPremium[0] %} style="opacity: 0;"{% endif %} /> <img src="{{ sakura.contentPath }}/images/flags/{{ post.poster.country|lower }}.png" alt="{{ post.poster.country(true) }}" />{% if post.poster.id == (thread.posts|first).poster.id %} <img src="{{ sakura.contentPath }}/images/op.png" alt="OP" title="Original Poster" />{% endif %}
<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi"{% if not post.poster.isPremium %} style="opacity: 0;"{% endif %} /> <img src="{{ sakura.contentPath }}/images/flags/{{ post.poster.country|lower }}.png" alt="{{ post.poster.country(true) }}" />{% if post.poster.id == (thread.posts|first).poster.id %} <img src="{{ sakura.contentPath }}/images/op.png" alt="OP" title="Original Poster" />{% endif %}
{% if session.checkLogin %}
<div class="actions">
{% if (user.id == post.poster.id and forum.permission(constant('Sakura\\Perms\\Forum::EDIT_OWN'), user.id)) or forum.permission(constant('Sakura\\Perms\\Forum::EDIT_ANY'), user.id) %}
@ -141,7 +141,7 @@
<img src="{{ route('file.avatar', user.id) }}" alt="{{ user.username }}" class="avatar" style="box-shadow: 0 3px 7px #484;" />
<div class="userdata">
<div class="usertitle">{{ user.title }}</div>
<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi"{% if not user.isPremium[0] %} style="opacity: 0;"{% endif %} /> <img src="{{ sakura.contentPath }}/images/flags/{{ user.country|lower }}.png" alt="{{ user.country(true) }}" />{% if user.id == (thread.posts|first).poster.id %} <img src="{{ sakura.contentPath }}/images/op.png" alt="OP" title="Original Poster" />{% endif %}
<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi"{% if not user.isPremium %} style="opacity: 0;"{% endif %} /> <img src="{{ sakura.contentPath }}/images/flags/{{ user.country|lower }}.png" alt="{{ user.country(true) }}" />{% if user.id == (thread.posts|first).poster.id %} <img src="{{ sakura.contentPath }}/images/op.png" alt="OP" title="Original Poster" />{% endif %}
</div>
</td>
<td class="post-content">

View file

@ -7,8 +7,8 @@
<div>
<h1>Information</h1>
<hr class="default" />
{{ page.message }}
{% if page.redirect %}<br /><a href="{{ page.redirect }}" class="default">Click here if you aren't being redirected.</a>{% endif %}
{{ message }}
{% if redirect %}<br /><a href="{{ redirect }}" class="default">Click here if you aren't being redirected.</a>{% endif %}
</div>
</div>
{% endblock %}

View file

@ -10,8 +10,14 @@
<meta name="msapplication-TileColor" content="#9475b2" />
<meta name="msapplication-TileImage" content="/content/images/icons/ms-icon-144x144.png" />
<meta name="theme-color" content="#9475B2" />
{% if page.redirect %}
<meta http-equiv="refresh" content="{{ page.redirectTimeout ? page.redirectTimeout : '3' }}; URL={{ page.redirect }}" />
{# want to start moving away from page.etc but older files are a thing #}
{% if message is not defined %}{% set message = page.message %}{% endif %}
{% if redirect is not defined %}{% set redirect = page.redirect %}{% endif %}
{% if redirectTimeout is not defined %}{% set redirectTimeout = page.redirectTimeout %}{% endif %}
{% if redirect %}
<meta http-equiv="refresh" content="{{ redirectTimeout ? redirectTimeout : '3' }}; URL={{ redirect }}" />
{% endif %}
<link rel="apple-touch-icon" sizes="57x57" href="/content/images/icons/apple-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/content/images/icons/apple-icon-60x60.png" />
@ -63,7 +69,7 @@
notifyUI({
"title": "An error has occurred!",
"text": "There was a problem while executing the JavaScript code for this page: " + msg + ", URL: " + url + ", Line: " + line + ", Column: " + col + ". Please report this to a developer.",
"img": "FONT:fa-warning"
"image": "FONT:fa-warning"
});
}
</script>

View file

@ -6,6 +6,6 @@
<div class="content standalone" style="text-align: center;">
<h1 class="stylised" style="margin: 1em auto;">Thank you for your contribution!</h1>
<h1 class="fa fa-heart stylised" style="font-size: 20em;"></h1>
<h3>Your Tenshi will expire on {{ page.expiration|date(sakura.dateFormat) }}.</h3>
<h3>Your Tenshi will expire on {{ user.isPremium|date(sakura.dateFormat) }}.</h3>
</div>
{% endblock %}

View file

@ -81,7 +81,7 @@
<div class="default-avatar-setting new-profile-avatar" style="background-image: url({{ route('file.avatar', profile.id) }}); box-shadow: 0 0 5px #{% if profile.isOnline %}484{% else %}844{% endif %};"></div>
<div class="new-profile-username">
<h1 style="color: {{ profile.colour }}; text-shadow: 0 0 7px {% if profile.colour != 'inherit' %}{{ profile.colour }}{% else %}#222{% endif %}; padding: 0 0 2px;" {% if profile.getUsernameHistory %} title="Known as {{ profile.getUsernameHistory[0].username_old }} before {{ profile.getUsernameHistory[0].change_time|date(sakura.dateFormat) }}." {% endif %}>{{ profile.username }}</h1>
{% if profile.isPremium[0] %}<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi" style="vertical-align: middle;" /> {% endif %}<img src="{{ sakura.contentPath }}/images/flags/{{ profile.country|lower }}.png" alt="{{ profile.country }}" style="vertical-align: middle;" title="{{ profile.country(true) }}" /> <span style="font-size: .8em;">{{ profile.title }}</span>
{% if profile.isPremium %}<img src="{{ sakura.contentPath }}/images/tenshi.png" alt="Tenshi" style="vertical-align: middle;" /> {% endif %}<img src="{{ sakura.contentPath }}/images/flags/{{ profile.country|lower }}.png" alt="{{ profile.country }}" style="vertical-align: middle;" title="{{ profile.country(true) }}" /> <span style="font-size: .8em;">{{ profile.title }}</span>
</div>
<div class="new-profile-dates">
<b>Joined</b> <time>{{ profile.registered|date(sakura.dateFormat) }}</time>

View file

@ -2,8 +2,10 @@
{% block title %}Support {{ sakura.siteName }}{% endblock %}
{% set persistentPremium = user.permission(constant('Sakura\\Perms\\Site::STATIC_PREMIUM')) %}
{% block content %}
{% if page.fail %}
{% if get.fail %}
<div class="headerNotify">
<h1>The payment failed or was cancelled!</h1>
<p>Something went wrong while processing the transaction, your PayPal account wasn't charged.</p>
@ -13,15 +15,14 @@
<div class="head">Support {{ sakura.siteName }}</div>
<div style="font-size: .9em; margin-bottom: 10px;">
<p>In order to keep the site, its services and improvements on it going I need money but I'm not that big of a fan of asking for money without giving anything special in return thus Tenshi exists. Tenshi is the name for our supporter rank which gives you access to an extra set of features (which are listed further down on this page). With your help we can keep adding new stuff, get new hardware and keep the site awesome!</p>
<h3><a href="{{ route('premium.tracker') }}" class="default">Ever wonder what happens on the financial side of things? View the donation tracker!</a></h3>
</div>
{% if page.current[0] %}
{% if user.isPremium %}
<div class="sectionHeader">
Your current Tenshi tag
</div>
<div style="margin-bottom: 10px;">
<h3>{% if page.current[0] == 2 %}Your rank has persistent Tenshi.{% else %}Your Tenshi tag is valid till {{ page.current[2]|date(sakura.dateFormat) }}.{% endif %}</h3>
<progress value="{{ page.current[0] == 2 ? 100 : (100 - (((php.time - page.current[1]) / (page.current[2] - page.current[1])) * 100)) }}" max="100" style="width: 100%"></progress>
<h3>{% if persistentPremium %}Your rank has persistent Tenshi.{% else %}Your Tenshi tag is valid till {{ user.premiumInfo.expire|date(sakura.dateFormat) }}.{% endif %}</h3>
<progress value="{{ persistentPremium ? 100 : (100 - (((php.time - user.premiumInfo.start) / (user.premiumInfo.expire - user.premiumInfo.start)) * 100)) }}" max="100" style="width: 100%"></progress>
</div>
{% endif %}
<div class="sectionHeader">
@ -91,7 +92,7 @@
</div>
{% if session.checkLogin and user.permission(constant('Sakura\\Perms\\Site::OBTAIN_PREMIUM')) %}
<div class="slider">
<input class="inputStyling" type="range" min="1" max="{{ page.amount_max }}" value="1" onchange="document.getElementById('monthsNo').value = this.value; document.getElementById('monthNoBtn').innerHTML = this.value; document.getElementById('monthsTrailingS').innerHTML = (this.value == 1 ? '' : 's'); document.getElementById('totalAmount').innerHTML = (this.value * {{ page.price }}).formatMoney(2);" />
<input class="inputStyling" type="range" min="1" max="{{ amountLimit }}" value="1" onchange="document.getElementById('monthsNo').value = this.value; document.getElementById('monthNoBtn').innerHTML = this.value; document.getElementById('monthsTrailingS').innerHTML = (this.value == 1 ? '' : 's'); document.getElementById('totalAmount').innerHTML = (this.value * {{ price }}).formatMoney(2);" />
</div>
<div class="checkout" style="line-height: 28px;">
<div style="float: left;">
@ -109,14 +110,14 @@
{% endif %}
</div>
{% if session.checkLogin and user.permission(constant('Sakura\\Perms\\Site::OBTAIN_PREMIUM')) %}
<form action="{{ route('premium.index') }}" method="post" id="purchaseForm" class="hidden">
<form action="{{ route('premium.purchase') }}" method="post" id="purchaseForm" class="hidden">
<input type="hidden" name="mode" value="purchase" />
<input type="hidden" name="time" value="{{ php.time }}" />
<input type="hidden" name="session" value="{{ php.sessionid }}" />
<input type="hidden" name="months" id="monthsNo" value="1" />
</form>
<script type="text/javascript">
window.onload = function() { document.getElementById('totalAmount').innerHTML = ({{ page.price }}).formatMoney(2); };
window.onload = function() { document.getElementById('totalAmount').innerHTML = ({{ price }}).formatMoney(2); };
</script>
{% endif %}
{% endblock %}