public function fetch($template = null, $cache_id = null, $compile_id = null, $parent = null)
{
$result = $this->_execute($template, $cache_id, $compile_id, $parent, 0);
return $result === null ? ob_get_clean() : $result;
}
/**
* displays a Smarty template
*
* @param string $template the resource handle of the template file or template object
* @param mixed $cache_id cache id to be used with this template
* @param mixed $compile_id compile id to be used with this template
* @param object $parent next higher level of Smarty variables
*
* @throws \Exception
* @throws \SmartyException
*/
public function display($template = null, $cache_id = null, $compile_id = null, $parent = null)
{
// display template
$this->_execute($template, $cache_id, $compile_id, $parent, 1);
}
/**
* test if cache is valid
*
* @api Smarty::isCached()
* @link https://www.smarty.net/docs/en/api.is.cached.tpl
*
* @param null|string|\Smarty_Internal_Template $template the resource handle of the template file or template
* object
* @param mixed $cache_id cache id to be used with this template
* @param mixed $compile_id compile id to be used with this template
* @param object $parent next higher level of Smarty variables
*
* @return bool cache status
* @throws \Exception
* @throws \SmartyException
*/
public function isCached($template = null, $cache_id = null, $compile_id = null, $parent = null)
{
public function displayTemplate(string $template, Smarty $smarty): void {
[$css, $js] = $this->assets()->compile();
// Put the assets at the start of the arrays, so they load first (SBAdmin requires JQuery first, etc.)
array_unshift($this->_css, ...$css);
array_unshift($this->_js, ...$js);
$smarty->assign([
'TEMPLATE_CSS' => $this->getCSS(),
'TEMPLATE_JS' => $this->getJS()
]);
if (defined('PHPDEBUGBAR') && PHPDEBUGBAR) {
$debugBar = DebugBarHelper::getInstance()->getDebugBar()->getJavascriptRenderer();
$smarty->assign([
'DEBUGBAR_JS' => $debugBar->renderHead(),
'DEBUGBAR_HTML' => $debugBar->render()
]);
}
$smarty->display($template);
}
/**
* Get all internal CSS styles.
*
* @return array Array of strings of CSS.
*/
public function getCSS(): array {
return $this->_css;
}
/**
* Get all internal JS code.
*
* @return array Array of strings of JS.
*/
public function getJS(): array {
return $this->_js;
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
* Get the display order of this widget.
*
* @return int Display order of widget.
*/
public function getOrder(): ?int {
return $this->_order;
}
/**
* Generate this widget's `$_content`.
*/
abstract public function initialise(): void;
/**
* Get the data (location, order, pages) for a widget.
*
* @param string $name The widget to get data for.
* @return object|null Widgets data.
*/
protected static function getData(string $name): ?object {
return DB::getInstance()->query('SELECT `location`, `order`, `pages` FROM nl2_widgets WHERE `name` = ?', [$name])->first();
}
/**
* Parse the widgets JSON pages string into an array.
*
* @param object|null $data The widget data to get pages from.
* @return array The parsed pages array.
*/
protected static function parsePages(?object $data): array {
if (isset($data->pages)) {
return json_decode($data->pages, true) ?? [];
}
return [];
}
}
}
$count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_posts WHERE deleted = 0 AND post_creator = ?', [$user_id])->first()->c;
self::$_count_cache["posts_$user_id"] = $count;
return $count;
}
return 0;
}
/**
* Get a user's topic count
*
* @param int|null $user_id User ID to check
* @return int Number of topics
*/
public function getTopicCount(int $user_id = null): int {
if ($user_id) {
if (isset(self::$_count_cache["topics_$user_id"])) {
return self::$_count_cache["topics_$user_id"];
}
$count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_topics WHERE deleted = 0 AND topic_creator = ?', [$user_id])->first()->c;
self::$_count_cache["topics_$user_id"] = $count;
return $count;
}
return 0;
}
/**
* Get posts on a specific topic.
*
* @param int|null $tid The topic ID to check.
* @return array|false Array of topics or false on failure.
*/
public function getPosts(int $tid = null) {
if ($tid) {
// Get posts from database
$posts = $this->_db->get('posts', ['topic_id', $tid]);
if ($posts->count()) {
$posts = $posts->results();
self::$_permission_cache[$cache_key] = true;
return true;
}
}
}
return false;
}
/**
* Get a user's post count
*
* @param int|null $user_id User ID to check
* @return int Number of posts
*/
public function getPostCount(int $user_id = null): int {
if ($user_id) {
if (isset(self::$_count_cache["posts_$user_id"])) {
return self::$_count_cache["posts_$user_id"];
}
$count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_posts WHERE deleted = 0 AND post_creator = ?', [$user_id])->first()->c;
self::$_count_cache["posts_$user_id"] = $count;
return $count;
}
return 0;
}
/**
* Get a user's topic count
*
* @param int|null $user_id User ID to check
* @return int Number of topics
*/
public function getTopicCount(int $user_id = null): int {
if ($user_id) {
if (isset(self::$_count_cache["topics_$user_id"])) {
return self::$_count_cache["topics_$user_id"];
}
$count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_topics WHERE deleted = 0 AND topic_creator = ?', [$user_id])->first()->c;
self::$_count_cache["topics_$user_id"] = $count;
/**
* Get if the current user is authenticated as an administrator.
*
* @return bool Whether they're logged in as admin.
*/
public function isAdmLoggedIn(): bool {
return $this->_isAdmLoggedIn;
}
/**
* Get profile fields for this user
*
* @param bool $show_private Whether to only return public fields or not (default `true`).
* @param bool $only_forum Whether to only return fields which display on forum posts, only if $public is true (default `false`).
*
* @return array<int, UserProfileField> Array of profile fields.
*/
public function getProfileFields(bool $show_private = false, bool $only_forum = false): array {
$rows = DB::getInstance()->query('SELECT pf.*, upf.id as upf_id, upf.value, upf.updated FROM nl2_profile_fields pf LEFT JOIN nl2_users_profile_fields upf ON (pf.id = upf.field_id AND upf.user_id = ?)', [
$this->data()->id,
])->results();
$fields = [];
foreach ($rows as $row) {
$field = new UserProfileField($row);
// Check that the field is public, or they are viewing private fields
// also if they're checking forum fields, check that the field is a forum field
// TODO: ideally within the query
if (($field->public || $show_private) && (!$only_forum || $field->forum_posts)) {
$fields[$row->id] = $field;
}
}
return $fields;
}
/**
* Is a user blocked?
*
if (isset(self::$_count_cache["topics_$user_id"])) {
return self::$_count_cache["topics_$user_id"];
}
$count = $this->_db->query('SELECT COUNT(*) AS c FROM nl2_topics WHERE deleted = 0 AND topic_creator = ?', [$user_id])->first()->c;
self::$_count_cache["topics_$user_id"] = $count;
return $count;
}
return 0;
}
/**
* Get posts on a specific topic.
*
* @param int|null $tid The topic ID to check.
* @return array|false Array of topics or false on failure.
*/
public function getPosts(int $tid = null) {
if ($tid) {
// Get posts from database
$posts = $this->_db->get('posts', ['topic_id', $tid]);
if ($posts->count()) {
$posts = $posts->results();
// Remove deleted posts
foreach ($posts as $key => $post) {
if ($post->deleted == 1) {
unset($posts[$key]);
}
}
return array_values($posts);
}
}
return false;
}
/**
* Get any subforums at any level for a forum
*
/**
* Find a user by unique identifier (username, ID, email, etc).
* Loads instance variables for this class.
*
* @param string|null $value Unique identifier.
* @param string $field What column to check for their unique identifier in.
*
* @return bool True/false on success or failure respectfully.
*/
public function find(string $value = null, string $field = 'id'): bool {
if ($value) {
if (isset(self::$_user_cache["$value.$field"])) {
$cache = self::$_user_cache["$value.$field"];
$this->_data = $cache['data'];
$this->_groups = $cache['groups'];
return true;
}
if ($field != 'hash') {
$data = $this->_db->get('users', [$field, $value]);
} else {
$data = $this->_db->query('SELECT nl2_users.* FROM nl2_users LEFT JOIN nl2_users_session ON nl2_users.id = user_id WHERE hash = ? AND nl2_users_session.active = 1', [$value]);
}
if ($data->count()) {
$this->_data = new UserData($data->first());
// Get user groups
$groups_query = $this->_db->query('SELECT nl2_groups.* FROM nl2_users_groups INNER JOIN nl2_groups ON group_id = nl2_groups.id WHERE user_id = ? AND deleted = 0 ORDER BY `order`;', [$this->data()->id]);
if ($groups_query->count()) {
$groups_query = $groups_query->results();
foreach ($groups_query as $item) {
$this->_groups[$item->id] = new Group($item);
}
self::$_user_cache["$value.$field"] = [
'data' => $this->_data,
'groups' => $this->_groups,
public function titleToURL(string $topic = null): string {
if ($topic) {
$topic = str_replace(self::URL_EXCLUDE_CHARS, '', Util::cyrillicToLatin($topic));
return Output::getClean(strtolower(urlencode(str_replace(' ', '-', $topic))));
}
return '';
}
// Returns true/false depending on whether the current user can view a forum
// Params: $forum_id (integer) - forum id to check, $groups (array) - user groups
public function canViewOtherTopics(int $forum_id, array $groups = [0]): bool {
$cache_key = 'topics_view_' . $forum_id . '_' . implode('_', $groups);
if (isset(self::$_permission_cache[$cache_key])) {
return true;
}
// Does the forum exist?
$exists = $this->_db->get('forums', ['id', $forum_id])->results();
if (count($exists)) {
// Can the user view other topics?
$access = $this->_db->get('forums_permissions', ['forum_id', $forum_id])->results();
foreach ($access as $item) {
if (in_array($item->group_id, $groups)) {
if ($item->view_other_topics == 1) {
self::$_permission_cache[$cache_key] = true;
return true;
}
}
}
}
return false;
}
/**
* Get the newest 50 topics this user/group can view
*
* @param array $groups Array of groups the user is in
* @param int $user_id User ID
* @return array 50 latest topics
return false;
}
public function titleToURL(string $topic = null): string {
if ($topic) {
$topic = str_replace(self::URL_EXCLUDE_CHARS, '', Util::cyrillicToLatin($topic));
return Output::getClean(strtolower(urlencode(str_replace(' ', '-', $topic))));
}
return '';
}
// Returns true/false depending on whether the current user can view a forum
// Params: $forum_id (integer) - forum id to check, $groups (array) - user groups
public function canViewOtherTopics(int $forum_id, array $groups = [0]): bool {
$cache_key = 'topics_view_' . $forum_id . '_' . implode('_', $groups);
if (isset(self::$_permission_cache[$cache_key])) {
return true;
}
// Does the forum exist?
$exists = $this->_db->get('forums', ['id', $forum_id])->results();
if (count($exists)) {
// Can the user view other topics?
$access = $this->_db->get('forums_permissions', ['forum_id', $forum_id])->results();
foreach ($access as $item) {
if (in_array($item->group_id, $groups)) {
if ($item->view_other_topics == 1) {
self::$_permission_cache[$cache_key] = true;
return true;
}
}
}
}
return false;
}
/**
* Get the newest 50 topics this user/group can view
*
if (count($exists)) {
return $this->hasPermission($forum_id, 'view', $groups);
}
return false;
}
/**
* Determines if any groups have permission to do a certain action on a forum
*
* @param int $forum_id ID of the forum
* @param string $required_permission Required permission
* @param array $groups Array of groups the user is in
* @return bool Whether the groups have permission or not
*/
private function hasPermission(int $forum_id, string $required_permission, array $groups): bool {
$cache_key = 'forum_permissions_' . $forum_id . '_' . $required_permission . '_' . implode('_', $groups);
if (isset(self::$_permission_cache[$cache_key])) {
return true;
}
$permissions = $this->_db->get('forums_permissions', ['forum_id', $forum_id])->results();
foreach ($permissions as $permission) {
if (in_array($permission->group_id, $groups)) {
if ($permission->{$required_permission} == 1) {
self::$_permission_cache[$cache_key] = true;
return true;
}
}
}
return false;
}
public function titleToURL(string $topic = null): string {
if ($topic) {
$topic = str_replace(self::URL_EXCLUDE_CHARS, '', Util::cyrillicToLatin($topic));
return Output::getClean(strtolower(urlencode(str_replace(' ', '-', $topic))));
}
return '';
}
true
)->results();
}
return DB::getInstance()->query(
'SELECT topics.id as id, topics.forum_id as forum_id, topics.topic_title as topic_title, topics.topic_creator as topic_creator, topics.topic_last_user as topic_last_user, topics.topic_date as topic_date, topics.topic_reply_date as topic_reply_date, topics.topic_views as topic_views, topics.locked as locked, topics.sticky as sticky, topics.label as label, topics.deleted as deleted, posts.id as last_post_id
FROM nl2_topics topics
LEFT JOIN nl2_posts posts ON topics.id = posts.topic_id AND posts.id = (SELECT MAX(id) FROM nl2_posts p WHERE p.topic_id = topics.id AND p.deleted = 0)
WHERE topics.deleted = 0 AND topics.forum_id IN (' . $all_topics_forums_string . ') ORDER BY topics.topic_reply_date DESC LIMIT 50',
)->results();
}
/**
* Determine if a topic exists or not.
*
* @param int $topic_id The topic ID
* @return bool Whether the topic exists or not
*/
public function topicExist(int $topic_id): bool {
// Does the topic exist?
$exists = $this->_db->get('topics', ['id', $topic_id])->results();
return count($exists) > 0;
}
/**
* Determine if the groups can view the forum or not.
*
* @param int $forum_id The forum ID
* @param array $groups The user's groups
* @return bool Whether the groups can view the forum or not
*/
public function canViewForum(int $forum_id, array $groups = [0]): bool {
return $this->hasPermission($forum_id, 'view', $groups);
}
/**
* Determine if the groups can post topics in the forum or not.
*
* @param int $forum_id The forum ID
* @param array $groups The user's groups
* @return bool Whether the groups can post topics in the forum or not
private static function setSettingsCache(?string $module, array $cache): void {
$cache_name = $module !== null ? $module : 'core';
self::$_cached_settings[$cache_name] = $cache;
}
/**
* Get a setting from the database table `nl2_settings`.
*
* @param string $setting Setting to check.
* @param ?string $fallback Fallback to return if $setting is not set in DB. Defaults to null.
* @param string $module Module name to keep settings separate from other modules. Set module
* to 'Core' for global settings.
* @return ?string Setting from DB or $fallback.
*/
public static function getSetting(string $setting, ?string $fallback = null, string $module = 'core'): ?string {
$cache = self::getSettingsCache($module);
if ($cache === null) {
// Load all settings for this module and store it as a dictionary
if ($module === 'core') {
$result = DB::getInstance()->query('SELECT `name`, `value` FROM `nl2_settings` WHERE `module` IS NULL')->results();
} else {
$result = DB::getInstance()->query('SELECT `name`, `value` FROM `nl2_settings` WHERE `module` = ?', [$module])->results();
}
$cache = [];
foreach ($result as $row) {
$cache[$row->name] = $row->value;
}
self::setSettingsCache($module, $cache);
}
return $cache[$setting] ?? $fallback;
}
/**
* Modify a setting in the database table `nl2_settings`.
*
* @param string $setting Setting name.
* @param string|null $new_value New setting value, or null to delete
* @param string $module Module name to keep settings separate from other modules. Set module
/**
* Checks the number of existing migration files compared to executed migrations in the database.
* Alternatively we could check the output of a Phinx command, but that takes ~8x as long to execute.
*
* @throws RuntimeException If these numbers don't match.
*/
public static function ensureUpToDate(): void {
$migration_files = array_map(
static function ($file_name) {
[$version, $migration_name] = explode('_', $file_name, 2);
$migration_name = str_replace(['.php', '_'], '', ucwords($migration_name, '_'));
return $version . '_' . $migration_name;
},
array_filter(scandir(__DIR__ . '/../../migrations'), static function ($file_name) {
return !in_array($file_name, ['.', '..', 'phinx.php']);
}),
);
$migration_database_entries = array_map(static function ($row) {
return $row->version . '_' . $row->migration_name;
}, DB::getInstance()->query('SELECT version, migration_name FROM nl2_phinxlog')->results());
$missing = array_diff($migration_files, $migration_database_entries);
$extra = array_diff($migration_database_entries, $migration_files);
// Likely a pull from the repo dev branch or migrations
// weren't run during an upgrade script.
if (($missing_count = count($missing)) > 0) {
echo "There are $missing_count migrations files which have not been executed:" . '<br>';
foreach ($missing as $missing_migration) {
echo " - $missing_migration" . '<br>';
}
}
// Something went wonky, either they've deleted migration files,
// or they've added stuff to the nl2_phinxlog table.
if (($extra_count = count($extra)) > 0) {
if ($missing_count > 0) {
echo '<br>';
}
echo "There are $extra_count executed migrations which do not have migration files associated:" . '<br>';
Important - only share this link with people you trust!