Import SMF 2.1.4

This commit is contained in:
Antoine Le Gonidec 2024-07-22 16:45:07 +02:00
commit 4c75893b86
Signed by: vv221
GPG key ID: 636B78F91CEB80D8
604 changed files with 264756 additions and 0 deletions

32
LICENSE Normal file
View file

@ -0,0 +1,32 @@
Copyright © 2023 Simple Machines. All rights reserved.
Developed by: Simple Machines Forum Project
Simple Machines
https://www.simplemachines.org
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of SMF2.1 nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

5
Packages/.htaccess Normal file
View file

@ -0,0 +1,5 @@
<Files *>
Order Deny,Allow
Deny from all
Allow from localhost
</Files>

View file

@ -0,0 +1,5 @@
<Files *>
Order Deny,Allow
Deny from all
Allow from localhost
</Files>

View file

@ -0,0 +1,9 @@
<?php
// Try to handle it with the upper level index.php. (it should know what to do.)
if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
include (dirname(dirname(__FILE__)) . '/index.php');
else
exit;
?>

18
Packages/index.php Normal file
View file

@ -0,0 +1,18 @@
<?php
/**
* This file is here solely to protect your Packages directory.
*/
// Look for Settings.php....
if (file_exists(dirname(dirname(__FILE__)) . '/Settings.php'))
{
// Found it!
require(dirname(dirname(__FILE__)) . '/Settings.php');
header('location: ' . $boardurl);
}
// Can't find it... just forget it.
else
exit;
?>

2528
SSI.php Normal file

File diff suppressed because it is too large Load diff

272
Settings.php Normal file
View file

@ -0,0 +1,272 @@
<?php
/**
* The settings file contains all of the basic settings that need to be present when a database/cache is not available.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
########## Maintenance ##########
/**
* The maintenance "mode"
* Set to 1 to enable Maintenance Mode, 2 to make the forum untouchable. (you'll have to make it 0 again manually!)
* 0 is default and disables maintenance mode.
*
* @var int 0, 1, 2
* @global int $maintenance
*/
$maintenance = 0;
/**
* Title for the Maintenance Mode message.
*
* @var string
* @global int $mtitle
*/
$mtitle = 'Maintenance Mode';
/**
* Description of why the forum is in maintenance mode.
*
* @var string
* @global string $mmessage
*/
$mmessage = 'Okay faithful users...we\'re attempting to restore an older backup of the database...news will be posted once we\'re back!';
########## Forum Info ##########
/**
* The name of your forum.
*
* @var string
*/
$mbname = 'My Community';
/**
* The default language file set for the forum.
*
* @var string
*/
$language = 'english';
/**
* URL to your forum's folder. (without the trailing /!)
*
* @var string
*/
$boardurl = 'http://127.0.0.1/smf';
/**
* Email address to send emails from. (like noreply@yourdomain.com.)
*
* @var string
*/
$webmaster_email = 'noreply@myserver.com';
/**
* Name of the cookie to set for authentication.
*
* @var string
*/
$cookiename = 'SMFCookie11';
########## Database Info ##########
/**
* The database type
* Default options: mysql, postgresql
*
* @var string
*/
$db_type = 'mysql';
/**
* The database port
* 0 to use default port for the database type
*
* @var int
*/
$db_port = 0;
/**
* The server to connect to (or a Unix socket)
*
* @var string
*/
$db_server = 'localhost';
/**
* The database name
*
* @var string
*/
$db_name = 'smf';
/**
* Database username
*
* @var string
*/
$db_user = 'root';
/**
* Database password
*
* @var string
*/
$db_passwd = '';
/**
* Database user for when connecting with SSI
*
* @var string
*/
$ssi_db_user = '';
/**
* Database password for when connecting with SSI
*
* @var string
*/
$ssi_db_passwd = '';
/**
* A prefix to put in front of your table names.
* This helps to prevent conflicts
*
* @var string
*/
$db_prefix = 'smf_';
/**
* Use a persistent database connection
*
* @var bool
*/
$db_persist = false;
/**
* Send emails on database connection error
*
* @var bool
*/
$db_error_send = false;
/**
* Override the default behavior of the database layer for mb4 handling
* null keep the default behavior untouched
*
* @var null|bool
*/
$db_mb4 = null;
########## Cache Info ##########
/**
* Select a cache system. You want to leave this up to the cache area of the admin panel for
* proper detection of memcached, output_cache or SMF file_system
* (you can add more with a mod).
*
* @var string
*/
$cache_accelerator = '';
/**
* The level at which you would like to cache. Between 0 (off) through 3 (cache a lot).
*
* @var int
*/
$cache_enable = 0;
/**
* This is only used for memcache / memcached. Should be a string of 'server:port,server:port'
*
* @var array
*/
$cache_memcached = '';
/**
* This is only for the 'smf' file cache system. It is the path to the cache directory.
* It is also recommended that you place this in /tmp/ if you are going to use this.
*
* @var string
*/
$cachedir = dirname(__FILE__) . '/cache';
########## Image Proxy ##########
# This is done entirely in Settings.php to avoid loading the DB while serving the images
/**
* Whether the proxy is enabled or not
*
* @var bool
*/
$image_proxy_enabled = true;
/**
* Secret key to be used by the proxy
*
* @var string
*/
$image_proxy_secret = 'smfisawesome';
/**
* Maximum file size (in KB) for individual files
*
* @var int
*/
$image_proxy_maxsize = 5192;
########## Directories/Files ##########
# Note: These directories do not have to be changed unless you move things.
/**
* The absolute path to the forum's folder. (not just '.'!)
*
* @var string
*/
$boarddir = dirname(__FILE__);
/**
* Path to the Sources directory.
*
* @var string
*/
$sourcedir = dirname(__FILE__) . '/Sources';
/**
* Path to the Packages directory.
*
* @var string
*/
$packagesdir = dirname(__FILE__) . '/Packages';
/**
* Path to the tasks directory.
*
* @var string
*/
$tasksdir = $sourcedir . '/tasks';
# Make sure the paths are correct... at least try to fix them.
if (!is_dir(realpath($boarddir)) && file_exists(dirname(__FILE__) . '/agreement.txt'))
$boarddir = dirname(__FILE__);
if (!is_dir(realpath($sourcedir)) && is_dir($boarddir . '/Sources'))
$sourcedir = $boarddir . '/Sources';
if (!is_dir(realpath($tasksdir)) && is_dir($sourcedir . '/tasks'))
$tasksdir = $sourcedir . '/tasks';
if (!is_dir(realpath($packagesdir)) && is_dir($boarddir . '/Packages'))
$packagesdir = $boarddir . '/Packages';
if (!is_dir(realpath($cachedir)) && is_dir($boarddir . '/cache'))
$cachedir = $boarddir . '/cache';
######### Legacy Settings #########
# UTF-8 is now the only character set supported in 2.1.
$db_character_set = 'utf8';
########## Error-Catching ##########
# Note: You shouldn't touch these settings.
if (file_exists((isset($cachedir) ? $cachedir : dirname(__FILE__)) . '/db_last_error.php'))
include((isset($cachedir) ? $cachedir : dirname(__FILE__)) . '/db_last_error.php');
if (!isset($db_last_error))
{
// File does not exist so lets try to create it
file_put_contents((isset($cachedir) ? $cachedir : dirname(__FILE__)) . '/db_last_error.php', '<' . '?' . "php\n" . '$db_last_error = 0;' . "\n" . '?' . '>');
$db_last_error = 0;
}
if (file_exists(dirname(__FILE__) . '/install.php'))
{
$secure = false;
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
$secure = true;
elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on')
$secure = true;
if (basename($_SERVER['PHP_SELF']) != 'install.php')
{
header('location: http' . ($secure ? 's' : '') . '://' . (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST']) . (strtr(dirname($_SERVER['PHP_SELF']), '\\', '/') == '/' ? '' : strtr(dirname($_SERVER['PHP_SELF']), '\\', '/')) . '/install.php');
exit;
}
}
?>

271
Settings_bak.php Normal file
View file

@ -0,0 +1,271 @@
<?php
/**
* The settings file contains all of the basic settings that need to be present when a database/cache is not available.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
########## Maintenance ##########
/**
* The maintenance "mode"
* Set to 1 to enable Maintenance Mode, 2 to make the forum untouchable. (you'll have to make it 0 again manually!)
* 0 is default and disables maintenance mode.
*
* @var int 0, 1, 2
* @global int $maintenance
*/
$maintenance = 0;
/**
* Title for the Maintenance Mode message.
*
* @var string
* @global int $mtitle
*/
$mtitle = 'Maintenance Mode';
/**
* Description of why the forum is in maintenance mode.
*
* @var string
* @global string $mmessage
*/
$mmessage = 'Okay faithful users...we\'re attempting to restore an older backup of the database...news will be posted once we\'re back!';
########## Forum Info ##########
/**
* The name of your forum.
*
* @var string
*/
$mbname = 'My Community';
/**
* The default language file set for the forum.
*
* @var string
*/
$language = 'english';
/**
* URL to your forum's folder. (without the trailing /!)
*
* @var string
*/
$boardurl = 'http://127.0.0.1/smf';
/**
* Email address to send emails from. (like noreply@yourdomain.com.)
*
* @var string
*/
$webmaster_email = 'noreply@myserver.com';
/**
* Name of the cookie to set for authentication.
*
* @var string
*/
$cookiename = 'SMFCookie11';
########## Database Info ##########
/**
* The database type
* Default options: mysql, postgresql
*
* @var string
*/
$db_type = 'mysql';
/**
* The database port
* 0 to use default port for the database type
*
* @var int
*/
$db_port = 0;
/**
* The server to connect to (or a Unix socket)
*
* @var string
*/
$db_server = 'localhost';
/**
* The database name
*
* @var string
*/
$db_name = 'smf';
/**
* Database username
*
* @var string
*/
$db_user = 'root';
/**
* Database password
*
* @var string
*/
$db_passwd = '';
/**
* Database user for when connecting with SSI
*
* @var string
*/
$ssi_db_user = '';
/**
* Database password for when connecting with SSI
*
* @var string
*/
$ssi_db_passwd = '';
/**
* A prefix to put in front of your table names.
* This helps to prevent conflicts
*
* @var string
*/
$db_prefix = 'smf_';
/**
* Use a persistent database connection
*
* @var bool
*/
$db_persist = false;
/**
* Send emails on database connection error
*
* @var bool
*/
$db_error_send = false;
/**
* Override the default behavior of the database layer for mb4 handling
* null keep the default behavior untouched
*
* @var null|bool
*/
$db_mb4 = null;
########## Cache Info ##########
/**
* Select a cache system. You want to leave this up to the cache area of the admin panel for
* proper detection of apc, memcached, output_cache, smf, or xcache
* (you can add more with a mod).
*
* @var string
*/
$cache_accelerator = '';
/**
* The level at which you would like to cache. Between 0 (off) through 3 (cache a lot).
*
* @var int
*/
$cache_enable = 0;
/**
* This is only used for memcache / memcached. Should be a string of 'server:port,server:port'
*
* @var array
*/
$cache_memcached = '';
/**
* This is only for the 'smf' file cache system. It is the path to the cache directory.
* It is also recommended that you place this in /tmp/ if you are going to use this.
*
* @var string
*/
$cachedir = dirname(__FILE__) . '/cache';
########## Image Proxy ##########
# This is done entirely in Settings.php to avoid loading the DB while serving the images
/**
* Whether the proxy is enabled or not
*
* @var bool
*/
$image_proxy_enabled = true;
/**
* Secret key to be used by the proxy
*
* @var string
*/
$image_proxy_secret = 'smfisawesome';
/**
* Maximum file size (in KB) for individual files
*
* @var int
*/
$image_proxy_maxsize = 5192;
########## Directories/Files ##########
# Note: These directories do not have to be changed unless you move things.
/**
* The absolute path to the forum's folder. (not just '.'!)
*
* @var string
*/
$boarddir = dirname(__FILE__);
/**
* Path to the Sources directory.
*
* @var string
*/
$sourcedir = dirname(__FILE__) . '/Sources';
/**
* Path to the Packages directory.
*
* @var string
*/
$packagesdir = dirname(__FILE__) . '/Packages';
/**
* Path to the tasks directory.
*
* @var string
*/
$tasksdir = $sourcedir . '/tasks';
# Make sure the paths are correct... at least try to fix them.
if (!is_dir(realpath($boarddir)) && file_exists(dirname(__FILE__) . '/agreement.txt'))
$boarddir = dirname(__FILE__);
if (!is_dir(realpath($sourcedir)) && is_dir($boarddir . '/Sources'))
$sourcedir = $boarddir . '/Sources';
if (!is_dir(realpath($tasksdir)) && is_dir($sourcedir . '/tasks'))
$tasksdir = $sourcedir . '/tasks';
if (!is_dir(realpath($packagesdir)) && is_dir($boarddir . '/Packages'))
$packagesdir = $boarddir . '/Packages';
if (!is_dir(realpath($cachedir)) && is_dir($boarddir . '/cache'))
$cachedir = $boarddir . '/cache';
######### Legacy Settings #########
# UTF-8 is now the only character set supported in 2.1.
$db_character_set = 'utf8';
########## Error-Catching ##########
# Note: You shouldn't touch these settings.
if (file_exists((isset($cachedir) ? $cachedir : dirname(__FILE__)) . '/db_last_error.php'))
include((isset($cachedir) ? $cachedir : dirname(__FILE__)) . '/db_last_error.php');
if (!isset($db_last_error))
{
// File does not exist so lets try to create it
file_put_contents((isset($cachedir) ? $cachedir : dirname(__FILE__)) . '/db_last_error.php', '<' . '?' . "php\n" . '$db_last_error = 0;' . "\n" . '?' . '>');
$db_last_error = 0;
}
if (file_exists(dirname(__FILE__) . '/install.php'))
{
$secure = false;
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
$secure = true;
elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on')
$secure = true;
if (basename($_SERVER['PHP_SELF']) != 'install.php')
{
header('location: http' . ($secure ? 's' : '') . '://' . (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST']) . (strtr(dirname($_SERVER['PHP_SELF']), '\\', '/') == '/' ? '' : strtr(dirname($_SERVER['PHP_SELF']), '\\', '/')) . '/install.php');
exit;
}
}
?>

BIN
Smileys/alienine/afro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

BIN
Smileys/alienine/angel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

BIN
Smileys/alienine/angry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

BIN
Smileys/alienine/azn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

BIN
Smileys/alienine/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

BIN
Smileys/alienine/cheesy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

BIN
Smileys/alienine/cool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

BIN
Smileys/alienine/cry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

BIN
Smileys/alienine/evil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

BIN
Smileys/alienine/grin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

BIN
Smileys/alienine/huh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

View file

@ -0,0 +1,9 @@
<?php
// Try to handle it with the upper level index.php. (it should know what to do.)
if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
include (dirname(dirname(__FILE__)) . '/index.php');
else
exit;
?>

BIN
Smileys/alienine/kiss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

BIN
Smileys/alienine/laugh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

BIN
Smileys/alienine/police.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

BIN
Smileys/alienine/sad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
Smileys/alienine/smiley.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
Smileys/alienine/tongue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

BIN
Smileys/alienine/wink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

BIN
Smileys/fugue/afro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
Smileys/fugue/angel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

BIN
Smileys/fugue/angry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
Smileys/fugue/azn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

BIN
Smileys/fugue/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

BIN
Smileys/fugue/cheesy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
Smileys/fugue/cool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

BIN
Smileys/fugue/cry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

BIN
Smileys/fugue/evil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

BIN
Smileys/fugue/grin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

BIN
Smileys/fugue/huh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

9
Smileys/fugue/index.php Normal file
View file

@ -0,0 +1,9 @@
<?php
// Try to handle it with the upper level index.php. (it should know what to do.)
if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
include (dirname(dirname(__FILE__)) . '/index.php');
else
exit;
?>

BIN
Smileys/fugue/kiss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

BIN
Smileys/fugue/laugh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

BIN
Smileys/fugue/police.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
Smileys/fugue/rolleyes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

BIN
Smileys/fugue/sad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

BIN
Smileys/fugue/shocked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

BIN
Smileys/fugue/smiley.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

BIN
Smileys/fugue/tongue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

BIN
Smileys/fugue/undecided.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

BIN
Smileys/fugue/wink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

18
Smileys/index.php Normal file
View file

@ -0,0 +1,18 @@
<?php
/**
* This file is here solely to protect your Smileys directory.
*/
// Look for Settings.php....
if (file_exists(dirname(dirname(__FILE__)) . '/Settings.php'))
{
// Found it!
require(dirname(dirname(__FILE__)) . '/Settings.php');
header('location: ' . $boardurl);
}
// Can't find it... just forget it.
else
exit;
?>

971
Sources/Admin.php Normal file
View file

@ -0,0 +1,971 @@
<?php
/**
* This file, unpredictable as this might be, handles basic administration.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* The main admin handling function.<br>
* It initialises all the basic context required for the admin center.<br>
* It passes execution onto the relevant admin section.<br>
* If the passed section is not found it shows the admin home page.
*/
function AdminMain()
{
global $txt, $context, $scripturl, $modSettings, $settings;
global $smcFunc, $sourcedir, $options, $boarddir;
// Load the language and templates....
loadLanguage('Admin');
loadTemplate('Admin');
loadJavaScriptFile('admin.js', array('minimize' => true), 'smf_admin');
loadCSSFile('admin.css', array(), 'smf_admin');
// No indexing evil stuff.
$context['robot_no_index'] = true;
require_once($sourcedir . '/Subs-Menu.php');
// Some preferences.
$context['admin_preferences'] = !empty($options['admin_preferences']) ? $smcFunc['json_decode']($options['admin_preferences'], true) : array();
/** @var array $admin_areas Defines the menu structure for the admin center. See {@link Subs-Menu.php Subs-Menu.php} for details! */
$admin_areas = array(
'forum' => array(
'title' => $txt['admin_main'],
'permission' => array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments'),
'areas' => array(
'index' => array(
'label' => $txt['admin_center'],
'function' => 'AdminHome',
'icon' => 'administration',
),
'credits' => array(
'label' => $txt['support_credits_title'],
'function' => 'AdminHome',
'icon' => 'support',
),
'news' => array(
'label' => $txt['news_title'],
'file' => 'ManageNews.php',
'function' => 'ManageNews',
'icon' => 'news',
'permission' => array('edit_news', 'send_mail', 'admin_forum'),
'subsections' => array(
'editnews' => array($txt['admin_edit_news'], 'edit_news'),
'mailingmembers' => array($txt['admin_newsletters'], 'send_mail'),
'settings' => array($txt['settings'], 'admin_forum'),
),
),
'packages' => array(
'label' => $txt['package'],
'file' => 'Packages.php',
'function' => 'Packages',
'permission' => array('admin_forum'),
'icon' => 'packages',
'subsections' => array(
'browse' => array($txt['browse_packages']),
'packageget' => array($txt['download_packages'], 'url' => $scripturl . '?action=admin;area=packages;sa=packageget;get'),
'perms' => array($txt['package_file_perms']),
'options' => array($txt['package_settings']),
),
),
'search' => array(
'function' => 'AdminSearch',
'permission' => array('admin_forum'),
'select' => 'index'
),
'adminlogoff' => array(
'label' => $txt['admin_logoff'],
'function' => 'AdminEndSession',
'enabled' => empty($modSettings['securityDisable']),
'icon' => 'exit',
),
),
),
'config' => array(
'title' => $txt['admin_config'],
'permission' => array('admin_forum'),
'areas' => array(
'featuresettings' => array(
'label' => $txt['modSettings_title'],
'file' => 'ManageSettings.php',
'function' => 'ModifyFeatureSettings',
'icon' => 'features',
'subsections' => array(
'basic' => array($txt['mods_cat_features']),
'bbc' => array($txt['manageposts_bbc_settings']),
'layout' => array($txt['mods_cat_layout']),
'sig' => array($txt['signature_settings_short']),
'profile' => array($txt['custom_profile_shorttitle']),
'likes' => array($txt['likes']),
'mentions' => array($txt['mentions']),
'alerts' => array($txt['notifications']),
),
),
'antispam' => array(
'label' => $txt['antispam_title'],
'file' => 'ManageSettings.php',
'function' => 'ModifyAntispamSettings',
'icon' => 'security',
),
'languages' => array(
'label' => $txt['language_configuration'],
'file' => 'ManageLanguages.php',
'function' => 'ManageLanguages',
'icon' => 'languages',
'subsections' => array(
'edit' => array($txt['language_edit']),
'add' => array($txt['language_add']),
'settings' => array($txt['language_settings']),
),
),
'current_theme' => array(
'label' => $txt['theme_current_settings'],
'file' => 'Themes.php',
'function' => 'ThemesMain',
'custom_url' => $scripturl . '?action=admin;area=theme;sa=list;th=' . $settings['theme_id'],
'icon' => 'current_theme',
),
'theme' => array(
'label' => $txt['theme_admin'],
'file' => 'Themes.php',
'function' => 'ThemesMain',
'custom_url' => $scripturl . '?action=admin;area=theme',
'icon' => 'themes',
'subsections' => array(
'admin' => array($txt['themeadmin_admin_title']),
'list' => array($txt['themeadmin_list_title']),
'reset' => array($txt['themeadmin_reset_title']),
'edit' => array($txt['themeadmin_edit_title']),
),
),
'modsettings' => array(
'label' => $txt['admin_modifications'],
'file' => 'ManageSettings.php',
'function' => 'ModifyModSettings',
'icon' => 'modifications',
'subsections' => array(
'general' => array($txt['mods_cat_modifications_misc']),
// Mod Authors for a "ADD AFTER" on this line. Ensure you end your change with a comma. For example:
// 'shout' => array($txt['shout']),
// Note the comma!! The setting with automatically appear with the first mod to be added.
),
),
),
),
'layout' => array(
'title' => $txt['layout_controls'],
'permission' => array('manage_boards', 'admin_forum', 'manage_smileys', 'manage_attachments', 'moderate_forum'),
'areas' => array(
'manageboards' => array(
'label' => $txt['admin_boards'],
'file' => 'ManageBoards.php',
'function' => 'ManageBoards',
'icon' => 'boards',
'permission' => array('manage_boards'),
'subsections' => array(
'main' => array($txt['boards_edit']),
'newcat' => array($txt['mboards_new_cat']),
'settings' => array($txt['settings'], 'admin_forum'),
),
),
'postsettings' => array(
'label' => $txt['manageposts'],
'file' => 'ManagePosts.php',
'function' => 'ManagePostSettings',
'permission' => array('admin_forum'),
'icon' => 'posts',
'subsections' => array(
'posts' => array($txt['manageposts_settings']),
'censor' => array($txt['admin_censored_words']),
'topics' => array($txt['manageposts_topic_settings']),
'drafts' => array($txt['manage_drafts']),
),
),
'managecalendar' => array(
'label' => $txt['manage_calendar'],
'file' => 'ManageCalendar.php',
'function' => 'ManageCalendar',
'icon' => 'calendar',
'permission' => array('admin_forum'),
'inactive' => empty($modSettings['cal_enabled']),
'subsections' => empty($modSettings['cal_enabled']) ? array() : array(
'holidays' => array($txt['manage_holidays'], 'admin_forum'),
'settings' => array($txt['calendar_settings'], 'admin_forum'),
),
),
'managesearch' => array(
'label' => $txt['manage_search'],
'file' => 'ManageSearch.php',
'function' => 'ManageSearch',
'icon' => 'search',
'permission' => array('admin_forum'),
'subsections' => array(
'weights' => array($txt['search_weights']),
'method' => array($txt['search_method']),
'settings' => array($txt['settings']),
),
),
'smileys' => array(
'label' => $txt['smileys_manage'],
'file' => 'ManageSmileys.php',
'function' => 'ManageSmileys',
'icon' => 'smiley',
'permission' => array('manage_smileys'),
'subsections' => array(
'editsets' => array($txt['smiley_sets']),
'addsmiley' => array($txt['smileys_add'], 'enabled' => !empty($modSettings['smiley_enable'])),
'editsmileys' => array($txt['smileys_edit'], 'enabled' => !empty($modSettings['smiley_enable'])),
'setorder' => array($txt['smileys_set_order'], 'enabled' => !empty($modSettings['smiley_enable'])),
'editicons' => array($txt['icons_edit_message_icons'], 'enabled' => !empty($modSettings['messageIcons_enable'])),
'settings' => array($txt['settings']),
),
),
'manageattachments' => array(
'label' => $txt['attachments_avatars'],
'file' => 'ManageAttachments.php',
'function' => 'ManageAttachments',
'icon' => 'attachment',
'permission' => array('manage_attachments'),
'subsections' => array(
'browse' => array($txt['attachment_manager_browse']),
'attachments' => array($txt['attachment_manager_settings']),
'avatars' => array($txt['attachment_manager_avatar_settings']),
'attachpaths' => array($txt['attach_directories']),
'maintenance' => array($txt['attachment_manager_maintenance']),
),
),
'sengines' => array(
'label' => $txt['search_engines'],
'inactive' => empty($modSettings['spider_mode']),
'file' => 'ManageSearchEngines.php',
'icon' => 'engines',
'function' => 'SearchEngines',
'permission' => 'admin_forum',
'subsections' => empty($modSettings['spider_mode']) ? array() : array(
'stats' => array($txt['spider_stats']),
'logs' => array($txt['spider_logs']),
'spiders' => array($txt['spiders']),
'settings' => array($txt['settings']),
),
),
),
),
'members' => array(
'title' => $txt['admin_manage_members'],
'permission' => array('moderate_forum', 'manage_membergroups', 'manage_bans', 'manage_permissions', 'admin_forum'),
'areas' => array(
'viewmembers' => array(
'label' => $txt['admin_users'],
'file' => 'ManageMembers.php',
'function' => 'ViewMembers',
'icon' => 'members',
'permission' => array('moderate_forum'),
'subsections' => array(
'all' => array($txt['view_all_members']),
'search' => array($txt['mlist_search']),
),
),
'membergroups' => array(
'label' => $txt['admin_groups'],
'file' => 'ManageMembergroups.php',
'function' => 'ModifyMembergroups',
'icon' => 'membergroups',
'permission' => array('manage_membergroups'),
'subsections' => array(
'index' => array($txt['membergroups_edit_groups'], 'manage_membergroups'),
'add' => array($txt['membergroups_new_group'], 'manage_membergroups'),
'settings' => array($txt['settings'], 'admin_forum'),
),
),
'permissions' => array(
'label' => $txt['edit_permissions'],
'file' => 'ManagePermissions.php',
'function' => 'ModifyPermissions',
'icon' => 'permissions',
'permission' => array('manage_permissions'),
'subsections' => array(
'index' => array($txt['permissions_groups'], 'manage_permissions'),
'board' => array($txt['permissions_boards'], 'manage_permissions'),
'profiles' => array($txt['permissions_profiles'], 'manage_permissions'),
'postmod' => array($txt['permissions_post_moderation'], 'manage_permissions'),
'settings' => array($txt['settings'], 'admin_forum'),
),
),
'regcenter' => array(
'label' => $txt['registration_center'],
'file' => 'ManageRegistration.php',
'function' => 'RegCenter',
'icon' => 'regcenter',
'permission' => array('admin_forum', 'moderate_forum'),
'subsections' => array(
'register' => array($txt['admin_browse_register_new'], 'moderate_forum'),
'agreement' => array($txt['registration_agreement'], 'admin_forum'),
'policy' => array($txt['privacy_policy'], 'admin_forum'),
'reservednames' => array($txt['admin_reserved_set'], 'admin_forum'),
'settings' => array($txt['settings'], 'admin_forum'),
),
),
'warnings' => array(
'label' => $txt['warnings'],
'file' => 'ManageSettings.php',
'function' => 'ModifyWarningSettings',
'icon' => 'warning',
'inactive' => $modSettings['warning_settings'][0] == 0,
'permission' => array('admin_forum'),
),
'ban' => array(
'label' => $txt['ban_title'],
'file' => 'ManageBans.php',
'function' => 'Ban',
'icon' => 'ban',
'permission' => 'manage_bans',
'subsections' => array(
'list' => array($txt['ban_edit_list']),
'add' => array($txt['ban_add_new']),
'browse' => array($txt['ban_trigger_browse']),
'log' => array($txt['ban_log']),
),
),
'paidsubscribe' => array(
'label' => $txt['paid_subscriptions'],
'inactive' => empty($modSettings['paid_enabled']),
'file' => 'ManagePaid.php',
'icon' => 'paid',
'function' => 'ManagePaidSubscriptions',
'permission' => 'admin_forum',
'subsections' => empty($modSettings['paid_enabled']) ? array() : array(
'view' => array($txt['paid_subs_view']),
'settings' => array($txt['settings']),
),
),
),
),
'maintenance' => array(
'title' => $txt['admin_maintenance'],
'permission' => array('admin_forum'),
'areas' => array(
'serversettings' => array(
'label' => $txt['admin_server_settings'],
'file' => 'ManageServer.php',
'function' => 'ModifySettings',
'icon' => 'server',
'subsections' => array(
'general' => array($txt['general_settings']),
'database' => array($txt['database_settings']),
'cookie' => array($txt['cookies_sessions_settings']),
'security' => array($txt['security_settings']),
'cache' => array($txt['caching_settings']),
'export' => array($txt['export_settings']),
'loads' => array($txt['load_balancing_settings']),
'phpinfo' => array($txt['phpinfo_settings']),
),
),
'maintain' => array(
'label' => $txt['maintain_title'],
'file' => 'ManageMaintenance.php',
'icon' => 'maintain',
'function' => 'ManageMaintenance',
'subsections' => array(
'routine' => array($txt['maintain_sub_routine'], 'admin_forum'),
'database' => array($txt['maintain_sub_database'], 'admin_forum'),
'members' => array($txt['maintain_sub_members'], 'admin_forum'),
'topics' => array($txt['maintain_sub_topics'], 'admin_forum'),
'hooks' => array($txt['hooks_title_list'], 'admin_forum'),
),
),
'scheduledtasks' => array(
'label' => $txt['maintain_tasks'],
'file' => 'ManageScheduledTasks.php',
'icon' => 'scheduled',
'function' => 'ManageScheduledTasks',
'subsections' => array(
'tasks' => array($txt['maintain_tasks'], 'admin_forum'),
'tasklog' => array($txt['scheduled_log'], 'admin_forum'),
'settings' => array($txt['scheduled_tasks_settings'], 'admin_forum'),
),
),
'mailqueue' => array(
'label' => $txt['mailqueue_title'],
'file' => 'ManageMail.php',
'function' => 'ManageMail',
'icon' => 'mail',
'subsections' => array(
'browse' => array($txt['mailqueue_browse'], 'admin_forum'),
'settings' => array($txt['mailqueue_settings'], 'admin_forum'),
'test' => array($txt['mailqueue_test'], 'admin_forum'),
),
),
'reports' => array(
'label' => $txt['generate_reports'],
'file' => 'Reports.php',
'function' => 'ReportsMain',
'icon' => 'reports',
),
'logs' => array(
'label' => $txt['logs'],
'function' => 'AdminLogs',
'icon' => 'logs',
'subsections' => array(
'errorlog' => array($txt['errorlog'], 'admin_forum', 'enabled' => !empty($modSettings['enableErrorLogging']), 'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc'),
'adminlog' => array($txt['admin_log'], 'admin_forum', 'enabled' => !empty($modSettings['adminlog_enabled'])),
'modlog' => array($txt['moderation_log'], 'admin_forum', 'enabled' => !empty($modSettings['modlog_enabled'])),
'banlog' => array($txt['ban_log'], 'manage_bans'),
'spiderlog' => array($txt['spider_logs'], 'admin_forum', 'enabled' => !empty($modSettings['spider_mode'])),
'tasklog' => array($txt['scheduled_log'], 'admin_forum'),
'settings' => array($txt['log_settings'], 'admin_forum'),
),
),
'repairboards' => array(
'label' => $txt['admin_repair'],
'file' => 'RepairBoards.php',
'function' => 'RepairBoards',
'select' => 'maintain',
'hidden' => true,
),
),
),
);
// Any files to include for administration?
if (!empty($modSettings['integrate_admin_include']))
{
$admin_includes = explode(',', $modSettings['integrate_admin_include']);
foreach ($admin_includes as $include)
{
$include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
if (file_exists($include))
require_once($include);
}
}
// Make sure the administrator has a valid session...
validateSession();
// Actually create the menu!
$admin_include_data = createMenu($admin_areas, array('do_big_icons' => true));
unset($admin_areas);
// Nothing valid?
if ($admin_include_data == false)
fatal_lang_error('no_access', false);
// Build the link tree.
$context['linktree'][] = array(
'url' => $scripturl . '?action=admin',
'name' => $txt['admin_center'],
);
if (isset($admin_include_data['current_area']) && $admin_include_data['current_area'] != 'index')
$context['linktree'][] = array(
'url' => $scripturl . '?action=admin;area=' . $admin_include_data['current_area'] . ';' . $context['session_var'] . '=' . $context['session_id'],
'name' => $admin_include_data['label'],
);
if (!empty($admin_include_data['current_subsection']) && $admin_include_data['subsections'][$admin_include_data['current_subsection']][0] != $admin_include_data['label'])
$context['linktree'][] = array(
'url' => $scripturl . '?action=admin;area=' . $admin_include_data['current_area'] . ';sa=' . $admin_include_data['current_subsection'] . ';' . $context['session_var'] . '=' . $context['session_id'],
'name' => $admin_include_data['subsections'][$admin_include_data['current_subsection']][0],
);
// Make a note of the Unique ID for this menu.
$context['admin_menu_id'] = $context['max_menu_id'];
$context['admin_menu_name'] = 'menu_data_' . $context['admin_menu_id'];
// Where in the admin are we?
$context['admin_area'] = $admin_include_data['current_area'];
// Now - finally - call the right place!
if (isset($admin_include_data['file']))
require_once($sourcedir . '/' . $admin_include_data['file']);
// Get the right callable.
$call = call_helper($admin_include_data['function'], true);
// Is it valid?
if (!empty($call))
call_user_func($call);
}
/**
* The main administration section.
* It prepares all the data necessary for the administration front page.
* It uses the Admin template along with the admin sub template.
* It requires the moderate_forum, manage_membergroups, manage_bans,
* admin_forum, manage_permissions, manage_attachments, manage_smileys,
* manage_boards, edit_news, or send_mail permission.
* It uses the index administrative area.
* It can be found by going to ?action=admin.
*/
function AdminHome()
{
global $sourcedir, $txt, $scripturl, $context, $user_info;
// You have to be able to do at least one of the below to see this page.
isAllowedTo(array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments'));
// Find all of this forum's administrators...
require_once($sourcedir . '/Subs-Membergroups.php');
if (listMembergroupMembers_Href($context['administrators'], 1, 32) && allowedTo('manage_membergroups'))
{
// Add a 'more'-link if there are more than 32.
$context['more_admins_link'] = '<a href="' . $scripturl . '?action=moderate;area=viewgroups;sa=members;group=1">' . $txt['more'] . '</a>';
}
// Load the credits stuff.
require_once($sourcedir . '/Who.php');
Credits(true);
// This makes it easier to get the latest news with your time format.
$context['time_format'] = urlencode($user_info['time_format']);
$context['forum_version'] = SMF_FULL_VERSION;
// Get a list of current server versions.
require_once($sourcedir . '/Subs-Admin.php');
$checkFor = array(
'gd',
'imagemagick',
'db_server',
'apcu',
'memcacheimplementation',
'memcachedimplementation',
'postgres',
'sqlite',
'zend',
'filebased',
'php',
'server',
);
$context['current_versions'] = getServerVersions($checkFor);
$context['can_admin'] = allowedTo('admin_forum');
$context['sub_template'] = $context['admin_area'] == 'credits' ? 'credits' : 'admin';
$context['page_title'] = $context['admin_area'] == 'credits' ? $txt['support_credits_title'] : $txt['admin_center'];
if ($context['admin_area'] != 'credits')
$context[$context['admin_menu_name']]['tab_data'] = array(
'title' => $txt['admin_center'],
'help' => '',
'description' => '<strong>' . $txt['hello_guest'] . ' ' . $context['user']['name'] . '!</strong>
' . sprintf($txt['admin_main_welcome'], $txt['admin_center'], $txt['help'], $txt['help']),
);
// Lastly, fill in the blanks in the support resources paragraphs.
$txt['support_resources_p1'] = sprintf($txt['support_resources_p1'],
'https://wiki.simplemachines.org/',
'https://wiki.simplemachines.org/smf/features2',
'https://wiki.simplemachines.org/smf/options2',
'https://wiki.simplemachines.org/smf/themes2',
'https://wiki.simplemachines.org/smf/packages2'
);
$txt['support_resources_p2'] = sprintf($txt['support_resources_p2'],
'https://www.simplemachines.org/community/',
'https://www.simplemachines.org/redirect/english_support',
'https://www.simplemachines.org/redirect/international_support_boards',
'https://www.simplemachines.org/redirect/smf_support',
'https://www.simplemachines.org/redirect/customize_support'
);
if ($context['admin_area'] == 'admin')
loadJavaScriptFile('admin.js', array('defer' => false, 'minimize' => true), 'smf_admin');
}
/**
* Get one of the admin information files from Simple Machines.
*/
function DisplayAdminFile()
{
global $context, $modSettings, $smcFunc;
setMemoryLimit('32M');
if (empty($_REQUEST['filename']) || !is_string($_REQUEST['filename']))
fatal_lang_error('no_access', false);
// Strip off the forum cache part or we won't find it...
$_REQUEST['filename'] = str_replace($context['browser_cache'], '', $_REQUEST['filename']);
$request = $smcFunc['db_query']('', '
SELECT data, filetype
FROM {db_prefix}admin_info_files
WHERE filename = {string:current_filename}
LIMIT 1',
array(
'current_filename' => $_REQUEST['filename'],
)
);
if ($smcFunc['db_num_rows']($request) == 0)
fatal_lang_error('admin_file_not_found', true, array($_REQUEST['filename']), 404);
list ($file_data, $filetype) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
// @todo Temp
// Figure out if sesc is still being used.
if (strpos($file_data, ';sesc=') !== false && $filetype == 'text/javascript')
$file_data = '
if (!(\'smfForum_sessionvar\' in window))
window.smfForum_sessionvar = \'sesc\';
' . strtr($file_data, array(';sesc=' => ';\' + window.smfForum_sessionvar + \'='));
$context['template_layers'] = array();
// Lets make sure we aren't going to output anything nasty.
@ob_end_clean();
if (!empty($modSettings['enableCompressedOutput']))
@ob_start('ob_gzhandler');
else
@ob_start();
// Make sure they know what type of file we are.
header('content-type: ' . $filetype);
echo $file_data;
obExit(false);
}
/**
* This function allocates out all the search stuff.
*/
function AdminSearch()
{
global $txt, $context, $smcFunc, $sourcedir;
isAllowedTo('admin_forum');
// What can we search for?
$subActions = array(
'internal' => 'AdminSearchInternal',
'online' => 'AdminSearchOM',
'member' => 'AdminSearchMember',
);
$context['search_type'] = !isset($_REQUEST['search_type']) || !isset($subActions[$_REQUEST['search_type']]) ? 'internal' : $_REQUEST['search_type'];
$context['search_term'] = isset($_REQUEST['search_term']) ? $smcFunc['htmlspecialchars']($_REQUEST['search_term'], ENT_QUOTES) : '';
$context['sub_template'] = 'admin_search_results';
$context['page_title'] = $txt['admin_search_results'];
// Keep track of what the admin wants.
if (empty($context['admin_preferences']['sb']) || $context['admin_preferences']['sb'] != $context['search_type'])
{
$context['admin_preferences']['sb'] = $context['search_type'];
// Update the preferences.
require_once($sourcedir . '/Subs-Admin.php');
updateAdminPreferences();
}
if (trim($context['search_term']) == '')
$context['search_results'] = array();
else
call_helper($subActions[$context['search_type']]);
}
/**
* A complicated but relatively quick internal search.
*/
function AdminSearchInternal()
{
global $context, $txt, $helptxt, $scripturl, $sourcedir;
// Try to get some more memory.
setMemoryLimit('128M');
// Load a lot of language files.
$language_files = array(
'Help', 'ManageMail', 'ManageSettings', 'ManageCalendar', 'ManageBoards', 'ManagePaid', 'ManagePermissions', 'Search',
'Login', 'ManageSmileys', 'Drafts',
);
// All the files we need to include.
$include_files = array(
'ManageSettings', 'ManageBoards', 'ManageNews', 'ManageAttachments', 'ManageCalendar', 'ManageMail', 'ManagePaid', 'ManagePermissions',
'ManagePosts', 'ManageRegistration', 'ManageSearch', 'ManageSearchEngines', 'ManageServer', 'ManageSmileys', 'ManageLanguages',
);
// This is a special array of functions that contain setting data - we query all these to simply pull all setting bits!
$settings_search = array(
array('ModifyBasicSettings', 'area=featuresettings;sa=basic'),
array('ModifyBBCSettings', 'area=featuresettings;sa=bbc'),
array('ModifyLayoutSettings', 'area=featuresettings;sa=layout'),
array('ModifyLikesSettings', 'area=featuresettings;sa=likes'),
array('ModifyMentionsSettings', 'area=featuresettings;sa=mentions'),
array('ModifySignatureSettings', 'area=featuresettings;sa=sig'),
array('ModifyAntispamSettings', 'area=antispam'),
array('ModifyWarningSettings', 'area=warnings'),
array('ModifyGeneralModSettings', 'area=modsettings;sa=general'),
// Mod authors if you want to be "real freaking good" then add any setting pages for your mod BELOW this line!
array('ManageAttachmentSettings', 'area=manageattachments;sa=attachments'),
array('ManageAvatarSettings', 'area=manageattachments;sa=avatars'),
array('ModifyCalendarSettings', 'area=managecalendar;sa=settings'),
array('EditBoardSettings', 'area=manageboards;sa=settings'),
array('ModifyMailSettings', 'area=mailqueue;sa=settings'),
array('ModifyNewsSettings', 'area=news;sa=settings'),
array('GeneralPermissionSettings', 'area=permissions;sa=settings'),
array('ModifyPostSettings', 'area=postsettings;sa=posts'),
array('ModifyTopicSettings', 'area=postsettings;sa=topics'),
array('ModifyDraftSettings', 'area=postsettings;sa=drafts'),
array('EditSearchSettings', 'area=managesearch;sa=settings'),
array('EditSmileySettings', 'area=smileys;sa=settings'),
array('ModifyGeneralSettings', 'area=serversettings;sa=general'),
array('ModifyDatabaseSettings', 'area=serversettings;sa=database'),
array('ModifyCookieSettings', 'area=serversettings;sa=cookie'),
array('ModifyGeneralSecuritySettings', 'area=serversettings;sa=security'),
array('ModifyCacheSettings', 'area=serversettings;sa=cache'),
array('ModifyLanguageSettings', 'area=languages;sa=settings'),
array('ModifyRegistrationSettings', 'area=regcenter;sa=settings'),
array('ManageSearchEngineSettings', 'area=sengines;sa=settings'),
array('ModifySubscriptionSettings', 'area=paidsubscribe;sa=settings'),
array('ModifyLogSettings', 'area=logs;sa=settings'),
);
call_integration_hook('integrate_admin_search', array(&$language_files, &$include_files, &$settings_search));
loadLanguage(implode('+', $language_files));
foreach ($include_files as $file)
require_once($sourcedir . '/' . $file . '.php');
/* This is the huge array that defines everything... it's a huge array of items formatted as follows:
0 = Language index (Can be array of indexes) to search through for this setting.
1 = URL for this indexes page.
2 = Help index for help associated with this item (If different from 0)
*/
$search_data = array(
// All the major sections of the forum.
'sections' => array(
),
'settings' => array(
array('COPPA', 'area=regcenter;sa=settings'),
array('CAPTCHA', 'area=antispam'),
),
);
// Go through the admin menu structure trying to find suitably named areas!
foreach ($context[$context['admin_menu_name']]['sections'] as $section)
{
foreach ($section['areas'] as $menu_key => $menu_item)
{
$search_data['sections'][] = array($menu_item['label'], 'area=' . $menu_key);
if (!empty($menu_item['subsections']))
foreach ($menu_item['subsections'] as $key => $sublabel)
{
if (isset($sublabel['label']))
$search_data['sections'][] = array($sublabel['label'], 'area=' . $menu_key . ';sa=' . $key);
}
}
}
foreach ($settings_search as $setting_area)
{
// Get a list of their variables.
$config_vars = call_user_func($setting_area[0], true);
foreach ($config_vars as $var)
if (!empty($var[1]) && !in_array($var[0], array('permissions', 'switch', 'desc')))
$search_data['settings'][] = array($var[(isset($var[2]) && in_array($var[2], array('file', 'db'))) ? 0 : 1], $setting_area[1], 'alttxt' => (isset($var[2]) && in_array($var[2], array('file', 'db'))) || isset($var[3]) ? (in_array($var[2], array('file', 'db')) ? $var[1] : $var[3]) : '');
}
$context['page_title'] = $txt['admin_search_results'];
$context['search_results'] = array();
$search_term = strtolower(un_htmlspecialchars($context['search_term']));
// Go through all the search data trying to find this text!
foreach ($search_data as $section => $data)
{
foreach ($data as $item)
{
$found = false;
if (!is_array($item[0]))
$item[0] = array($item[0]);
foreach ($item[0] as $term)
{
if (stripos($term, $search_term) !== false || (isset($txt[$term]) && stripos($txt[$term], $search_term) !== false) || (isset($txt['setting_' . $term]) && stripos($txt['setting_' . $term], $search_term) !== false))
{
$found = $term;
break;
}
}
if ($found)
{
// Format the name - and remove any descriptions the entry may have.
$name = isset($txt[$found]) ? $txt[$found] : (isset($txt['setting_' . $found]) ? $txt['setting_' . $found] : (!empty($item['alttxt']) ? $item['alttxt'] : $found));
$name = preg_replace('~<(?:div|span)\sclass="smalltext">.+?</(?:div|span)>~', '', $name);
$context['search_results'][] = array(
'url' => (substr($item[1], 0, 4) == 'area' ? $scripturl . '?action=admin;' . $item[1] : $item[1]) . ';' . $context['session_var'] . '=' . $context['session_id'] . ((substr($item[1], 0, 4) == 'area' && $section == 'settings' ? '#' . $item[0][0] : '')),
'name' => $name,
'type' => $section,
'help' => shorten_subject(isset($item[2]) ? strip_tags($helptxt[$item[2]]) : (isset($helptxt[$found]) ? strip_tags($helptxt[$found]) : ''), 255),
);
}
}
}
}
/**
* All this does is pass through to manage members.
* {@see ViewMembers()}
*/
function AdminSearchMember()
{
global $context, $sourcedir;
require_once($sourcedir . '/ManageMembers.php');
$_REQUEST['sa'] = 'query';
$_POST['membername'] = un_htmlspecialchars($context['search_term']);
$_POST['types'] = '';
ViewMembers();
}
/**
* This file allows the user to search the SM online manual for a little of help.
*/
function AdminSearchOM()
{
global $context, $sourcedir;
$context['doc_apiurl'] = 'https://wiki.simplemachines.org/api.php';
$context['doc_scripturl'] = 'https://wiki.simplemachines.org/smf/';
// Set all the parameters search might expect.
$postVars = explode(' ', $context['search_term']);
// Encode the search data.
foreach ($postVars as $k => $v)
$postVars[$k] = urlencode($v);
// This is what we will send.
$postVars = implode('+', $postVars);
// Get the results from the doc site.
// Demo URL:
// https://wiki.simplemachines.org/api.php?action=query&list=search&srprop=timestamp|snippet&format=xml&srwhat=text&srsearch=template+eval
$search_results = fetch_web_data($context['doc_apiurl'] . '?action=query&list=search&srprop=timestamp|snippet&format=xml&srwhat=text&srsearch=' . $postVars);
// If we didn't get any xml back we are in trouble - perhaps the doc site is overloaded?
if (!$search_results || preg_match('~<' . '\?xml\sversion="\d+\.\d+"\?' . '>\s*(<api\b[^>]*>.+?</api>)~is', $search_results, $matches) != true)
fatal_lang_error('cannot_connect_doc_site');
$search_results = $matches[1];
// Otherwise we simply walk through the XML and stick it in context for display.
$context['search_results'] = array();
require_once($sourcedir . '/Class-Package.php');
// Get the results loaded into an array for processing!
$results = new xmlArray($search_results, false);
// Move through the api layer.
if (!$results->exists('api'))
fatal_lang_error('cannot_connect_doc_site');
// Are there actually some results?
if ($results->exists('api/query/search/p'))
{
$relevance = 0;
foreach ($results->set('api/query/search/p') as $result)
{
$context['search_results'][$result->fetch('@title')] = array(
'title' => $result->fetch('@title'),
'relevance' => $relevance++,
'snippet' => str_replace('class=\'searchmatch\'', 'class="highlight"', un_htmlspecialchars($result->fetch('@snippet'))),
);
}
}
}
/**
* This function decides which log to load.
*/
function AdminLogs()
{
global $sourcedir, $context, $txt, $scripturl, $modSettings;
// These are the logs they can load.
$log_functions = array(
'errorlog' => array('ManageErrors.php', 'ViewErrorLog'),
'adminlog' => array('Modlog.php', 'ViewModlog', 'disabled' => empty($modSettings['adminlog_enabled'])),
'modlog' => array('Modlog.php', 'ViewModlog', 'disabled' => empty($modSettings['modlog_enabled'])),
'banlog' => array('ManageBans.php', 'BanLog'),
'spiderlog' => array('ManageSearchEngines.php', 'SpiderLogs'),
'tasklog' => array('ManageScheduledTasks.php', 'TaskLog'),
'settings' => array('ManageSettings.php', 'ModifyLogSettings'),
);
// If it's not got a sa set it must have come here for first time, pretend error log should be reversed.
if (!isset($_REQUEST['sa']))
$_REQUEST['desc'] = true;
// Setup some tab stuff.
$context[$context['admin_menu_name']]['tab_data'] = array(
'title' => $txt['logs'],
'help' => '',
'description' => $txt['maintain_info'],
'tabs' => array(
'errorlog' => array(
'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
'description' => sprintf($txt['errorlog_desc'], $txt['remove']),
),
'adminlog' => array(
'description' => $txt['admin_log_desc'],
),
'modlog' => array(
'description' => $txt['moderation_log_desc'],
),
'banlog' => array(
'description' => $txt['ban_log_description'],
),
'spiderlog' => array(
'description' => $txt['spider_log_desc'],
),
'tasklog' => array(
'description' => $txt['scheduled_log_desc'],
),
'settings' => array(
'description' => $txt['log_settings_desc'],
),
),
);
call_integration_hook('integrate_manage_logs', array(&$log_functions));
$subAction = isset($_REQUEST['sa']) && isset($log_functions[$_REQUEST['sa']]) && empty($log_functions[$_REQUEST['sa']]['disabled']) ? $_REQUEST['sa'] : 'errorlog';
require_once($sourcedir . '/' . $log_functions[$subAction][0]);
call_helper($log_functions[$subAction][1]);
}
/**
* This ends a admin session, requiring authentication to access the ACP again.
*/
function AdminEndSession()
{
// This is so easy!
unset($_SESSION['admin_time']);
// Clean any admin tokens as well.
foreach ($_SESSION['token'] as $key => $token)
if (strpos($key, '-admin') !== false)
unset($_SESSION['token'][$key]);
redirectexit();
}
?>

184
Sources/Agreement.php Normal file
View file

@ -0,0 +1,184 @@
<?php
/**
* This file handles the user and privacy policy agreements.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/* The purpose of this file is to show the user an updated registration
agreement, and get them to agree to it.
bool prepareAgreementContext()
// !!!
bool canRequireAgreement()
// !!!
bool canRequirePrivacyPolicy()
// !!!
void Agreement()
- Show the new registration agreement
void AcceptAgreement()
- Called when they actually accept the agreement
- Save the date of the current agreement to the members database table
- Redirect back to wherever they came from
*/
function prepareAgreementContext()
{
global $boarddir, $context, $language, $modSettings, $user_info;
// What, if anything, do they need to accept?
$context['can_accept_agreement'] = !empty($modSettings['requireAgreement']) && canRequireAgreement();
$context['can_accept_privacy_policy'] = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy();
$context['accept_doc'] = $context['can_accept_agreement'] || $context['can_accept_privacy_policy'];
if (!$context['accept_doc'] || $context['can_accept_agreement'])
{
// Grab the agreement.
// Have we got a localized one?
if (file_exists($boarddir . '/agreement.' . $user_info['language'] . '.txt'))
$context['agreement_file'] = $boarddir . '/agreement.' . $user_info['language'] . '.txt';
elseif (file_exists($boarddir . '/agreement.txt'))
$context['agreement_file'] = $boarddir . '/agreement.txt';
if (!empty($context['agreement_file']))
{
$cache_id = strtr($context['agreement_file'], array($boarddir => '', '.txt' => '', '.' => '_'));
$context['agreement'] = parse_bbc(file_get_contents($context['agreement_file']), true, $cache_id);
}
elseif ($context['can_accept_agreement'])
fatal_lang_error('error_no_agreement', false);
}
if (!$context['accept_doc'] || $context['can_accept_privacy_policy'])
{
// Have we got a localized policy?
if (!empty($modSettings['policy_' . $user_info['language']]))
$context['privacy_policy'] = parse_bbc($modSettings['policy_' . $user_info['language']]);
elseif (!empty($modSettings['policy_' . $language]))
$context['privacy_policy'] = parse_bbc($modSettings['policy_' . $language]);
// Then I guess we've got nothing
elseif ($context['can_accept_privacy_policy'])
fatal_lang_error('error_no_privacy_policy', false);
}
}
function canRequireAgreement()
{
global $boarddir, $context, $modSettings, $options, $user_info;
// Guests can't agree
if (!empty($user_info['is_guest']) || empty($modSettings['requireAgreement']))
return false;
$agreement_lang = file_exists($boarddir . '/agreement.' . $user_info['language'] . '.txt') ? $user_info['language'] : 'default';
if (empty($modSettings['agreement_updated_' . $agreement_lang]))
return false;
$context['agreement_accepted_date'] = empty($options['agreement_accepted']) ? 0 : $options['agreement_accepted'];
// A new timestamp means that there are new changes to the registration agreement and must therefore be shown.
return empty($options['agreement_accepted']) || $modSettings['agreement_updated_' . $agreement_lang] > $options['agreement_accepted'];
}
function canRequirePrivacyPolicy()
{
global $modSettings, $options, $user_info, $language, $context;
if (!empty($user_info['is_guest']) || empty($modSettings['requirePolicyAgreement']))
return false;
$policy_lang = !empty($modSettings['policy_' . $user_info['language']]) ? $user_info['language'] : $language;
if (empty($modSettings['policy_updated_' . $policy_lang]))
return false;
$context['privacy_policy_accepted_date'] = empty($options['policy_accepted']) ? 0 : $options['policy_accepted'];
return empty($options['policy_accepted']) || $modSettings['policy_updated_' . $policy_lang] > $options['policy_accepted'];
}
// Let's tell them there's a new agreement
function Agreement()
{
global $context, $modSettings, $scripturl, $smcFunc, $txt;
prepareAgreementContext();
loadLanguage('Agreement');
loadTemplate('Agreement');
$page_title = '';
if (!empty($context['agreement']) && !empty($context['privacy_policy']))
$page_title = $txt['agreement_and_privacy_policy'];
elseif (!empty($context['agreement']))
$page_title = $txt['agreement'];
elseif (!empty($context['privacy_policy']))
$page_title = $txt['privacy_policy'];
$context['page_title'] = $page_title;
$context['linktree'][] = array(
'url' => $scripturl . '?action=agreement',
'name' => $context['page_title'],
);
if (isset($_SESSION['old_url']))
$_SESSION['redirect_url'] = $_SESSION['old_url'];
}
// I solemly swear to no longer chase squirrels.
function AcceptAgreement()
{
global $context, $modSettings, $smcFunc, $user_info;
$can_accept_agreement = !empty($modSettings['requireAgreement']) && canRequireAgreement();
$can_accept_privacy_policy = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy();
if ($can_accept_agreement || $can_accept_privacy_policy)
{
checkSession();
if ($can_accept_agreement)
{
$smcFunc['db_insert']('replace',
'{db_prefix}themes',
array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string', 'value' => 'string'),
array($user_info['id'], 1, 'agreement_accepted', time()),
array('id_member', 'id_theme', 'variable')
);
logAction('agreement_accepted', array('applicator' => $user_info['id']), 'user');
}
if ($can_accept_privacy_policy)
{
$smcFunc['db_insert']('replace',
'{db_prefix}themes',
array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string', 'value' => 'string'),
array($user_info['id'], 1, 'policy_accepted', time()),
array('id_member', 'id_theme', 'variable')
);
logAction('policy_accepted', array('applicator' => $user_info['id']), 'user');
}
}
// Redirect back to chasing those squirrels, er, viewing those memes.
redirectexit(!empty($_SESSION['redirect_url']) ? $_SESSION['redirect_url'] : '');
}
?>

538
Sources/Attachments.php Normal file
View file

@ -0,0 +1,538 @@
<?php
/**
* This file contains handling attachments.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Class Attachments
*
* This class handles adding/deleting attachments
*/
class Attachments
{
/**
* @var int $_msg The ID of the message this attachment is associated with
*/
protected $_msg = 0;
/**
* @var int|null $_board The ID of the board this attachment's post is in or null if it's not set
*/
protected $_board = null;
/**
* @var string|bool $_attachmentUploadDir An array of info about attachment upload directories or false
*/
protected $_attachmentUploadDir = false;
/**
* @var string $_attchDir The path to the current attachment directory
*/
protected $_attchDir = '';
/**
* @var int $_currentAttachmentUploadDir ID of the current attachment directory
*/
protected $_currentAttachmentUploadDir;
/**
* @var bool $_canPostAttachment Whether or not an attachment can be posted
*/
protected $_canPostAttachment;
/**
* @var array $_generalErrors An array of information about any errors that occurred
*/
protected $_generalErrors = array();
/**
* @var mixed $_initialError Not used?
*/
protected $_initialError;
/**
* @var array $_attachments Not used?
*/
protected $_attachments = array();
/**
* @var array $_attachResults An array of information about the results of each file
*/
protected $_attachResults = array();
/**
* @var array $_attachSuccess An array of information about successful attachments
*/
protected $_attachSuccess = array();
/**
* @var array $_response An array of response information. @used-by \sendResponse() when adding attachments
*/
protected $_response = array(
'error' => true,
'data' => array(),
'extra' => '',
);
/**
* @var array $_subActions An array of all valid sub-actions
*/
protected $_subActions = array(
'add',
'delete',
);
/**
* @var string|bool $_sa The current sub-action, or false if there isn't one
*/
protected $_sa = false;
/**
* Attachments constructor.
*
* Sets up some initial information - the message ID, board, current attachment upload dir, etc.
*/
public function __construct()
{
global $modSettings, $context;
$this->_msg = (int) !empty($_REQUEST['msg']) ? $_REQUEST['msg'] : 0;
$this->_board = (int) !empty($_REQUEST['board']) ? $_REQUEST['board'] : null;
$this->_currentAttachmentUploadDir = $modSettings['currentAttachmentUploadDir'];
$this->_attachmentUploadDir = $modSettings['attachmentUploadDir'];
$this->_attchDir = $context['attach_dir'] = $this->_attachmentUploadDir[$modSettings['currentAttachmentUploadDir']];
$this->_canPostAttachment = $context['can_post_attachment'] = !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1 && (allowedTo('post_attachment', $this->_board) || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments', $this->_board)));
}
/**
* Handles calling the appropriate function based on the sub-action
*/
public function call()
{
global $smcFunc, $sourcedir;
require_once($sourcedir . '/Subs-Attachments.php');
// Need this. For reasons...
loadLanguage('Post');
$this->_sa = !empty($_REQUEST['sa']) ? $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($_REQUEST['sa'])) : false;
if ($this->_canPostAttachment && $this->_sa && in_array($this->_sa, $this->_subActions))
$this->{$this->_sa}();
// Just send a generic message.
else
$this->setResponse(array(
'text' => $this->_sa == 'add' ? 'attach_error_title' : 'attached_file_deleted_error',
'type' => 'error',
'data' => false,
));
// Back to the future, oh, to the browser!
$this->sendResponse();
}
/**
* Handles deleting the attachment
*/
public function delete()
{
global $sourcedir;
// Need this, don't ask why just nod your head.
require_once($sourcedir . '/ManageAttachments.php');
$attachID = !empty($_REQUEST['attach']) && is_numeric($_REQUEST['attach']) ? (int) $_REQUEST['attach'] : 0;
// Need something to work with.
if (!$attachID || (!empty($_SESSION['already_attached']) && !isset($_SESSION['already_attached'][$attachID])))
return $this->setResponse(array(
'text' => 'attached_file_deleted_error',
'type' => 'error',
'data' => false,
));
// Lets pass some params and see what happens :P
$affectedMessage = removeAttachments(array('id_attach' => $attachID), '', true, true);
// Gotta also remove the attachment from the session var.
unset($_SESSION['already_attached'][$attachID]);
// $affectedMessage returns an empty array array(0) which php treats as non empty... awesome...
$this->setResponse(array(
'text' => !empty($affectedMessage) ? 'attached_file_deleted' : 'attached_file_deleted_error',
'type' => !empty($affectedMessage) ? 'info' : 'warning',
'data' => $affectedMessage,
));
}
/**
* Handles adding an attachment
*/
public function add()
{
// You gotta be able to post attachments.
if (!$this->_canPostAttachment)
return $this->setResponse(array(
'text' => 'attached_file_cannot',
'type' => 'error',
'data' => false,
));
// Process them at once!
$this->processAttachments();
// The attachments was created and moved the the right folder, time to update the DB.
if (!empty($_SESSION['temp_attachments']))
$this->createAttach();
// Set the response.
$this->setResponse();
}
/**
* Moves an attachment to the proper directory and set the relevant data into $_SESSION['temp_attachments']
*/
protected function processAttachments()
{
global $context, $modSettings, $smcFunc, $user_info, $txt;
if (!isset($_FILES['attachment']['name']))
$_FILES['attachment']['tmp_name'] = array();
// If there are attachments, calculate the total size and how many.
$context['attachments']['total_size'] = 0;
$context['attachments']['quantity'] = 0;
// If this isn't a new post, check the current attachments.
if (isset($_REQUEST['msg']))
{
$context['attachments']['quantity'] = count($context['current_attachments']);
foreach ($context['current_attachments'] as $attachment)
$context['attachments']['total_size'] += $attachment['size'];
}
// A bit of house keeping first.
if (!empty($_SESSION['temp_attachments']) && count($_SESSION['temp_attachments']) == 1)
unset($_SESSION['temp_attachments']);
// Our infamous SESSION var, we are gonna have soo much fun with it!
if (!isset($_SESSION['temp_attachments']))
$_SESSION['temp_attachments'] = array();
// Make sure we're uploading to the right place.
if (!empty($modSettings['automanage_attachments']))
automanage_attachments_check_directory();
// Is the attachments folder actually there?
if (!empty($context['dir_creation_error']))
$this->_generalErrors[] = $context['dir_creation_error'];
// The current attach folder ha some issues...
elseif (!is_dir($this->_attchDir))
{
$this->_generalErrors[] = 'attach_folder_warning';
log_error(sprintf($txt['attach_folder_admin_warning'], $this->_attchDir), 'critical');
}
// If this isn't a new post, check the current attachments.
if (empty($this->_generalErrors) && $this->_msg)
{
$context['attachments'] = array();
$request = $smcFunc['db_query']('', '
SELECT COUNT(*), SUM(size)
FROM {db_prefix}attachments
WHERE id_msg = {int:id_msg}
AND attachment_type = {int:attachment_type}',
array(
'id_msg' => (int) $this->_msg,
'attachment_type' => 0,
)
);
list ($context['attachments']['quantity'], $context['attachments']['total_size']) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
}
else
$context['attachments'] = array(
'quantity' => 0,
'total_size' => 0,
);
// Check for other general errors here.
// If we have an initial error, delete the files.
if (!empty($this->_generalErrors))
{
// And delete the files 'cos they ain't going nowhere.
foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy)
if (file_exists($_FILES['attachment']['tmp_name'][$n]))
unlink($_FILES['attachment']['tmp_name'][$n]);
$_FILES['attachment']['tmp_name'] = array();
// No point in going further with this.
return;
}
// Loop through $_FILES['attachment'] array and move each file to the current attachments folder.
foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy)
{
if ($_FILES['attachment']['name'][$n] == '')
continue;
// First, let's first check for PHP upload errors.
$errors = array();
if (!empty($_FILES['attachment']['error'][$n]))
{
if ($_FILES['attachment']['error'][$n] == 2)
$errors[] = array('file_too_big', array($modSettings['attachmentSizeLimit']));
else
log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$n]]);
// Log this one, because...
if ($_FILES['attachment']['error'][$n] == 6)
log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_6'], 'critical');
// Weird, no errors were cached, still fill out a generic one.
if (empty($errors))
$errors[] = 'attach_php_error';
}
// Try to move and rename the file before doing any more checks on it.
$attachID = 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand());
$destName = $this->_attchDir . '/' . $attachID;
// No errors, YAY!
if (empty($errors))
{
// The reported MIME type of the attachment might not be reliable.
$detected_mime_type = get_mime_type($_FILES['attachment']['tmp_name'][$n], true);
if ($detected_mime_type !== false)
$_FILES['attachment']['type'][$n] = $detected_mime_type;
$_SESSION['temp_attachments'][$attachID] = array(
'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])),
'tmp_name' => $destName,
'size' => $_FILES['attachment']['size'][$n],
'type' => $_FILES['attachment']['type'][$n],
'id_folder' => $modSettings['currentAttachmentUploadDir'],
'errors' => array(),
);
// Move the file to the attachments folder with a temp name for now.
if (@move_uploaded_file($_FILES['attachment']['tmp_name'][$n], $destName))
smf_chmod($destName, 0644);
// This is madness!!
else
{
// File couldn't be moved.
$_SESSION['temp_attachments'][$attachID]['errors'][] = 'attach_timeout';
if (file_exists($_FILES['attachment']['tmp_name'][$n]))
unlink($_FILES['attachment']['tmp_name'][$n]);
}
}
// Fill up a nice array with some data from the file and the errors encountered so far.
else
{
$_SESSION['temp_attachments'][$attachID] = array(
'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])),
'tmp_name' => $destName,
'errors' => $errors,
);
if (file_exists($_FILES['attachment']['tmp_name'][$n]))
unlink($_FILES['attachment']['tmp_name'][$n]);
}
// If there's no errors to this point. We still do need to apply some additional checks before we are finished.
if (empty($_SESSION['temp_attachments'][$attachID]['errors']))
attachmentChecks($attachID);
}
// Mod authors, finally a hook to hang an alternate attachment upload system upon
// Upload to the current attachment folder with the file name $attachID or 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand())
// Populate $_SESSION['temp_attachments'][$attachID] with the following:
// name => The file name
// tmp_name => Path to the temp file ($this->_attchDir . '/' . $attachID).
// size => File size (required).
// type => MIME type (optional if not available on upload).
// id_folder => $modSettings['currentAttachmentUploadDir']
// errors => An array of errors (use the index of the $txt variable for that error).
// Template changes can be done using "integrate_upload_template".
call_integration_hook('integrate_attachment_upload', array());
}
/**
* Actually attaches the file
*/
protected function createAttach()
{
global $txt, $user_info, $modSettings;
// Create an empty session var to keep track of all the files we attached.
if (!isset($_SESSION['already_attached']))
$_SESSION['already_attached'] = array();
foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
{
$attachmentOptions = array(
'post' => $this->_msg,
'poster' => $user_info['id'],
'name' => $attachment['name'],
'tmp_name' => $attachment['tmp_name'],
'size' => isset($attachment['size']) ? $attachment['size'] : 0,
'mime_type' => isset($attachment['type']) ? $attachment['type'] : '',
'id_folder' => isset($attachment['id_folder']) ? $attachment['id_folder'] : $modSettings['currentAttachmentUploadDir'],
'approved' => !$modSettings['postmod_active'] || allowedTo('post_attachment'),
'errors' => array(),
);
if (empty($attachment['errors']))
{
if (createAttachment($attachmentOptions))
{
// Avoid JS getting confused.
$attachmentOptions['attachID'] = $attachmentOptions['id'];
unset($attachmentOptions['id']);
$_SESSION['already_attached'][$attachmentOptions['attachID']] = $attachmentOptions['attachID'];
if (!empty($attachmentOptions['thumb']))
$_SESSION['already_attached'][$attachmentOptions['thumb']] = $attachmentOptions['thumb'];
if ($this->_msg)
assignAttachments($_SESSION['already_attached'], $this->_msg);
}
}
else
{
// Sort out the errors for display and delete any associated files.
$log_these = array('attachments_no_create', 'attachments_no_write', 'attach_timeout', 'ran_out_of_space', 'cant_access_upload_path', 'attach_0_byte_file');
foreach ($attachment['errors'] as $error)
{
$attachmentOptions['errors'][] = sprintf($txt['attach_warning'], $attachment['name']);
if (!is_array($error))
{
$attachmentOptions['errors'][] = $txt[$error];
if (in_array($error, $log_these))
log_error($attachment['name'] . ': ' . $txt[$error], 'critical');
}
else
$attachmentOptions['errors'][] = vsprintf($txt[$error[0]], (array) $error[1]);
}
if (file_exists($attachment['tmp_name']))
unlink($attachment['tmp_name']);
}
// You don't need to know.
unset($attachmentOptions['tmp_name']);
unset($attachmentOptions['destination']);
// Regardless of errors, pass the results.
$this->_attachResults[] = $attachmentOptions;
}
// Temp save this on the db.
if (!empty($_SESSION['already_attached']))
$this->_attachSuccess = $_SESSION['already_attached'];
unset($_SESSION['temp_attachments']);
// Allow user to see previews for all of this post's attachments, even if the post hasn't been submitted yet.
if (!isset($_SESSION['attachments_can_preview']))
$_SESSION['attachments_can_preview'] = array();
if (!empty($_SESSION['already_attached']))
$_SESSION['attachments_can_preview'] += array_fill_keys(array_keys($_SESSION['already_attached']), true);
}
/**
* Sets up the response information
*
* @param array $data Data for the response if we're not adding an attachment
*/
protected function setResponse($data = array())
{
global $txt;
// Some default values in case something is missed or neglected :P
$this->_response = array(
'text' => 'attach_php_error',
'type' => 'error',
'data' => false,
);
// Adding needs some VIP treatment.
if ($this->_sa == 'add')
{
// Is there any generic errors? made some sense out of them!
if ($this->_generalErrors)
foreach ($this->_generalErrors as $k => $v)
$this->_generalErrors[$k] = (is_array($v) ? vsprintf($txt[$v[0]], (array) $v[1]) : $txt[$v]);
// Gotta urlencode the filename.
if ($this->_attachResults)
foreach ($this->_attachResults as $k => $v)
$this->_attachResults[$k]['name'] = urlencode($this->_attachResults[$k]['name']);
$this->_response = array(
'files' => $this->_attachResults ? $this->_attachResults : false,
'generalErrors' => $this->_generalErrors ? $this->_generalErrors : false,
);
}
// Rest of us mere mortals gets no special treatment...
elseif (!empty($data))
if (!empty($data['text']) && !empty($txt[$data['text']]))
$this->_response['text'] = $txt[$data['text']];
}
/**
* Sends the response data
*/
protected function sendResponse()
{
global $smcFunc, $modSettings, $context;
ob_end_clean();
if (!empty($modSettings['enableCompressedOutput']))
@ob_start('ob_gzhandler');
else
ob_start();
// Set the header.
header('content-type: application/json; charset=' . $context['character_set'] . '');
echo $smcFunc['json_encode']($this->_response ? $this->_response : array());
// Done.
obExit(false);
die;
}
}
?>

150
Sources/BoardIndex.php Normal file
View file

@ -0,0 +1,150 @@
<?php
/**
* The single function this file contains is used to display the main
* board index.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
if (!defined('SMF'))
die('No direct access...');
/**
* This function shows the board index.
* It uses the BoardIndex template, and main sub template.
* It updates the most online statistics.
* It is accessed by ?action=boardindex.
*/
function BoardIndex()
{
global $txt, $user_info, $sourcedir, $modSettings, $context, $settings, $scripturl;
loadTemplate('BoardIndex');
$context['template_layers'][] = 'boardindex_outer';
// Set a canonical URL for this page.
$context['canonical_url'] = $scripturl;
// Do not let search engines index anything if there is a random thing in $_GET.
if (!empty($_GET))
$context['robot_no_index'] = true;
// Retrieve the categories and boards.
require_once($sourcedir . '/Subs-BoardIndex.php');
$boardIndexOptions = array(
'include_categories' => true,
'base_level' => 0,
'parent_id' => 0,
'set_latest_post' => true,
'countChildPosts' => !empty($modSettings['countChildPosts']),
);
$context['categories'] = getBoardIndex($boardIndexOptions);
// Now set up for the info center.
$context['info_center'] = array();
// Retrieve the latest posts if the theme settings require it.
if (!empty($settings['number_recent_posts']))
{
if ($settings['number_recent_posts'] > 1)
{
$latestPostOptions = array(
'number_posts' => $settings['number_recent_posts'],
);
$context['latest_posts'] = cache_quick_get('boardindex-latest_posts:' . md5($user_info['query_wanna_see_board'] . $user_info['language']), 'Subs-Recent.php', 'cache_getLastPosts', array($latestPostOptions));
}
if (!empty($context['latest_posts']) || !empty($context['latest_post']))
$context['info_center'][] = array(
'tpl' => 'recent',
'txt' => 'recent_posts',
);
}
// Load the calendar?
if (!empty($modSettings['cal_enabled']) && allowedTo('calendar_view'))
{
// Retrieve the calendar data (events, birthdays, holidays).
$eventOptions = array(
'include_holidays' => $modSettings['cal_showholidays'] > 1,
'include_birthdays' => $modSettings['cal_showbdays'] > 1,
'include_events' => $modSettings['cal_showevents'] > 1,
'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
);
$context += cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
// Whether one or multiple days are shown on the board index.
$context['calendar_only_today'] = $modSettings['cal_days_for_index'] == 1;
// This is used to show the "how-do-I-edit" help.
$context['calendar_can_edit'] = allowedTo('calendar_edit_any');
if (!empty($context['show_calendar']))
$context['info_center'][] = array(
'tpl' => 'calendar',
'txt' => $context['calendar_only_today'] ? 'calendar_today' : 'calendar_upcoming',
);
}
// And stats.
$context['show_stats'] = allowedTo('view_stats') && !empty($modSettings['trackStats']);
if ($settings['show_stats_index'])
$context['info_center'][] = array(
'tpl' => 'stats',
'txt' => 'forum_stats',
);
// Now the online stuff
require_once($sourcedir . '/Subs-MembersOnline.php');
$membersOnlineOptions = array(
'show_hidden' => allowedTo('moderate_forum'),
'sort' => 'log_time',
'reverse_sort' => true,
);
$context += getMembersOnlineStats($membersOnlineOptions);
$context['show_buddies'] = !empty($user_info['buddies']);
$context['show_who'] = allowedTo('who_view') && !empty($modSettings['who_enabled']);
$context['info_center'][] = array(
'tpl' => 'online',
'txt' => 'online_users',
);
// Track most online statistics? (Subs-MembersOnline.php)
if (!empty($modSettings['trackStats']))
trackStatsUsersOnline($context['num_guests'] + $context['num_users_online']);
// Are we showing all membergroups on the board index?
if (!empty($settings['show_group_key']))
$context['membergroups'] = cache_quick_get('membergroup_list', 'Subs-Membergroups.php', 'cache_getMembergroupList', array());
// And back to normality.
$context['page_title'] = sprintf($txt['forum_index'], $context['forum_name']);
// Mark read button
$context['mark_read_button'] = array(
'markread' => array('text' => 'mark_as_read', 'image' => 'markread.png', 'custom' => 'data-confirm="' . $txt['are_sure_mark_read'] . '"', 'class' => 'you_sure', 'url' => $scripturl . '?action=markasread;sa=all;' . $context['session_var'] . '=' . $context['session_id']),
);
// Replace the collapse and expand default alts.
addJavaScriptVar('smf_expandAlt', $txt['show_category'], true);
addJavaScriptVar('smf_collapseAlt', $txt['hide_category'], true);
// Allow mods to add additional buttons here
call_integration_hook('integrate_mark_read_button');
if (!empty($settings['show_newsfader']))
{
loadJavaScriptFile('slippry.min.js', array(), 'smf_jquery_slippry');
loadCSSFile('slider.min.css', array(), 'smf_jquery_slider');
}
}
?>

View file

@ -0,0 +1,96 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
namespace SMF\Cache\APIs;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
if (!defined('SMF'))
die('No direct access...');
/**
* Our Cache API class
*
* @package CacheAPI
*/
class Apcu extends CacheApi implements CacheApiInterface
{
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
$supported = function_exists('apcu_fetch') && function_exists('apcu_store');
if ($test)
return $supported;
return parent::isSupported() && $supported;
}
/**
* {@inheritDoc}
*/
public function connect()
{
return true;
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
$value = apcu_fetch($key . 'smf');
return !empty($value) ? $value : null;
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
// An extended key is needed to counteract a bug in APC.
if ($value === null)
return apcu_delete($key . 'smf');
else
return apcu_store($key . 'smf', $value, $ttl !== null ? $ttl : $this->ttl);
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
$this->invalidateCache();
return apcu_clear_cache();
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
return phpversion('apcu');
}
}
?>

View file

@ -0,0 +1,274 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
namespace SMF\Cache\APIs;
use GlobIterator;
use FilesystemIterator;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
if (!defined('SMF'))
die('No direct access...');
/**
* Our Cache API class
*
* @package CacheAPI
*/
class FileBased extends CacheApi implements CacheApiInterface
{
/**
* @var string The path to the current $cachedir directory.
*/
private $cachedir = null;
/**
* {@inheritDoc}
*/
public function __construct()
{
parent::__construct();
// Set our default cachedir.
$this->setCachedir();
}
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
$supported = is_writable($this->cachedir);
if ($test)
return $supported;
return parent::isSupported() && $supported;
}
private function readFile($file)
{
if (($fp = @fopen($file, 'rb')) !== false)
{
if (!flock($fp, LOCK_SH))
{
fclose($fp);
return false;
}
$string = '';
while (!feof($fp))
$string .= fread($fp, 8192);
flock($fp, LOCK_UN);
fclose($fp);
return $string;
}
return false;
}
private function writeFile($file, $string)
{
if (($fp = fopen($file, 'cb')) !== false)
{
if (!flock($fp, LOCK_EX))
{
fclose($fp);
return false;
}
ftruncate($fp, 0);
$bytes = 0;
$pieces = str_split($string, 8192);
foreach ($pieces as $piece)
{
if (($val = fwrite($fp, $piece, 8192)) !== false)
$bytes += $val;
else
return false;
}
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return $bytes;
}
return false;
}
/**
* {@inheritDoc}
*/
public function connect()
{
return true;
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$file = sprintf('%s/data_%s.cache',
$this->cachedir,
$this->prefix . strtr($key, ':/', '-_')
);
// SMF Data returns $value and $expired. $expired has a unix timestamp of when this expires.
if (file_exists($file) && ($raw = $this->readFile($file)) !== false)
{
if (($value = smf_json_decode($raw, true, false)) !== array() && isset($value['expiration']) && $value['expiration'] >= time())
return $value['value'];
else
@unlink($file);
}
return null;
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$file = sprintf('%s/data_%s.cache',
$this->cachedir,
$this->prefix . strtr($key, ':/', '-_')
);
$ttl = $ttl !== null ? $ttl : $this->ttl;
if ($value === null)
@unlink($file);
else
{
$cache_data = json_encode(
array(
'expiration' => time() + $ttl,
'value' => $value
),
JSON_NUMERIC_CHECK
);
// Write out the cache file, check that the cache write was successful; all the data must be written
// If it fails due to low diskspace, or other, remove the cache file
if ($this->writeFile($file, $cache_data) !== strlen($cache_data))
{
@unlink($file);
return false;
}
else
return true;
}
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
// No directory = no game.
if (!is_dir($this->cachedir))
return;
// Remove the files in SMF's own disk cache, if any
$files = new GlobIterator($this->cachedir . '/' . $type . '*.cache', FilesystemIterator::NEW_CURRENT_AND_KEY);
foreach ($files as $file => $info)
unlink($this->cachedir . '/' . $file);
// Make this invalid.
$this->invalidateCache();
return true;
}
/**
* {@inheritDoc}
*/
public function invalidateCache()
{
// We don't worry about $cachedir here, since the key is based on the real $cachedir.
parent::invalidateCache();
// Since SMF is file based, be sure to clear the statcache.
clearstatcache();
return true;
}
/**
* {@inheritDoc}
*/
public function cacheSettings(array &$config_vars)
{
global $context, $txt;
$class_name = $this->getImplementationClassKeyName();
$class_name_txt_key = strtolower($class_name);
$config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings'];
$config_vars[] = array('cachedir', $txt['cachedir'], 'file', 'text', 36, 'cache_cachedir');
if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#cachedir").prop("disabled", cache_type != "'. $class_name .'");
});';
}
/**
* Sets the $cachedir or uses the SMF default $cachedir..
*
* @access public
* @param string $dir A valid path
* @return boolean If this was successful or not.
*/
public function setCachedir($dir = null)
{
global $cachedir;
// If its invalid, use SMF's.
if (is_null($dir) || !is_writable($dir))
$this->cachedir = $cachedir;
else
$this->cachedir = $dir;
}
/**
* Gets the current $cachedir.
*
* @access public
* @return string the value of $ttl.
*/
public function getCachedir()
{
return $this->cachedir;
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
return SMF_VERSION;
}
}
?>

View file

@ -0,0 +1,193 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/
namespace SMF\Cache\APIs;
use Memcache;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
if (!defined('SMF'))
die('No direct access...');
/**
* Our Cache API class
*
* @package CacheAPI
*/
class MemcacheImplementation extends CacheApi implements CacheApiInterface
{
const CLASS_KEY = 'cache_memcached';
/**
* @var Memcache The memcache instance.
*/
private $memcache = null;
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
global $cache_memcached;
$supported = class_exists('Memcache');
if ($test)
return $supported;
return parent::isSupported() && $supported && !empty($cache_memcached);
}
/**
* {@inheritDoc}
*/
public function connect()
{
global $db_persist, $cache_memcached;
$this->memcache = new Memcache();
$servers = explode(',', $cache_memcached);
$port = 0;
// Don't try more times than we have servers!
$connected = false;
$level = 0;
// We should keep trying if a server times out, but only for the amount of servers we have.
while (!$connected && $level < count($servers))
{
++$level;
$server = trim($servers[array_rand($servers)]);
// No server, can't connect to this.
if (empty($server))
continue;
// Normal host names do not contain slashes, while e.g. unix sockets do. Assume alternative transport pipe with port 0.
if (strpos($server, '/') !== false)
$host = $server;
else
{
$server = explode(':', $server);
$host = $server[0];
$port = isset($server[1]) ? $server[1] : 11211;
}
// Don't wait too long: yes, we want the server, but we might be able to run the query faster!
if (empty($db_persist))
$connected = $this->memcache->connect($host, $port);
else
$connected = $this->memcache->pconnect($host, $port);
}
return $connected;
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
$value = $this->memcache->get($key);
// $value should return either data or false (from failure, key not found or empty array).
if ($value === false)
return null;
return $value;
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
return $this->memcache->set($key, $value, 0, $ttl !== null ? $ttl : $this->ttl);
}
/**
* {@inheritDoc}
*/
public function quit()
{
return $this->memcache->close();
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
$this->invalidateCache();
return $this->memcache->flush();
}
/**
* {@inheritDoc}
*/
public function cacheSettings(array &$config_vars)
{
global $context, $txt;
if (!in_array($txt[self::CLASS_KEY .'_settings'], $config_vars))
{
$config_vars[] = $txt[self::CLASS_KEY .'_settings'];
$config_vars[] = array(
self::CLASS_KEY,
$txt[self::CLASS_KEY .'_servers'],
'file',
'text',
0,
'subtext' => $txt[self::CLASS_KEY .'_servers_subtext']);
}
if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#'. self::CLASS_KEY .'").prop("disabled", cache_type != "MemcacheImplementation" && cache_type != "MemcachedImplementation");
});';
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
if (!is_object($this->memcache))
return false;
// This gets called in Subs-Admin getServerVersions when loading up support information. If we can't get a connection, return nothing.
$result = $this->memcache->getVersion();
if (!empty($result))
return $result;
return false;
}
}
?>

View file

@ -0,0 +1,213 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/
namespace SMF\Cache\APIs;
use Memcached;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
if (!defined('SMF'))
die('No direct access...');
/**
* Our Cache API class
*
* @package CacheAPI
*/
class MemcachedImplementation extends CacheApi implements CacheApiInterface
{
const CLASS_KEY = 'cache_memcached';
/** @var Memcached The memcache instance. */
private $memcached = null;
/** @var string[] */
private $servers;
/**
* {@inheritDoc}
*/
public function __construct()
{
global $cache_memcached;
$this->servers = array_map(
function($server)
{
if (strpos($server, '/') !== false)
return array($server, 0);
else
{
$server = explode(':', $server);
return array($server[0], isset($server[1]) ? (int) $server[1] : 11211);
}
},
explode(',', $cache_memcached)
);
parent::__construct();
}
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
global $cache_memcached;
$supported = class_exists('Memcached');
if ($test)
return $supported;
return parent::isSupported() && $supported && !empty($cache_memcached);
}
/**
* {@inheritDoc}
*/
public function connect()
{
$this->memcached = new Memcached;
return $this->addServers();
}
/**
* Add memcached servers.
*
* Don't add servers if they already exist. Ideal for persistent connections.
*
* @return bool True if there are servers in the daemon, false if not.
*/
protected function addServers()
{
$currentServers = $this->memcached->getServerList();
$retVal = !empty($currentServers);
foreach ($this->servers as $server)
{
// Figure out if we have this server or not
$foundServer = false;
foreach ($currentServers as $currentServer)
{
if ($server[0] == $currentServer['host'] && $server[1] == $currentServer['port'])
{
$foundServer = true;
break;
}
}
// Found it?
if (empty($foundServer))
$retVal |= $this->memcached->addServer($server[0], $server[1]);
}
return $retVal;
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
$value = $this->memcached->get($key);
// $value should return either data or false (from failure, key not found or empty array).
if ($value === false)
return null;
return $value;
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
return $this->memcached->set($key, $value, $ttl !== null ? $ttl : $this->ttl);
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
$this->invalidateCache();
// Memcached accepts a delay parameter, always use 0 (instant).
return $this->memcached->flush(0);
}
/**
* {@inheritDoc}
*/
public function quit()
{
return $this->memcached->quit();
}
/**
* {@inheritDoc}
*/
public function cacheSettings(array &$config_vars)
{
global $context, $txt;
if (!in_array($txt[self::CLASS_KEY .'_settings'], $config_vars))
{
$config_vars[] = $txt[self::CLASS_KEY .'_settings'];
$config_vars[] = array(
self::CLASS_KEY,
$txt[self::CLASS_KEY .'_servers'],
'file',
'text',
0,
'subtext' => $txt[self::CLASS_KEY .'_servers_subtext']);
}
if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#'. self::CLASS_KEY .'").prop("disabled", cache_type != "MemcacheImplementation" && cache_type != "MemcachedImplementation");
});';
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
if (!is_object($this->memcached))
return false;
// This gets called in Subs-Admin getServerVersions when loading up support information. If we can't get a connection, return nothing.
$result = $this->memcached->getVersion();
if (!empty($result))
return current($result);
return false;
}
}
?>

View file

@ -0,0 +1,220 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
namespace SMF\Cache\APIs;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
if (!defined('SMF'))
die('No direct access...');
/**
* PostgreSQL Cache API class
*
* @package CacheAPI
*/
class Postgres extends CacheApi implements CacheApiInterface
{
/** @var string */
private $db_prefix;
/** @var resource result of pg_connect. */
private $db_connection;
public function __construct()
{
global $db_prefix, $db_connection;
$this->db_prefix = $db_prefix;
$this->db_connection = $db_connection;
parent::__construct();
}
/**
* {@inheritDoc}
*/
public function connect()
{
$result = pg_query_params($this->db_connection, 'SELECT 1
FROM pg_tables
WHERE schemaname = $1
AND tablename = $2',
array(
'public',
$this->db_prefix . 'cache',
)
);
if (pg_affected_rows($result) === 0)
pg_query($this->db_connection, 'CREATE UNLOGGED TABLE ' . $this->db_prefix . 'cache (key text, value text, ttl bigint, PRIMARY KEY (key))');
$this->prepareQueries(
array(
'smf_cache_get_data',
'smf_cache_put_data',
'smf_cache_delete_data',
),
array(
'SELECT value FROM ' . $this->db_prefix . 'cache WHERE key = $1 AND ttl >= $2 LIMIT 1',
'INSERT INTO ' . $this->db_prefix . 'cache(key,value,ttl) VALUES($1,$2,$3)
ON CONFLICT(key) DO UPDATE SET value = $2, ttl = $3',
'DELETE FROM ' . $this->db_prefix . 'cache WHERE key = $1',
)
);
return true;
}
/**
* Stores a prepared SQL statement, ensuring that it's not done twice.
*
* @param array $stmtnames
* @param array $queries
*/
private function prepareQueries(array $stmtnames, array $queries)
{
$result = pg_query_params(
$this->db_connection,
'SELECT name FROM pg_prepared_statements WHERE name = ANY ($1)',
array('{' . implode(', ', $stmtnames) . '}')
);
$arr = pg_num_rows($result) == 0 ? array() : array_map(
function($el)
{
return $el['name'];
},
pg_fetch_all($result)
);
foreach ($stmtnames as $idx => $stmtname)
if (!in_array($stmtname, $arr))
pg_prepare($this->db_connection, $stmtname, $queries[$idx]);
}
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
global $smcFunc;
if ($smcFunc['db_title'] !== POSTGRE_TITLE)
return false;
$result = pg_query($this->db_connection, 'SHOW server_version_num');
$res = pg_fetch_assoc($result);
if ($res['server_version_num'] < 90500)
return false;
return $test ? true : parent::isSupported();
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$result = pg_execute($this->db_connection, 'smf_cache_get_data', array($key, time()));
if (pg_affected_rows($result) === 0)
return null;
$res = pg_fetch_assoc($result);
return $res['value'];
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$ttl = time() + (int) ($ttl !== null ? $ttl : $this->ttl);
if ($value === null)
$result = pg_execute($this->db_connection, 'smf_cache_delete_data', array($key));
else
$result = pg_execute($this->db_connection, 'smf_cache_put_data', array($key, $value, $ttl));
return pg_affected_rows($result) > 0;
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
if ($type == 'expired')
pg_query($this->db_connection, 'DELETE FROM ' . $this->db_prefix . 'cache WHERE ttl < ' . time() . ';');
else
pg_query($this->db_connection, 'TRUNCATE ' . $this->db_prefix . 'cache');
$this->invalidateCache();
return true;
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
return pg_version($this->db_connection)['server'];
}
/**
* {@inheritDoc}
*/
public function housekeeping()
{
$this->createTempTable();
$this->cleanCache();
$this->retrieveData();
$this->deleteTempTable();
}
/**
* Create the temp table of valid data.
*
* @return void
*/
private function createTempTable()
{
pg_query($this->db_connection, 'CREATE LOCAL TEMP TABLE IF NOT EXISTS ' . $this->db_prefix . 'cache_tmp AS SELECT * FROM ' . $this->db_prefix . 'cache WHERE ttl >= ' . time());
}
/**
* Delete the temp table.
*
* @return void
*/
private function deleteTempTable()
{
pg_query($this->db_connection, 'DROP TABLE IF EXISTS ' . $this->db_prefix . 'cache_tmp');
}
/**
* Retrieve the valid data from temp table.
*
* @return void
*/
private function retrieveData()
{
pg_query($this->db_connection, 'INSERT INTO ' . $this->db_prefix . 'cache SELECT * FROM ' . $this->db_prefix . 'cache_tmp ON CONFLICT DO NOTHING');
}
}
?>

207
Sources/Cache/APIs/Sqlite.php Executable file
View file

@ -0,0 +1,207 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/
namespace SMF\Cache\APIs;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
use SQLite3;
if (!defined('SMF'))
die('No direct access...');
/**
* SQLite Cache API class
*
* @package CacheAPI
*/
class Sqlite extends CacheApi implements CacheApiInterface
{
/**
* @var string The path to the current $cachedir directory.
*/
private $cachedir = null;
/**
* @var SQLite3
*/
private $cacheDB = null;
public function __construct()
{
parent::__construct();
// Set our default cachedir.
$this->setCachedir();
}
/**
* {@inheritDoc}
*/
public function connect()
{
$database = $this->cachedir . '/' . 'SQLite3Cache.db3';
$this->cacheDB = new SQLite3($database);
$this->cacheDB->busyTimeout(1000);
if (filesize($database) == 0)
{
$this->cacheDB->exec('CREATE TABLE cache (key text unique, value blob, ttl int);');
$this->cacheDB->exec('CREATE INDEX ttls ON cache(ttl);');
}
}
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
$supported = class_exists("SQLite3") && is_writable($this->cachedir);
if ($test)
return $supported;
return parent::isSupported() && $supported;
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$query = 'SELECT value FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\' AND ttl >= ' . time() . ' LIMIT 1';
$result = $this->cacheDB->query($query);
$value = null;
while ($res = $result->fetchArray(SQLITE3_ASSOC))
$value = $res['value'];
return !empty($value) ? $value : null;
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$ttl = time() + (int) ($ttl !== null ? $ttl : $this->ttl);
if ($value === null)
$query = 'DELETE FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\';';
else
$query = 'REPLACE INTO cache VALUES (\'' . $this->cacheDB->escapeString($key) . '\', \'' . $this->cacheDB->escapeString($value) . '\', ' . $ttl . ');';
$result = $this->cacheDB->exec($query);
return $result;
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
if ($type == 'expired')
$query = 'DELETE FROM cache WHERE ttl < ' . time() . ';';
else
$query = 'DELETE FROM cache;';
$result = $this->cacheDB->exec($query);
$query = 'VACUUM;';
$this->cacheDB->exec($query);
$this->invalidateCache();
return $result;
}
/**
* {@inheritDoc}
*/
public function cacheSettings(array &$config_vars)
{
global $context, $txt;
$class_name = $this->getImplementationClassKeyName();
$class_name_txt_key = strtolower($class_name);
$config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings'];
$config_vars[] = array(
'cachedir_'. $class_name_txt_key,
$txt['cachedir_'. $class_name_txt_key],
'file',
'text',
36,
'cache_'. $class_name_txt_key .'_cachedir',
);
if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#cachedir_'. $class_name_txt_key .'").prop("disabled", cache_type != "'. $class_name .'");
});';
}
/**
* Sets the $cachedir or uses the SMF default $cachedir..
*
* @access public
*
* @param string $dir A valid path
*
* @return boolean If this was successful or not.
*/
public function setCachedir($dir = null)
{
global $cachedir, $cachedir_sqlite, $sourcedir;
// If its invalid, use SMF's.
if (!isset($dir) || !is_writable($dir))
{
if (!isset($cachedir_sqlite) || !is_writable($cachedir_sqlite))
{
$cachedir_sqlite = $cachedir;
require_once($sourcedir . '/Subs-Admin.php');
updateSettingsFile(array('cachedir_sqlite' => $cachedir_sqlite));
}
$this->cachedir = $cachedir_sqlite;
}
else
$this->cachedir = $dir;
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
if (null == $this->cacheDB)
$this->connect();
return $this->cacheDB->version()['versionString'];
}
/**
* {@inheritDoc}
*/
public function housekeeping()
{
$this->cleanCache('expired');
}
}
?>

View file

@ -0,0 +1,95 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
namespace SMF\Cache\APIs;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
if (!defined('SMF'))
die('No direct access...');
/**
* Our Cache API class
*
* @package CacheAPI
*/
class Zend extends CacheApi implements CacheApiInterface
{
/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
$supported = function_exists('zend_shm_cache_fetch') || function_exists('output_cache_get');
if ($test)
return $supported;
return parent::isSupported() && $supported;
}
public function connect()
{
return true;
}
/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
// Zend's pricey stuff.
if (function_exists('zend_shm_cache_fetch'))
return zend_shm_cache_fetch('SMF::' . $key);
elseif (function_exists('output_cache_get'))
return output_cache_get($key, $ttl);
}
/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
if (function_exists('zend_shm_cache_store'))
return zend_shm_cache_store('SMF::' . $key, $value, $ttl);
elseif (function_exists('output_cache_put'))
return output_cache_put($key, $value);
}
/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
$this->invalidateCache();
return zend_shm_cache_clear('SMF');
}
/**
* {@inheritDoc}
*/
public function getVersion()
{
return zend_version();
}
}
?>

252
Sources/Cache/CacheApi.php Normal file
View file

@ -0,0 +1,252 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
namespace SMF\Cache;
if (!defined('SMF'))
die('No direct access...');
abstract class CacheApi
{
const APIS_FOLDER = 'APIs';
const APIS_NAMESPACE = 'SMF\Cache\APIs\\';
const APIS_DEFAULT = 'FileBased';
/**
* @var string The maximum SMF version that this will work with.
*/
protected $version_compatible = '2.1.999';
/**
* @var string The minimum SMF version that this will work with.
*/
protected $min_smf_version = '2.1 RC1';
/**
* @var string The prefix for all keys.
*/
protected $prefix = '';
/**
* @var int The default TTL.
*/
protected $ttl = 120;
/**
* Does basic setup of a cache method when we create the object but before we call connect.
*
* @access public
*/
public function __construct()
{
$this->setPrefix();
}
/**
* Checks whether we can use the cache method performed by this API.
*
* @access public
* @param bool $test Test if this is supported or enabled.
* @return bool Whether or not the cache is supported
*/
public function isSupported($test = false)
{
global $cache_enable;
if ($test)
return true;
return !empty($cache_enable);
}
/**
* Sets the cache prefix.
*
* @access public
* @param string $prefix The prefix to use.
* If empty, the prefix will be generated automatically.
* @return bool If this was successful or not.
*/
public function setPrefix($prefix = '')
{
global $boardurl, $cachedir, $boarddir;
if (!is_string($prefix))
$prefix = '';
// Use the supplied prefix, if there is one.
if (!empty($prefix))
{
$this->prefix = $prefix;
return true;
}
// Ideally the prefix should reflect the last time the cache was reset.
if (!empty($cachedir) && file_exists($cachedir . '/index.php'))
{
$mtime = filemtime($cachedir . '/index.php');
}
// Fall back to the last time that Settings.php was updated.
elseif (!empty($boarddir) && file_exists($boarddir . '/Settings.php'))
{
$mtime = filemtime($boarddir . '/Settings.php');
}
// This should never happen, but just in case...
else
{
$mtime = filemtime(realpath($_SERVER['SCRIPT_FILENAME']));
}
$this->prefix = md5($boardurl . $mtime) . '-SMF-';
return true;
}
/**
* Gets the prefix as defined from set or the default.
*
* @access public
* @return string the value of $key.
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Sets a default Time To Live, if this isn't specified we let the class define it.
*
* @access public
* @param int $ttl The default TTL
* @return bool If this was successful or not.
*/
public function setDefaultTTL($ttl = 120)
{
$this->ttl = $ttl;
return true;
}
/**
* Gets the TTL as defined from set or the default.
*
* @access public
* @return int the value of $ttl.
*/
public function getDefaultTTL()
{
return $this->ttl;
}
/**
* Invalidate all cached data.
*
* @return bool Whether or not we could invalidate the cache.
*/
public function invalidateCache()
{
global $cachedir;
// Invalidate cache, to be sure!
// ... as long as index.php can be modified, anyway.
if (is_writable($cachedir . '/' . 'index.php'))
@touch($cachedir . '/' . 'index.php');
return true;
}
/**
* Closes connections to the cache method.
*
* @access public
* @return bool Whether the connections were closed.
*/
public function quit()
{
return true;
}
/**
* Specify custom settings that the cache API supports.
*
* @access public
* @param array $config_vars Additional config_vars, see ManageSettings.php for usage.
*/
public function cacheSettings(array &$config_vars)
{
}
/**
* Gets the latest version of SMF this is compatible with.
*
* @access public
* @return string the value of $key.
*/
public function getCompatibleVersion()
{
return $this->version_compatible;
}
/**
* Gets the min version that we support.
*
* @access public
* @return string the value of $key.
*/
public function getMinimumVersion()
{
return $this->min_smf_version;
}
/**
* Gets the Version of the Caching API.
*
* @access public
* @return string the value of $key.
*/
public function getVersion()
{
return $this->min_smf_version;
}
/**
* Run housekeeping of this cache
* exp. clean up old data or do optimization
*
* @access public
* @return void
*/
public function housekeeping()
{
}
/**
* Gets the class identifier of the current caching API implementation.
*
* @access public
* @return string the unique identifier for the current class implementation.
*/
public function getImplementationClassKeyName()
{
$class_name = get_class($this);
if ($position = strrpos($class_name, '\\'))
return substr($class_name, $position + 1);
else
return get_class($this);
}
}
?>

View file

@ -0,0 +1,82 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
namespace SMF\Cache;
if (!defined('SMF'))
die('No direct access...');
interface CacheApiInterface
{
/**
* Checks whether we can use the cache method performed by this API.
*
* @access public
* @param bool $test Test if this is supported or enabled.
* @return bool Whether or not the cache is supported
*/
public function isSupported($test = false);
/**
* Connects to the cache method. This defines our $key. If this fails, we return false, otherwise we return true.
*
* @access public
* @return bool Whether or not the cache method was connected to.
*/
public function connect();
/**
* Retrieves an item from the cache.
*
* @access public
* @param string $key The key to use, the prefix is applied to the key name.
* @param int $ttl Overrides the default TTL. Not really used anymore,
* but is kept for backwards compatibility.
* @return mixed The result from the cache, if there is no data or it is invalid, we return null.
* @todo Seperate existence checking into its own method
*/
public function getData($key, $ttl = null);
/**
* Stores a value, regardless of whether or not the key already exists (in
* which case it will overwrite the existing value for that key).
*
* @access public
* @param string $key The key to use, the prefix is applied to the key name.
* @param mixed $value The data we wish to save. Use null to delete.
* @param int $ttl How long (in seconds) the data should be cached for.
* The default TTL will be used if this is null.
* @return bool Whether or not we could save this to the cache.
* @todo Seperate deletion into its own method
*/
public function putData($key, $value, $ttl = null);
/**
* Clean out the cache.
*
* @param string $type If supported, the type of cache to clear, blank/data or user.
* @return bool Whether or not we could clean the cache.
*/
public function cleanCache($type = '');
/**
* Gets the class identifier of the current caching API implementation.
*
* @access public
* @return string the unique identifier for the current class implementation.
*/
public function getImplementationClassKeyName();
}
?>

736
Sources/Calendar.php Normal file
View file

@ -0,0 +1,736 @@
<?php
/**
* This file has only one real task, showing the calendar.
* Original module by Aaron O'Neil - aaron@mud-master.com
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Show the calendar.
* It loads the specified month's events, holidays, and birthdays.
* It requires the calendar_view permission.
* It depends on the cal_enabled setting, and many of the other cal_ settings.
* It uses the calendar_start_day theme option. (Monday/Sunday)
* It uses the main sub template in the Calendar template.
* It goes to the month and year passed in 'month' and 'year' by get or post.
* It is accessed through ?action=calendar.
*
* @return void
*/
function CalendarMain()
{
global $txt, $context, $modSettings, $scripturl, $options, $sourcedir, $user_info, $smcFunc;
// Permissions, permissions, permissions.
isAllowedTo('calendar_view');
// Some global template resources.
$context['calendar_resources'] = array(
'min_year' => $modSettings['cal_minyear'],
'max_year' => $modSettings['cal_maxyear'],
);
// Doing something other than calendar viewing?
$subActions = array(
'ical' => 'iCalDownload',
'post' => 'CalendarPost',
);
if (isset($_GET['sa']) && isset($subActions[$_GET['sa']]))
return call_helper($subActions[$_GET['sa']]);
// You can't do anything if the calendar is off.
if (empty($modSettings['cal_enabled']))
fatal_lang_error('calendar_off', false);
// This is gonna be needed...
loadTemplate('Calendar');
loadCSSFile('calendar.css', array('force_current' => false, 'validate' => true, 'rtl' => 'calendar.rtl.css'), 'smf_calendar');
// Did the specify an individual event ID? If so, let's splice the year/month in to what we would otherwise be doing.
if (isset($_GET['event']))
{
$evid = (int) $_GET['event'];
if ($evid > 0)
{
$request = $smcFunc['db_query']('', '
SELECT start_date
FROM {db_prefix}calendar
WHERE id_event = {int:event_id}',
array(
'event_id' => $evid,
)
);
if ($row = $smcFunc['db_fetch_assoc']($request))
{
$_REQUEST['start_date'] = $row['start_date'];
// We might use this later.
$context['selected_event'] = $evid;
}
$smcFunc['db_free_result']($request);
}
unset ($_GET['event']);
}
// Set the page title to mention the calendar ;).
$context['page_title'] = $txt['calendar'];
// Ensure a default view is defined
if (empty($options['calendar_default_view']))
$options['calendar_default_view'] = 'viewlist';
// What view do we want?
if (isset($_GET['viewweek']))
$context['calendar_view'] = 'viewweek';
elseif (isset($_GET['viewmonth']))
$context['calendar_view'] = 'viewmonth';
elseif (isset($_GET['viewlist']))
$context['calendar_view'] = 'viewlist';
else
$context['calendar_view'] = $options['calendar_default_view'];
// Don't let search engines index the non-default calendar pages
if ($context['calendar_view'] !== $options['calendar_default_view'])
$context['robot_no_index'] = true;
// Get the current day of month...
require_once($sourcedir . '/Subs-Calendar.php');
$today = getTodayInfo();
// Need a start date for all views
if (!empty($_REQUEST['start_date']))
{
$start_parsed = date_parse(str_replace(',', '', convertDateToEnglish($_REQUEST['start_date'])));
if (empty($start_parsed['error_count']) && empty($start_parsed['warning_count']))
{
$_REQUEST['year'] = $start_parsed['year'];
$_REQUEST['month'] = $start_parsed['month'];
$_REQUEST['day'] = $start_parsed['day'];
}
}
$year = !empty($_REQUEST['year']) ? (int) $_REQUEST['year'] : $today['year'];
$month = !empty($_REQUEST['month']) ? (int) $_REQUEST['month'] : $today['month'];
$day = !empty($_REQUEST['day']) ? (int) $_REQUEST['day'] : (!empty($_REQUEST['month']) ? 1 : $today['day']);
$start_object = checkdate($month, $day, $year) === true ? date_create(implode('-', array($year, $month, $day)) . ' ' . getUserTimezone()) : date_create(implode('-', array($today['year'], $today['month'], $today['day'])) . ' ' . getUserTimezone());
// Need an end date for the list view
if (!empty($_REQUEST['end_date']))
{
$end_parsed = date_parse(str_replace(',', '', convertDateToEnglish($_REQUEST['end_date'])));
if (empty($end_parsed['error_count']) && empty($end_parsed['warning_count']))
{
$_REQUEST['end_year'] = $end_parsed['year'];
$_REQUEST['end_month'] = $end_parsed['month'];
$_REQUEST['end_day'] = $end_parsed['day'];
}
}
$end_year = !empty($_REQUEST['end_year']) ? (int) $_REQUEST['end_year'] : null;
$end_month = !empty($_REQUEST['end_month']) ? (int) $_REQUEST['end_month'] : null;
$end_day = !empty($_REQUEST['end_day']) ? (int) $_REQUEST['end_day'] : null;
$end_object = null;
if (isset($end_month, $end_day, $end_year) && checkdate($end_month, $end_day, $end_year))
{
$end_object = date_create(implode('-', array($end_year, $end_month, $end_day)) . ' ' . getUserTimezone());
}
if (empty($end_object) || $start_object >= $end_object)
{
$num_days_shown = empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'];
$end_object = date_create(date_format($start_object, 'Y-m-d') . ' ' . getUserTimezone());
date_add($end_object, date_interval_create_from_date_string($num_days_shown . ' days'));
}
$curPage = array(
'year' => date_format($start_object, 'Y'),
'month' => date_format($start_object, 'n'),
'day' => date_format($start_object, 'j'),
'start_date' => date_format($start_object, 'Y-m-d'),
'end_year' => date_format($end_object, 'Y'),
'end_month' => date_format($end_object, 'n'),
'end_day' => date_format($end_object, 'j'),
'end_date' => date_format($end_object, 'Y-m-d'),
);
// Make sure the year and month are in valid ranges.
if ($curPage['month'] < 1 || $curPage['month'] > 12)
fatal_lang_error('invalid_month', false);
if ($curPage['year'] < $modSettings['cal_minyear'] || $curPage['year'] > $modSettings['cal_maxyear'])
fatal_lang_error('invalid_year', false);
// If we have a day clean that too.
if ($context['calendar_view'] != 'viewmonth')
{
$isValid = checkdate($curPage['month'], $curPage['day'], $curPage['year']);
if (!$isValid)
fatal_lang_error('invalid_day', false);
}
// Load all the context information needed to show the calendar grid.
$calendarOptions = array(
'start_day' => !empty($options['calendar_start_day']) ? $options['calendar_start_day'] : 0,
'show_birthdays' => in_array($modSettings['cal_showbdays'], array(1, 2)),
'show_events' => in_array($modSettings['cal_showevents'], array(1, 2)),
'show_holidays' => in_array($modSettings['cal_showholidays'], array(1, 2)),
'show_week_num' => true,
'short_day_titles' => !empty($modSettings['cal_short_days']),
'short_month_titles' => !empty($modSettings['cal_short_months']),
'show_next_prev' => !empty($modSettings['cal_prev_next_links']),
'show_week_links' => isset($modSettings['cal_week_links']) ? $modSettings['cal_week_links'] : 0,
);
// Load up the main view.
if ($context['calendar_view'] == 'viewlist')
$context['calendar_grid_main'] = getCalendarList($curPage['start_date'], $curPage['end_date'], $calendarOptions);
elseif ($context['calendar_view'] == 'viewweek')
$context['calendar_grid_main'] = getCalendarWeek($curPage['start_date'], $calendarOptions);
else
$context['calendar_grid_main'] = getCalendarGrid($curPage['start_date'], $calendarOptions);
// Load up the previous and next months.
$context['calendar_grid_current'] = getCalendarGrid($curPage['start_date'], $calendarOptions, false, false);
// Only show previous month if it isn't pre-January of the min-year
if ($context['calendar_grid_current']['previous_calendar']['year'] > $modSettings['cal_minyear'] || $curPage['month'] != 1)
$context['calendar_grid_prev'] = getCalendarGrid($context['calendar_grid_current']['previous_calendar']['start_date'], $calendarOptions, true, false);
// Only show next month if it isn't post-December of the max-year
if ($context['calendar_grid_current']['next_calendar']['year'] < $modSettings['cal_maxyear'] || $curPage['month'] != 12)
$context['calendar_grid_next'] = getCalendarGrid($context['calendar_grid_current']['next_calendar']['start_date'], $calendarOptions, false, false);
// Basic template stuff.
$context['allow_calendar_event'] = allowedTo('calendar_post');
// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
if ($context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
{
$boards_can_post = boardsAllowedTo('post_new');
$context['allow_calendar_event'] &= !empty($boards_can_post);
}
$context['can_post'] = $context['allow_calendar_event'];
$context['current_day'] = $curPage['day'];
$context['current_month'] = $curPage['month'];
$context['current_year'] = $curPage['year'];
$context['show_all_birthdays'] = isset($_GET['showbd']);
$context['blocks_disabled'] = !empty($modSettings['cal_disable_prev_next']) ? 1 : 0;
// Set the page title to mention the month or week, too
if ($context['calendar_view'] != 'viewlist')
$context['page_title'] .= ' - ' . ($context['calendar_view'] == 'viewweek' ? $context['calendar_grid_main']['week_title'] : $txt['months_titles'][$context['current_month']] . ' ' . $context['current_year']);
// Load up the linktree!
$context['linktree'][] = array(
'url' => $scripturl . '?action=calendar',
'name' => $txt['calendar']
);
// Add the current month to the linktree.
$context['linktree'][] = array(
'url' => $scripturl . '?action=calendar;year=' . $context['current_year'] . ';month=' . $context['current_month'],
'name' => $txt['months_titles'][$context['current_month']] . ' ' . $context['current_year']
);
// If applicable, add the current week to the linktree.
if ($context['calendar_view'] == 'viewweek')
$context['linktree'][] = array(
'url' => $scripturl . '?action=calendar;viewweek;year=' . $context['current_year'] . ';month=' . $context['current_month'] . ';day=' . $context['current_day'],
'name' => $context['calendar_grid_main']['week_title'],
);
// Build the calendar button array.
$context['calendar_buttons'] = array();
if ($context['can_post'])
$context['calendar_buttons']['post_event'] = array('text' => 'calendar_post_event', 'image' => 'calendarpe.png', 'url' => $scripturl . '?action=calendar;sa=post;month=' . $context['current_month'] . ';year=' . $context['current_year'] . ';' . $context['session_var'] . '=' . $context['session_id']);
// Allow mods to add additional buttons here
call_integration_hook('integrate_calendar_buttons');
}
/**
* This function processes posting/editing/deleting a calendar event.
*
* - calls {@link Post.php|Post() Post()} function if event is linked to a post.
* - calls {@link Subs-Calendar.php|insertEvent() insertEvent()} to insert the event if not linked to post.
*
* It requires the calendar_post permission to use.
* It uses the event_post sub template in the Calendar template.
* It is accessed with ?action=calendar;sa=post.
*/
function CalendarPost()
{
global $context, $txt, $user_info, $sourcedir, $scripturl;
global $modSettings, $topic, $smcFunc;
// Well - can they?
isAllowedTo('calendar_post');
// We need these for all kinds of useful functions.
require_once($sourcedir . '/Subs-Calendar.php');
require_once($sourcedir . '/Subs.php');
// Cast this for safety...
if (isset($_REQUEST['eventid']))
$_REQUEST['eventid'] = (int) $_REQUEST['eventid'];
// We want a fairly compact version of the time, but as close as possible to the user's settings.
$time_string = strtr(get_date_or_time_format('time'), array(
'%I' => '%l',
'%H' => '%k',
'%S' => '',
'%r' => '%l:%M %p',
'%R' => '%k:%M',
'%T' => '%l:%M',
));
$time_string = preg_replace('~:(?=\s|$|%[pPzZ])~', '', $time_string);
// Submitting?
if (isset($_POST[$context['session_var']], $_REQUEST['eventid']))
{
checkSession();
// Validate the post...
if (!isset($_POST['link_to_board']))
validateEventPost();
// If you're not allowed to edit any events, you have to be the poster.
if ($_REQUEST['eventid'] > 0 && !allowedTo('calendar_edit_any'))
isAllowedTo('calendar_edit_' . (!empty($user_info['id']) && getEventPoster($_REQUEST['eventid']) == $user_info['id'] ? 'own' : 'any'));
// New - and directing?
if (isset($_POST['link_to_board']) || empty($modSettings['cal_allow_unlinked']))
{
$_REQUEST['calendar'] = 1;
require_once($sourcedir . '/Post.php');
return Post();
}
// New...
elseif ($_REQUEST['eventid'] == -1)
{
$eventOptions = array(
'board' => 0,
'topic' => 0,
'title' => $smcFunc['substr']($_REQUEST['evtitle'], 0, 100),
'location' => $smcFunc['substr']($_REQUEST['event_location'], 0, 255),
'member' => $user_info['id'],
);
insertEvent($eventOptions);
}
// Deleting...
elseif (isset($_REQUEST['deleteevent']))
removeEvent($_REQUEST['eventid']);
// ... or just update it?
else
{
$eventOptions = array(
'title' => $smcFunc['substr']($_REQUEST['evtitle'], 0, 100),
'location' => $smcFunc['substr']($_REQUEST['event_location'], 0, 255),
);
modifyEvent($_REQUEST['eventid'], $eventOptions);
}
updateSettings(array(
'calendar_updated' => time(),
));
// No point hanging around here now...
if (isset($_POST['start_date']))
{
$d = date_parse($_POST['start_date']);
$year = $d['year'];
$month = $d['month'];
$day = $d['day'];
}
elseif (isset($_POST['start_datetime']))
{
$d = date_parse($_POST['start_datetime']);
$year = $d['year'];
$month = $d['month'];
$day = $d['day'];
}
else
{
$today = getdate();
$year = isset($_POST['year']) ? $_POST['year'] : $today['year'];
$month = isset($_POST['month']) ? $_POST['month'] : $today['mon'];
$day = isset($_POST['day']) ? $_POST['day'] : $today['mday'];
}
redirectexit($scripturl . '?action=calendar;month=' . $month . ';year=' . $year . ';day=' . $day);
}
// If we are not enabled... we are not enabled.
if (empty($modSettings['cal_allow_unlinked']) && empty($_REQUEST['eventid']))
{
$_REQUEST['calendar'] = 1;
require_once($sourcedir . '/Post.php');
return Post();
}
// New?
if (!isset($_REQUEST['eventid']))
{
$context['event'] = array(
'boards' => array(),
'board' => 0,
'new' => 1,
'eventid' => -1,
'title' => '',
'location' => '',
);
$eventDatetimes = getNewEventDatetimes();
$context['event'] = array_merge($context['event'], $eventDatetimes);
$context['event']['last_day'] = (int) smf_strftime('%d', mktime(0, 0, 0, $context['event']['month'] == 12 ? 1 : $context['event']['month'] + 1, 0, $context['event']['month'] == 12 ? $context['event']['year'] + 1 : $context['event']['year']));
}
else
{
$context['event'] = getEventProperties($_REQUEST['eventid']);
if ($context['event'] === false)
fatal_lang_error('no_access', false);
// If it has a board, then they should be editing it within the topic.
if (!empty($context['event']['topic']['id']) && !empty($context['event']['topic']['first_msg']))
{
// We load the board up, for a check on the board access rights...
$topic = $context['event']['topic']['id'];
loadBoard();
}
// Make sure the user is allowed to edit this event.
if ($context['event']['member'] != $user_info['id'])
isAllowedTo('calendar_edit_any');
elseif (!allowedTo('calendar_edit_any'))
isAllowedTo('calendar_edit_own');
}
// An all day event? Set up some nice defaults in case the user wants to change that
if ($context['event']['allday'] == true)
{
$context['event']['tz'] = getUserTimezone();
$context['event']['start_time'] = timeformat(time(), $time_string);
$context['event']['end_time'] = timeformat(time() + 3600, $time_string);
}
// Otherwise, just adjust these to look nice on the input form
else
{
$context['event']['start_time'] = $context['event']['start_time_orig'];
$context['event']['end_time'] = $context['event']['end_time_orig'];
}
// Need this so the user can select a timezone for the event.
$context['all_timezones'] = smf_list_timezones($context['event']['start_date']);
// If the event's timezone is not in SMF's standard list of time zones, try to fix it.
if (!isset($context['all_timezones'][$context['event']['tz']]))
{
$later = strtotime('@' . $context['event']['start_timestamp'] . ' + 1 year');
$tzinfo = timezone_transitions_get(timezone_open($context['event']['tz']), $context['event']['start_timestamp'], $later);
$found = false;
foreach ($context['all_timezones'] as $possible_tzid => $dummy)
{
// Ignore the "-----" option
if (empty($possible_tzid))
continue;
$possible_tzinfo = timezone_transitions_get(timezone_open($possible_tzid), $context['event']['start_timestamp'], $later);
if ($tzinfo === $possible_tzinfo)
{
$context['event']['tz'] = $possible_tzid;
$found = true;
break;
}
}
// Hm. That's weird. Well, just prepend it to the list and let the user deal with it.
if (!$found)
{
$d = date_create($context['event']['start_datetime'] . ' ' . $context['event']['tz']);
$context['all_timezones'] = array($context['event']['tz'] => '[UTC' . date_format($d, 'P') . '] - ' . $context['event']['tz']) + $context['all_timezones'];
}
}
// Get list of boards that can be posted in.
$boards = boardsAllowedTo('post_new');
if (empty($boards))
{
// You can post new events but can't link them to anything...
$context['event']['categories'] = array();
}
else
{
// Load the list of boards and categories in the context.
require_once($sourcedir . '/Subs-MessageIndex.php');
$boardListOptions = array(
'included_boards' => in_array(0, $boards) ? null : $boards,
'not_redirection' => true,
'use_permissions' => true,
'selected_board' => $modSettings['cal_defaultboard'],
);
$context['event']['categories'] = getBoardList($boardListOptions);
}
// Template, sub template, etc.
loadTemplate('Calendar');
$context['sub_template'] = 'event_post';
$context['page_title'] = isset($_REQUEST['eventid']) ? $txt['calendar_edit'] : $txt['calendar_post_event'];
$context['linktree'][] = array(
'name' => $context['page_title'],
);
loadDatePicker('#event_time_input .date_input');
loadTimePicker('#event_time_input .time_input', $time_string);
loadDatePair('#event_time_input', 'date_input', 'time_input');
addInlineJavaScript('
$("#allday").click(function(){
$("#start_time").attr("disabled", this.checked);
$("#end_time").attr("disabled", this.checked);
$("#tz").attr("disabled", this.checked);
});', true);
}
/**
* This function offers up a download of an event in iCal 2.0 format.
*
* Follows the conventions in {@link https://tools.ietf.org/html/rfc5546 RFC5546}
* Sets events as all day events since we don't have hourly events
* Will honor and set multi day events
* Sets a sequence number if the event has been modified
*
* @todo .... allow for week or month export files as well?
*/
function iCalDownload()
{
global $smcFunc, $sourcedir, $modSettings, $webmaster_email, $mbname;
// You can't export if the calendar export feature is off.
if (empty($modSettings['cal_export']))
fatal_lang_error('calendar_export_off', false);
// Goes without saying that this is required.
if (!isset($_REQUEST['eventid']))
fatal_lang_error('no_access', false);
// This is kinda wanted.
require_once($sourcedir . '/Subs-Calendar.php');
// Load up the event in question and check it exists.
$event = getEventProperties($_REQUEST['eventid']);
if ($event === false)
fatal_lang_error('no_access', false);
// Check the title isn't too long - iCal requires some formatting if so.
$title = str_split($event['title'], 30);
foreach ($title as $id => $line)
{
if ($id != 0)
$title[$id] = ' ' . $title[$id];
$title[$id] .= "\n";
}
// Format the dates.
$datestamp = date('Ymd\THis\Z', time());
$start_date = date_create($event['start_date'] . (isset($event['start_time']) ? ' ' . $event['start_time'] : '') . (isset($event['tz']) ? ' ' . $event['tz'] : ''));
$end_date = date_create($event['end_date'] . (isset($event['end_time']) ? ' ' . $event['end_time'] : '') . (isset($event['tz']) ? ' ' . $event['tz'] : ''));
if (!empty($event['start_time']))
{
$datestart = date_format($start_date, 'Ymd\THis');
$dateend = date_format($end_date, 'Ymd\THis');
}
else
{
$datestart = date_format($start_date, 'Ymd');
date_add($end_date, date_interval_create_from_date_string('1 day'));
$dateend = date_format($end_date, 'Ymd');
}
// This is what we will be sending later
$filecontents = '';
$filecontents .= 'BEGIN:VCALENDAR' . "\n";
$filecontents .= 'METHOD:PUBLISH' . "\n";
$filecontents .= 'PRODID:-//SimpleMachines//' . SMF_FULL_VERSION . '//EN' . "\n";
$filecontents .= 'VERSION:2.0' . "\n";
$filecontents .= 'BEGIN:VEVENT' . "\n";
// @TODO - Should be the members email who created the event rather than $webmaster_email.
$filecontents .= 'ORGANIZER;CN="' . $event['realname'] . '":MAILTO:' . $webmaster_email . "\n";
$filecontents .= 'DTSTAMP:' . $datestamp . "\n";
$filecontents .= 'DTSTART' . (!empty($event['start_time']) ? ';TZID=' . $event['tz'] : ';VALUE=DATE') . ':' . $datestart . "\n";
// event has a duration
if ($event['start_iso_gmdate'] != $event['end_iso_gmdate'])
$filecontents .= 'DTEND' . (!empty($event['end_time']) ? ';TZID=' . $event['tz'] : ';VALUE=DATE') . ':' . $dateend . "\n";
// event has changed? advance the sequence for this UID
if ($event['sequence'] > 0)
$filecontents .= 'SEQUENCE:' . $event['sequence'] . "\n";
if (!empty($event['location']))
$filecontents .= 'LOCATION:' . str_replace(',', '\,', $event['location']) . "\n";
$filecontents .= 'SUMMARY:' . implode('', $title);
$filecontents .= 'UID:' . $event['eventid'] . '@' . str_replace(' ', '-', $mbname) . "\n";
$filecontents .= 'END:VEVENT' . "\n";
$filecontents .= 'END:VCALENDAR';
// Send some standard headers.
ob_end_clean();
if (!empty($modSettings['enableCompressedOutput']))
@ob_start('ob_gzhandler');
else
ob_start();
// Send the file headers
header('pragma: ');
header('cache-control: no-cache');
if (!isBrowser('gecko'))
header('content-transfer-encoding: binary');
header('expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT');
header('last-modified: ' . gmdate('D, d M Y H:i:s', time()) . 'GMT');
header('accept-ranges: bytes');
header('connection: close');
header('content-disposition: attachment; filename="' . $event['title'] . '.ics"');
if (empty($modSettings['enableCompressedOutput']))
header('content-length: ' . $smcFunc['strlen']($filecontents));
// This is a calendar item!
header('content-type: text/calendar');
// Chuck out the card.
echo $filecontents;
// Off we pop - lovely!
obExit(false);
}
/**
* Nothing to see here. Move along.
*/
function clock()
{
global $smcFunc, $settings, $context, $scripturl;
$context['onimg'] = $settings['images_url'] . '/bbc/bbc_hoverbg.png';
$context['offimg'] = $settings['images_url'] . '/bbc/bbc_bg.png';
$context['page_title'] = 'Anyone know what time it is?';
$context['linktree'][] = array(
'url' => $scripturl . '?action=clock',
'name' => 'Clock',
);
$context['robot_no_index'] = true;
$omfg = isset($_REQUEST['omfg']);
$bcd = !isset($_REQUEST['rb']) && !isset($_REQUEST['omfg']) && !isset($_REQUEST['time']);
loadTemplate('Calendar');
if ($bcd)
{
$context['sub_template'] = 'bcd';
$context['linktree'][] = array('url' => $scripturl . '?action=clock;bcd', 'name' => 'BCD');
$context['clockicons'] = $smcFunc['json_decode'](base64_decode('eyJoMSI6WzIsMV0sImgyIjpbOCw0LDIsMV0sIm0xIjpbNCwyLDFdLCJtMiI6WzgsNCwyLDFdLCJzMSI6WzQsMiwxXSwiczIiOls4LDQsMiwxXX0='), true);
}
elseif (!$omfg && !isset($_REQUEST['time']))
{
$context['sub_template'] = 'hms';
$context['linktree'][] = array('url' => $scripturl . '?action=clock', 'name' => 'Binary');
$context['clockicons'] = $smcFunc['json_decode'](base64_decode('eyJoIjpbMTYsOCw0LDIsMV0sIm0iOlszMiwxNiw4LDQsMiwxXSwicyI6WzMyLDE2LDgsNCwyLDFdfQ'), true);
}
elseif ($omfg)
{
$context['sub_template'] = 'omfg';
$context['linktree'][] = array('url' => $scripturl . '?action=clock;omfg', 'name' => 'OMFG');
$context['clockicons'] = $smcFunc['json_decode'](base64_decode('eyJ5ZWFyIjpbNjQsMzIsMTYsOCw0LDIsMV0sIm1vbnRoIjpbOCw0LDIsMV0sImRheSI6WzE2LDgsNCwyLDFdLCJob3VyIjpbMTYsOCw0LDIsMV0sIm1pbiI6WzMyLDE2LDgsNCwyLDFdLCJzZWMiOlszMiwxNiw4LDQsMiwxXX0='), true);
}
elseif (isset($_REQUEST['time']))
{
$context['sub_template'] = 'thetime';
$time = getdate($_REQUEST['time'] == 'now' ? time() : (int) $_REQUEST['time']);
$context['linktree'][] = array('url' => $scripturl . '?action=clock;time=' . $_REQUEST['time'], 'name' => 'Requested Time');
$context['clockicons'] = array(
'year' => array(
64 => false,
32 => false,
16 => false,
8 => false,
4 => false,
2 => false,
1 => false
),
'month' => array(
8 => false,
4 => false,
2 => false,
1 => false
),
'day' => array(
16 => false,
4 => false,
8 => false,
2 => false,
1 => false
),
'hour' => array(
32 => false,
16 => false,
8 => false,
4 => false,
2 => false,
1 => false
),
'min' => array(
32 => false,
16 => false,
8 => false,
4 => false,
2 => false,
1 => false
),
'sec' => array(
32 => false,
16 => false,
8 => false,
4 => false,
2 => false,
1 => false
),
);
foreach ($context['clockicons'] as $t => $vs)
foreach ($vs as $v => $dumb)
{
if ($$t >= $v)
{
$$t -= $v;
$context['clockicons'][$t][$v] = true;
}
}
}
}
?>

View file

@ -0,0 +1,462 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Class browser_detector
* This class is an experiment for the job of correctly detecting browsers and settings needed for them.
* - Detects the following browsers
* - Opera, Webkit, Firefox, Web_tv, Konqueror, IE, Gecko
* - Webkit variants: Chrome, iphone, blackberry, android, safari, ipad, ipod
* - Opera Versions: 6, 7, 8 ... 10 ... and mobile mini and mobi
* - Firefox Versions: 1, 2, 3 .... 11 ...
* - Chrome Versions: 1 ... 18 ...
* - IE Versions: 4, 5, 5.5, 6, 7, 8, 9, 10 ... mobile and Mac
* - MS Edge
* - Nokia
*/
class browser_detector
{
/**
* @var array Holds all the browser information. Its contents will be placed into $context['browser']
*/
private $_browsers = null;
/**
* @var boolean Whether or not this might be a mobile device
*/
private $_is_mobile = null;
/**
* The main method of this class, you know the one that does the job: detect the thing.
* - determines the user agent (browser) as best it can.
*/
function detectBrowser()
{
global $context, $user_info;
// Init
$this->_browsers = array();
$this->_is_mobile = false;
// Initialize some values we'll set differently if necessary...
$this->_browsers['needs_size_fix'] = false;
// One at a time, one at a time, and in this order too
if ($this->isOpera())
$this->setupOpera();
// Meh...
elseif ($this->isEdge())
$this->setupEdge();
// Them webkits need to be set up too
elseif ($this->isWebkit())
$this->setupWebkit();
// We may have work to do on Firefox...
elseif ($this->isFirefox())
$this->setupFirefox();
// Old friend, old frenemy
elseif ($this->isIe())
$this->setupIe();
// Just a few mobile checks
$this->isOperaMini();
$this->isOperaMobi();
// IE11 seems to be fine by itself without being lumped into the "is_ie" category
$this->isIe11();
// Be you robot or human?
if ($user_info['possibly_robot'])
{
// This isn't meant to be reliable, it's just meant to catch most bots to prevent PHPSESSID from showing up.
$this->_browsers['possibly_robot'] = !empty($user_info['possibly_robot']);
// Robots shouldn't be logging in or registering. So, they aren't a bot. Better to be wrong than sorry (or people won't be able to log in!), anyway.
if ((isset($_REQUEST['action']) && in_array($_REQUEST['action'], array('login', 'login2', 'register', 'signup'))) || !$user_info['is_guest'])
$this->_browsers['possibly_robot'] = false;
}
else
$this->_browsers['possibly_robot'] = false;
// Fill out the historical array as needed to support old mods that don't use isBrowser
$this->fillInformation();
// Make it easy to check if the browser is on a mobile device.
$this->_browsers['is_mobile'] = $this->_is_mobile;
// Last step ...
$this->setupBrowserPriority();
// Now see what you've done!
$context['browser'] = $this->_browsers;
}
/**
* Determine if the browser is Opera or not
*
* @return boolean Whether or not this is Opera
*/
function isOpera()
{
if (!isset($this->_browsers['is_opera']))
$this->_browsers['is_opera'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') !== false;
return $this->_browsers['is_opera'];
}
/**
* Determine if the browser is IE or not
*
* @return boolean true Whether or not the browser is IE
*/
function isIe()
{
// I'm IE, Yes I'm the real IE; All you other IEs are just imitating.
if (!isset($this->_browsers['is_ie']))
$this->_browsers['is_ie'] = !$this->isOpera() && !$this->isGecko() && !$this->isWebTv() && preg_match('~MSIE \d+~', $_SERVER['HTTP_USER_AGENT']) === 1;
return $this->_browsers['is_ie'];
}
/**
* Determine if the browser is IE11 or not
*
* @return boolean Whether or not the browser is IE11
*/
function isIe11()
{
// IE11 is a bit different than earlier versions
// The isGecko() part is to ensure we get this right...
if (!isset($this->_browsers['is_ie11']))
$this->_browsers['is_ie11'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Trident') !== false && $this->isGecko();
return $this->_browsers['is_ie11'];
}
/**
* Determine if the browser is Edge or not
*
* @return boolean Whether or not the browser is Edge
*/
function isEdge()
{
if (!isset($this->_browsers['is_edge']))
$this->_browsers['is_edge'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Edge') !== false;
return $this->_browsers['is_edge'];
}
/**
* Determine if the browser is a Webkit based one or not
*
* @return boolean Whether or not this is a Webkit-based browser
*/
function isWebkit()
{
if (!isset($this->_browsers['is_webkit']))
$this->_browsers['is_webkit'] = strpos($_SERVER['HTTP_USER_AGENT'], 'AppleWebKit') !== false;
return $this->_browsers['is_webkit'];
}
/**
* Determine if the browser is Firefox or one of its variants
*
* @return boolean Whether or not this is Firefox (or one of its variants)
*/
function isFirefox()
{
if (!isset($this->_browsers['is_firefox']))
$this->_browsers['is_firefox'] = preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)/~', $_SERVER['HTTP_USER_AGENT']) === 1 && $this->isGecko();
return $this->_browsers['is_firefox'];
}
/**
* Determine if the browser is WebTv or not
*
* @return boolean Whether or not this is WebTV
*/
function isWebTv()
{
if (!isset($this->_browsers['is_web_tv']))
$this->_browsers['is_web_tv'] = strpos($_SERVER['HTTP_USER_AGENT'], 'WebTV') !== false;
return $this->_browsers['is_web_tv'];
}
/**
* Determine if the browser is konqueror or not
*
* @return boolean Whether or not this is Konqueror
*/
function isKonqueror()
{
if (!isset($this->_browsers['is_konqueror']))
$this->_browsers['is_konqueror'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Konqueror') !== false;
return $this->_browsers['is_konqueror'];
}
/**
* Determine if the browser is Gecko or not
*
* @return boolean Whether or not this is a Gecko-based browser
*/
function isGecko()
{
if (!isset($this->_browsers['is_gecko']))
$this->_browsers['is_gecko'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Gecko') !== false && !$this->isWebkit() && !$this->isKonqueror();
return $this->_browsers['is_gecko'];
}
/**
* Determine if the browser is Opera Mini or not
*
* @return boolean Whether or not this is Opera Mini
*/
function isOperaMini()
{
if (!isset($this->_browsers['is_opera_mini']))
$this->_browsers['is_opera_mini'] = (isset($_SERVER['HTTP_X_OPERAMINI_PHONE_UA']) || stripos($_SERVER['HTTP_USER_AGENT'], 'opera mini') !== false);
if ($this->_browsers['is_opera_mini'])
$this->_is_mobile = true;
return $this->_browsers['is_opera_mini'];
}
/**
* Determine if the browser is Opera Mobile or not
*
* @return boolean Whether or not this is Opera Mobile
*/
function isOperaMobi()
{
if (!isset($this->_browsers['is_opera_mobi']))
$this->_browsers['is_opera_mobi'] = stripos($_SERVER['HTTP_USER_AGENT'], 'opera mobi') !== false;
if ($this->_browsers['is_opera_mobi'])
$this->_is_mobile = true;
return $this->_browsers['is_opera_mini'];
}
/**
* Detect Safari / Chrome / iP[ao]d / iPhone / Android / Blackberry from webkit.
* - set the browser version for Safari and Chrome
* - set the mobile flag for mobile based useragents
*/
private function setupWebkit()
{
$this->_browsers += array(
'is_chrome' => strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome') !== false,
'is_iphone' => (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'iPod') !== false) && strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') === false,
'is_blackberry' => stripos($_SERVER['HTTP_USER_AGENT'], 'BlackBerry') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'PlayBook') !== false,
'is_android' => strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false,
'is_nokia' => strpos($_SERVER['HTTP_USER_AGENT'], 'SymbianOS') !== false,
);
// blackberry, playbook, iphone, nokia, android and ipods set a mobile flag
if ($this->_browsers['is_iphone'] || $this->_browsers['is_blackberry'] || $this->_browsers['is_android'] || $this->_browsers['is_nokia'])
$this->_is_mobile = true;
// @todo what to do with the blaPad? ... for now leave it detected as Safari ...
$this->_browsers['is_safari'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') !== false && !$this->_browsers['is_chrome'] && !$this->_browsers['is_iphone'];
$this->_browsers['is_ipad'] = strpos($_SERVER['HTTP_USER_AGENT'], 'iPad') !== false;
// if Chrome, get the major version
if ($this->_browsers['is_chrome'])
{
if (preg_match('~chrome[/]([0-9][0-9]?[.])~i', $_SERVER['HTTP_USER_AGENT'], $match) === 1)
$this->_browsers['is_chrome' . (int) $match[1]] = true;
}
// or if Safari get its major version
if ($this->_browsers['is_safari'])
{
if (preg_match('~version/?(.*)safari.*~i', $_SERVER['HTTP_USER_AGENT'], $match) === 1)
$this->_browsers['is_safari' . (int) trim($match[1])] = true;
}
}
/**
* Additional IE checks and settings.
* - determines the version of the IE browser in use
* - detects ie4 onward
* - attempts to distinguish between IE and IE in compatibility view
* - checks for old IE on macs as well, since we can
*/
private function setupIe()
{
$this->_browsers['is_ie_compat_view'] = false;
// get the version of the browser from the msie tag
if (preg_match('~MSIE\s?([0-9][0-9]?.[0-9])~i', $_SERVER['HTTP_USER_AGENT'], $msie_match) === 1)
{
$msie_match[1] = trim($msie_match[1]);
$msie_match[1] = (($msie_match[1] - (int) $msie_match[1]) == 0) ? (int) $msie_match[1] : $msie_match[1];
$this->_browsers['is_ie' . $msie_match[1]] = true;
}
// "modern" ie uses trident 4=ie8, 5=ie9, 6=ie10, 7=ie11 even in compatibility view
if (preg_match('~Trident/([0-9.])~i', $_SERVER['HTTP_USER_AGENT'], $trident_match) === 1)
{
$this->_browsers['is_ie' . ((int) $trident_match[1] + 4)] = true;
// If trident is set, see the (if any) msie tag in the user agent matches ... if not its in some compatibility view
if (isset($msie_match[1]) && ($msie_match[1] < $trident_match[1] + 4))
$this->_browsers['is_ie_compat_view'] = true;
}
// Detect true IE6 and IE7 and not IE in compat mode.
$this->_browsers['is_ie7'] = !empty($this->_browsers['is_ie7']) && ($this->_browsers['is_ie_compat_view'] === false);
$this->_browsers['is_ie6'] = !empty($this->_browsers['is_ie6']) && ($this->_browsers['is_ie_compat_view'] === false);
// IE mobile 7 or 9, ... shucks why not
if ((!empty($this->_browsers['is_ie7']) && strpos($_SERVER['HTTP_USER_AGENT'], 'IEMobile/7') !== false) || (!empty($this->_browsers['is_ie9']) && strpos($_SERVER['HTTP_USER_AGENT'], 'IEMobile/9') !== false))
{
$this->_browsers['is_ie_mobi'] = true;
$this->_is_mobile = true;
}
// And some throwbacks to a bygone era, deposited here like cholesterol in your arteries
$this->_browsers += array(
'is_ie4' => !empty($this->_browsers['is_ie4']) && !$this->_browsers['is_web_tv'],
'is_mac_ie' => strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 5.') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') !== false
);
// Before IE8 we need to fix IE... lots!
$this->_browsers['ie_standards_fix'] = (($this->_browsers['is_ie6'] === true) || ($this->_browsers['is_ie7'] === true)) ? true : false;
// We may even need a size fix...
$this->_browsers['needs_size_fix'] = (!empty($this->_browsers['is_ie5']) || !empty($this->_browsers['is_ie5.5']) || !empty($this->_browsers['is_ie4'])) && !$this->_browsers['is_mac_ie'];
}
/**
* Additional firefox checks.
* - Gets the version of the FF browser in use
* - Considers all FF variants as FF including IceWeasel, IceCat, Shiretoko and Minefiled
*/
private function setupFirefox()
{
if (preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)[\/ \(]([^ ;\)]+)~', $_SERVER['HTTP_USER_AGENT'], $match) === 1)
$this->_browsers['is_firefox' . (int) $match[1]] = true;
}
/**
* More Opera checks if we are opera.
* - checks for the version of Opera in use
* - uses checks for 10 first and falls through to <9
*/
private function setupOpera()
{
// Opera 10+ uses the version tag at the end of the string
if (preg_match('~\sVersion/([0-9]+)\.[0-9]+(?:\s*|$)~', $_SERVER['HTTP_USER_AGENT'], $match))
$this->_browsers['is_opera' . (int) $match[1]] = true;
// Opera pre 10 is supposed to uses the Opera tag alone, as do some spoofers
elseif (preg_match('~Opera[ /]([0-9]+)(?!\\.[89])~', $_SERVER['HTTP_USER_AGENT'], $match))
$this->_browsers['is_opera' . (int) $match[1]] = true;
// Needs size fix?
$this->_browsers['needs_size_fix'] = !empty($this->_browsers['is_opera6']);
}
/**
* Sets the version number for MS edge.
*/
private function setupEdge()
{
if (preg_match('~Edge[\/]([0-9][0-9]?[\.][0-9][0-9])~i', $_SERVER['HTTP_USER_AGENT'], $match) === 1)
$this->_browsers['is_edge' . (int) $match[1]] = true;
}
/**
* Get the browser name that we will use in the <body id="this_browser">
* - The order of each browser in $browser_priority is important
* - if you want to have id='ie6' and not id='ie' then it must appear first in the list of ie browsers
* - only sets browsers that may need some help via css for compatibility
*/
private function setupBrowserPriority()
{
global $context;
if ($this->_is_mobile)
$context['browser_body_id'] = 'mobile';
else
{
// add in any specific detection conversions here if you want a special body id e.g. 'is_opera9' => 'opera9'
$browser_priority = array(
'is_ie6' => 'ie6',
'is_ie7' => 'ie7',
'is_ie8' => 'ie8',
'is_ie9' => 'ie9',
'is_ie10' => 'ie10',
'is_ie11' => 'ie11',
'is_ie' => 'ie',
'is_edge' => 'edge',
'is_firefox' => 'firefox',
'is_chrome' => 'chrome',
'is_safari' => 'safari',
'is_opera10' => 'opera10',
'is_opera11' => 'opera11',
'is_opera12' => 'opera12',
'is_opera' => 'opera',
'is_konqueror' => 'konqueror',
);
$context['browser_body_id'] = 'smf';
$active = array_reverse(array_keys($this->_browsers, true));
foreach ($active as $browser)
{
if (array_key_exists($browser, $browser_priority))
{
$context['browser_body_id'] = $browser_priority[$browser];
break;
}
}
}
}
/**
* Fill out the historical array
* - needed to support old mods that don't use isBrowser
*/
function fillInformation()
{
$this->_browsers += array(
'is_opera' => false,
'is_opera6' => false,
'is_opera7' => false,
'is_opera8' => false,
'is_opera9' => false,
'is_opera10' => false,
'is_webkit' => false,
'is_mac_ie' => false,
'is_web_tv' => false,
'is_konqueror' => false,
'is_firefox' => false,
'is_firefox1' => false,
'is_firefox2' => false,
'is_firefox3' => false,
'is_iphone' => false,
'is_android' => false,
'is_chrome' => false,
'is_safari' => false,
'is_gecko' => false,
'is_edge' => false,
'is_ie8' => false,
'is_ie7' => false,
'is_ie6' => false,
'is_ie5.5' => false,
'is_ie5' => false,
'is_ie' => false,
'is_ie4' => false,
'ie_standards_fix' => false,
'needs_size_fix' => false,
'possibly_robot' => false,
);
}
}
?>

View file

@ -0,0 +1,371 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Class curl_fetch_web_data
* Simple cURL class to fetch a web page
* Properly redirects even with safe mode and basedir restrictions
* Can provide simple post options to a page
*
* ### Load class
* Initiate as
* ```
* $fetch_data = new cURL_fetch_web_data();
* ```
* Optionally pass an array of cURL options and redirect count
* ```
* $fetch_data = new cURL_fetch_web_data(array(CURLOPT_SSL_VERIFYPEER => 1), 5);
* ```
*
* ### Make the call
* Fetch a page
* ```
* $fetch_data->get_url_data('https://www.simplemachines.org');
* ```
* Post to a page providing an array
* ```
* $fetch_data->get_url_data('https://www.simplemachines.org', array('user' => 'name', 'password' => 'password'));
* ```
* Post to a page providing a string
* ```
* $fetch_data->get_url_data('https://www.simplemachines.org', parameter1&parameter2&parameter3);
* ```
*
* ### Get the data
* Just the page content
* ```
* $fetch_data->result('body');
* ```
* An array of results, body, header, http result codes
* ```
* $fetch_data->result();
* ```
* Show all results of all calls (in the event of a redirect)
* ```
* $fetch_data->result_raw();
* ```
* Show the results of a specific call (in the event of a redirect)
* ```
* $fetch_data->result_raw(0);
* ```
*/
class curl_fetch_web_data
{
/**
* Set the default items for this class
*
* @var array $default_options
*/
private $default_options = array(
CURLOPT_RETURNTRANSFER => 1, // Get returned value as a string (don't output it)
CURLOPT_HEADER => 1, // We need the headers to do our own redirect
CURLOPT_FOLLOWLOCATION => 0, // Don't follow, we will do it ourselves so safe mode and open_basedir will dig it
CURLOPT_USERAGENT => SMF_USER_AGENT, // set a normal looking useragent
CURLOPT_CONNECTTIMEOUT => 15, // Don't wait forever on a connection
CURLOPT_TIMEOUT => 90, // A page should load in this amount of time
CURLOPT_MAXREDIRS => 5, // stop after this many redirects
CURLOPT_ENCODING => 'gzip,deflate', // accept gzip and decode it
CURLOPT_SSL_VERIFYPEER => 0, // stop cURL from verifying the peer's certificate
CURLOPT_SSL_VERIFYHOST => 0, // stop cURL from verifying the peer's host
CURLOPT_POST => 0, // no post data unless its passed
);
/**
* @var int Maximum number of redirects
*/
public $max_redirect;
/**
* @var array An array of cURL options
*/
public $user_options = array();
/**
* @var string Any post data as form name => value
*/
public $post_data;
/**
* @var array An array of cURL options
*/
public $options;
/**
* @var int ???
*/
public $current_redirect;
/**
* @var array Stores responses (url, code, error, headers, body) in the response array
*/
public $response = array();
/**
* @var string The header
*/
public $headers;
/**
* Start the curl object
* - allow for user override values
*
* @param array $options An array of cURL options
* @param int $max_redirect Maximum number of redirects
*/
public function __construct($options = array(), $max_redirect = 3)
{
// Initialize class variables
$this->max_redirect = intval($max_redirect);
$this->user_options = $options;
}
/**
* Main calling function,
* - will request the page data from a given $url
* - optionally will post data to the page form if post data is supplied
* - passed arrays will be converted to a post string joined with &'s
* - calls set_options to set the curl opts array values based on the defaults and user input
*
* @param string $url the site we are going to fetch
* @param array $post_data any post data as form name => value
* @return object An instance of the curl_fetch_web_data class
*/
public function get_url_data($url, $post_data = array())
{
// POSTing some data perhaps?
if (!empty($post_data) && is_array($post_data))
$this->post_data = $this->build_post_data($post_data);
elseif (!empty($post_data))
$this->post_data = trim($post_data);
// set the options and get it
$this->set_options();
$this->curl_request(str_replace(' ', '%20', $url));
return $this;
}
/**
* Makes the actual cURL call
* - stores responses (url, code, error, headers, body) in the response array
* - detects 301, 302, 307 codes and will redirect to the given response header location
*
* @param string $url The site to fetch
* @param bool $redirect Whether or not this was a redirect request
* @return void|bool Sets various properties of the class or returns false if the URL isn't specified
*/
private function curl_request($url, $redirect = false)
{
// we do have a url I hope
if ($url == '')
return false;
else
$this->options[CURLOPT_URL] = $url;
// if we have not already been redirected, set it up so we can if needed
if (!$redirect)
{
$this->current_redirect = 1;
$this->response = array();
}
// Initialize the curl object and make the call
$cr = curl_init();
curl_setopt_array($cr, $this->options);
curl_exec($cr);
// Get what was returned
$curl_info = curl_getinfo($cr);
$curl_content = curl_multi_getcontent($cr);
$url = $curl_info['url']; // Last effective URL
$http_code = $curl_info['http_code']; // Last HTTP code
$body = (!curl_error($cr)) ? substr($curl_content, $curl_info['header_size']) : false;
$error = (curl_error($cr)) ? curl_error($cr) : false;
// close this request
curl_close($cr);
// store this 'loops' data, someone may want all of these :O
$this->response[] = array(
'url' => $url,
'code' => $http_code,
'error' => $error,
'headers' => isset($this->headers) ? $this->headers : false,
'body' => $body,
'size' => $curl_info['download_content_length'],
);
// If this a redirect with a location header and we have not given up, then do it again
if (preg_match('~30[127]~i', $http_code) === 1 && $this->headers['location'] != '' && $this->current_redirect <= $this->max_redirect)
{
$this->current_redirect++;
$header_location = $this->get_redirect_url($url, $this->headers['location']);
$this->redirect($header_location, $url);
}
}
/**
* Used if being redirected to ensure we have a fully qualified address
*
* @param string $last_url The URL we went to
* @param string $new_url The URL we were redirected to
* @return string The new URL that was in the HTTP header
*/
private function get_redirect_url($last_url = '', $new_url = '')
{
// Get the elements for these urls
$last_url_parse = parse_url($last_url);
$new_url_parse = parse_url($new_url);
// redirect headers are often incomplete or relative so we need to make sure they are fully qualified
$new_url_parse['scheme'] = isset($new_url_parse['scheme']) ? $new_url_parse['scheme'] : $last_url_parse['scheme'];
$new_url_parse['host'] = isset($new_url_parse['host']) ? $new_url_parse['host'] : $last_url_parse['host'];
$new_url_parse['path'] = isset($new_url_parse['path']) ? $new_url_parse['path'] : $last_url_parse['path'];
$new_url_parse['query'] = isset($new_url_parse['query']) ? $new_url_parse['query'] : '';
// Build the new URL that was in the http header
return $new_url_parse['scheme'] . '://' . $new_url_parse['host'] . $new_url_parse['path'] . (!empty($new_url_parse['query']) ? '?' . $new_url_parse['query'] : '');
}
/**
* Used to return the results to the calling program
* - called as ->result() will return the full final array
* - called as ->result('body') to just return the page source of the result
*
* @param string $area Used to return an area such as body, header, error
* @return string The response
*/
public function result($area = '')
{
$max_result = count($this->response) - 1;
// just return a specifed area or the entire result?
if ($area == '')
return $this->response[$max_result];
else
return isset($this->response[$max_result][$area]) ? $this->response[$max_result][$area] : $this->response[$max_result];
}
/**
* Will return all results from all loops (redirects)
* - Can be called as ->result_raw(x) where x is a specific loop results.
* - Call as ->result_raw() for everything.
*
* @param string $response_number Which response we want to get
* @return array|string The entire response array or just the specified response
*/
public function result_raw($response_number = '')
{
if (!is_numeric($response_number))
return $this->response;
else
{
$response_number = min($response_number, count($this->response) - 1);
return $this->response[$response_number];
}
}
/**
* Takes supplied POST data and url encodes it
* - forms the date (for post) in to a string var=xyz&var2=abc&var3=123
* - drops vars with @ since we don't support sending files (uploading)
*
* @param array|string $post_data The raw POST data
* @return string A string of post data
*/
private function build_post_data($post_data)
{
if (is_array($post_data))
{
$postvars = array();
// build the post data, drop ones with leading @'s since those can be used to send files, we don't support that.
foreach ($post_data as $name => $value)
$postvars[] = $name . '=' . urlencode($value[0] == '@' ? '' : $value);
return implode('&', $postvars);
}
else
return $post_data;
}
/**
* Sets the final cURL options for the current call
* - overwrites our default values with user supplied ones or appends new user ones to what we have
* - sets the callback function now that $this is existing
*
* @return void
*/
private function set_options()
{
// Callback to parse the returned headers, if any
$this->default_options[CURLOPT_HEADERFUNCTION] = array($this, 'header_callback');
// Any user options to account for
if (is_array($this->user_options))
{
$keys = array_merge(array_keys($this->default_options), array_keys($this->user_options));
$vals = array_merge($this->default_options, $this->user_options);
$this->options = array_combine($keys, $vals);
}
else
$this->options = $this->default_options;
// POST data options, here we don't allow any overide
if (isset($this->post_data))
{
$this->options[CURLOPT_POST] = 1;
$this->options[CURLOPT_POSTFIELDS] = $this->post_data;
}
}
/**
* Called to initiate a redirect from a 301, 302 or 307 header
* - resets the cURL options for the loop, sets the referrer flag
*
* @param string $target_url The URL we want to redirect to
* @param string $referer_url The URL that we're redirecting from
*/
private function redirect($target_url, $referer_url)
{
// no no I last saw that over there ... really, 301, 302, 307
$this->set_options();
$this->options[CURLOPT_REFERER] = $referer_url;
$this->curl_request($target_url, true);
}
/**
* Callback function to parse returned headers
* - lowercases everything to make it consistent
*
* @param curl_fetch_web_data $cr The curl request
* @param string $header The header
* @return int The length of the header
*/
private function header_callback($cr, $header)
{
$_header = trim($header);
$temp = explode(': ', $_header, 2);
// set proper headers only
if (isset($temp[0]) && isset($temp[1]))
$this->headers[strtolower($temp[0])] = strtolower(trim($temp[1]));
// return the length of what was passed unless you want a Failed writing header error ;)
return strlen($header);
}
}
?>

705
Sources/Class-Graphics.php Normal file
View file

@ -0,0 +1,705 @@
<?php
/**
* Classes used for reading gif files (in case PHP's GD doesn't provide the
* proper gif-functions).
*
* Gif Util copyright 2003 by Yamasoft (S/C). All rights reserved.
* Do not remove this portion of the header, or use these functions except
* from the original author. To get it, please navigate to:
* http://www.yamasoft.com/php-gif.zip
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Class gif_lzw_compression
*
* An implementation of the LZW compression algorithm
*/
class gif_lzw_compression
{
public $MAX_LZW_BITS;
public $Fresh, $CodeSize, $SetCodeSize, $MaxCode, $MaxCodeSize, $FirstCode, $OldCode;
public $ClearCode, $EndCode, $Next, $Vals, $Stack, $sp, $Buf, $CurBit, $LastBit, $Done, $LastByte;
public function __construct()
{
$this->MAX_LZW_BITS = 12;
unset($this->Next, $this->Vals, $this->Stack, $this->Buf);
$this->Next = range(0, (1 << $this->MAX_LZW_BITS) - 1);
$this->Vals = range(0, (1 << $this->MAX_LZW_BITS) - 1);
$this->Stack = range(0, (1 << ($this->MAX_LZW_BITS + 1)) - 1);
$this->Buf = range(0, 279);
}
public function decompress($data, &$datLen)
{
$stLen = strlen($data);
$datLen = 0;
$ret = '';
$this->LZWCommand($data, true);
while (($iIndex = $this->LZWCommand($data, false)) >= 0)
$ret .= chr($iIndex);
$datLen = $stLen - strlen($data);
if ($iIndex != -2)
return false;
return $ret;
}
public function LZWCommand(&$data, $bInit)
{
if ($bInit)
{
$this->SetCodeSize = ord($data[0]);
$data = substr($data, 1);
$this->CodeSize = $this->SetCodeSize + 1;
$this->ClearCode = 1 << $this->SetCodeSize;
$this->EndCode = $this->ClearCode + 1;
$this->MaxCode = $this->ClearCode + 2;
$this->MaxCodeSize = $this->ClearCode << 1;
$this->GetCode($data, $bInit);
$this->Fresh = 1;
for ($i = 0; $i < $this->ClearCode; $i++)
{
$this->Next[$i] = 0;
$this->Vals[$i] = $i;
}
for (; $i < (1 << $this->MAX_LZW_BITS); $i++)
{
$this->Next[$i] = 0;
$this->Vals[$i] = 0;
}
$this->sp = 0;
return 1;
}
if ($this->Fresh)
{
$this->Fresh = 0;
do
{
$this->FirstCode = $this->GetCode($data, $bInit);
$this->OldCode = $this->FirstCode;
}
while ($this->FirstCode == $this->ClearCode);
return $this->FirstCode;
}
if ($this->sp > 0)
{
$this->sp--;
return $this->Stack[$this->sp];
}
while (($Code = $this->GetCode($data, $bInit)) >= 0)
{
if ($Code == $this->ClearCode)
{
for ($i = 0; $i < $this->ClearCode; $i++)
{
$this->Next[$i] = 0;
$this->Vals[$i] = $i;
}
for (; $i < (1 << $this->MAX_LZW_BITS); $i++)
{
$this->Next[$i] = 0;
$this->Vals[$i] = 0;
}
$this->CodeSize = $this->SetCodeSize + 1;
$this->MaxCodeSize = $this->ClearCode << 1;
$this->MaxCode = $this->ClearCode + 2;
$this->sp = 0;
$this->FirstCode = $this->GetCode($data, $bInit);
$this->OldCode = $this->FirstCode;
return $this->FirstCode;
}
if ($Code == $this->EndCode)
return -2;
$InCode = $Code;
if ($Code >= $this->MaxCode)
{
$this->Stack[$this->sp] = $this->FirstCode;
$this->sp++;
$Code = $this->OldCode;
}
while ($Code >= $this->ClearCode)
{
$this->Stack[$this->sp] = $this->Vals[$Code];
$this->sp++;
if ($Code == $this->Next[$Code]) // Circular table entry, big GIF Error!
return -1;
$Code = $this->Next[$Code];
}
$this->FirstCode = $this->Vals[$Code];
$this->Stack[$this->sp] = $this->FirstCode;
$this->sp++;
if (($Code = $this->MaxCode) < (1 << $this->MAX_LZW_BITS))
{
$this->Next[$Code] = $this->OldCode;
$this->Vals[$Code] = $this->FirstCode;
$this->MaxCode++;
if (($this->MaxCode >= $this->MaxCodeSize) && ($this->MaxCodeSize < (1 << $this->MAX_LZW_BITS)))
{
$this->MaxCodeSize *= 2;
$this->CodeSize++;
}
}
$this->OldCode = $InCode;
if ($this->sp > 0)
{
$this->sp--;
return $this->Stack[$this->sp];
}
}
return $Code;
}
public function GetCode(&$data, $bInit)
{
if ($bInit)
{
$this->CurBit = 0;
$this->LastBit = 0;
$this->Done = 0;
$this->LastByte = 2;
return 1;
}
if (($this->CurBit + $this->CodeSize) >= $this->LastBit)
{
if ($this->Done)
{
// Ran off the end of my bits...
if ($this->CurBit >= $this->LastBit)
return 0;
return -1;
}
$this->Buf[0] = $this->Buf[$this->LastByte - 2];
$this->Buf[1] = $this->Buf[$this->LastByte - 1];
$count = ord($data[0]);
$data = substr($data, 1);
if ($count)
{
for ($i = 0; $i < $count; $i++)
$this->Buf[2 + $i] = ord($data[$i]);
$data = substr($data, $count);
}
else
$this->Done = 1;
$this->LastByte = 2 + $count;
$this->CurBit = ($this->CurBit - $this->LastBit) + 16;
$this->LastBit = (2 + $count) << 3;
}
$iRet = 0;
for ($i = $this->CurBit, $j = 0; $j < $this->CodeSize; $i++, $j++)
$iRet |= (($this->Buf[intval($i / 8)] & (1 << ($i % 8))) != 0) << $j;
$this->CurBit += $this->CodeSize;
return $iRet;
}
}
class gif_color_table
{
public $m_nColors;
public $m_arColors;
public function __construct()
{
unset($this->m_nColors, $this->m_arColors);
}
public function load($lpData, $num)
{
$this->m_nColors = 0;
$this->m_arColors = array();
for ($i = 0; $i < $num; $i++)
{
$rgb = substr($lpData, $i * 3, 3);
if (strlen($rgb) < 3)
return false;
$this->m_arColors[] = (ord($rgb[2]) << 16) + (ord($rgb[1]) << 8) + ord($rgb[0]);
$this->m_nColors++;
}
return true;
}
public function toString()
{
$ret = '';
for ($i = 0; $i < $this->m_nColors; $i++)
{
$ret .=
chr(($this->m_arColors[$i] & 0x000000FF)) . // R
chr(($this->m_arColors[$i] & 0x0000FF00) >> 8) . // G
chr(($this->m_arColors[$i] & 0x00FF0000) >> 16); // B
}
return $ret;
}
public function colorIndex($rgb)
{
$dif = 0;
$rgb = intval($rgb) & 0xFFFFFF;
$r1 = ($rgb & 0x0000FF);
$g1 = ($rgb & 0x00FF00) >> 8;
$b1 = ($rgb & 0xFF0000) >> 16;
$idx = -1;
for ($i = 0; $i < $this->m_nColors; $i++)
{
$r2 = ($this->m_arColors[$i] & 0x000000FF);
$g2 = ($this->m_arColors[$i] & 0x0000FF00) >> 8;
$b2 = ($this->m_arColors[$i] & 0x00FF0000) >> 16;
$d = abs($r2 - $r1) + abs($g2 - $g1) + abs($b2 - $b1);
if (($idx == -1) || ($d < $dif))
{
$idx = $i;
$dif = $d;
}
}
return $idx;
}
}
class gif_file_header
{
public $m_lpVer, $m_nWidth, $m_nHeight, $m_bGlobalClr, $m_nColorRes;
public $m_bSorted, $m_nTableSize, $m_nBgColor, $m_nPixelRatio;
public $m_colorTable;
public function __construct()
{
unset($this->m_lpVer, $this->m_nWidth, $this->m_nHeight, $this->m_bGlobalClr, $this->m_nColorRes);
unset($this->m_bSorted, $this->m_nTableSize, $this->m_nBgColor, $this->m_nPixelRatio, $this->m_colorTable);
}
public function load($lpData, &$hdrLen)
{
$hdrLen = 0;
$this->m_lpVer = substr($lpData, 0, 6);
if (($this->m_lpVer != 'GIF87a') && ($this->m_lpVer != 'GIF89a'))
return false;
list ($this->m_nWidth, $this->m_nHeight) = array_values(unpack('v2', substr($lpData, 6, 4)));
if (!$this->m_nWidth || !$this->m_nHeight)
return false;
$b = ord(substr($lpData, 10, 1));
$this->m_bGlobalClr = ($b & 0x80) ? true : false;
$this->m_nColorRes = ($b & 0x70) >> 4;
$this->m_bSorted = ($b & 0x08) ? true : false;
$this->m_nTableSize = 2 << ($b & 0x07);
$this->m_nBgColor = ord(substr($lpData, 11, 1));
$this->m_nPixelRatio = ord(substr($lpData, 12, 1));
$hdrLen = 13;
if ($this->m_bGlobalClr)
{
$this->m_colorTable = new gif_color_table();
if (!$this->m_colorTable->load(substr($lpData, $hdrLen), $this->m_nTableSize))
return false;
$hdrLen += 3 * $this->m_nTableSize;
}
return true;
}
}
class gif_image_header
{
public $m_nLeft, $m_nTop, $m_nWidth, $m_nHeight, $m_bLocalClr;
public $m_bInterlace, $m_bSorted, $m_nTableSize, $m_colorTable;
public function __construct()
{
unset($this->m_nLeft, $this->m_nTop, $this->m_nWidth, $this->m_nHeight, $this->m_bLocalClr);
unset($this->m_bInterlace, $this->m_bSorted, $this->m_nTableSize, $this->m_colorTable);
}
public function load($lpData, &$hdrLen)
{
$hdrLen = 0;
// Get the width/height/etc. from the header.
list ($this->m_nLeft, $this->m_nTop, $this->m_nWidth, $this->m_nHeight) = array_values(unpack('v4', substr($lpData, 0, 8)));
if (!$this->m_nWidth || !$this->m_nHeight)
return false;
$b = ord($lpData[8]);
$this->m_bLocalClr = ($b & 0x80) ? true : false;
$this->m_bInterlace = ($b & 0x40) ? true : false;
$this->m_bSorted = ($b & 0x20) ? true : false;
$this->m_nTableSize = 2 << ($b & 0x07);
$hdrLen = 9;
if ($this->m_bLocalClr)
{
$this->m_colorTable = new gif_color_table();
if (!$this->m_colorTable->load(substr($lpData, $hdrLen), $this->m_nTableSize))
return false;
$hdrLen += 3 * $this->m_nTableSize;
}
return true;
}
}
class gif_image
{
public $m_disp, $m_bUser, $m_bTrans, $m_nDelay, $m_nTrans, $m_lpComm;
public $m_gih, $m_data, $m_lzw;
public function __construct()
{
unset($this->m_disp, $this->m_bUser, $this->m_nDelay, $this->m_nTrans, $this->m_lpComm, $this->m_data);
$this->m_gih = new gif_image_header();
$this->m_lzw = new gif_lzw_compression();
}
public function load($data, &$datLen)
{
$datLen = 0;
while (true)
{
$b = ord($data[0]);
$data = substr($data, 1);
$datLen++;
switch ($b)
{
// Extension...
case 0x21:
$len = 0;
if (!$this->skipExt($data, $len))
return false;
$datLen += $len;
break;
// Image...
case 0x2C:
// Load the header and color table.
$len = 0;
if (!$this->m_gih->load($data, $len))
return false;
$data = substr($data, $len);
$datLen += $len;
// Decompress the data, and ride on home ;).
$len = 0;
if (!($this->m_data = $this->m_lzw->decompress($data, $len)))
return false;
$datLen += $len;
if ($this->m_gih->m_bInterlace)
$this->deInterlace();
return true;
case 0x3B: // EOF
default:
return false;
}
}
return false;
}
public function skipExt(&$data, &$extLen)
{
$extLen = 0;
$b = ord($data[0]);
$data = substr($data, 1);
$extLen++;
switch ($b)
{
// Graphic Control...
case 0xF9:
$b = ord($data[1]);
$this->m_disp = ($b & 0x1C) >> 2;
$this->m_bUser = ($b & 0x02) ? true : false;
$this->m_bTrans = ($b & 0x01) ? true : false;
list ($this->m_nDelay) = array_values(unpack('v', substr($data, 2, 2)));
$this->m_nTrans = ord($data[4]);
break;
// Comment...
case 0xFE:
$this->m_lpComm = substr($data, 1, ord($data[0]));
break;
// Plain text...
case 0x01:
break;
// Application...
case 0xFF:
break;
}
// Skip default as defs may change.
$b = ord($data[0]);
$data = substr($data, 1);
$extLen++;
while ($b > 0)
{
$data = substr($data, $b);
$extLen += $b;
$b = ord($data[0]);
$data = substr($data, 1);
$extLen++;
}
return true;
}
public function deInterlace()
{
$data = $this->m_data;
for ($i = 0; $i < 4; $i++)
{
switch ($i)
{
case 0:
$s = 8;
$y = 0;
break;
case 1:
$s = 8;
$y = 4;
break;
case 2:
$s = 4;
$y = 2;
break;
case 3:
$s = 2;
$y = 1;
break;
}
for (; $y < $this->m_gih->m_nHeight; $y += $s)
{
$lne = substr($this->m_data, 0, $this->m_gih->m_nWidth);
$this->m_data = substr($this->m_data, $this->m_gih->m_nWidth);
$data =
substr($data, 0, $y * $this->m_gih->m_nWidth) .
$lne .
substr($data, ($y + 1) * $this->m_gih->m_nWidth);
}
}
$this->m_data = $data;
}
}
class gif_file
{
public $header, $image, $data, $loaded;
public function __construct()
{
$this->data = '';
$this->loaded = false;
$this->header = new gif_file_header();
$this->image = new gif_image();
}
public function loadFile($filename, $iIndex)
{
if ($iIndex < 0)
return false;
$this->data = @file_get_contents($filename);
if ($this->data === false)
return false;
// Tell the header to load up....
$len = 0;
if (!$this->header->load($this->data, $len))
return false;
$this->data = substr($this->data, $len);
// Keep reading (at least once) so we get to the actual image we're looking for.
for ($j = 0; $j <= $iIndex; $j++)
{
$imgLen = 0;
if (!$this->image->load($this->data, $imgLen))
return false;
$this->data = substr($this->data, $imgLen);
}
$this->loaded = true;
return true;
}
public function get_png_data($background_color)
{
if (!$this->loaded)
return false;
// Prepare the color table.
if ($this->image->m_gih->m_bLocalClr)
{
$colors = $this->image->m_gih->m_nTableSize;
$pal = $this->image->m_gih->m_colorTable->toString();
if ($background_color != -1)
$background_color = $this->image->m_gih->m_colorTable->colorIndex($background_color);
}
elseif ($this->header->m_bGlobalClr)
{
$colors = $this->header->m_nTableSize;
$pal = $this->header->m_colorTable->toString();
if ($background_color != -1)
$background_color = $this->header->m_colorTable->colorIndex($background_color);
}
else
{
$colors = 0;
$background_color = -1;
}
if ($background_color == -1)
$background_color = $this->header->m_nBgColor;
$data = &$this->image->m_data;
$header = &$this->image->m_gih;
$i = 0;
$bmp = '';
// Prepare the bitmap itself.
for ($y = 0; $y < $this->header->m_nHeight; $y++)
{
$bmp .= "\x00";
for ($x = 0; $x < $this->header->m_nWidth; $x++, $i++)
{
// Is this in the proper range? If so, get the specific pixel data...
if ($x >= $header->m_nLeft && $y >= $header->m_nTop && $x < ($header->m_nLeft + $header->m_nWidth) && $y < ($header->m_nTop + $header->m_nHeight))
$bmp .= $data[$i];
// Otherwise, this is background...
else
$bmp .= chr($background_color);
}
}
$bmp = gzcompress($bmp, 9);
// Output the basic signature first of all.
$out = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
// Now, we want the header...
$out .= "\x00\x00\x00\x0D";
$tmp = 'IHDR' . pack('N', (int) $this->header->m_nWidth) . pack('N', (int) $this->header->m_nHeight) . "\x08\x03\x00\x00\x00";
$out .= $tmp . pack('N', smf_crc32($tmp));
// The palette, assuming we have one to speak of...
if ($colors > 0)
{
$out .= pack('N', (int) $colors * 3);
$tmp = 'PLTE' . $pal;
$out .= $tmp . pack('N', smf_crc32($tmp));
}
// Do we have any transparency we want to make available?
if ($this->image->m_bTrans && $colors > 0)
{
$out .= pack('N', (int) $colors);
$tmp = 'tRNS';
// Stick each color on - full transparency or none.
for ($i = 0; $i < $colors; $i++)
$tmp .= $i == $this->image->m_nTrans ? "\x00" : "\xFF";
$out .= $tmp . pack('N', smf_crc32($tmp));
}
// Here's the data itself!
$out .= pack('N', strlen($bmp));
$tmp = 'IDAT' . $bmp;
$out .= $tmp . pack('N', smf_crc32($tmp));
// EOF marker...
$out .= "\x00\x00\x00\x00" . 'IEND' . "\xAE\x42\x60\x82";
return $out;
}
}
// 64-bit only functions?
if (!function_exists('smf_crc32'))
{
require_once $sourcedir . '/Subs-Compat.php';
}
?>

1229
Sources/Class-Package.php Normal file

File diff suppressed because it is too large Load diff

611
Sources/Class-Punycode.php Executable file
View file

@ -0,0 +1,611 @@
<?php
/**
* A class for encoding/decoding Punycode.
*
* Derived from this library: https://github.com/true/php-punycode
*
* @author TrueServer B.V. <support@true.nl>
* @package php-punycode
* @license MIT
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Punycode implementation as described in RFC 3492
*
* @link http://tools.ietf.org/html/rfc3492
*/
class Punycode
{
/**
* Bootstring parameter values
*
*/
const BASE = 36;
const TMIN = 1;
const TMAX = 26;
const SKEW = 38;
const DAMP = 700;
const INITIAL_BIAS = 72;
const INITIAL_N = 128;
const PREFIX = 'xn--';
const DELIMITER = '-';
/**
* IDNA Error constants
*/
const IDNA_ERROR_EMPTY_LABEL = 1;
const IDNA_ERROR_LABEL_TOO_LONG = 2;
const IDNA_ERROR_DOMAIN_NAME_TOO_LONG = 4;
const IDNA_ERROR_LEADING_HYPHEN = 8;
const IDNA_ERROR_TRAILING_HYPHEN = 16;
const IDNA_ERROR_HYPHEN_3_4 = 32;
const IDNA_ERROR_LEADING_COMBINING_MARK = 64;
const IDNA_ERROR_DISALLOWED = 128;
const IDNA_ERROR_PUNYCODE = 256;
const IDNA_ERROR_LABEL_HAS_DOT = 512;
const IDNA_ERROR_INVALID_ACE_LABEL = 1024;
const IDNA_ERROR_BIDI = 2048;
const IDNA_ERROR_CONTEXTJ = 4096;
/**
* Encode table
*
* @param array
*/
protected static $encodeTable = array(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
);
/**
* Decode table
*
* @param array
*/
protected static $decodeTable = array(
'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3, 'e' => 4, 'f' => 5,
'g' => 6, 'h' => 7, 'i' => 8, 'j' => 9, 'k' => 10, 'l' => 11,
'm' => 12, 'n' => 13, 'o' => 14, 'p' => 15, 'q' => 16, 'r' => 17,
's' => 18, 't' => 19, 'u' => 20, 'v' => 21, 'w' => 22, 'x' => 23,
'y' => 24, 'z' => 25, '0' => 26, '1' => 27, '2' => 28, '3' => 29,
'4' => 30, '5' => 31, '6' => 32, '7' => 33, '8' => 34, '9' => 35
);
/**
* Character encoding
*
* @param string
*/
protected $encoding;
/**
* Whether to use Non-Transitional Processing.
* Setting this to true breaks backward compatibility with IDNA2003.
*
* @param bool
*/
protected $nonTransitional = false;
/**
* Whether to use STD3 ASCII rules.
*
* @param bool
*/
protected $std3 = false;
/**
* Constructor
*
* @param string $encoding Character encoding
*/
public function __construct($encoding = 'UTF-8')
{
$this->encoding = $encoding;
}
/**
* Enable/disable Non-Transitional Processing
*
* @param bool $nonTransitional Whether to use Non-Transitional Processing
*/
public function useNonTransitional(bool $nonTransitional)
{
$this->nonTransitional = $nonTransitional;
}
/**
* Enable/disable STD3 ASCII rules
*
* @param bool $std3 Whether to use STD3 ASCII rules
*/
public function useStd3(bool $std3)
{
$this->std3 = $std3;
}
/**
* Encode a domain to its Punycode version
*
* @param string $input Domain name in Unicode to be encoded
* @return string Punycode representation in ASCII
*/
public function encode($input)
{
// For compatibility with idn_to_* functions
if ($this->decode($input) === false)
return false;
$errors = array();
$preprocessed = $this->preprocess($input, $errors);
if (!empty($errors))
{
return false;
}
$parts = explode('.', $preprocessed);
foreach ($parts as $p => &$part) {
$part = $this->encodePart($part);
$validation_status = $this->validateLabel($part, true);
switch ($validation_status) {
case self::IDNA_ERROR_LABEL_TOO_LONG:
case self::IDNA_ERROR_LEADING_HYPHEN:
case self::IDNA_ERROR_TRAILING_HYPHEN:
case self::IDNA_ERROR_LEADING_COMBINING_MARK:
case self::IDNA_ERROR_DISALLOWED:
case self::IDNA_ERROR_PUNYCODE:
case self::IDNA_ERROR_LABEL_HAS_DOT:
case self::IDNA_ERROR_INVALID_ACE_LABEL:
case self::IDNA_ERROR_BIDI:
case self::IDNA_ERROR_CONTEXTJ:
return false;
break;
case self::IDNA_ERROR_HYPHEN_3_4:
$part = $parts[$p];
break;
case self::IDNA_ERROR_EMPTY_LABEL:
$parts_count = count($parts);
if ($parts_count === 1 || $p !== $parts_count - 1)
return false;
break;
default:
break;
}
}
$output = implode('.', $parts);
// IDNA_ERROR_DOMAIN_NAME_TOO_LONG
if (strlen(rtrim($output, '.')) > 253)
return false;
return $output;
}
/**
* Encode a part of a domain name, such as tld, to its Punycode version
*
* @param string $input Part of a domain name
* @return string Punycode representation of a domain part
*/
protected function encodePart($input)
{
$codePoints = $this->listCodePoints($input);
$n = static::INITIAL_N;
$bias = static::INITIAL_BIAS;
$delta = 0;
$h = $b = count($codePoints['basic']);
$output = '';
foreach ($codePoints['basic'] as $code) {
$output .= $this->codePointToChar($code);
}
if ($input === $output) {
return $output;
}
if ($b > 0) {
$output .= static::DELIMITER;
}
$codePoints['nonBasic'] = array_unique($codePoints['nonBasic']);
sort($codePoints['nonBasic']);
$i = 0;
$length = mb_strlen($input, $this->encoding);
while ($h < $length) {
$m = $codePoints['nonBasic'][$i++];
$delta = $delta + ($m - $n) * ($h + 1);
$n = $m;
foreach ($codePoints['all'] as $c) {
if ($c < $n || $c < static::INITIAL_N) {
$delta++;
}
if ($c === $n) {
$q = $delta;
for ($k = static::BASE;; $k += static::BASE) {
$t = $this->calculateThreshold($k, $bias);
if ($q < $t) {
break;
}
$code = $t + (((int) $q - $t) % (static::BASE - $t));
$output .= static::$encodeTable[$code];
$q = ($q - $t) / (static::BASE - $t);
}
$output .= static::$encodeTable[(int) $q];
$bias = $this->adapt($delta, $h + 1, ($h === $b));
$delta = 0;
$h++;
}
}
$delta++;
$n++;
}
$out = static::PREFIX . $output;
return $out;
}
/**
* Decode a Punycode domain name to its Unicode counterpart
*
* @param string $input Domain name in Punycode
* @return string Unicode domain name
*/
public function decode($input)
{
$errors = array();
$preprocessed = $this->preprocess($input, $errors);
if (!empty($errors))
{
return false;
}
$parts = explode('.', $preprocessed);
foreach ($parts as $p => &$part)
{
if (strpos($part, static::PREFIX) === 0)
{
$part = substr($part, strlen(static::PREFIX));
$part = $this->decodePart($part);
if ($part === false)
return false;
}
if ($this->validateLabel($part, false) !== 0)
{
if ($part === '')
{
$parts_count = count($parts);
if ($parts_count === 1 || $p !== $parts_count - 1)
return false;
}
else
return false;
}
}
$output = implode('.', $parts);
return $output;
}
/**
* Decode a part of domain name, such as tld
*
* @param string $input Part of a domain name
* @return string Unicode domain part
*/
protected function decodePart($input)
{
$n = static::INITIAL_N;
$i = 0;
$bias = static::INITIAL_BIAS;
$output = '';
$pos = strrpos($input, static::DELIMITER);
if ($pos !== false)
{
$output = substr($input, 0, $pos++);
}
else
{
$pos = 0;
}
$outputLength = strlen($output);
$inputLength = strlen($input);
while ($pos < $inputLength)
{
$oldi = $i;
$w = 1;
for ($k = static::BASE;; $k += static::BASE)
{
if (!isset($input[$pos]) || !isset(static::$decodeTable[$input[$pos]]))
return false;
$digit = static::$decodeTable[$input[$pos++]];
$i = $i + ($digit * $w);
$t = $this->calculateThreshold($k, $bias);
if ($digit < $t)
{
break;
}
$w = $w * (static::BASE - $t);
}
$bias = $this->adapt($i - $oldi, ++$outputLength, ($oldi === 0));
$n = $n + (int) ($i / $outputLength);
$i = $i % ($outputLength);
$output = mb_substr($output, 0, $i, $this->encoding) . $this->codePointToChar($n) . mb_substr($output, $i, $outputLength - 1, $this->encoding);
$i++;
}
return $output;
}
/**
* Calculate the bias threshold to fall between TMIN and TMAX
*
* @param integer $k
* @param integer $bias
* @return integer
*/
protected function calculateThreshold($k, $bias)
{
if ($k <= $bias + static::TMIN)
{
return static::TMIN;
}
elseif ($k >= $bias + static::TMAX)
{
return static::TMAX;
}
return $k - $bias;
}
/**
* Bias adaptation
*
* @param integer $delta
* @param integer $numPoints
* @param boolean $firstTime
* @return integer
*/
protected function adapt($delta, $numPoints, $firstTime)
{
$delta = (int) (
($firstTime)
? $delta / static::DAMP
: $delta / 2
);
$delta += (int) ($delta / $numPoints);
$k = 0;
while ($delta > ((static::BASE - static::TMIN) * static::TMAX) / 2)
{
$delta = (int) ($delta / (static::BASE - static::TMIN));
$k = $k + static::BASE;
}
$k = $k + (int) (((static::BASE - static::TMIN + 1) * $delta) / ($delta + static::SKEW));
return $k;
}
/**
* List code points for a given input
*
* @param string $input
* @return array Multi-dimension array with basic, non-basic and aggregated code points
*/
protected function listCodePoints($input)
{
$codePoints = array(
'all' => array(),
'basic' => array(),
'nonBasic' => array(),
);
$length = mb_strlen($input, $this->encoding);
for ($i = 0; $i < $length; $i++)
{
$char = mb_substr($input, $i, 1, $this->encoding);
$code = $this->charToCodePoint($char);
if ($code < 128)
{
$codePoints['all'][] = $codePoints['basic'][] = $code;
}
else
{
$codePoints['all'][] = $codePoints['nonBasic'][] = $code;
}
}
return $codePoints;
}
/**
* Convert a single or multi-byte character to its code point
*
* @param string $char
* @return integer
*/
protected function charToCodePoint($char)
{
$code = ord($char[0]);
if ($code < 128)
{
return $code;
}
elseif ($code < 224)
{
return (($code - 192) * 64) + (ord($char[1]) - 128);
}
elseif ($code < 240)
{
return (($code - 224) * 4096) + ((ord($char[1]) - 128) * 64) + (ord($char[2]) - 128);
}
else
{
return (($code - 240) * 262144) + ((ord($char[1]) - 128) * 4096) + ((ord($char[2]) - 128) * 64) + (ord($char[3]) - 128);
}
}
/**
* Convert a code point to its single or multi-byte character
*
* @param integer $code
* @return string
*/
protected function codePointToChar($code)
{
if ($code <= 0x7F)
{
return chr($code);
}
elseif ($code <= 0x7FF)
{
return chr(($code >> 6) + 192) . chr(($code & 63) + 128);
}
elseif ($code <= 0xFFFF)
{
return chr(($code >> 12) + 224) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128);
}
else
{
return chr(($code >> 18) + 240) . chr((($code >> 12) & 63) + 128) . chr((($code >> 6) & 63) + 128) . chr(($code & 63) + 128);
}
}
/**
* Prepare domain name string for Punycode processing.
* See https://www.unicode.org/reports/tr46/#Processing
*
* @param string $domain A domain name
* @param array $errors Will record any errors encountered during preprocessing
*/
protected function preprocess(string $domain, array &$errors = array())
{
global $sourcedir;
require_once($sourcedir . '/Unicode/Idna.php');
require_once($sourcedir . '/Subs-Charset.php');
$regexes = idna_regex();
if (preg_match('/[' . $regexes['disallowed'] . ($this->std3 ? $regexes['disallowed_std3'] : '') . ']/u', $domain))
$errors[] = 'disallowed';
$domain = preg_replace('/[' . $regexes['ignored'] . ']/u', '', $domain);
unset($regexes);
$maps = idna_maps();
if (!$this->nonTransitional)
$maps = array_merge($maps, idna_maps_deviation());
if (!$this->std3)
$maps = array_merge($maps, idna_maps_not_std3());
return utf8_normalize_c(strtr($domain, $maps));
}
/**
* Validates an individual part of a domain name.
*
* @param string $label Individual part of a domain name.
* @param bool $toPunycode True for encoding to Punycode, false for decoding.
*/
protected function validateLabel(string $label, bool $toPunycode = true)
{
global $sourcedir;
$length = strlen($label);
if ($length === 0)
{
return self::IDNA_ERROR_EMPTY_LABEL;
}
if ($toPunycode)
{
if ($length > 63)
{
return self::IDNA_ERROR_LABEL_TOO_LONG;
}
if ($this->std3 && $length !== strspn($label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-'))
{
return self::IDNA_ERROR_PUNYCODE;
}
}
if (strpos($label, '-') === 0)
{
return self::IDNA_ERROR_LEADING_HYPHEN;
}
if (strrpos($label, '-') === $length - 1)
{
return self::IDNA_ERROR_TRAILING_HYPHEN;
}
if (substr($label, 2, 2) === '--')
{
return self::IDNA_ERROR_HYPHEN_3_4;
}
if (preg_match('/^\p{M}/u', $label))
{
return self::IDNA_ERROR_LEADING_COMBINING_MARK;
}
require_once($sourcedir . '/Unicode/Idna.php');
require_once($sourcedir . '/Subs-Charset.php');
$regexes = idna_regex();
if (preg_match('/[' . $regexes['disallowed'] . ($this->std3 ? $regexes['disallowed_std3'] : '') . ']/u', $label))
{
return self::IDNA_ERROR_INVALID_ACE_LABEL;
}
if (!$toPunycode && $label !== utf8_normalize_kc($label))
{
return self::IDNA_ERROR_INVALID_ACE_LABEL;
}
return 0;
}
}
?>

291
Sources/Class-SearchAPI.php Normal file
View file

@ -0,0 +1,291 @@
<?php
/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
/**
* Interface search_api_interface
*/
interface search_api_interface
{
/**
* Check whether the specific search operation can be performed by this API.
* The operations are the functions listed in the interface, if not supported
* they need not be declared
*
* @access public
* @param string $methodName The method
* @param array $query_params Any parameters for the query
* @return boolean Whether or not the specified method is supported
*/
public function supportsMethod($methodName, $query_params = array());
/**
* Whether this method is valid for implementation or not
*
* @access public
* @return bool Whether or not this method is valid
*/
public function isValid();
/**
* Callback function for usort used to sort the fulltext results.
* the order of sorting is: large words, small words, large words that
* are excluded from the search, small words that are excluded.
*
* @access public
* @param string $a Word A
* @param string $b Word B
* @return int An integer indicating how the words should be sorted
*/
public function searchSort($a, $b);
/**
* Callback while preparing indexes for searching
*
* @access public
* @param string $word A word to index
* @param array $wordsSearch Search words
* @param array $wordsExclude Words to exclude
* @param bool $isExcluded Whether the specfied word should be excluded
*/
public function prepareIndexes($word, array &$wordsSearch, array &$wordsExclude, $isExcluded);
/**
* Search for indexed words.
*
* @access public
* @param array $words An array of words
* @param array $search_data An array of search data
* @return mixed
*/
public function indexedWordQuery(array $words, array $search_data);
/**
* Callback when a post is created
*
* @see createPost()
*
* @access public
* @param array $msgOptions An array of post data
* @param array $topicOptions An array of topic data
* @param array $posterOptions An array of info about the person who made this post
* @return void
*/
public function postCreated(array &$msgOptions, array &$topicOptions, array &$posterOptions);
/**
* Callback when a post is modified
*
* @see modifyPost()
*
* @access public
* @param array $msgOptions An array of post data
* @param array $topicOptions An array of topic data
* @param array $posterOptions An array of info about the person who made this post
* @return void
*/
public function postModified(array &$msgOptions, array &$topicOptions, array &$posterOptions);
/**
* Callback when a post is removed
*
* @access public
* @param int $id_msg The ID of the post that was removed
* @return void
*/
public function postRemoved($id_msg);
/**
* Callback when a topic is removed
*
* @access public
* @param array $topics The ID(s) of the removed topic(s)
* @return void
*/
public function topicsRemoved(array $topics);
/**
* Callback when a topic is moved
*
* @access public
* @param array $topics The ID(s) of the moved topic(s)
* @param int $board_to The board that the topics were moved to
* @return void
*/
public function topicsMoved(array $topics, $board_to);
/**
* Callback for actually performing the search query
*
* @access public
* @param array $query_params An array of parameters for the query
* @param array $searchWords The words that were searched for
* @param array $excludedIndexWords Indexed words that should be excluded
* @param array $participants
* @param array $searchArray
* @return mixed
*/
public function searchQuery(array $query_params, array $searchWords, array $excludedIndexWords, array &$participants, array &$searchArray);
}
/**
* Class search_api
*/
abstract class search_api implements search_api_interface
{
/**
* @var string The maximum SMF version that this will work with.
*/
public $version_compatible = '2.1.999';
/**
* @var string The minimum SMF version that this will work with.
*/
public $min_smf_version = '2.1 RC1';
/**
* @var bool Whether or not it's supported
*/
public $is_supported = true;
/**
* {@inheritDoc}
*/
public function supportsMethod($methodName, $query_params = null)
{
switch ($methodName)
{
case 'postRemoved':
return true;
break;
// All other methods, too bad dunno you.
default:
return false;
break;
}
}
/**
* {@inheritDoc}
*/
public function isValid()
{
}
/**
* {@inheritDoc}
*/
public function searchSort($a, $b)
{
}
/**
* {@inheritDoc}
*/
public function prepareIndexes($word, array &$wordsSearch, array &$wordsExclude, $isExcluded)
{
}
/**
* {@inheritDoc}
*/
public function indexedWordQuery(array $words, array $search_data)
{
}
/**
* {@inheritDoc}
*/
public function postCreated(array &$msgOptions, array &$topicOptions, array &$posterOptions)
{
}
/**
* {@inheritDoc}
*/
public function postModified(array &$msgOptions, array &$topicOptions, array &$posterOptions)
{
}
/**
* {@inheritDoc}
*/
public function postRemoved($id_msg)
{
global $smcFunc;
$result = $smcFunc['db_query']('', '
SELECT DISTINCT id_search
FROM {db_prefix}log_search_results
WHERE id_msg = {int:id_msg}',
array(
'id_msg' => $id_msg,
)
);
$id_searchs = array();
while ($row = $smcFunc['db_fetch_assoc']($result))
$id_searchs[] = $row['id_search'];
if (count($id_searchs) < 1)
return;
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_search_results
WHERE id_search in ({array_int:id_searchs})',
array(
'id_searchs' => $id_searchs,
)
);
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_search_topics
WHERE id_search in ({array_int:id_searchs})',
array(
'id_searchs' => $id_searchs,
)
);
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_search_messages
WHERE id_search in ({array_int:id_searchs})',
array(
'id_searchs' => $id_searchs,
)
);
}
/**
* {@inheritDoc}
*/
public function topicsRemoved(array $topics)
{
}
/**
* {@inheritDoc}
*/
public function topicsMoved(array $topics, $board_to)
{
}
/**
* {@inheritDoc}
*/
public function searchQuery(array $query_params, array $searchWords, array $excludedIndexWords, array &$participants, array &$searchArray)
{
}
}
?>

371
Sources/Class-TOTP.php Normal file
View file

@ -0,0 +1,371 @@
<?php
/**
* A class for generating the codes compatible with the Google Authenticator and similar TOTP
* clients.
*
* NOTE: A lot of the logic from this class has been borrowed from this class:
* https://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt
*
* @author Chris Cornutt <ccornutt@phpdeveloper.org>
* @package GAuth
* @license MIT
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
namespace TOTP;
/**
* Class Auth
*
* @package TOTP
*/
class Auth
{
/**
* @var array Internal lookup table
*/
private $lookup = array();
/**
* @var string Initialization key
*/
private $initKey = null;
/**
* @var integer Seconds between key refreshes
*/
private $refreshSeconds = 30;
/**
* @var integer The length of codes to generate
*/
private $codeLength = 6;
/**
* @var integer Range plus/minus for "window of opportunity" on allowed codes
*/
private $range = 2;
/**
* Initialize the object and set up the lookup table
* Optionally the Initialization key
*
* @param string $initKey Initialization key
*/
public function __construct($initKey = null)
{
$this->buildLookup();
if ($initKey !== null)
{
$this->setInitKey($initKey);
}
}
/**
* Build the base32 lookup table
*/
public function buildLookup()
{
$lookup = array_combine(
array_merge(range('A', 'Z'), range(2, 7)),
range(0, 31)
);
$this->setLookup($lookup);
}
/**
* Get the current "range" value
*
* @return integer Range value
*/
public function getRange()
{
return $this->range;
}
/**
* Set the "range" value
*
* @param integer $range Range value
* @return \TOTP\Auth instance
*/
public function setRange($range)
{
if (!is_numeric($range))
{
throw new \InvalidArgumentException('Invalid window range');
}
$this->range = $range;
return $this;
}
/**
* Set the initialization key for the object
*
* @param string $key Initialization key
* @throws \InvalidArgumentException If hash is not valid base32
* @return \TOTP\Auth instance
*/
public function setInitKey($key)
{
if (preg_match('/^[' . implode('', array_keys($this->getLookup())) . ']+$/', $key) == false)
{
throw new \InvalidArgumentException('Invalid base32 hash!');
}
$this->initKey = $key;
return $this;
}
/**
* Get the current Initialization key
*
* @return string Initialization key
*/
public function getInitKey()
{
return $this->initKey;
}
/**
* Set the contents of the internal lookup table
*
* @param array $lookup Lookup data set
* @throws \InvalidArgumentException If lookup given is not an array
* @return \TOTP\Auth instance
*/
public function setLookup($lookup)
{
if (!is_array($lookup))
{
throw new \InvalidArgumentException('Lookup value must be an array');
}
$this->lookup = $lookup;
return $this;
}
/**
* Get the current lookup data set
*
* @return array Lookup data
*/
public function getLookup()
{
return $this->lookup;
}
/**
* Get the number of seconds for code refresh currently set
*
* @return integer Refresh in seconds
*/
public function getRefresh()
{
return $this->refreshSeconds;
}
/**
* Set the number of seconds to refresh codes
*
* @param integer $seconds Seconds to refresh
* @throws \InvalidArgumentException If seconds value is not numeric
* @return \TOTP\Auth instance
*/
public function setRefresh($seconds)
{
if (!is_numeric($seconds))
{
throw new \InvalidArgumentException('Seconds must be numeric');
}
$this->refreshSeconds = $seconds;
return $this;
}
/**
* Get the current length for generated codes
*
* @return integer Code length
*/
public function getCodeLength()
{
return $this->codeLength;
}
/**
* Set the length of the generated codes
*
* @param integer $length Code length
* @return \TOTP\Auth instance
*/
public function setCodeLength($length)
{
$this->codeLength = $length;
return $this;
}
/**
* Validate the given code
*
* @param string $code Code entered by user
* @param string $initKey Initialization key
* @param string $timestamp Timestamp for calculation
* @param integer $range Seconds before/after to validate hash against
* @throws \InvalidArgumentException If incorrect code length
* @return boolean Pass/fail of validation
*/
public function validateCode($code, $initKey = null, $timestamp = null, $range = null)
{
if (strlen($code) !== $this->getCodeLength())
{
throw new \InvalidArgumentException('Incorrect code length');
}
$range = ($range == null) ? $this->getRange() : $range;
$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp;
$initKey = ($initKey == null) ? $this->getInitKey() : $initKey;
$binary = $this->base32_decode($initKey);
for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++)
{
if ($this->generateOneTime($binary, $time) == $code)
{
return true;
}
}
return false;
}
/**
* Generate a one-time code
*
* @param string $initKey Initialization key [optional]
* @param string $timestamp Timestamp for calculation [optional]
* @return string Generated code/hash
*/
public function generateOneTime($initKey = null, $timestamp = null)
{
$initKey = ($initKey == null) ? $this->getInitKey() : $initKey;
$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp;
$hash = hash_hmac(
'sha1',
pack('N*', 0) . pack('N*', $timestamp),
$initKey,
true
);
return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT);
}
/**
* Generate a code/hash
* Useful for making Initialization codes
*
* @param integer $length Length for the generated code
* @return string Generated code
*/
public function generateCode($length = 16)
{
global $smcFunc;
$lookup = implode('', array_keys($this->getLookup()));
$code = '';
for ($i = 0; $i < $length; $i++)
{
$code .= $lookup[$smcFunc['random_int'](0, strlen($lookup) - 1)];
}
return $code;
}
/**
* Generate the timestamp for the calculation
*
* @return integer Timestamp
*/
public function generateTimestamp()
{
return floor(microtime(true) / $this->getRefresh());
}
/**
* Truncate the given hash down to just what we need
*
* @param string $hash Hash to truncate
* @return string Truncated hash value
*/
public function truncateHash($hash)
{
$offset = ord($hash[19]) & 0xf;
return (
((ord($hash[$offset + 0]) & 0x7f) << 24) |
((ord($hash[$offset + 1]) & 0xff) << 16) |
((ord($hash[$offset + 2]) & 0xff) << 8) |
(ord($hash[$offset + 3]) & 0xff)
) % pow(10, $this->getCodeLength());
}
/**
* Base32 decoding function
*
* @param string $hash The base32-encoded hash
* @throws \InvalidArgumentException When hash is not valid
* @return string Binary value of hash
*/
public function base32_decode($hash)
{
$lookup = $this->getLookup();
if (preg_match('/^[' . implode('', array_keys($lookup)) . ']+$/', $hash) == false)
{
throw new \InvalidArgumentException('Invalid base32 hash!');
}
$hash = strtoupper($hash);
$buffer = 0;
$length = 0;
$binary = '';
for ($i = 0; $i < strlen($hash); $i++)
{
$buffer = $buffer << 5;
$buffer += $lookup[$hash[$i]];
$length += 5;
if ($length >= 8)
{
$length -= 8;
$binary .= chr(($buffer & (0xFF << $length)) >> $length);
}
}
return $binary;
}
/**
* Returns a URL to QR code for embedding the QR code
*
* @param string $name The name
* @param string $code The generated code
* @return string The URL to the QR code
*/
public function getQrCodeUrl($name, $code)
{
$url = 'otpauth://totp/' . urlencode($name) . '?secret=' . $code;
return $url;
}
}
?>

436
Sources/DbExtra-mysql.php Normal file
View file

@ -0,0 +1,436 @@
<?php
/**
* This file contains rarely used extended database functionality.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Add the functions implemented in this file to the $smcFunc array.
*/
function db_extra_init()
{
global $smcFunc;
if (!isset($smcFunc['db_backup_table']) || $smcFunc['db_backup_table'] != 'smf_db_backup_table')
$smcFunc += array(
'db_backup_table' => 'smf_db_backup_table',
'db_optimize_table' => 'smf_db_optimize_table',
'db_table_sql' => 'smf_db_table_sql',
'db_list_tables' => 'smf_db_list_tables',
'db_get_version' => 'smf_db_get_version',
'db_get_vendor' => 'smf_db_get_vendor',
'db_allow_persistent' => 'smf_db_allow_persistent',
);
}
/**
* Backup $table to $backup_table.
*
* @param string $table The name of the table to backup
* @param string $backup_table The name of the backup table for this table
* @return resource -the request handle to the table creation query
*/
function smf_db_backup_table($table, $backup_table)
{
global $smcFunc, $db_prefix;
$table = str_replace('{db_prefix}', $db_prefix, $table);
// First, get rid of the old table.
$smcFunc['db_query']('', '
DROP TABLE IF EXISTS {raw:backup_table}',
array(
'backup_table' => $backup_table,
)
);
// Can we do this the quick way?
$result = $smcFunc['db_query']('', '
CREATE TABLE {raw:backup_table} LIKE {raw:table}',
array(
'backup_table' => $backup_table,
'table' => $table
)
);
// If this failed, we go old school.
if ($result)
{
$request = $smcFunc['db_query']('', '
INSERT INTO {raw:backup_table}
SELECT *
FROM {raw:table}',
array(
'backup_table' => $backup_table,
'table' => $table
)
);
// Old school or no school?
if ($request)
return $request;
}
// At this point, the quick method failed.
$result = $smcFunc['db_query']('', '
SHOW CREATE TABLE {raw:table}',
array(
'table' => $table,
)
);
list (, $create) = $smcFunc['db_fetch_row']($result);
$smcFunc['db_free_result']($result);
$create = preg_split('/[\n\r]/', $create);
$auto_inc = '';
// Default engine type.
$engine = 'MyISAM';
$charset = '';
$collate = '';
foreach ($create as $k => $l)
{
// Get the name of the auto_increment column.
if (strpos($l, 'auto_increment'))
$auto_inc = trim($l);
// For the engine type, see if we can work out what it is.
if (strpos($l, 'ENGINE') !== false || strpos($l, 'TYPE') !== false)
{
// Extract the engine type.
preg_match('~(ENGINE|TYPE)=(\w+)(\sDEFAULT)?(\sCHARSET=(\w+))?(\sCOLLATE=(\w+))?~', $l, $match);
if (!empty($match[1]))
$engine = $match[1];
if (!empty($match[2]))
$engine = $match[2];
if (!empty($match[5]))
$charset = $match[5];
if (!empty($match[7]))
$collate = $match[7];
}
// Skip everything but keys...
if (strpos($l, 'KEY') === false)
unset($create[$k]);
}
if (!empty($create))
$create = '(
' . implode('
', $create) . ')';
else
$create = '';
$request = $smcFunc['db_query']('', '
CREATE TABLE {raw:backup_table} {raw:create}
ENGINE={raw:engine}' . (empty($charset) ? '' : ' CHARACTER SET {raw:charset}' . (empty($collate) ? '' : ' COLLATE {raw:collate}')) . '
SELECT *
FROM {raw:table}',
array(
'backup_table' => $backup_table,
'table' => $table,
'create' => $create,
'engine' => $engine,
'charset' => empty($charset) ? '' : $charset,
'collate' => empty($collate) ? '' : $collate,
)
);
if ($auto_inc != '')
{
if (preg_match('~\`(.+?)\`\s~', $auto_inc, $match) != 0 && substr($auto_inc, -1, 1) == ',')
$auto_inc = substr($auto_inc, 0, -1);
$smcFunc['db_query']('', '
ALTER TABLE {raw:backup_table}
CHANGE COLUMN {raw:column_detail} {raw:auto_inc}',
array(
'backup_table' => $backup_table,
'column_detail' => $match[1],
'auto_inc' => $auto_inc,
)
);
}
return $request;
}
/**
* This function optimizes a table.
*
* @param string $table The table to be optimized
* @return int How much space was gained
*/
function smf_db_optimize_table($table)
{
global $smcFunc, $db_prefix;
$table = str_replace('{db_prefix}', $db_prefix, $table);
// Get how much overhead there is.
$request = $smcFunc['db_query']('', '
SHOW TABLE STATUS LIKE {string:table_name}',
array(
'table_name' => str_replace('_', '\_', $table),
)
);
$row = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
$data_before = isset($row['Data_free']) ? $row['Data_free'] : 0;
$request = $smcFunc['db_query']('', '
OPTIMIZE TABLE `{raw:table}`',
array(
'table' => $table,
)
);
if (!$request)
return -1;
// How much left?
$request = $smcFunc['db_query']('', '
SHOW TABLE STATUS LIKE {string:table}',
array(
'table' => str_replace('_', '\_', $table),
)
);
$row = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
$total_change = isset($row['Data_free']) && $data_before > $row['Data_free'] ? $data_before / 1024 : 0;
return $total_change;
}
/**
* This function lists all tables in the database.
* The listing could be filtered according to $filter.
*
* @param string|boolean $db string The database name or false to use the current DB
* @param string|boolean $filter String to filter by or false to list all tables
* @return array An array of table names
*/
function smf_db_list_tables($db = false, $filter = false)
{
global $db_name, $smcFunc;
$db = $db == false ? $db_name : $db;
$db = trim($db);
$filter = $filter == false ? '' : ' LIKE \'' . $filter . '\'';
$request = $smcFunc['db_query']('', '
SHOW TABLES
FROM `{raw:db}`
{raw:filter}',
array(
'db' => $db[0] == '`' ? strtr($db, array('`' => '')) : $db,
'filter' => $filter,
)
);
$tables = array();
while ($row = $smcFunc['db_fetch_row']($request))
$tables[] = $row[0];
$smcFunc['db_free_result']($request);
return $tables;
}
/**
* Dumps the schema (CREATE) for a table.
*
* @todo why is this needed for?
* @param string $tableName The name of the table
* @return string The "CREATE TABLE" SQL string for this table
*/
function smf_db_table_sql($tableName)
{
global $smcFunc, $db_prefix;
$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
// This will be needed...
$crlf = "\r\n";
// Drop it if it exists.
$schema_create = 'DROP TABLE IF EXISTS `' . $tableName . '`;' . $crlf . $crlf;
// Start the create table...
$schema_create .= 'CREATE TABLE `' . $tableName . '` (' . $crlf;
// Find all the fields.
$result = $smcFunc['db_query']('', '
SHOW FIELDS
FROM `{raw:table}`',
array(
'table' => $tableName,
)
);
while ($row = $smcFunc['db_fetch_assoc']($result))
{
// Make the CREATE for this column.
$schema_create .= ' `' . $row['Field'] . '` ' . $row['Type'] . ($row['Null'] != 'YES' ? ' NOT NULL' : '');
// Add a default...?
if (!empty($row['Default']) || $row['Null'] !== 'YES')
{
// Make a special case of auto-timestamp.
if ($row['Default'] == 'CURRENT_TIMESTAMP')
$schema_create .= ' /*!40102 NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP */';
// Text shouldn't have a default.
elseif ($row['Default'] !== null)
{
// If this field is numeric the default needs no escaping.
$type = strtolower($row['Type']);
$isNumericColumn = strpos($type, 'int') !== false || strpos($type, 'bool') !== false || strpos($type, 'bit') !== false || strpos($type, 'float') !== false || strpos($type, 'double') !== false || strpos($type, 'decimal') !== false;
$schema_create .= ' default ' . ($isNumericColumn ? $row['Default'] : '\'' . $smcFunc['db_escape_string']($row['Default']) . '\'');
}
}
// And now any extra information. (such as auto_increment.)
$schema_create .= ($row['Extra'] != '' ? ' ' . $row['Extra'] : '') . ',' . $crlf;
}
$smcFunc['db_free_result']($result);
// Take off the last comma.
$schema_create = substr($schema_create, 0, -strlen($crlf) - 1);
// Find the keys.
$result = $smcFunc['db_query']('', '
SHOW KEYS
FROM `{raw:table}`',
array(
'table' => $tableName,
)
);
$indexes = array();
while ($row = $smcFunc['db_fetch_assoc']($result))
{
// IS this a primary key, unique index, or regular index?
$row['Key_name'] = $row['Key_name'] == 'PRIMARY' ? 'PRIMARY KEY' : (empty($row['Non_unique']) ? 'UNIQUE ' : ($row['Comment'] == 'FULLTEXT' || (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT') ? 'FULLTEXT ' : 'KEY ')) . '`' . $row['Key_name'] . '`';
// Is this the first column in the index?
if (empty($indexes[$row['Key_name']]))
$indexes[$row['Key_name']] = array();
// A sub part, like only indexing 15 characters of a varchar.
if (!empty($row['Sub_part']))
$indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`(' . $row['Sub_part'] . ')';
else
$indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`';
}
$smcFunc['db_free_result']($result);
// Build the CREATEs for the keys.
foreach ($indexes as $keyname => $columns)
{
// Ensure the columns are in proper order.
ksort($columns);
$schema_create .= ',' . $crlf . ' ' . $keyname . ' (' . implode(', ', $columns) . ')';
}
// Now just get the comment and engine... (MyISAM, etc.)
$result = $smcFunc['db_query']('', '
SHOW TABLE STATUS
LIKE {string:table}',
array(
'table' => strtr($tableName, array('_' => '\\_', '%' => '\\%')),
)
);
$row = $smcFunc['db_fetch_assoc']($result);
$smcFunc['db_free_result']($result);
// Probably MyISAM.... and it might have a comment.
$schema_create .= $crlf . ') ENGINE=' . $row['Engine'] . ($row['Comment'] != '' ? ' COMMENT="' . $row['Comment'] . '"' : '');
return $schema_create;
}
/**
* Get the version number.
*
* @return string The version
*/
function smf_db_get_version()
{
static $ver;
if (!empty($ver))
return $ver;
global $smcFunc;
$request = $smcFunc['db_query']('', '
SELECT VERSION()',
array(
)
);
list ($ver) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
return $ver;
}
/**
* Figures out if we are using MySQL, Percona or MariaDB
*
* @return string The database engine we are using
*/
function smf_db_get_vendor()
{
global $smcFunc;
static $db_type;
if (!empty($db_type))
return $db_type;
$request = $smcFunc['db_query']('', 'SELECT @@version_comment');
list ($comment) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
// Skip these if we don't have a comment.
if (!empty($comment))
{
if (stripos($comment, 'percona') !== false)
return 'Percona';
if (stripos($comment, 'mariadb') !== false)
return 'MariaDB';
}
else
return 'fail';
return 'MySQL';
}
/**
* Figures out if persistent connection is allowed
*
* @return boolean
*/
function smf_db_allow_persistent()
{
$value = ini_get('mysqli.allow_persistent');
if (strtolower($value) == 'on' || strtolower($value) == 'true' || $value == '1')
return true;
else
return false;
}
?>

View file

@ -0,0 +1,341 @@
<?php
/**
* This file contains rarely used extended database functionality.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Add the functions implemented in this file to the $smcFunc array.
*/
function db_extra_init()
{
global $smcFunc;
if (!isset($smcFunc['db_backup_table']) || $smcFunc['db_backup_table'] != 'smf_db_backup_table')
$smcFunc += array(
'db_backup_table' => 'smf_db_backup_table',
'db_optimize_table' => 'smf_db_optimize_table',
'db_table_sql' => 'smf_db_table_sql',
'db_list_tables' => 'smf_db_list_tables',
'db_get_version' => 'smf_db_get_version',
'db_get_vendor' => 'smf_db_get_vendor',
'db_allow_persistent' => 'smf_db_allow_persistent',
);
}
/**
* Backup $table to $backup_table.
*
* @param string $table The name of the table to backup
* @param string $backup_table The name of the backup table for this table
* @return resource -the request handle to the table creation query
*/
function smf_db_backup_table($table, $backup_table)
{
global $smcFunc, $db_prefix;
$table = str_replace('{db_prefix}', $db_prefix, $table);
// Do we need to drop it first?
$tables = smf_db_list_tables(false, $backup_table);
if (!empty($tables))
$smcFunc['db_query']('', '
DROP TABLE {raw:backup_table}',
array(
'backup_table' => $backup_table,
)
);
/**
* @todo Should we create backups of sequences as well?
*/
$smcFunc['db_query']('', '
CREATE TABLE {raw:backup_table}
(
LIKE {raw:table}
INCLUDING DEFAULTS
)',
array(
'backup_table' => $backup_table,
'table' => $table,
)
);
$smcFunc['db_query']('', '
INSERT INTO {raw:backup_table}
SELECT * FROM {raw:table}',
array(
'backup_table' => $backup_table,
'table' => $table,
)
);
}
/**
* This function optimizes a table.
*
* @param string $table The table to be optimized
* @return int How much space was gained
*/
function smf_db_optimize_table($table)
{
global $smcFunc, $db_prefix;
$table = str_replace('{db_prefix}', $db_prefix, $table);
$pg_tables = array('pg_catalog', 'information_schema');
$request = $smcFunc['db_query']('', '
SELECT pg_relation_size(C.oid) AS "size"
FROM pg_class C
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ({array_string:pg_tables})
AND relname = {string:table}',
array(
'table' => $table,
'pg_tables' => $pg_tables,
)
);
$row = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
$old_size = $row['size'];
$request = $smcFunc['db_query']('', '
VACUUM FULL ANALYZE {raw:table}',
array(
'table' => $table,
)
);
if (!$request)
return -1;
$request = $smcFunc['db_query']('', '
SELECT pg_relation_size(C.oid) AS "size"
FROM pg_class C
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ({array_string:pg_tables})
AND relname = {string:table}',
array(
'table' => $table,
'pg_tables' => $pg_tables,
)
);
$row = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
if (isset($row['size']))
return ($old_size - $row['size']) / 1024;
else
return 0;
}
/**
* This function lists all tables in the database.
* The listing could be filtered according to $filter.
*
* @param string|boolean $db string The database name or false to use the current DB
* @param string|boolean $filter String to filter by or false to list all tables
* @return array An array of table names
*/
function smf_db_list_tables($db = false, $filter = false)
{
global $smcFunc;
$request = $smcFunc['db_query']('', '
SELECT tablename
FROM pg_tables
WHERE schemaname = {string:schema_public}' . ($filter == false ? '' : '
AND tablename LIKE {string:filter}') . '
ORDER BY tablename',
array(
'schema_public' => 'public',
'filter' => $filter,
)
);
$tables = array();
while ($row = $smcFunc['db_fetch_row']($request))
$tables[] = $row[0];
$smcFunc['db_free_result']($request);
return $tables;
}
/**
* Dumps the schema (CREATE) for a table.
*
* @todo why is this needed for?
* @param string $tableName The name of the table
* @return string The "CREATE TABLE" SQL string for this table
*/
function smf_db_table_sql($tableName)
{
global $smcFunc, $db_prefix;
$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
// This will be needed...
$crlf = "\r\n";
// Drop it if it exists.
$schema_create = 'DROP TABLE IF EXISTS ' . $tableName . ';' . $crlf . $crlf;
// Start the create table...
$schema_create .= 'CREATE TABLE ' . $tableName . ' (' . $crlf;
$index_create = '';
$seq_create = '';
// Find all the fields.
$result = $smcFunc['db_query']('', '
SELECT column_name, column_default, is_nullable, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = {string:table}
ORDER BY ordinal_position',
array(
'table' => $tableName,
)
);
while ($row = $smcFunc['db_fetch_assoc']($result))
{
if ($row['data_type'] == 'character varying')
$row['data_type'] = 'varchar';
elseif ($row['data_type'] == 'character')
$row['data_type'] = 'char';
if ($row['character_maximum_length'])
$row['data_type'] .= '(' . $row['character_maximum_length'] . ')';
// Make the CREATE for this column.
$schema_create .= ' "' . $row['column_name'] . '" ' . $row['data_type'] . ($row['is_nullable'] != 'YES' ? ' NOT NULL' : '');
// Add a default...?
if (trim($row['column_default']) != '')
{
$schema_create .= ' default ' . $row['column_default'] . '';
// Auto increment?
if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0)
{
// Get to find the next variable first!
$count_req = $smcFunc['db_query']('', '
SELECT MAX("{raw:column}")
FROM {raw:table}',
array(
'column' => $row['column_name'],
'table' => $tableName,
)
);
list ($max_ind) = $smcFunc['db_fetch_row']($count_req);
$smcFunc['db_free_result']($count_req);
// Get the right bloody start!
$seq_create .= 'CREATE SEQUENCE ' . $matches[1] . ' START WITH ' . ($max_ind + 1) . ';' . $crlf . $crlf;
}
}
$schema_create .= ',' . $crlf;
}
$smcFunc['db_free_result']($result);
// Take off the last comma.
$schema_create = substr($schema_create, 0, -strlen($crlf) - 1);
$result = $smcFunc['db_query']('', '
SELECT pg_get_indexdef(i.indexrelid) AS inddef
FROM pg_class AS c
INNER JOIN pg_index AS i ON (i.indrelid = c.oid)
INNER JOIN pg_class AS c2 ON (c2.oid = i.indexrelid)
WHERE c.relname = {string:table} AND i.indisprimary is {raw:pk}',
array(
'table' => $tableName,
'pk' => 'false',
)
);
while ($row = $smcFunc['db_fetch_assoc']($result))
{
$index_create .= $crlf . $row['inddef'] . ';';
}
$smcFunc['db_free_result']($result);
$result = $smcFunc['db_query']('', '
SELECT pg_get_constraintdef(c.oid) as pkdef
FROM pg_constraint as c
WHERE c.conrelid::regclass::text = {string:table} AND
c.contype = {string:constraintType}',
array(
'table' => $tableName,
'constraintType' => 'p',
)
);
while ($row = $smcFunc['db_fetch_assoc']($result))
{
$index_create .= $crlf . 'ALTER TABLE ' . $tableName . ' ADD ' . $row['pkdef'] . ';';
}
$smcFunc['db_free_result']($result);
// Finish it off!
$schema_create .= $crlf . ');';
return $seq_create . $schema_create . $index_create;
}
/**
* Get the version number.
*
* @return string The version
*/
function smf_db_get_version()
{
global $db_connection;
static $ver;
if (!empty($ver))
return $ver;
$ver = pg_version($db_connection)['server'];
return $ver;
}
/**
* Return PostgreSQL
*
* @return string The database engine we are using
*/
function smf_db_get_vendor()
{
return 'PostgreSQL';
}
/**
* Figures out if persistent connection is allowed
*
* @return boolean
*/
function smf_db_allow_persistent()
{
$value = ini_get('pgsql.allow_persistent');
if (strtolower($value) == 'on' || strtolower($value) == 'true' || $value == '1')
return true;
else
return false;
}
?>

View file

@ -0,0 +1,927 @@
<?php
/**
* This file contains database functionality specifically designed for packages (mods) to utilize.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Add the file functions to the $smcFunc array.
*/
function db_packages_init()
{
global $smcFunc, $reservedTables, $db_package_log, $db_prefix;
if (!isset($smcFunc['db_create_table']) || $smcFunc['db_create_table'] != 'smf_db_create_table')
{
$smcFunc += array(
'db_add_column' => 'smf_db_add_column',
'db_add_index' => 'smf_db_add_index',
'db_calculate_type' => 'smf_db_calculate_type',
'db_change_column' => 'smf_db_change_column',
'db_create_table' => 'smf_db_create_table',
'db_drop_table' => 'smf_db_drop_table',
'db_table_structure' => 'smf_db_table_structure',
'db_list_columns' => 'smf_db_list_columns',
'db_list_indexes' => 'smf_db_list_indexes',
'db_remove_column' => 'smf_db_remove_column',
'db_remove_index' => 'smf_db_remove_index',
);
$db_package_log = array();
}
// We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up!
$reservedTables = array(
'admin_info_files', 'approval_queue', 'attachments',
'background_tasks', 'ban_groups', 'ban_items', 'board_permissions',
'board_permissions_view', 'boards', 'calendar', 'calendar_holidays',
'categories', 'custom_fields', 'group_moderators', 'log_actions',
'log_activity', 'log_banned', 'log_boards', 'log_comments',
'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests',
'log_mark_read', 'log_member_notices', 'log_notify', 'log_online',
'log_packages', 'log_polls', 'log_reported', 'log_reported_comments',
'log_scheduled_tasks', 'log_search_messages', 'log_search_results',
'log_search_subjects', 'log_search_topics', 'log_spider_hits',
'log_spider_stats', 'log_subscribed', 'log_topics', 'mail_queue',
'member_logins', 'membergroups', 'members', 'mentions',
'message_icons', 'messages', 'moderator_groups', 'moderators',
'package_servers', 'permission_profiles', 'permissions',
'personal_messages', 'pm_labeled_messages', 'pm_labels',
'pm_recipients', 'pm_rules', 'poll_choices', 'polls', 'qanda',
'scheduled_tasks', 'sessions', 'settings', 'smiley_files', 'smileys',
'spiders', 'subscriptions', 'themes', 'topics', 'user_alerts',
'user_alerts_prefs', 'user_drafts', 'user_likes',
);
foreach ($reservedTables as $k => $table_name)
$reservedTables[$k] = strtolower($db_prefix . $table_name);
// We in turn may need the extra stuff.
db_extend('extra');
}
/**
* This function can be used to create a table without worrying about schema
* compatibilities across supported database systems.
* - If the table exists will, by default, do nothing.
* - Builds table with columns as passed to it - at least one column must be sent.
* The columns array should have one sub-array for each column - these sub arrays contain:
* 'name' = Column name
* 'type' = Type of column - values from (smallint, mediumint, int, text, varchar, char, tinytext, mediumtext, largetext)
* 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc.
* If not set SMF will pick a size.
* - 'default' = Default value - do not set if no default required.
* - 'not_null' => Can it be null (true or false) - if not set default will be false.
* - 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set from what
* it should begin counting.
* - Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are:
* - 'name' => Index name (If left empty SMF will generate).
* - 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'.
* - 'columns' => Array containing columns that form part of key - in the order the index is to be created.
* - parameters: (None yet)
* - if_exists values:
* - 'ignore' will do nothing if the table exists. (And will return true)
* - 'overwrite' will drop any existing table of the same name.
* - 'error' will return false if the table already exists.
* - 'update' will update the table if the table already exists (no change of ai field and only colums with the same name keep the data)
*
* @param string $table_name The name of the table to create
* @param array $columns An array of column info in the specified format
* @param array $indexes An array of index info in the specified format
* @param array $parameters Extra parameters. Currently only 'engine', the desired MySQL storage engine, is used.
* @param string $if_exists What to do if the table exists.
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal')
{
global $reservedTables, $smcFunc, $db_package_log, $db_prefix, $db_character_set, $db_name;
static $engines = array();
$old_table_exists = false;
// Strip out the table name, we might not need it in some cases
$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
$database = !empty($match[2]) ? $match[2] : $db_name;
// With or without the database name, the fullname looks like this.
$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
// Do not overwrite $table_name, this causes issues if we pass it onto a helper function.
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// First - no way do we touch SMF tables.
if (in_array(strtolower($short_table_name), $reservedTables))
return false;
// Log that we'll want to remove this on uninstall.
$db_package_log[] = array('remove_table', $short_table_name);
// Slightly easier on MySQL than the others...
$tables = $smcFunc['db_list_tables']($database);
if (in_array($full_table_name, $tables))
{
// This is a sad day... drop the table? If not, return false (error) by default.
if ($if_exists == 'overwrite')
$smcFunc['db_drop_table']($table_name);
elseif ($if_exists == 'update')
{
$smcFunc['db_transaction']('begin');
$db_trans = true;
$smcFunc['db_drop_table']($short_table_name . '_old');
$smcFunc['db_query']('', '
RENAME TABLE ' . $short_table_name . ' TO ' . $short_table_name . '_old',
array(
'security_override' => true,
)
);
$old_table_exists = true;
}
else
return $if_exists == 'ignore';
}
// Righty - let's do the damn thing!
$table_query = 'CREATE TABLE ' . $short_table_name . "\n" . '(';
foreach ($columns as $column)
$table_query .= "\n\t" . smf_db_create_query_column($column) . ',';
// Loop through the indexes next...
foreach ($indexes as $index)
{
// MySQL If its a text column, we need to add a size.
foreach ($index['columns'] as &$c)
{
$c = trim($c);
// If a size was already specified, we won't be able to match it anyways.
$key = array_search($c, array_column($columns, 'name'));
$columns[$key]['size'] = isset($columns[$key]['size']) && is_numeric($columns[$key]['size']) ? $columns[$key]['size'] : null;
list ($type, $size) = $smcFunc['db_calculate_type']($columns[$key]['type'], $columns[$key]['size']);
if (
$key === false
|| !isset($columns[$key])
|| !in_array($columns[$key]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char'))
|| (
isset($size)
&& $size <= 191
)
)
continue;
$c .= '(191)';
}
$idx_columns = implode(',', $index['columns']);
// Is it the primary?
if (isset($index['type']) && $index['type'] == 'primary')
$table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),';
else
{
if (empty($index['name']))
$index['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index['columns'])));
$table_query .= "\n\t" . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : 'KEY') . ' ' . $index['name'] . ' (' . $idx_columns . '),';
}
}
// No trailing commas!
if (substr($table_query, -1) == ',')
$table_query = substr($table_query, 0, -1);
// Which engine do we want here?
if (empty($engines))
{
// Figure out which engines we have
$get_engines = $smcFunc['db_query']('', 'SHOW ENGINES', array());
while ($row = $smcFunc['db_fetch_assoc']($get_engines))
{
if ($row['Support'] == 'YES' || $row['Support'] == 'DEFAULT')
$engines[] = $row['Engine'];
}
$smcFunc['db_free_result']($get_engines);
}
// If we don't have this engine, or didn't specify one, default to InnoDB or MyISAM
// depending on which one is available
if (!isset($parameters['engine']) || !in_array($parameters['engine'], $engines))
{
$parameters['engine'] = in_array('InnoDB', $engines) ? 'InnoDB' : 'MyISAM';
}
$table_query .= ') ENGINE=' . $parameters['engine'];
if (!empty($db_character_set) && $db_character_set == 'utf8')
$table_query .= ' DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci';
// Create the table!
$smcFunc['db_query']('', $table_query,
array(
'security_override' => true,
)
);
// Fill the old data
if ($old_table_exists)
{
$same_col = array();
$request = $smcFunc['db_query']('', '
SELECT count(*), column_name
FROM information_schema.columns
WHERE table_name in ({string:table1},{string:table2}) AND table_schema = {string:schema}
GROUP BY column_name
HAVING count(*) > 1',
array(
'table1' => $short_table_name,
'table2' => $short_table_name . '_old',
'schema' => $db_name,
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$same_col[] = $row['column_name'];
}
$smcFunc['db_query']('', '
INSERT INTO ' . $short_table_name . '('
. implode(',', $same_col) .
')
SELECT ' . implode(',', $same_col) . '
FROM ' . $short_table_name . '_old',
array()
);
$smcFunc['db_drop_table']($short_table_name . '_old');
}
return true;
}
/**
* Drop a table.
*
* @param string $table_name The name of the table to drop
* @param array $parameters Not used at the moment
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal')
{
global $reservedTables, $smcFunc, $db_prefix, $db_name;
// After stripping away the database name, this is what's left.
$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
$database = !empty($match[2]) ? $match[2] : $db_name;
// Get some aliases.
$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
// Do not overwrite $table_name, this causes issues if we pass it onto a helper function.
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// God no - dropping one of these = bad.
if (in_array(strtolower($short_table_name), $reservedTables))
return false;
// Does it exist?
$tables = $smcFunc['db_list_tables']($database);
if (in_array($full_table_name, $tables))
{
$query = 'DROP TABLE ' . $short_table_name;
$smcFunc['db_query']('',
$query,
array(
'security_override' => true,
)
);
return true;
}
// Otherwise do 'nout.
return false;
}
/**
* This function adds a column.
*
* @param string $table_name The name of the table to add the column to
* @param array $column_info An array of column info ({@see smf_db_create_table})
* @param array $parameters Not used?
* @param string $if_exists What to do if the column exists. If 'update', column is updated.
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
{
global $smcFunc, $db_package_log, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$column_info = array_change_key_case($column_info);
// Log that we will want to uninstall this!
$db_package_log[] = array('remove_column', $short_table_name, $column_info['name']);
// Does it exist - if so don't add it again!
$columns = $smcFunc['db_list_columns']($table_name, false);
foreach ($columns as $column)
if ($column == $column_info['name'])
{
// If we're going to overwrite then use change column.
if ($if_exists == 'update')
return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
else
return false;
}
// Get the specifics...
$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
// Now add the thing!
$query = '
ALTER TABLE ' . $short_table_name . '
ADD ' . smf_db_create_query_column($column_info) . (empty($column_info['auto']) ? '' : ' primary key'
);
$smcFunc['db_query']('', $query,
array(
'security_override' => true,
)
);
return true;
}
/**
* Removes a column.
*
* @param string $table_name The name of the table to drop the column from
* @param string $column_name The name of the column to drop
* @param array $parameters Not used?
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal')
{
global $smcFunc, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// Does it exist?
$columns = $smcFunc['db_list_columns']($table_name, true);
foreach ($columns as $column)
if ($column['name'] == $column_name)
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
DROP COLUMN ' . $column_name,
array(
'security_override' => true,
)
);
return true;
}
// If here we didn't have to work - joy!
return false;
}
/**
* Change a column. You only need to specify the column attributes that are changing.
*
* @param string $table_name The name of the table this column is in
* @param string $old_column The name of the column we want to change
* @param array $column_info An array of info about the "new" column definition (see {@link smf_db_create_table()})
* Note that $column_info also supports two additional parameters that only make sense when changing columns:
* - drop_default - to drop a default that was previously specified
* @return bool
*/
function smf_db_change_column($table_name, $old_column, $column_info)
{
global $smcFunc, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$column_info = array_change_key_case($column_info);
// Check it does exist!
$columns = $smcFunc['db_list_columns']($table_name, true);
$old_info = null;
foreach ($columns as $column)
if ($column['name'] == $old_column)
$old_info = $column;
// Nothing?
if ($old_info == null)
return false;
// backward compatibility
if (isset($column_info['null']) && !isset($column_info['not_null']))
$column_info['not_null'] = !$column_info['null'];
// Get the right bits.
if (isset($column_info['drop_default']) && !empty($column_info['drop_default']))
$column_info['drop_default'] = true;
else
$column_info['drop_default'] = false;
if (!isset($column_info['name']))
$column_info['name'] = $old_column;
if (!array_key_exists('default', $column_info) && array_key_exists('default', $old_info) && empty($column_info['drop_default']))
$column_info['default'] = $old_info['default'];
if (!isset($column_info['not_null']))
$column_info['not_null'] = $old_info['not_null'];
if (!isset($column_info['auto']))
$column_info['auto'] = $old_info['auto'];
if (!isset($column_info['type']))
$column_info['type'] = $old_info['type'];
if (!isset($column_info['size']) || !is_numeric($column_info['size']))
$column_info['size'] = $old_info['size'];
if (!isset($column_info['unsigned']) || !in_array($column_info['type'], array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')))
$column_info['unsigned'] = '';
// If truly unspecified, make that clear, otherwise, might be confused with NULL...
// (Unspecified = no default whatsoever = column is not nullable with a value of null...)
if (($column_info['not_null'] === true) && !$column_info['drop_default'] && array_key_exists('default', $column_info) && is_null($column_info['default']))
unset($column_info['default']);
list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
// Allow for unsigned integers (mysql only)
$unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column_info['unsigned']) ? 'unsigned ' : '';
// If you need to drop the default, that needs it's own thing...
// Must be done first, in case the default type is inconsistent with the other changes.
if ($column_info['drop_default'])
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ALTER COLUMN `' . $old_column . '` DROP DEFAULT',
array(
'security_override' => true,
)
);
}
// Set the default clause.
$default_clause = '';
if (!$column_info['drop_default'] && array_key_exists('default', $column_info))
{
if (is_null($column_info['default']))
$default_clause = 'DEFAULT NULL';
elseif (is_numeric($column_info['default']))
$default_clause = 'DEFAULT ' . (strpos($column_info['default'], '.') ? floatval($column_info['default']) : intval($column_info['default']));
elseif (is_string($column_info['default']))
$default_clause = 'DEFAULT \'' . $smcFunc['db_escape_string']($column_info['default']) . '\'';
}
if ($size !== null)
$type = $type . '(' . $size . ')';
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
CHANGE COLUMN `' . $old_column . '` `' . $column_info['name'] . '` ' . $type . ' ' .
(!empty($unsigned) ? $unsigned : '') . (!empty($column_info['not_null']) ? 'NOT NULL' : '') . ' ' .
$default_clause . ' ' .
(empty($column_info['auto']) ? '' : 'auto_increment') . ' ',
array(
'security_override' => true,
)
);
}
/**
* Add an index.
*
* @param string $table_name The name of the table to add the index to
* @param array $index_info An array of index info (see {@link smf_db_create_table()})
* @param array $parameters Not used?
* @param string $if_exists What to do if the index exists. If 'update', the definition will be updated.
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
{
global $smcFunc, $db_package_log, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// No columns = no index.
if (empty($index_info['columns']))
return false;
// MySQL If its a text column, we need to add a size.
$cols = $smcFunc['db_list_columns']($table_name, true);
foreach ($index_info['columns'] as &$c)
{
$c = trim($c);
$cols[$c]['size'] = isset($cols[$c]['size']) && is_numeric($cols[$c]['size']) ? $cols[$c]['size'] : null;
list ($type, $size) = $smcFunc['db_calculate_type']($cols[$c]['type'], $cols[$c]['size']);
// If a size was already specified, we won't be able to match it anyways.
if (
!isset($cols[$c])
|| !in_array($cols[$c]['type'], array('text', 'mediumntext', 'largetext', 'varchar', 'char'))
|| (
isset($size)
&& $size <= 191
)
)
continue;
$c .= '(191)';
}
$columns = implode(',', $index_info['columns']);
// No name - make it up!
if (empty($index_info['name']))
{
// No need for primary.
if (isset($index_info['type']) && $index_info['type'] == 'primary')
$index_info['name'] = '';
else
$index_info['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index_info['columns'])));
}
// Log that we are going to want to remove this!
$db_package_log[] = array('remove_index', $short_table_name, $index_info['name']);
// Let's get all our indexes.
$indexes = $smcFunc['db_list_indexes']($table_name, true);
// Do we already have it?
foreach ($indexes as $index)
{
if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary'))
{
// If we want to overwrite simply remove the current one then continue.
if ($if_exists != 'update' || $index['type'] == 'primary')
return false;
else
$smcFunc['db_remove_index']($table_name, $index_info['name']);
}
}
// If we're here we know we don't have the index - so just add it.
if (!empty($index_info['type']) && $index_info['type'] == 'primary')
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ADD PRIMARY KEY (' . $columns . ')',
array(
'security_override' => true,
)
);
}
else
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ADD ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : 'INDEX') . ' ' . $index_info['name'] . ' (' . $columns . ')',
array(
'security_override' => true,
)
);
}
}
/**
* Remove an index.
*
* @param string $table_name The name of the table to remove the index from
* @param string $index_name The name of the index to remove
* @param array $parameters Not used?
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal')
{
global $smcFunc, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// Better exist!
$indexes = $smcFunc['db_list_indexes']($table_name, true);
foreach ($indexes as $index)
{
// If the name is primary we want the primary key!
if ($index['type'] == 'primary' && $index_name == 'primary')
{
// Dropping primary key?
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
DROP PRIMARY KEY',
array(
'security_override' => true,
)
);
return true;
}
if ($index['name'] == $index_name)
{
// Drop the bugger...
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
DROP INDEX ' . $index_name,
array(
'security_override' => true,
)
);
return true;
}
}
// Not to be found ;(
return false;
}
/**
* Get the schema formatted name for a type.
*
* @param string $type_name The data type (int, varchar, smallint, etc.)
* @param int $type_size The size (8, 255, etc.)
* @param boolean $reverse
* @return array An array containing the appropriate type and size for this DB type
*/
function smf_db_calculate_type($type_name, $type_size = null, $reverse = false)
{
// MySQL is actually the generic baseline.
$type_name = strtolower($type_name);
// Generic => Specific.
if (!$reverse)
{
$types = array(
'inet' => 'varbinary',
);
}
else
{
$types = array(
'varbinary' => 'inet',
);
}
// Got it? Change it!
if (isset($types[$type_name]))
{
if ($type_name == 'inet' && !$reverse)
{
$type_size = 16;
$type_name = 'varbinary';
}
elseif ($type_name == 'varbinary' && $reverse && $type_size == 16)
{
$type_name = 'inet';
$type_size = null;
}
elseif ($type_name == 'varbinary')
$type_name = 'varbinary';
else
$type_name = $types[$type_name];
}
elseif ($type_name == 'boolean')
$type_size = null;
return array($type_name, $type_size);
}
/**
* Get table structure.
*
* @param string $table_name The name of the table
* @return array An array of table structure - the name, the column info from {@link smf_db_list_columns()} and the index info from {@link smf_db_list_indexes()}
*/
function smf_db_table_structure($table_name)
{
global $smcFunc, $db_prefix, $db_name;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
$database = !empty($match[2]) ? $match[2] : $db_name;
// Find the table engine and add that to the info as well
$table_status = $smcFunc['db_query']('', '
SHOW TABLE STATUS
IN {raw:db}
LIKE {string:table}',
array(
'db' => $database,
'table' => $real_table_name
)
);
// Only one row, so no need for a loop...
$row = $smcFunc['db_fetch_assoc']($table_status);
$smcFunc['db_free_result']($table_status);
return array(
'name' => $parsed_table_name,
'columns' => $smcFunc['db_list_columns']($table_name, true),
'indexes' => $smcFunc['db_list_indexes']($table_name, true),
'engine' => $row['Engine'],
);
}
/**
* Return column information for a table.
*
* @param string $table_name The name of the table to get column info for
* @param bool $detail Whether or not to return detailed info. If true, returns the column info. If false, just returns the column names.
* @param array $parameters Not used?
* @return array An array of column names or detailed column info, depending on $detail
*/
function smf_db_list_columns($table_name, $detail = false, $parameters = array())
{
global $smcFunc, $db_prefix, $db_name;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
$database = !empty($match[2]) ? $match[2] : $db_name;
$result = $smcFunc['db_query']('', '
SELECT column_name "Field", COLUMN_TYPE "Type", is_nullable "Null", COLUMN_KEY "Key" , column_default "Default", extra "Extra"
FROM information_schema.columns
WHERE table_name = {string:table_name}
AND table_schema = {string:db_name}
ORDER BY ordinal_position',
array(
'table_name' => $real_table_name,
'db_name' => $db_name,
)
);
$columns = array();
while ($row = $smcFunc['db_fetch_assoc']($result))
{
if (!$detail)
{
$columns[] = $row['Field'];
}
else
{
// Is there an auto_increment?
$auto = strpos($row['Extra'], 'auto_increment') !== false ? true : false;
// Can we split out the size?
if (preg_match('~(.+?)\s*\((\d+)\)(?:(?:\s*)?(unsigned))?~i', $row['Type'], $matches) === 1)
{
$type = $matches[1];
$size = $matches[2];
if (!empty($matches[3]) && $matches[3] == 'unsigned')
$unsigned = true;
}
else
{
$type = $row['Type'];
$size = null;
}
$columns[$row['Field']] = array(
'name' => $row['Field'],
'not_null' => $row['Null'] != 'YES',
'null' => $row['Null'] == 'YES',
'default' => isset($row['Default']) ? $row['Default'] : null,
'type' => $type,
'size' => $size,
'auto' => $auto,
);
if (isset($unsigned))
{
$columns[$row['Field']]['unsigned'] = $unsigned;
unset($unsigned);
}
}
}
$smcFunc['db_free_result']($result);
return $columns;
}
/**
* Get index information.
*
* @param string $table_name The name of the table to get indexes for
* @param bool $detail Whether or not to return detailed info.
* @param array $parameters Not used?
* @return array An array of index names or a detailed array of index info, depending on $detail
*/
function smf_db_list_indexes($table_name, $detail = false, $parameters = array())
{
global $smcFunc, $db_prefix, $db_name;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
$database = !empty($match[2]) ? $match[2] : $db_name;
$result = $smcFunc['db_query']('', '
SHOW KEYS
FROM {raw:table_name}
IN {raw:db}',
array(
'db' => $database,
'table_name' => $real_table_name,
)
);
$indexes = array();
while ($row = $smcFunc['db_fetch_assoc']($result))
{
if (!$detail)
$indexes[] = $row['Key_name'];
else
{
// What is the type?
if ($row['Key_name'] == 'PRIMARY')
$type = 'primary';
elseif (empty($row['Non_unique']))
$type = 'unique';
elseif (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT')
$type = 'fulltext';
else
$type = 'index';
// This is the first column we've seen?
if (empty($indexes[$row['Key_name']]))
{
$indexes[$row['Key_name']] = array(
'name' => $row['Key_name'],
'type' => $type,
'columns' => array(),
);
}
// Is it a partial index?
if (!empty($row['Sub_part']))
$indexes[$row['Key_name']]['columns'][] = $row['Column_name'] . '(' . $row['Sub_part'] . ')';
else
$indexes[$row['Key_name']]['columns'][] = $row['Column_name'];
}
}
$smcFunc['db_free_result']($result);
return $indexes;
}
/**
* Creates a query for a column
*
* @param array $column An array of column info
* @return string The column definition
*/
function smf_db_create_query_column($column)
{
global $smcFunc;
$column = array_change_key_case($column);
// Auto increment is easy here!
if (!empty($column['auto']))
$default = 'auto_increment';
// Make it null.
elseif (array_key_exists('default', $column) && is_null($column['default']))
$default = 'DEFAULT NULL';
// Numbers don't need quotes.
elseif (isset($column['default']) && is_numeric($column['default']))
$default = 'DEFAULT ' . (strpos($column['default'], '.') ? floatval($column['default']) : intval($column['default']));
// Non empty string.
elseif (isset($column['default']))
$default = 'DEFAULT \'' . $smcFunc['db_escape_string']($column['default']) . '\'';
else
$default = '';
// Backwards compatible with the nullable column.
if (isset($column['null']) && !isset($column['not_null']))
$column['not_null'] = !$column['null'];
// Sort out the size... and stuff...
$column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null;
list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']);
// Allow unsigned integers (mysql only)
$unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column['unsigned']) ? 'unsigned ' : '';
if ($size !== null)
$type = $type . '(' . $size . ')';
// Now just put it together!
return '`' . $column['name'] . '` ' . $type . ' ' . (!empty($unsigned) ? $unsigned : '') . (!empty($column['not_null']) ? 'NOT NULL' : '') . ' ' . $default;
}
?>

View file

@ -0,0 +1,959 @@
<?php
/**
* This file contains database functionality specifically designed for packages (mods) to utilize.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Add the file functions to the $smcFunc array.
*/
function db_packages_init()
{
global $smcFunc, $reservedTables, $db_package_log, $db_prefix;
if (!isset($smcFunc['db_create_table']) || $smcFunc['db_create_table'] != 'smf_db_create_table')
{
$smcFunc += array(
'db_add_column' => 'smf_db_add_column',
'db_add_index' => 'smf_db_add_index',
'db_calculate_type' => 'smf_db_calculate_type',
'db_change_column' => 'smf_db_change_column',
'db_create_table' => 'smf_db_create_table',
'db_drop_table' => 'smf_db_drop_table',
'db_table_structure' => 'smf_db_table_structure',
'db_list_columns' => 'smf_db_list_columns',
'db_list_indexes' => 'smf_db_list_indexes',
'db_remove_column' => 'smf_db_remove_column',
'db_remove_index' => 'smf_db_remove_index',
);
$db_package_log = array();
}
// We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up!
$reservedTables = array(
'admin_info_files', 'approval_queue', 'attachments',
'background_tasks', 'ban_groups', 'ban_items', 'board_permissions',
'board_permissions_view', 'boards', 'calendar', 'calendar_holidays',
'categories', 'custom_fields', 'group_moderators', 'log_actions',
'log_activity', 'log_banned', 'log_boards', 'log_comments',
'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests',
'log_mark_read', 'log_member_notices', 'log_notify', 'log_online',
'log_packages', 'log_polls', 'log_reported', 'log_reported_comments',
'log_scheduled_tasks', 'log_search_messages', 'log_search_results',
'log_search_subjects', 'log_search_topics', 'log_spider_hits',
'log_spider_stats', 'log_subscribed', 'log_topics', 'mail_queue',
'member_logins', 'membergroups', 'members', 'mentions',
'message_icons', 'messages', 'moderator_groups', 'moderators',
'package_servers', 'permission_profiles', 'permissions',
'personal_messages', 'pm_labeled_messages', 'pm_labels',
'pm_recipients', 'pm_rules', 'poll_choices', 'polls', 'qanda',
'scheduled_tasks', 'sessions', 'settings', 'smiley_files', 'smileys',
'spiders', 'subscriptions', 'themes', 'topics', 'user_alerts',
'user_alerts_prefs', 'user_drafts', 'user_likes',
);
foreach ($reservedTables as $k => $table_name)
$reservedTables[$k] = strtolower($db_prefix . $table_name);
// We in turn may need the extra stuff.
db_extend('extra');
}
/**
* This function can be used to create a table without worrying about schema
* compatibilities across supported database systems.
* - If the table exists will, by default, do nothing.
* - Builds table with columns as passed to it - at least one column must be sent.
* The columns array should have one sub-array for each column - these sub arrays contain:
* 'name' = Column name
* 'type' = Type of column - values from (smallint, mediumint, int, text, varchar, char, tinytext, mediumtext, largetext)
* 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc.
* If not set SMF will pick a size.
* - 'default' = Default value - do not set if no default required.
* - 'not_null' => Can it be null (true or false) - if not set default will be false.
* - 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set from what
* it should begin counting.
* - Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are:
* - 'name' => Index name (If left empty SMF will generate).
* - 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'.
* - 'columns' => Array containing columns that form part of key - in the order the index is to be created.
* - parameters: (None yet)
* - if_exists values:
* - 'ignore' will do nothing if the table exists. (And will return true)
* - 'overwrite' will drop any existing table of the same name.
* - 'error' will return false if the table already exists.
* - 'update' will update the table if the table already exists (no change of ai field and only colums with the same name keep the data)
*
* @param string $table_name The name of the table to create
* @param array $columns An array of column info in the specified format
* @param array $indexes An array of index info in the specified format
* @param array $parameters Currently not used
* @param string $if_exists What to do if the table exists.
* @param string $error
*/
function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal')
{
global $reservedTables, $smcFunc, $db_package_log, $db_prefix, $db_name;
$db_trans = false;
$old_table_exists = false;
// Strip out the table name, we might not need it in some cases
$real_prefix = preg_match('~^("?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
$database = !empty($match[2]) ? $match[2] : $db_name;
// With or without the database name, the fullname looks like this.
$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// First - no way do we touch SMF tables.
if (in_array(strtolower($short_table_name), $reservedTables))
return false;
// Log that we'll want to remove this on uninstall.
$db_package_log[] = array('remove_table', $short_table_name);
// This... my friends... is a function in a half - let's start by checking if the table exists!
$tables = $smcFunc['db_list_tables']($database);
if (in_array($full_table_name, $tables))
{
// This is a sad day... drop the table? If not, return false (error) by default.
if ($if_exists == 'overwrite')
$smcFunc['db_drop_table']($table_name);
elseif ($if_exists == 'update')
{
$smcFunc['db_drop_table']($table_name . '_old');
$smcFunc['db_transaction']('begin');
$db_trans = true;
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . ' RENAME TO ' . $short_table_name . '_old',
array(
'security_override' => true,
)
);
$old_table_exists = true;
}
else
return $if_exists == 'ignore';
}
// If we've got this far - good news - no table exists. We can build our own!
if (!$db_trans)
$smcFunc['db_transaction']('begin');
$table_query = 'CREATE TABLE ' . $short_table_name . "\n" . '(';
foreach ($columns as $column)
{
$column = array_change_key_case($column);
// If we have an auto increment do it!
if (!empty($column['auto']))
{
if (!$old_table_exists)
$smcFunc['db_query']('', '
DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq',
array(
'security_override' => true,
)
);
if (!$old_table_exists)
$smcFunc['db_query']('', '
CREATE SEQUENCE ' . $short_table_name . '_seq',
array(
'security_override' => true,
)
);
$default = 'default nextval(\'' . $short_table_name . '_seq\')';
}
elseif (isset($column['default']) && $column['default'] !== null)
$default = 'default \'' . $smcFunc['db_escape_string']($column['default']) . '\'';
else
$default = '';
// Sort out the size...
$column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null;
list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']);
if ($size !== null)
$type = $type . '(' . $size . ')';
// backward compatibility
if (isset($column['null']) && !isset($column['not_null']))
$column['not_null'] = !$column['null'];
// Now just put it together!
$table_query .= "\n\t\"" . $column['name'] . '" ' . $type . ' ' . (!empty($column['not_null']) ? 'NOT NULL' : '') . ' ' . $default . ',';
}
// Loop through the indexes a sec...
$index_queries = array();
foreach ($indexes as $index)
{
// MySQL you can do a "column_name (length)", postgresql does not allow this. Strip it.
foreach ($index['columns'] as &$c)
$c = preg_replace('~\s+(\(\d+\))~', '', $c);
$idx_columns = implode(',', $index['columns']);
// Primary goes in the table...
if (isset($index['type']) && $index['type'] == 'primary')
$table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),';
else
{
if (empty($index['name']))
$index['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index['columns'])));
$index_queries[] = 'CREATE ' . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $short_table_name . '_' . $index['name'] . ' ON ' . $short_table_name . ' (' . $idx_columns . ')';
}
}
// No trailing commas!
if (substr($table_query, -1) == ',')
$table_query = substr($table_query, 0, -1);
$table_query .= ')';
// Create the table!
$smcFunc['db_query']('', $table_query,
array(
'security_override' => true,
)
);
// Fill the old data
if ($old_table_exists)
{
$same_col = array();
$request = $smcFunc['db_query']('', '
SELECT count(*), column_name
FROM information_schema.columns
WHERE table_name in ({string:table1},{string:table2}) AND table_schema = {string:schema}
GROUP BY column_name
HAVING count(*) > 1',
array(
'table1' => $short_table_name,
'table2' => $short_table_name . '_old',
'schema' => 'public',
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$same_col[] = $row['column_name'];
}
$smcFunc['db_query']('', '
INSERT INTO ' . $short_table_name . '('
. implode(',', $same_col) .
')
SELECT ' . implode(',', $same_col) . '
FROM ' . $short_table_name . '_old',
array()
);
}
// And the indexes...
foreach ($index_queries as $query)
$smcFunc['db_query']('', $query,
array(
'security_override' => true,
)
);
// Go, go power rangers!
$smcFunc['db_transaction']('commit');
if ($old_table_exists)
$smcFunc['db_drop_table']($table_name . '_old');
return true;
}
/**
* Drop a table and its associated sequences.
*
* @param string $table_name The name of the table to drop
* @param array $parameters Not used at the moment
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal')
{
global $reservedTables, $smcFunc, $db_prefix, $db_name;
// After stripping away the database name, this is what's left.
$real_prefix = preg_match('~^("?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
$database = !empty($match[2]) ? $match[2] : $db_name;
// Get some aliases.
$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// God no - dropping one of these = bad.
if (in_array(strtolower($table_name), $reservedTables))
return false;
// Does it exist?
$tables = $smcFunc['db_list_tables']($database);
if (in_array($full_table_name, $tables))
{
// We can then drop the table.
$smcFunc['db_transaction']('begin');
// the table
$table_query = 'DROP TABLE ' . $short_table_name;
// and the assosciated sequence, if any
$sequence_query = 'DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq';
// drop them
$smcFunc['db_query']('',
$table_query,
array(
'security_override' => true,
)
);
$smcFunc['db_query']('',
$sequence_query,
array(
'security_override' => true,
)
);
$smcFunc['db_transaction']('commit');
return true;
}
// Otherwise do 'nout.
return false;
}
/**
* This function adds a column.
*
* @param string $table_name The name of the table to add the column to
* @param array $column_info An array of column info (see {@link smf_db_create_table()})
* @param array $parameters Not used?
* @param string $if_exists What to do if the column exists. If 'update', column is updated.
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
{
global $smcFunc, $db_package_log, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$column_info = array_change_key_case($column_info);
// Log that we will want to uninstall this!
$db_package_log[] = array('remove_column', $short_table_name, $column_info['name']);
// Does it exist - if so don't add it again!
$columns = $smcFunc['db_list_columns']($table_name, false);
foreach ($columns as $column)
if ($column == $column_info['name'])
{
// If we're going to overwrite then use change column.
if ($if_exists == 'update')
return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
else
return false;
}
// Get the specifics...
$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
if ($size !== null)
$type = $type . '(' . $size . ')';
// Now add the thing!
$query = '
ALTER TABLE ' . $short_table_name . '
ADD COLUMN ' . $column_info['name'] . ' ' . $type;
$smcFunc['db_query']('', $query,
array(
'security_override' => true,
)
);
// If there's more attributes they need to be done via a change on PostgreSQL.
unset($column_info['type'], $column_info['size']);
if (count($column_info) != 1)
return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
else
return true;
}
/**
* Removes a column.
*
* @param string $table_name The name of the table to drop the column from
* @param string $column_name The name of the column to drop
* @param array $parameters Not used?
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal')
{
global $smcFunc, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
// Does it exist?
$columns = $smcFunc['db_list_columns']($table_name, true);
foreach ($columns as $column)
if (strtolower($column['name']) == strtolower($column_name))
{
// If there is an auto we need remove it!
if ($column['auto'])
$smcFunc['db_query']('', '
DROP SEQUENCE IF EXISTS ' . $short_table_name . '_seq',
array(
'security_override' => true,
)
);
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
DROP COLUMN ' . $column_name,
array(
'security_override' => true,
)
);
return true;
}
// If here we didn't have to work - joy!
return false;
}
/**
* Change a column. You only need to specify the column attributes that are changing.
*
* @param string $table_name The name of the table this column is in
* @param string $old_column The name of the column we want to change
* @param array $column_info An array of info about the "new" column definition (see {@link smf_db_create_table()})
* Note that $column_info also supports two additional parameters that only make sense when changing columns:
* - drop_default - to drop a default that was previously specified
* @return bool
*/
function smf_db_change_column($table_name, $old_column, $column_info)
{
global $smcFunc, $db_prefix;
$short_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$column_info = array_change_key_case($column_info);
// Check it does exist!
$columns = $smcFunc['db_list_columns']($table_name, true);
$old_info = null;
foreach ($columns as $column)
if ($column['name'] == $old_column)
$old_info = $column;
// Nothing?
if ($old_info == null)
return false;
// backward compatibility
if (isset($column_info['null']) && !isset($column_info['not_null']))
$column_info['not_null'] = !$column_info['null'];
// Get the right bits.
if (isset($column_info['drop_default']) && !empty($column_info['drop_default']))
$column_info['drop_default'] = true;
else
$column_info['drop_default'] = false;
if (!isset($column_info['name']))
$column_info['name'] = $old_column;
if (!array_key_exists('default', $column_info) && array_key_exists('default', $old_info) && empty($column_info['drop_default']))
$column_info['default'] = $old_info['default'];
if (!isset($column_info['not_null']))
$column_info['not_null'] = $old_info['not_null'];
if (!isset($column_info['auto']))
$column_info['auto'] = $old_info['auto'];
if (!isset($column_info['type']))
$column_info['type'] = $old_info['type'];
if (!isset($column_info['size']) || !is_numeric($column_info['size']))
$column_info['size'] = $old_info['size'];
if (!isset($column_info['unsigned']) || !in_array($column_info['type'], array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')))
$column_info['unsigned'] = '';
// If truly unspecified, make that clear, otherwise, might be confused with NULL...
// (Unspecified = no default whatsoever = column is not nullable with a value of null...)
if (($column_info['not_null'] === true) && !$column_info['drop_default'] && array_key_exists('default', $column_info) && is_null($column_info['default']))
unset($column_info['default']);
// If you need to drop the default, that needs it's own thing...
// Must be done first, in case the default type is inconsistent with the other changes.
if ($column_info['drop_default'])
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ALTER COLUMN ' . $old_column . ' DROP DEFAULT',
array(
'security_override' => true,
)
);
}
// Now we check each bit individually and ALTER as required.
if (isset($column_info['name']) && $column_info['name'] != $old_column)
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
RENAME COLUMN ' . $old_column . ' TO ' . $column_info['name'],
array(
'security_override' => true,
)
);
}
// What about a change in type?
if (isset($column_info['type']) && ($column_info['type'] != $old_info['type'] || (isset($column_info['size']) && $column_info['size'] != $old_info['size'])))
{
$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
if ($size !== null)
$type = $type . '(' . $size . ')';
// The alter is a pain.
$smcFunc['db_transaction']('begin');
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ADD COLUMN ' . $column_info['name'] . '_tempxx ' . $type,
array(
'security_override' => true,
)
);
$smcFunc['db_query']('', '
UPDATE ' . $short_table_name . '
SET ' . $column_info['name'] . '_tempxx = CAST(' . $column_info['name'] . ' AS ' . $type . ')',
array(
'security_override' => true,
)
);
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
DROP COLUMN ' . $column_info['name'],
array(
'security_override' => true,
)
);
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
RENAME COLUMN ' . $column_info['name'] . '_tempxx TO ' . $column_info['name'],
array(
'security_override' => true,
)
);
$smcFunc['db_transaction']('commit');
}
// Different default?
// Just go ahead & honor the setting. Type changes above introduce defaults that we might need to override here...
if (!$column_info['drop_default'] && array_key_exists('default', $column_info))
{
// Fix the default.
$default = '';
if (is_null($column_info['default']))
$default = 'NULL';
elseif (isset($column_info['default']) && is_numeric($column_info['default']))
$default = strpos($column_info['default'], '.') ? floatval($column_info['default']) : intval($column_info['default']);
else
$default = '\'' . $smcFunc['db_escape_string']($column_info['default']) . '\'';
$action = 'SET DEFAULT ' . $default;
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ALTER COLUMN ' . $column_info['name'] . ' ' . $action,
array(
'security_override' => true,
)
);
}
// Is it null - or otherwise?
// Just go ahead & honor the setting. Type changes above introduce defaults that we might need to override here...
if ($column_info['not_null'])
$action = 'SET NOT NULL';
else
$action = 'DROP NOT NULL';
$smcFunc['db_query']('', '
ALTER TABLE ' . $short_table_name . '
ALTER COLUMN ' . $column_info['name'] . ' ' . $action,
array(
'security_override' => true,
)
);
return true;
}
/**
* Add an index.
*
* @param string $table_name The name of the table to add the index to
* @param array $index_info An array of index info (see {@link smf_db_create_table()})
* @param array $parameters Not used?
* @param string $if_exists What to do if the index exists. If 'update', the definition will be updated.
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
{
global $smcFunc, $db_package_log, $db_prefix;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
// No columns = no index.
if (empty($index_info['columns']))
return false;
// MySQL you can do a "column_name (length)", postgresql does not allow this. Strip it.
foreach ($index_info['columns'] as &$c)
$c = preg_replace('~\s+(\(\d+\))~', '', $c);
$columns = implode(',', $index_info['columns']);
// No name - make it up!
if (empty($index_info['name']))
{
// No need for primary.
if (isset($index_info['type']) && $index_info['type'] == 'primary')
$index_info['name'] = '';
else
$index_info['name'] = trim(implode('_', preg_replace('~(\(\d+\))~', '', $index_info['columns'])));
}
else
$index_info['name'] = $index_info['name'];
// Log that we are going to want to remove this!
$db_package_log[] = array('remove_index', $parsed_table_name, $index_info['name']);
// Let's get all our indexes.
$indexes = $smcFunc['db_list_indexes']($table_name, true);
// Do we already have it?
foreach ($indexes as $index)
{
if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary'))
{
// If we want to overwrite simply remove the current one then continue.
if ($if_exists != 'update' || $index['type'] == 'primary')
return false;
else
$smcFunc['db_remove_index']($table_name, $index_info['name']);
}
}
// If we're here we know we don't have the index - so just add it.
if (!empty($index_info['type']) && $index_info['type'] == 'primary')
{
$smcFunc['db_query']('', '
ALTER TABLE ' . $real_table_name . '
ADD PRIMARY KEY (' . $columns . ')',
array(
'security_override' => true,
)
);
}
else
{
$smcFunc['db_query']('', '
CREATE ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $real_table_name . '_' . $index_info['name'] . ' ON ' . $real_table_name . ' (' . $columns . ')',
array(
'security_override' => true,
)
);
}
}
/**
* Remove an index.
*
* @param string $table_name The name of the table to remove the index from
* @param string $index_name The name of the index to remove
* @param array $parameters Not used?
* @param string $error
* @return boolean Whether or not the operation was successful
*/
function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal')
{
global $smcFunc, $db_prefix;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
// Better exist!
$indexes = $smcFunc['db_list_indexes']($table_name, true);
// Do not add the table name to the index if it is arleady there.
if ($index_name != 'primary' && strpos($index_name, $real_table_name) !== false)
$index_name = str_replace($real_table_name . '_', '', $index_name);
foreach ($indexes as $index)
{
// If the name is primary we want the primary key!
if ($index['type'] == 'primary' && $index_name == 'primary')
{
// Dropping primary key is odd...
$smcFunc['db_query']('', '
ALTER TABLE ' . $real_table_name . '
DROP CONSTRAINT ' . $index['name'],
array(
'security_override' => true,
)
);
return true;
}
if ($index['name'] == $index_name)
{
// Drop the bugger...
$smcFunc['db_query']('', '
DROP INDEX ' . $real_table_name . '_' . $index_name,
array(
'security_override' => true,
)
);
return true;
}
}
// Not to be found ;(
return false;
}
/**
* Get the schema formatted name for a type.
*
* @param string $type_name The data type (int, varchar, smallint, etc.)
* @param int $type_size The size (8, 255, etc.)
* @param boolean $reverse If true, returns specific types for a generic type
* @return array An array containing the appropriate type and size for this DB type
*/
function smf_db_calculate_type($type_name, $type_size = null, $reverse = false)
{
// Let's be sure it's lowercase MySQL likes both, others no.
$type_name = strtolower($type_name);
// Generic => Specific.
if (!$reverse)
{
$types = array(
'varchar' => 'character varying',
'char' => 'character',
'mediumint' => 'int',
'tinyint' => 'smallint',
'tinytext' => 'character varying',
'mediumtext' => 'text',
'largetext' => 'text',
'inet' => 'inet',
'time' => 'time without time zone',
'datetime' => 'timestamp without time zone',
'timestamp' => 'timestamp without time zone',
);
}
else
{
$types = array(
'character varying' => 'varchar',
'character' => 'char',
'integer' => 'int',
'inet' => 'inet',
'time without time zone' => 'time',
'timestamp without time zone' => 'datetime',
'numeric' => 'decimal',
);
}
// Got it? Change it!
if (isset($types[$type_name]))
{
if ($type_name == 'tinytext')
$type_size = 255;
$type_name = $types[$type_name];
}
// Only char fields got size
if (strpos($type_name, 'char') === false)
$type_size = null;
return array($type_name, $type_size);
}
/**
* Get table structure.
*
* @param string $table_name The name of the table
* @return array An array of table structure - the name, the column info from {@link smf_db_list_columns()} and the index info from {@link smf_db_list_indexes()}
*/
function smf_db_table_structure($table_name)
{
global $smcFunc, $db_prefix;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
return array(
'name' => $real_table_name,
'columns' => $smcFunc['db_list_columns']($table_name, true),
'indexes' => $smcFunc['db_list_indexes']($table_name, true),
);
}
/**
* Return column information for a table.
*
* @param string $table_name The name of the table to get column info for
* @param bool $detail Whether or not to return detailed info. If true, returns the column info. If false, just returns the column names.
* @param array $parameters Not used?
* @return array An array of column names or detailed column info, depending on $detail
*/
function smf_db_list_columns($table_name, $detail = false, $parameters = array())
{
global $smcFunc, $db_prefix, $db_name;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
$database = !empty($match[2]) ? $match[2] : $db_name;
$result = $smcFunc['db_query']('', '
SELECT column_name, column_default, is_nullable, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = {string:schema_public}
AND table_name = {string:table_name}
ORDER BY ordinal_position',
array(
'schema_public' => 'public',
'table_name' => $real_table_name,
)
);
$columns = array();
while ($row = $smcFunc['db_fetch_assoc']($result))
{
if (!$detail)
{
$columns[] = $row['column_name'];
}
else
{
$auto = false;
$default = null;
// What is the default?
if ($row['column_default'] !== null)
{
if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0)
$auto = true;
elseif (substr($row['column_default'], 0, 4) != 'NULL' && trim($row['column_default']) != '')
{
$pos = strpos($row['column_default'], '::');
$default = trim($pos === false ? $row['column_default'] : substr($row['column_default'], 0, $pos), '\'');
}
}
// Make the type generic.
list ($type, $size) = $smcFunc['db_calculate_type']($row['data_type'], $row['character_maximum_length'], true);
$columns[$row['column_name']] = array(
'name' => $row['column_name'],
'not_null' => $row['is_nullable'] != 'YES',
'null' => $row['is_nullable'] == 'YES',
'default' => $default,
'type' => $type,
'size' => $size,
'auto' => $auto,
);
}
}
$smcFunc['db_free_result']($result);
return $columns;
}
/**
* Get index information.
*
* @param string $table_name The name of the table to get indexes for
* @param bool $detail Whether or not to return detailed info.
* @param array $parameters Not used?
* @return array An array of index names or a detailed array of index info, depending on $detail
*/
function smf_db_list_indexes($table_name, $detail = false, $parameters = array())
{
global $smcFunc, $db_prefix, $db_name;
$parsed_table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
$real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name;
$database = !empty($match[2]) ? $match[2] : $db_name;
$result = $smcFunc['db_query']('', '
SELECT CASE WHEN i.indisprimary THEN 1 ELSE 0 END AS is_primary,
CASE WHEN i.indisunique THEN 1 ELSE 0 END AS is_unique,
c2.relname AS name,
pg_get_indexdef(i.indexrelid) AS inddef
FROM pg_class AS c, pg_class AS c2, pg_index AS i
WHERE c.relname = {string:table_name}
AND c.oid = i.indrelid
AND i.indexrelid = c2.oid',
array(
'table_name' => $real_table_name,
)
);
$indexes = array();
while ($row = $smcFunc['db_fetch_assoc']($result))
{
// Try get the columns that make it up.
if (preg_match('~\(([^\)]+?)\)~i', $row['inddef'], $matches) == 0)
continue;
$columns = explode(',', $matches[1]);
if (empty($columns))
continue;
foreach ($columns as $k => $v)
$columns[$k] = trim($v);
// Fix up the name to be consistent cross databases
if (substr($row['name'], -5) == '_pkey' && $row['is_primary'] == 1)
$row['name'] = 'PRIMARY';
else
$row['name'] = str_replace($real_table_name . '_', '', $row['name']);
if (!$detail)
$indexes[] = $row['name'];
else
{
$indexes[$row['name']] = array(
'name' => $row['name'],
'type' => $row['is_primary'] ? 'primary' : ($row['is_unique'] ? 'unique' : 'index'),
'columns' => $columns,
);
}
}
$smcFunc['db_free_result']($result);
return $indexes;
}
?>

View file

@ -0,0 +1,81 @@
<?php
/**
* This file contains database functions specific to search related activity.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Add the file functions to the $smcFunc array.
*/
function db_search_init()
{
global $smcFunc;
if (!isset($smcFunc['db_search_query']) || $smcFunc['db_search_query'] != 'smf_db_query')
$smcFunc += array(
'db_search_query' => 'smf_db_query',
'db_search_support' => 'smf_db_search_support',
'db_create_word_search' => 'smf_db_create_word_search',
'db_support_ignore' => true,
);
db_extend();
$version = $smcFunc['db_get_version']();
$smcFunc['db_supports_pcre'] = version_compare($version, strpos($version, 'MariaDB') !== false ? '10.0.5' : '8.0.4', '>=');
}
/**
* This function will tell you whether this database type supports this search type.
*
* @param string $search_type The search type.
* @return boolean Whether or not the specified search type is supported by this db system
*/
function smf_db_search_support($search_type)
{
$supported_types = array('fulltext');
return in_array($search_type, $supported_types);
}
/**
* Highly specific function, to create the custom word index table.
*
* @param string $size The size of the desired index.
*/
function smf_db_create_word_search($size)
{
global $smcFunc;
if ($size == 'small')
$size = 'smallint(5)';
elseif ($size == 'medium')
$size = 'mediumint(8)';
else
$size = 'int(10)';
$smcFunc['db_query']('', '
CREATE TABLE {db_prefix}log_search_words (
id_word {raw:size} unsigned NOT NULL default {string:string_zero},
id_msg int(10) unsigned NOT NULL default {string:string_zero},
PRIMARY KEY (id_word, id_msg)
) ENGINE=InnoDB',
array(
'string_zero' => '0',
'size' => $size,
)
);
}
?>

View file

@ -0,0 +1,189 @@
<?php
/**
* This file contains database functions specific to search related activity.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Add the file functions to the $smcFunc array.
*/
function db_search_init()
{
global $smcFunc;
if (!isset($smcFunc['db_search_query']) || $smcFunc['db_search_query'] != 'smf_db_search_query')
$smcFunc += array(
'db_search_query' => 'smf_db_search_query',
'db_search_support' => 'smf_db_search_support',
'db_create_word_search' => 'smf_db_create_word_search',
'db_support_ignore' => false,
'db_search_language' => 'smf_db_search_language',
);
db_extend();
$smcFunc['db_support_ignore'] = true;
$smcFunc['db_supports_pcre'] = true;
}
/**
* This function will tell you whether this database type supports this search type.
*
* @param string $search_type The search type
* @return boolean Whether or not the specified search type is supported by this DB system.
*/
function smf_db_search_support($search_type)
{
$supported_types = array('custom', 'fulltext');
return in_array($search_type, $supported_types);
}
/**
* Returns the correct query for this search type.
*
* @param string $identifier A query identifier
* @param string $db_string The query text
* @param array $db_values An array of values to pass to $smcFunc['db_query']
* @param resource $connection The current DB connection resource
* @return resource The query result resource from $smcFunc['db_query']
*/
function smf_db_search_query($identifier, $db_string, $db_values = array(), $connection = null)
{
global $smcFunc;
$replacements = array(
'create_tmp_log_search_topics' => array(
'~ENGINE=MEMORY~i' => '',
),
'create_tmp_log_search_messages' => array(
'~ENGINE=MEMORY~i' => '',
),
'insert_into_log_messages_fulltext' => array(
'/NOT\sLIKE/' => 'NOT ILIKE',
'/\bLIKE\b/' => 'ILIKE',
'/NOT RLIKE/' => '!~*',
'/RLIKE/' => '~*',
),
'insert_log_search_results_subject' => array(
'/NOT\sLIKE/' => 'NOT ILIKE',
'/\bLIKE\b/' => 'ILIKE',
'/NOT RLIKE/' => '!~*',
'/RLIKE/' => '~*',
),
'insert_log_search_topics' => array(
'/NOT\sLIKE/' => 'NOT ILIKE',
'/\bLIKE\b/' => 'ILIKE',
'/NOT RLIKE/' => '!~*',
'/RLIKE/' => '~*',
),
'insert_log_search_results_no_index' => array(
'/NOT\sLIKE/' => 'NOT ILIKE',
'/\bLIKE\b/' => 'ILIKE',
'/NOT RLIKE/' => '!~*',
'/RLIKE/' => '~*',
),
);
if (isset($replacements[$identifier]))
$db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string);
if (preg_match('~^\s*INSERT\sIGNORE~i', $db_string) != 0)
{
$db_string = preg_replace('~^\s*INSERT\sIGNORE~i', 'INSERT', $db_string);
if ($smcFunc['db_support_ignore'])
{
//pg style "INSERT INTO.... ON CONFLICT DO NOTHING"
$db_string = $db_string . ' ON CONFLICT DO NOTHING';
}
else
{
// Don't error on multi-insert.
$db_values['db_error_skip'] = true;
}
}
//fix double quotes
if ($identifier == 'insert_into_log_messages_fulltext')
$db_string = str_replace('"', "'", $db_string);
$return = $smcFunc['db_query']('', $db_string,
$db_values, $connection
);
return $return;
}
/**
* Highly specific function, to create the custom word index table.
*
* @param string $size The column size type (int, mediumint (8), etc.). Not used here.
*/
function smf_db_create_word_search($size)
{
global $smcFunc;
$size = 'int';
$smcFunc['db_query']('', '
CREATE TABLE {db_prefix}log_search_words (
id_word {raw:size} NOT NULL default {string:string_zero},
id_msg int NOT NULL default {string:string_zero},
PRIMARY KEY (id_word, id_msg)
)',
array(
'size' => $size,
'string_zero' => '0',
)
);
}
/**
* Return the language for the textsearch index
*/
function smf_db_search_language()
{
global $smcFunc, $modSettings;
$language_ftx = 'english';
if (!empty($modSettings['search_language']))
$language_ftx = $modSettings['search_language'];
else
{
$request = $smcFunc['db_query']('', '
SELECT cfgname FROM pg_ts_config WHERE oid = current_setting({string:default_language})::regconfig',
array(
'default_language' => 'default_text_search_config'
)
);
if ($request !== false && $smcFunc['db_num_rows']($request) == 1)
{
$row = $smcFunc['db_fetch_assoc']($request);
$language_ftx = $row['cfgname'];
$smcFunc['db_insert']('replace',
'{db_prefix}settings',
array('variable' => 'string', 'value' => 'string'),
array('search_language', $language_ftx),
array('variable')
);
}
}
return $language_ftx;
}
?>

1799
Sources/Display.php Normal file

File diff suppressed because it is too large Load diff

868
Sources/Drafts.php Normal file
View file

@ -0,0 +1,868 @@
<?php
/**
* This file contains all the functions that allow for the saving,
* retrieving, deleting and settings for the drafts function.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
loadLanguage('Drafts');
/**
* Saves a post draft in the user_drafts table
* The core draft feature must be enabled, as well as the post draft option
* Determines if this is a new or an existing draft
* Returns errors in $post_errors for display in the template
*
* @param string[] $post_errors Any errors encountered trying to save this draft
* @return boolean Always returns true
*/
function SaveDraft(&$post_errors)
{
global $context, $user_info, $smcFunc, $modSettings, $board;
// can you be, should you be ... here?
if (empty($modSettings['drafts_post_enabled']) || !allowedTo('post_draft') || !isset($_POST['save_draft']) || !isset($_POST['id_draft']))
return false;
// read in what they sent us, if anything
$id_draft = (int) $_POST['id_draft'];
$draft_info = ReadDraft($id_draft);
// A draft has been saved less than 5 seconds ago, let's not do the autosave again
if (isset($_REQUEST['xml']) && !empty($draft_info['poster_time']) && time() < $draft_info['poster_time'] + 5)
{
$context['draft_saved_on'] = $draft_info['poster_time'];
// since we were called from the autosave function, send something back
if (!empty($id_draft))
XmlDraft($id_draft);
return true;
}
if (!isset($_POST['message']))
$_POST['message'] = isset($_POST['quickReply']) ? $_POST['quickReply'] : '';
// prepare any data from the form
$topic_id = empty($_REQUEST['topic']) ? 0 : (int) $_REQUEST['topic'];
$draft['icon'] = empty($_POST['icon']) ? 'xx' : preg_replace('~[\./\\\\*:"\'<>]~', '', $_POST['icon']);
$draft['smileys_enabled'] = isset($_POST['ns']) ? (int) $_POST['ns'] : 1;
$draft['locked'] = isset($_POST['lock']) ? (int) $_POST['lock'] : 0;
$draft['sticky'] = isset($_POST['sticky']) ? (int) $_POST['sticky'] : 0;
$draft['subject'] = strtr($smcFunc['htmlspecialchars']($_POST['subject']), array("\r" => '', "\n" => '', "\t" => ''));
$draft['body'] = $smcFunc['htmlspecialchars']($_POST['message'], ENT_QUOTES);
// message and subject still need a bit more work
preparsecode($draft['body']);
if ($smcFunc['strlen']($draft['subject']) > 100)
$draft['subject'] = $smcFunc['substr']($draft['subject'], 0, 100);
// Modifying an existing draft, like hitting the save draft button or autosave enabled?
if (!empty($id_draft) && !empty($draft_info))
{
$smcFunc['db_query']('', '
UPDATE {db_prefix}user_drafts
SET
id_topic = {int:id_topic},
id_board = {int:id_board},
poster_time = {int:poster_time},
subject = {string:subject},
smileys_enabled = {int:smileys_enabled},
body = {string:body},
icon = {string:icon},
locked = {int:locked},
is_sticky = {int:is_sticky}
WHERE id_draft = {int:id_draft}',
array(
'id_topic' => $topic_id,
'id_board' => $board,
'poster_time' => time(),
'subject' => $draft['subject'],
'smileys_enabled' => (int) $draft['smileys_enabled'],
'body' => $draft['body'],
'icon' => $draft['icon'],
'locked' => $draft['locked'],
'is_sticky' => $draft['sticky'],
'id_draft' => $id_draft,
)
);
// some items to return to the form
$context['draft_saved'] = true;
$context['id_draft'] = $id_draft;
// cleanup
unset($_POST['save_draft']);
}
// otherwise creating a new draft
else
{
$id_draft = $smcFunc['db_insert']('',
'{db_prefix}user_drafts',
array(
'id_topic' => 'int',
'id_board' => 'int',
'type' => 'int',
'poster_time' => 'int',
'id_member' => 'int',
'subject' => 'string-255',
'smileys_enabled' => 'int',
'body' => (!empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] > 65534 ? 'string-' . $modSettings['max_messageLength'] : 'string-65534'),
'icon' => 'string-16',
'locked' => 'int',
'is_sticky' => 'int'
),
array(
$topic_id,
$board,
0,
time(),
$user_info['id'],
$draft['subject'],
$draft['smileys_enabled'],
$draft['body'],
$draft['icon'],
$draft['locked'],
$draft['sticky']
),
array(
'id_draft'
),
1
);
// everything go as expected?
if (!empty($id_draft))
{
$context['draft_saved'] = true;
$context['id_draft'] = $id_draft;
}
else
$post_errors[] = 'draft_not_saved';
// cleanup
unset($_POST['save_draft']);
}
// if we were called from the autosave function, send something back
if (!empty($id_draft) && isset($_REQUEST['xml']) && (!in_array('session_timeout', $post_errors)))
{
$context['draft_saved_on'] = time();
XmlDraft($id_draft);
}
return true;
}
/**
* Saves a PM draft in the user_drafts table
* The core draft feature must be enabled, as well as the pm draft option
* Determines if this is a new or and update to an existing pm draft
*
* @param string $post_errors A string of info about errors encountered trying to save this draft
* @param array $recipientList An array of data about who this PM is being sent to
* @return boolean false if you can't save the draft, true if we're doing this via XML more than 5 seconds after the last save, nothing otherwise
*/
function SavePMDraft(&$post_errors, $recipientList)
{
global $context, $user_info, $smcFunc, $modSettings;
// PM survey says ... can you stay or must you go
if (empty($modSettings['drafts_pm_enabled']) || !allowedTo('pm_draft') || !isset($_POST['save_draft']))
return false;
// read in what you sent us
$id_pm_draft = (int) $_POST['id_pm_draft'];
$draft_info = ReadDraft($id_pm_draft, 1);
// 5 seconds is the same limit we have for posting
if (isset($_REQUEST['xml']) && !empty($draft_info['poster_time']) && time() < $draft_info['poster_time'] + 5)
{
$context['draft_saved_on'] = $draft_info['poster_time'];
// Send something back to the javascript caller
if (!empty($id_draft))
XmlDraft($id_draft);
return true;
}
// determine who this is being sent to
if (isset($_REQUEST['xml']))
{
$recipientList['to'] = isset($_POST['recipient_to']) ? explode(',', $_POST['recipient_to']) : array();
$recipientList['bcc'] = isset($_POST['recipient_bcc']) ? explode(',', $_POST['recipient_bcc']) : array();
}
elseif (!empty($draft_info['to_list']) && empty($recipientList))
$recipientList = $smcFunc['json_decode']($draft_info['to_list'], true);
// prepare the data we got from the form
$reply_id = empty($_POST['replied_to']) ? 0 : (int) $_POST['replied_to'];
$draft['body'] = $smcFunc['htmlspecialchars']($_POST['message'], ENT_QUOTES);
$draft['subject'] = strtr($smcFunc['htmlspecialchars']($_POST['subject']), array("\r" => '', "\n" => '', "\t" => ''));
// message and subject always need a bit more work
preparsecode($draft['body']);
if ($smcFunc['strlen']($draft['subject']) > 100)
$draft['subject'] = $smcFunc['substr']($draft['subject'], 0, 100);
// Modifying an existing PM draft?
if (!empty($id_pm_draft) && !empty($draft_info))
{
$smcFunc['db_query']('', '
UPDATE {db_prefix}user_drafts
SET id_reply = {int:id_reply},
type = {int:type},
poster_time = {int:poster_time},
subject = {string:subject},
body = {string:body},
to_list = {string:to_list}
WHERE id_draft = {int:id_pm_draft}',
array(
'id_reply' => $reply_id,
'type' => 1,
'poster_time' => time(),
'subject' => $draft['subject'],
'body' => $draft['body'],
'id_pm_draft' => $id_pm_draft,
'to_list' => $smcFunc['json_encode']($recipientList),
)
);
// some items to return to the form
$context['draft_saved'] = true;
$context['id_pm_draft'] = $id_pm_draft;
}
// otherwise creating a new PM draft.
else
{
$id_pm_draft = $smcFunc['db_insert']('',
'{db_prefix}user_drafts',
array(
'id_reply' => 'int',
'type' => 'int',
'poster_time' => 'int',
'id_member' => 'int',
'subject' => 'string-255',
'body' => 'string-65534',
'to_list' => 'string-255',
),
array(
$reply_id,
1,
time(),
$user_info['id'],
$draft['subject'],
$draft['body'],
$smcFunc['json_encode']($recipientList),
),
array(
'id_draft'
),
1
);
// everything go as expected, if not toss back an error
if (!empty($id_pm_draft))
{
$context['draft_saved'] = true;
$context['id_pm_draft'] = $id_pm_draft;
}
else
$post_errors[] = 'draft_not_saved';
}
// if we were called from the autosave function, send something back
if (!empty($id_pm_draft) && isset($_REQUEST['xml']) && !in_array('session_timeout', $post_errors))
{
$context['draft_saved_on'] = time();
XmlDraft($id_pm_draft);
}
return;
}
/**
* Reads a draft in from the user_drafts table
* Validates that the draft is the user''s draft
* Optionally loads the draft in to context or superglobal for loading in to the form
*
* @param int $id_draft ID of the draft to load
* @param int $type Type of draft - 0 for post or 1 for PM
* @param boolean $check Validate that this draft belongs to the current user
* @param boolean $load Whether or not to load the data into variables for use on a form
* @return boolean|array False if the data couldn't be loaded, true if it's a PM draft or an array of info about the draft if it's a post draft
*/
function ReadDraft($id_draft, $type = 0, $check = true, $load = false)
{
global $context, $user_info, $smcFunc, $modSettings;
// like purell always clean to be sure
$id_draft = (int) $id_draft;
$type = (int) $type;
// nothing to read, nothing to do
if (empty($id_draft))
return false;
// load in this draft from the DB
$request = $smcFunc['db_query']('', '
SELECT is_sticky, locked, smileys_enabled, icon, body , subject,
id_board, id_draft, id_reply, to_list
FROM {db_prefix}user_drafts
WHERE id_draft = {int:id_draft}' . ($check ? '
AND id_member = {int:id_member}' : '') . '
AND type = {int:type}' . (!empty($modSettings['drafts_keep_days']) ? '
AND poster_time > {int:time}' : '') . '
LIMIT 1',
array(
'id_member' => $user_info['id'],
'id_draft' => $id_draft,
'type' => $type,
'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
)
);
// no results?
if (!$smcFunc['db_num_rows']($request))
return false;
// load up the data
$draft_info = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
// Load it up for the templates as well
if (!empty($load))
{
if ($type === 0)
{
// a standard post draft?
$context['sticky'] = !empty($draft_info['is_sticky']) ? $draft_info['is_sticky'] : '';
$context['locked'] = !empty($draft_info['locked']) ? $draft_info['locked'] : '';
$context['use_smileys'] = !empty($draft_info['smileys_enabled']) ? true : false;
$context['icon'] = !empty($draft_info['icon']) ? $draft_info['icon'] : 'xx';
$context['message'] = !empty($draft_info['body']) ? str_replace('<br>', "\n", un_htmlspecialchars(stripslashes($draft_info['body']))) : '';
$context['subject'] = !empty($draft_info['subject']) ? stripslashes($draft_info['subject']) : '';
$context['board'] = !empty($draft_info['id_board']) ? $draft_info['id_board'] : '';
$context['id_draft'] = !empty($draft_info['id_draft']) ? $draft_info['id_draft'] : 0;
}
elseif ($type === 1)
{
// one of those pm drafts? then set it up like we have an error
$_REQUEST['subject'] = !empty($draft_info['subject']) ? stripslashes($draft_info['subject']) : '';
$_REQUEST['message'] = !empty($draft_info['body']) ? str_replace('<br>', "\n", un_htmlspecialchars(stripslashes($draft_info['body']))) : '';
$_REQUEST['replied_to'] = !empty($draft_info['id_reply']) ? $draft_info['id_reply'] : 0;
$context['id_pm_draft'] = !empty($draft_info['id_draft']) ? $draft_info['id_draft'] : 0;
$recipients = $smcFunc['json_decode']($draft_info['to_list'], true);
// make sure we only have integers in this array
$recipients['to'] = array_map('intval', $recipients['to']);
$recipients['bcc'] = array_map('intval', $recipients['bcc']);
// pretend we messed up to populate the pm message form
messagePostError(array(), array(), $recipients);
return true;
}
}
return $draft_info;
}
/**
* Deletes one or many drafts from the DB
* Validates the drafts are from the user
* is supplied an array of drafts will attempt to remove all of them
*
* @param int $id_draft The ID of the draft to delete
* @param boolean $check Whether or not to check that the draft belongs to the current user
* @return boolean False if it couldn't be deleted (doesn't return anything otherwise)
*/
function DeleteDraft($id_draft, $check = true)
{
global $user_info, $smcFunc;
// Only a single draft.
if (is_numeric($id_draft))
$id_draft = array($id_draft);
// can't delete nothing
if (empty($id_draft) || ($check && empty($user_info['id'])))
return false;
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}user_drafts
WHERE id_draft IN ({array_int:id_draft})' . ($check ? '
AND id_member = {int:id_member}' : ''),
array(
'id_draft' => $id_draft,
'id_member' => empty($user_info['id']) ? -1 : $user_info['id'],
)
);
}
/**
* Loads in a group of drafts for the user of a given type (0/posts, 1/pm's)
* loads a specific draft for forum use if selected.
* Used in the posting screens to allow draft selection
* Will load a draft if selected is supplied via post
*
* @param int $member_id ID of the member to show drafts for
* @param boolean|integer $topic If $type is 1, this can be set to only load drafts for posts in the specific topic
* @param int $draft_type The type of drafts to show - 0 for post drafts, 1 for PM drafts
* @return boolean False if the drafts couldn't be loaded, nothing otherwise
*/
function ShowDrafts($member_id, $topic = false, $draft_type = 0)
{
global $smcFunc, $scripturl, $context, $txt, $modSettings;
// Permissions
if (($draft_type === 0 && empty($context['drafts_save'])) || ($draft_type === 1 && empty($context['drafts_pm_save'])) || empty($member_id))
return false;
$context['drafts'] = array();
// has a specific draft has been selected? Load it up if there is not a message already in the editor
if (isset($_REQUEST['id_draft']) && empty($_POST['subject']) && empty($_POST['message']))
ReadDraft((int) $_REQUEST['id_draft'], $draft_type, true, true);
// load the drafts this user has available
$request = $smcFunc['db_query']('', '
SELECT subject, poster_time, id_board, id_topic, id_draft
FROM {db_prefix}user_drafts
WHERE id_member = {int:id_member}' . ((!empty($topic) && empty($draft_type)) ? '
AND id_topic = {int:id_topic}' : (!empty($topic) ? '
AND id_reply = {int:id_topic}' : '')) . '
AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
AND poster_time > {int:time}' : '') . '
ORDER BY poster_time DESC',
array(
'id_member' => $member_id,
'id_topic' => (int) $topic,
'draft_type' => $draft_type,
'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
)
);
// add them to the draft array for display
while ($row = $smcFunc['db_fetch_assoc']($request))
{
if (empty($row['subject']))
$row['subject'] = $txt['no_subject'];
// Post drafts
if ($draft_type === 0)
{
$tmp_subject = shorten_subject(stripslashes($row['subject']), 24);
$context['drafts'][] = array(
'subject' => censorText($tmp_subject),
'poster_time' => timeformat($row['poster_time']),
'link' => '<a href="' . $scripturl . '?action=post;board=' . $row['id_board'] . ';' . (!empty($row['id_topic']) ? 'topic=' . $row['id_topic'] . '.0;' : '') . 'id_draft=' . $row['id_draft'] . '">' . $row['subject'] . '</a>',
);
}
// PM drafts
elseif ($draft_type === 1)
{
$tmp_subject = shorten_subject(stripslashes($row['subject']), 24);
$context['drafts'][] = array(
'subject' => censorText($tmp_subject),
'poster_time' => timeformat($row['poster_time']),
'link' => '<a href="' . $scripturl . '?action=pm;sa=send;id_draft=' . $row['id_draft'] . '">' . (!empty($row['subject']) ? $row['subject'] : $txt['drafts_none']) . '</a>',
);
}
}
$smcFunc['db_free_result']($request);
}
/**
* Returns an xml response to an autosave ajax request
* provides the id of the draft saved and the time it was saved
*
* @param int $id_draft
*/
function XmlDraft($id_draft)
{
global $txt, $context;
header('content-type: text/xml; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
echo '<?xml version="1.0" encoding="', $context['character_set'], '"?>
<drafts>
<draft id="', $id_draft, '"><![CDATA[', $txt['draft_saved_on'], ': ', timeformat($context['draft_saved_on']), ']]></draft>
</drafts>';
obExit(false);
}
/**
* Show all drafts of a given type by the current user
* Uses the showdraft template
* Allows for the deleting and loading/editing of drafts
*
* @param int $memID
* @param int $draft_type
*/
function showProfileDrafts($memID, $draft_type = 0)
{
global $txt, $scripturl, $modSettings, $context, $smcFunc, $options;
// Some initial context.
$context['start'] = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0;
$context['current_member'] = $memID;
// If just deleting a draft, do it and then redirect back.
if (!empty($_REQUEST['delete']))
{
checkSession('get');
$id_delete = (int) $_REQUEST['delete'];
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}user_drafts
WHERE id_draft = {int:id_draft}
AND id_member = {int:id_member}
AND type = {int:draft_type}',
array(
'id_draft' => $id_delete,
'id_member' => $memID,
'draft_type' => $draft_type,
)
);
redirectexit('action=profile;u=' . $memID . ';area=showdrafts;start=' . $context['start']);
}
// Default to 10.
if (empty($_REQUEST['viewscount']) || !is_numeric($_REQUEST['viewscount']))
$_REQUEST['viewscount'] = 10;
// Get the count of applicable drafts on the boards they can (still) see ...
// @todo .. should we just let them see their drafts even if they have lost board access ?
$request = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}user_drafts AS ud
INNER JOIN {db_prefix}boards AS b ON (b.id_board = ud.id_board AND {query_see_board})
WHERE id_member = {int:id_member}
AND type={int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
AND poster_time > {int:time}' : ''),
array(
'id_member' => $memID,
'draft_type' => $draft_type,
'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
)
);
list ($msgCount) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
$maxPerPage = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? $options['messages_per_page'] : $modSettings['defaultMaxMessages'];
$maxIndex = $maxPerPage;
// Make sure the starting place makes sense and construct our friend the page index.
$context['page_index'] = constructPageIndex($scripturl . '?action=profile;u=' . $memID . ';area=showdrafts', $context['start'], $msgCount, $maxIndex);
$context['current_page'] = $context['start'] / $maxIndex;
// Reverse the query if we're past 50% of the pages for better performance.
$start = $context['start'];
$reverse = $_REQUEST['start'] > $msgCount / 2;
if ($reverse)
{
$maxIndex = $msgCount < $context['start'] + $maxPerPage + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : $maxPerPage;
$start = $msgCount < $context['start'] + $maxPerPage + 1 || $msgCount < $context['start'] + $maxPerPage ? 0 : $msgCount - $context['start'] - $maxPerPage;
}
// Find this user's drafts for the boards they can access
// @todo ... do we want to do this? If they were able to create a draft, do we remove thier access to said draft if they loose
// access to the board or if the topic moves to a board they can not see?
$request = $smcFunc['db_query']('', '
SELECT
b.id_board, b.name AS bname,
ud.id_member, ud.id_draft, ud.body, ud.smileys_enabled, ud.subject, ud.poster_time, ud.icon, ud.id_topic, ud.locked, ud.is_sticky
FROM {db_prefix}user_drafts AS ud
INNER JOIN {db_prefix}boards AS b ON (b.id_board = ud.id_board AND {query_see_board})
WHERE ud.id_member = {int:current_member}
AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
AND poster_time > {int:time}' : '') . '
ORDER BY ud.id_draft ' . ($reverse ? 'ASC' : 'DESC') . '
LIMIT {int:start}, {int:max}',
array(
'current_member' => $memID,
'draft_type' => $draft_type,
'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
'start' => $start,
'max' => $maxIndex,
)
);
// Start counting at the number of the first message displayed.
$counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start'];
$context['posts'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// Censor....
if (empty($row['body']))
$row['body'] = '';
$row['subject'] = $smcFunc['htmltrim']($row['subject']);
if (empty($row['subject']))
$row['subject'] = $txt['no_subject'];
censorText($row['body']);
censorText($row['subject']);
// BBC-ilize the message.
$row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], 'draft' . $row['id_draft']);
// And the array...
$context['drafts'][$counter += $reverse ? -1 : 1] = array(
'body' => $row['body'],
'counter' => $counter,
'board' => array(
'name' => $row['bname'],
'id' => $row['id_board']
),
'topic' => array(
'id' => $row['id_topic'],
'link' => empty($row['id']) ? $row['subject'] : '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
),
'subject' => $row['subject'],
'time' => timeformat($row['poster_time']),
'timestamp' => $row['poster_time'],
'icon' => $row['icon'],
'id_draft' => $row['id_draft'],
'locked' => $row['locked'],
'sticky' => $row['is_sticky'],
'quickbuttons' => array(
'edit' => array(
'label' => $txt['draft_edit'],
'href' => $scripturl.'?action=post;'.(empty($row['id_topic']) ? 'board='.$row['id_board'] : 'topic='.$row['id_topic']).'.0;id_draft='.$row['id_draft'],
'icon' => 'modify_button'
),
'delete' => array(
'label' => $txt['draft_delete'],
'href' => $scripturl.'?action=profile;u='.$context['member']['id'].';area=showdrafts;delete='.$row['id_draft'].';'.$context['session_var'].'='.$context['session_id'],
'javascript' => 'data-confirm="'.$txt['draft_remove'].'"',
'class' => 'you_sure',
'icon' => 'remove_button'
),
),
);
}
$smcFunc['db_free_result']($request);
// If the drafts were retrieved in reverse order, get them right again.
if ($reverse)
$context['drafts'] = array_reverse($context['drafts'], true);
// Menu tab
$context[$context['profile_menu_name']]['tab_data'] = array(
'title' => $txt['drafts_show'],
'description' => $txt['drafts_show_desc'],
'icon_class' => 'main_icons drafts'
);
$context['sub_template'] = 'showDrafts';
}
/**
* Show all PM drafts of the current user
* Uses the showpmdraft template
* Allows for the deleting and loading/editing of drafts
*
* @param int $memID
*/
function showPMDrafts($memID = -1)
{
global $txt, $user_info, $scripturl, $modSettings, $context, $smcFunc, $options;
// init
$draft_type = 1;
$context['start'] = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0;
// If just deleting a draft, do it and then redirect back.
if (!empty($_REQUEST['delete']))
{
checkSession('get');
$id_delete = (int) $_REQUEST['delete'];
$start = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0;
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}user_drafts
WHERE id_draft = {int:id_draft}
AND id_member = {int:id_member}
AND type = {int:draft_type}',
array(
'id_draft' => $id_delete,
'id_member' => $memID,
'draft_type' => $draft_type,
)
);
// now redirect back to the list
redirectexit('action=pm;sa=showpmdrafts;start=' . $start);
}
// perhaps a draft was selected for editing? if so pass this off
if (!empty($_REQUEST['id_draft']) && !empty($context['drafts_pm_save']) && $memID == $user_info['id'])
{
checkSession('get');
$id_draft = (int) $_REQUEST['id_draft'];
redirectexit('action=pm;sa=send;id_draft=' . $id_draft);
}
// Default to 10.
if (empty($_REQUEST['viewscount']) || !is_numeric($_REQUEST['viewscount']))
$_REQUEST['viewscount'] = 10;
// Get the count of applicable drafts
$request = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}user_drafts
WHERE id_member = {int:id_member}
AND type={int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
AND poster_time > {int:time}' : ''),
array(
'id_member' => $memID,
'draft_type' => $draft_type,
'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
)
);
list ($msgCount) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
$maxPerPage = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) ? $options['messages_per_page'] : $modSettings['defaultMaxMessages'];
$maxIndex = $maxPerPage;
// Make sure the starting place makes sense and construct our friend the page index.
$context['page_index'] = constructPageIndex($scripturl . '?action=pm;sa=showpmdrafts', $context['start'], $msgCount, $maxIndex);
$context['current_page'] = $context['start'] / $maxIndex;
// Reverse the query if we're past 50% of the total for better performance.
$start = $context['start'];
$reverse = $_REQUEST['start'] > $msgCount / 2;
if ($reverse)
{
$maxIndex = $msgCount < $context['start'] + $maxPerPage + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : $maxPerPage;
$start = $msgCount < $context['start'] + $maxPerPage + 1 || $msgCount < $context['start'] + $maxPerPage ? 0 : $msgCount - $context['start'] - $maxPerPage;
}
// Load in this user's PM drafts
$request = $smcFunc['db_query']('', '
SELECT
ud.id_member, ud.id_draft, ud.body, ud.subject, ud.poster_time, ud.id_reply, ud.to_list
FROM {db_prefix}user_drafts AS ud
WHERE ud.id_member = {int:current_member}
AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
AND poster_time > {int:time}' : '') . '
ORDER BY ud.id_draft ' . ($reverse ? 'ASC' : 'DESC') . '
LIMIT {int:start}, {int:max}',
array(
'current_member' => $memID,
'draft_type' => $draft_type,
'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
'start' => $start,
'max' => $maxIndex,
)
);
// Start counting at the number of the first message displayed.
$counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start'];
$context['posts'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// Censor....
if (empty($row['body']))
$row['body'] = '';
$row['subject'] = $smcFunc['htmltrim']($row['subject']);
if (empty($row['subject']))
$row['subject'] = $txt['no_subject'];
censorText($row['body']);
censorText($row['subject']);
// BBC-ilize the message.
$row['body'] = parse_bbc($row['body'], true, 'draft' . $row['id_draft']);
// Have they provide who this will go to?
$recipients = array(
'to' => array(),
'bcc' => array(),
);
$recipient_ids = (!empty($row['to_list'])) ? $smcFunc['json_decode']($row['to_list'], true) : array();
// @todo ... this is a bit ugly since it runs an extra query for every message, do we want this?
// at least its only for draft PM's and only the user can see them ... so not heavily used .. still
if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
{
$recipient_ids['to'] = array_map('intval', $recipient_ids['to']);
$recipient_ids['bcc'] = array_map('intval', $recipient_ids['bcc']);
$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
$request_2 = $smcFunc['db_query']('', '
SELECT id_member, real_name
FROM {db_prefix}members
WHERE id_member IN ({array_int:member_list})',
array(
'member_list' => $allRecipients,
)
);
while ($result = $smcFunc['db_fetch_assoc']($request_2))
{
$recipientType = in_array($result['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
$recipients[$recipientType][] = $result['real_name'];
}
$smcFunc['db_free_result']($request_2);
}
// Add the items to the array for template use
$context['drafts'][$counter += $reverse ? -1 : 1] = array(
'body' => $row['body'],
'counter' => $counter,
'subject' => $row['subject'],
'time' => timeformat($row['poster_time']),
'timestamp' => $row['poster_time'],
'id_draft' => $row['id_draft'],
'recipients' => $recipients,
'age' => floor((time() - $row['poster_time']) / 86400),
'remaining' => (!empty($modSettings['drafts_keep_days']) ? floor($modSettings['drafts_keep_days'] - ((time() - $row['poster_time']) / 86400)) : 0),
'quickbuttons' => array(
'edit' => array(
'label' => $txt['draft_edit'],
'href' => $scripturl.'?action=pm;sa=showpmdrafts;id_draft='.$row['id_draft'].';'.$context['session_var'].'='.$context['session_id'],
'icon' => 'modify_button'
),
'delete' => array(
'label' => $txt['draft_delete'],
'href' => $scripturl.'?action=pm;sa=showpmdrafts;delete='.$row['id_draft'].';'.$context['session_var'].'='.$context['session_id'],
'javascript' => 'data-confirm="'.$txt['draft_remove'].'?"',
'class' => 'you_sure',
'icon' => 'remove_button'
),
),
);
}
$smcFunc['db_free_result']($request);
// if the drafts were retrieved in reverse order, then put them in the right order again.
if ($reverse)
$context['drafts'] = array_reverse($context['drafts'], true);
// off to the template we go
$context['page_title'] = $txt['drafts'];
$context['sub_template'] = 'showPMDrafts';
$context['linktree'][] = array(
'url' => $scripturl . '?action=pm;sa=showpmdrafts',
'name' => $txt['drafts'],
);
}
?>

586
Sources/Errors.php Normal file
View file

@ -0,0 +1,586 @@
<?php
/**
* The purpose of this file is... errors. (hard to guess, I guess?) It takes
* care of logging, error messages, error handling, database errors, and
* error log administration.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Log an error, if the error logging is enabled.
* filename and line should be __FILE__ and __LINE__, respectively.
* Example use:
* die(log_error($msg));
*
* @param string $error_message The message to log
* @param string|bool $error_type The type of error
* @param string $file The name of the file where this error occurred
* @param int $line The line where the error occurred
* @return string The message that was logged
*/
function log_error($error_message, $error_type = 'general', $file = null, $line = null)
{
global $modSettings, $sc, $user_info, $smcFunc, $scripturl, $last_error, $context, $db_show_debug;
static $tried_hook = false;
static $error_call = 0;
$error_call++;
// Collect a backtrace
if (!isset($db_show_debug) || $db_show_debug === false)
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
else
$backtrace = debug_backtrace();
// are we in a loop?
if ($error_call > 2)
{
var_dump($backtrace);
die('Error loop.');
}
// Check if error logging is actually on.
if (empty($modSettings['enableErrorLogging']))
return $error_message;
// Basically, htmlspecialchars it minus &. (for entities!)
$error_message = strtr($error_message, array('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));
$error_message = strtr($error_message, array('&lt;br /&gt;' => '<br>', '&lt;br&gt;' => '<br>', '&lt;b&gt;' => '<strong>', '&lt;/b&gt;' => '</strong>', "\n" => '<br>'));
// Add a file and line to the error message?
// Don't use the actual txt entries for file and line but instead use %1$s for file and %2$s for line
if ($file == null)
$file = '';
else
// Windows style slashes don't play well, lets convert them to the unix style.
$file = str_replace('\\', '/', $file);
if ($line == null)
$line = 0;
else
$line = (int) $line;
// Just in case there's no id_member or IP set yet.
if (empty($user_info['id']))
$user_info['id'] = 0;
if (empty($user_info['ip']))
$user_info['ip'] = '';
// Find the best query string we can...
$query_string = empty($_SERVER['QUERY_STRING']) ? (empty($_SERVER['REQUEST_URL']) ? '' : str_replace($scripturl, '', $_SERVER['REQUEST_URL'])) : $_SERVER['QUERY_STRING'];
// Don't log the session hash in the url twice, it's a waste.
if (!empty($smcFunc['htmlspecialchars']))
$query_string = $smcFunc['htmlspecialchars']((SMF == 'SSI' || SMF == 'BACKGROUND' ? '' : '?') . preg_replace(array('~;sesc=[^&;]+~', '~' . session_name() . '=' . session_id() . '[&;]~'), array(';sesc', ''), $query_string));
// Just so we know what board error messages are from.
if (isset($_POST['board']) && !isset($_GET['board']))
$query_string .= ($query_string == '' ? 'board=' : ';board=') . $_POST['board'];
// What types of categories do we have?
$known_error_types = array(
'general',
'critical',
'database',
'undefined_vars',
'user',
'ban',
'template',
'debug',
'cron',
'paidsubs',
'backup',
'login',
);
// This prevents us from infinite looping if the hook or call produces an error.
$other_error_types = array();
if (empty($tried_hook))
{
$tried_hook = true;
// Allow the hook to change the error_type and know about the error.
call_integration_hook('integrate_error_types', array(&$other_error_types, &$error_type, $error_message, $file, $line));
$known_error_types += $other_error_types;
}
// Make sure the category that was specified is a valid one
$error_type = in_array($error_type, $known_error_types) && $error_type !== true ? $error_type : 'general';
// leave out the call to log_error
array_splice($backtrace, 0, 1);
$backtrace = !empty($smcFunc['json_encode']) ? $smcFunc['json_encode']($backtrace) : json_encode($backtrace);
// Don't log the same error countless times, as we can get in a cycle of depression...
$error_info = array($user_info['id'], time(), $user_info['ip'], $query_string, $error_message, (string) $sc, $error_type, $file, $line, $backtrace);
if (empty($last_error) || $last_error != $error_info)
{
// Insert the error into the database.
$smcFunc['db_error_insert']($error_info);
$last_error = $error_info;
// Get an error count, if necessary
if (!isset($context['num_errors']))
{
$query = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}log_errors',
array()
);
list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
$smcFunc['db_free_result']($query);
}
else
$context['num_errors']++;
}
// reset error call
$error_call = 0;
// Return the message to make things simpler.
return $error_message;
}
/**
* An irrecoverable error. This function stops execution and displays an error message.
* It logs the error message if $log is specified.
*
* @param string $error The error message
* @param string|bool $log = 'general' What type of error to log this as (false to not log it))
* @param int $status The HTTP status code associated with this error
*/
function fatal_error($error, $log = 'general', $status = 500)
{
global $txt;
// Send the appropriate HTTP status header - set this to 0 or false if you don't want to send one at all
if (!empty($status))
send_http_status($status);
// We don't have $txt yet, but that's okay...
if (empty($txt))
die($error);
log_error_online($error);
setup_fatal_error_context($log ? log_error($error, $log) : $error);
}
/**
* Shows a fatal error with a message stored in the language file.
*
* This function stops execution and displays an error message by key.
* - uses the string with the error_message_key key.
* - logs the error in the forum's default language while displaying the error
* message in the user's language.
* - uses Errors language file and applies the $sprintf information if specified.
* - the information is logged if log is specified.
*
* @param string $error The error message
* @param string|false $log The type of error, or false to not log it
* @param array $sprintf An array of data to be sprintf()'d into the specified message
* @param int $status = false The HTTP status code associated with this error
*/
function fatal_lang_error($error, $log = 'general', $sprintf = array(), $status = 403)
{
global $txt, $language, $user_info, $context;
static $fatal_error_called = false;
// Ensure this is an array.
$sprintf = (array) $sprintf;
// Send the status header - set this to 0 or false if you don't want to send one at all
if (!empty($status))
send_http_status($status);
// Try to load a theme if we don't have one.
if (empty($context['theme_loaded']) && empty($fatal_error_called))
{
$fatal_error_called = true;
loadTheme();
}
// If we have no theme stuff we can't have the language file...
if (empty($context['theme_loaded']))
die($error);
$reload_lang_file = true;
// Log the error in the forum's language, but don't waste the time if we aren't logging
if ($log)
{
loadLanguage('Errors', $language);
$reload_lang_file = $language != $user_info['language'];
if (empty($txt[$error]))
$error_message = $error;
else
$error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf);
log_error($error_message, $log);
}
// Load the language file, only if it needs to be reloaded
if ($reload_lang_file)
{
loadLanguage('Errors');
$error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf);
}
log_error_online($error, $sprintf);
setup_fatal_error_context($error_message, $error);
}
/**
* Handler for standard error messages, standard PHP error handler replacement.
* It dies with fatal_error() if the error_level matches with error_reporting.
*
* @param int $error_level A pre-defined error-handling constant (see {@link https://php.net/errorfunc.constants})
* @param string $error_string The error message
* @param string $file The file where the error occurred
* @param int $line The line where the error occurred
*/
function smf_error_handler($error_level, $error_string, $file, $line)
{
global $settings, $modSettings, $db_show_debug;
// Error was suppressed with the @-operator.
if (error_reporting() == 0 || error_reporting() == (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR))
return true;
// Ignore errors that should should not be logged.
$error_match = error_reporting() & $error_level;
if (empty($error_match) || empty($modSettings['enableErrorLogging']))
return false;
if (strpos($file, 'eval()') !== false && !empty($settings['current_include_filename']))
{
$array = debug_backtrace();
$count = count($array);
for ($i = 0; $i < $count; $i++)
{
if ($array[$i]['function'] != 'loadSubTemplate')
continue;
// This is a bug in PHP, with eval, it seems!
if (empty($array[$i]['args']))
$i++;
break;
}
if (isset($array[$i]) && !empty($array[$i]['args']))
$file = realpath($settings['current_include_filename']) . ' (' . $array[$i]['args'][0] . ' sub template - eval?)';
else
$file = realpath($settings['current_include_filename']) . ' (eval?)';
}
if (isset($db_show_debug) && $db_show_debug === true)
{
// Commonly, undefined indexes will occur inside attributes; try to show them anyway!
if ($error_level % 255 != E_ERROR)
{
$temporary = ob_get_contents();
if (substr($temporary, -2) == '="')
echo '"';
}
// Debugging! This should look like a PHP error message.
echo '<br>
<strong>', $error_level % 255 == E_ERROR ? 'Error' : ($error_level % 255 == E_WARNING ? 'Warning' : 'Notice'), '</strong>: ', $error_string, ' in <strong>', $file, '</strong> on line <strong>', $line, '</strong><br>';
}
$error_type = stripos($error_string, 'undefined') !== false ? 'undefined_vars' : 'general';
$message = log_error($error_level . ': ' . $error_string, $error_type, $file, $line);
// Let's give integrations a chance to ouput a bit differently
call_integration_hook('integrate_output_error', array($message, $error_type, $error_level, $file, $line));
// Dying on these errors only causes MORE problems (blank pages!)
if ($file == 'Unknown')
return;
// If this is an E_ERROR or E_USER_ERROR.... die. Violently so.
if ($error_level % 255 == E_ERROR)
obExit(false);
else
return;
// If this is an E_ERROR, E_USER_ERROR, E_WARNING, or E_USER_WARNING.... die. Violently so.
if ($error_level % 255 == E_ERROR || $error_level % 255 == E_WARNING)
fatal_error(allowedTo('admin_forum') ? $message : $error_string, false);
// We should NEVER get to this point. Any fatal error MUST quit, or very bad things can happen.
if ($error_level % 255 == E_ERROR)
die('No direct access...');
}
/**
* It is called by {@link fatal_error()} and {@link fatal_lang_error()}.
*
* @uses template_fatal_error()
*
* @param string $error_message The error message
* @param string $error_code An error code
* @return void|false Normally doesn't return anything, but returns false if a recursive loop is detected
*/
function setup_fatal_error_context($error_message, $error_code = null)
{
global $context, $txt, $ssi_on_error_method;
static $level = 0;
// Attempt to prevent a recursive loop.
++$level;
if ($level > 1)
return false;
// Maybe they came from dlattach or similar?
if (SMF != 'SSI' && SMF != 'BACKGROUND' && empty($context['theme_loaded']))
loadTheme();
// Don't bother indexing errors mate...
$context['robot_no_index'] = true;
if (!isset($context['error_title']))
$context['error_title'] = $txt['error_occured'];
$context['error_message'] = isset($context['error_message']) ? $context['error_message'] : $error_message;
$context['error_code'] = isset($error_code) ? 'id="' . $error_code . '" ' : '';
$context['error_link'] = isset($context['error_link']) ? $context['error_link'] : 'javascript:document.location=document.referrer';
if (empty($context['page_title']))
$context['page_title'] = $context['error_title'];
loadTemplate('Errors');
$context['sub_template'] = 'fatal_error';
// If this is SSI, what do they want us to do?
if (SMF == 'SSI')
{
if (!empty($ssi_on_error_method) && $ssi_on_error_method !== true && is_callable($ssi_on_error_method))
$ssi_on_error_method();
elseif (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
loadSubTemplate('fatal_error');
// No layers?
if (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
exit;
}
// Alternatively from the cron call?
elseif (SMF == 'BACKGROUND')
{
// We can't rely on even having language files available.
if (defined('FROM_CLI') && FROM_CLI)
echo 'cron error: ', $context['error_message'];
else
echo 'An error occurred. More information may be available in your logs.';
exit;
}
// We want whatever for the header, and a footer. (footer includes sub template!)
obExit(null, true, false, true);
/* DO NOT IGNORE:
If you are creating a bridge to SMF or modifying this function, you MUST
make ABSOLUTELY SURE that this function quits and DOES NOT RETURN TO NORMAL
PROGRAM FLOW. Otherwise, security error messages will not be shown, and
your forum will be in a very easily hackable state.
*/
trigger_error('No direct access...', E_USER_ERROR);
}
/**
* Show a message for the (full block) maintenance mode.
* It shows a complete page independent of language files or themes.
* It is used only if $maintenance = 2 in Settings.php.
* It stops further execution of the script.
*/
function display_maintenance_message()
{
global $maintenance, $mtitle, $mmessage;
set_fatal_error_headers();
if (!empty($maintenance))
echo '<!DOCTYPE html>
<html>
<head>
<meta name="robots" content="noindex">
<title>', $mtitle, '</title>
</head>
<body>
<h3>', $mtitle, '</h3>
', $mmessage, '
</body>
</html>';
die();
}
/**
* Show an error message for the connection problems.
* It shows a complete page independent of language files or themes.
* It is used only if there's no way to connect to the database.
* It stops further execution of the script.
*/
function display_db_error()
{
global $mbname, $modSettings, $maintenance;
global $db_connection, $webmaster_email, $db_last_error, $db_error_send, $smcFunc, $sourcedir, $cache_enable;
require_once($sourcedir . '/Logging.php');
set_fatal_error_headers();
// For our purposes, we're gonna want this on if at all possible.
$cache_enable = '1';
if (($temp = cache_get_data('db_last_error', 600)) !== null)
$db_last_error = max($db_last_error, $temp);
if ($db_last_error < time() - 3600 * 24 * 3 && empty($maintenance) && !empty($db_error_send))
{
// Avoid writing to the Settings.php file if at all possible; use shared memory instead.
cache_put_data('db_last_error', time(), 600);
if (($temp = cache_get_data('db_last_error', 600)) === null)
logLastDatabaseError();
// Language files aren't loaded yet :(.
$db_error = @$smcFunc['db_error']($db_connection);
@mail($webmaster_email, $mbname . ': SMF Database Error!', 'There has been a problem with the database!' . ($db_error == '' ? '' : "\n" . $smcFunc['db_title'] . ' reported:' . "\n" . $db_error) . "\n\n" . 'This is a notice email to let you know that SMF could not connect to the database, contact your host if this continues.');
}
// What to do? Language files haven't and can't be loaded yet...
echo '<!DOCTYPE html>
<html>
<head>
<meta name="robots" content="noindex">
<title>Connection Problems</title>
</head>
<body>
<h3>Connection Problems</h3>
Sorry, SMF was unable to connect to the database. This may be caused by the server being busy. Please try again later.
</body>
</html>';
die();
}
/**
* Show an error message for load average blocking problems.
* It shows a complete page independent of language files or themes.
* It is used only if the load averages are too high to continue execution.
* It stops further execution of the script.
*/
function display_loadavg_error()
{
// If this is a load average problem, display an appropriate message (but we still don't have language files!)
set_fatal_error_headers();
echo '<!DOCTYPE html>
<html>
<head>
<meta name="robots" content="noindex">
<title>Temporarily Unavailable</title>
</head>
<body>
<h3>Temporarily Unavailable</h3>
Due to high stress on the server the forum is temporarily unavailable. Please try again later.
</body>
</html>';
die();
}
/**
* Small utility function for fatal error pages.
* Used by {@link display_db_error()}, {@link display_loadavg_error()},
* {@link display_maintenance_message()}
*/
function set_fatal_error_headers()
{
if (headers_sent())
return;
// Don't cache this page!
header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('cache-control: no-cache');
// Send the right error codes.
send_http_status(503, 'Service Temporarily Unavailable');
header('status: 503 Service Temporarily Unavailable');
header('retry-after: 3600');
}
/**
* Small utility function for fatal error pages.
* Used by fatal_error(), fatal_lang_error()
*
* @param string $error The error
* @param array $sprintf An array of data to be sprintf()'d into the specified message
*/
function log_error_online($error, $sprintf = array())
{
global $smcFunc, $user_info, $modSettings;
// Don't bother if Who's Online is disabled.
if (empty($modSettings['who_enabled']))
return;
// Maybe they came from SSI or similar where sessions are not recorded?
if (SMF == 'SSI' || SMF == 'BACKGROUND')
return;
$session_id = !empty($user_info['is_guest']) ? 'ip' . $user_info['ip'] : session_id();
// First, we have to get the online log, because we need to break apart the serialized string.
$request = $smcFunc['db_query']('', '
SELECT url
FROM {db_prefix}log_online
WHERE session = {string:session}',
array(
'session' => $session_id,
)
);
if ($smcFunc['db_num_rows']($request) != 0)
{
// If this happened very early on in SMF startup, $smcFunc may not fully be defined.
if (!isset($smcFunc['json_decode']))
{
$smcFunc['json_decode'] = 'smf_json_decode';
$smcFunc['json_encode'] = 'json_encode';
}
list ($url) = $smcFunc['db_fetch_row']($request);
$url = $smcFunc['json_decode']($url, true);
$url['error'] = $error;
// Url field got a max length of 1024 in db
if (strlen($url['error']) > 500)
$url['error'] = substr($url['error'], 0, 500);
if (!empty($sprintf))
$url['error_params'] = $sprintf;
$smcFunc['db_query']('', '
UPDATE {db_prefix}log_online
SET url = {string:url}
WHERE session = {string:session}',
array(
'url' => $smcFunc['json_encode']($url),
'session' => $session_id,
)
);
}
$smcFunc['db_free_result']($request);
}
?>

785
Sources/Groups.php Normal file
View file

@ -0,0 +1,785 @@
<?php
/**
* This file currently just shows group info, and allows certain priviledged members to add/remove members.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Entry point function, permission checks, admin bars, etc.
* It allows moderators and users to access the group showing functions.
* It handles permission checks, and puts the moderation bar on as required.
*/
function Groups()
{
global $context, $txt, $scripturl, $sourcedir, $user_info;
// The sub-actions that we can do. Format "Function Name, Mod Bar Index if appropriate".
$subActions = array(
'index' => array('GroupList', 'view_groups'),
'members' => array('MembergroupMembers', 'view_groups'),
'requests' => array('GroupRequests', 'group_requests'),
);
call_integration_hook('integrate_manage_groups', array(&$subActions));
// Default to sub action 'index'.
$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'index';
// Get the template stuff up and running.
loadLanguage('ManageMembers');
loadLanguage('ModerationCenter');
loadTemplate('ManageMembergroups');
// If we can see the moderation center, and this has a mod bar entry, add the mod center bar.
if (allowedTo('access_mod_center') || $user_info['mod_cache']['bq'] != '0=1' || $user_info['mod_cache']['gq'] != '0=1' || allowedTo('manage_membergroups'))
{
require_once($sourcedir . '/ModerationCenter.php');
$_GET['area'] = $_REQUEST['sa'] == 'requests' ? 'groups' : 'viewgroups';
ModerationMain(true);
}
// Otherwise add something to the link tree, for normal people.
else
{
isAllowedTo('view_mlist');
$context['linktree'][] = array(
'url' => $scripturl . '?action=groups',
'name' => $txt['groups'],
);
}
// Call the actual function.
call_helper($subActions[$_REQUEST['sa']][0]);
}
/**
* This very simply lists the groups, nothing snazy.
*/
function GroupList()
{
global $txt, $context, $sourcedir, $scripturl;
$context['page_title'] = $txt['viewing_groups'];
// Making a list is not hard with this beauty.
require_once($sourcedir . '/Subs-List.php');
// Use the standard templates for showing this.
$listOptions = array(
'id' => 'group_lists',
'title' => $context['page_title'],
'base_href' => $scripturl . '?action=moderate;area=viewgroups;sa=view',
'default_sort_col' => 'group',
'get_items' => array(
'file' => $sourcedir . '/Subs-Membergroups.php',
'function' => 'list_getMembergroups',
'params' => array(
'regular',
),
),
'columns' => array(
'group' => array(
'header' => array(
'value' => $txt['name'],
),
'data' => array(
'function' => function($rowData) use ($scripturl)
{
// Since the moderator group has no explicit members, no link is needed.
if ($rowData['id_group'] == 3)
$group_name = $rowData['group_name'];
else
{
$color_style = empty($rowData['online_color']) ? '' : sprintf(' style="color: %1$s;"', $rowData['online_color']);
if (allowedTo('manage_membergroups'))
{
$group_name = sprintf('<a href="%1$s?action=admin;area=membergroups;sa=members;group=%2$d"%3$s>%4$s</a>', $scripturl, $rowData['id_group'], $color_style, $rowData['group_name']);
}
else
{
$group_name = sprintf('<a href="%1$s?action=groups;sa=members;group=%2$d"%3$s>%4$s</a>', $scripturl, $rowData['id_group'], $color_style, $rowData['group_name']);
}
}
// Add a help option for moderator and administrator.
if ($rowData['id_group'] == 1)
$group_name .= sprintf(' (<a href="%1$s?action=helpadmin;help=membergroup_administrator" onclick="return reqOverlayDiv(this.href);">?</a>)', $scripturl);
elseif ($rowData['id_group'] == 3)
$group_name .= sprintf(' (<a href="%1$s?action=helpadmin;help=membergroup_moderator" onclick="return reqOverlayDiv(this.href);">?</a>)', $scripturl);
return $group_name;
},
),
'sort' => array(
'default' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, mg.group_name',
'reverse' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, mg.group_name DESC',
),
),
'icons' => array(
'header' => array(
'value' => $txt['membergroups_icons'],
),
'data' => array(
'db' => 'icons',
),
'sort' => array(
'default' => 'mg.icons',
'reverse' => 'mg.icons DESC',
)
),
'moderators' => array(
'header' => array(
'value' => $txt['moderators'],
),
'data' => array(
'function' => function($group) use ($txt)
{
return empty($group['moderators']) ? '<em>' . $txt['membergroups_new_copy_none'] . '</em>' : implode(', ', $group['moderators']);
},
),
),
'members' => array(
'header' => array(
'value' => $txt['membergroups_members_top'],
),
'data' => array(
'function' => function($rowData) use ($txt)
{
// No explicit members for the moderator group.
return $rowData['id_group'] == 3 ? $txt['membergroups_guests_na'] : comma_format($rowData['num_members']);
},
'class' => 'centercol',
),
'sort' => array(
'default' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, 1',
'reverse' => 'CASE WHEN mg.id_group < 4 THEN mg.id_group ELSE 4 END, 1 DESC',
),
),
),
);
// Create the request list.
createList($listOptions);
$context['sub_template'] = 'show_list';
$context['default_list'] = 'group_lists';
}
/**
* Display members of a group, and allow adding of members to a group. Silly function name though ;)
* It can be called from ManageMembergroups if it needs templating within the admin environment.
* It shows a list of members that are part of a given membergroup.
* It is called by ?action=moderate;area=viewgroups;sa=members;group=x
* It requires the manage_membergroups permission.
* It allows to add and remove members from the selected membergroup.
* It allows sorting on several columns.
* It redirects to itself.
*
* @uses template_group_members()
* @todo: use createList
*/
function MembergroupMembers()
{
global $txt, $scripturl, $context, $modSettings, $sourcedir, $user_info, $settings, $smcFunc;
$_REQUEST['group'] = isset($_REQUEST['group']) ? (int) $_REQUEST['group'] : 0;
// No browsing of guests, membergroup 0 or moderators.
if (in_array($_REQUEST['group'], array(-1, 0, 3)))
fatal_lang_error('membergroup_does_not_exist', false);
// Load up the group details.
$request = $smcFunc['db_query']('', '
SELECT id_group AS id, group_name AS name, CASE WHEN min_posts = {int:min_posts} THEN 1 ELSE 0 END AS assignable, hidden, online_color,
icons, description, CASE WHEN min_posts != {int:min_posts} THEN 1 ELSE 0 END AS is_post_group, group_type
FROM {db_prefix}membergroups
WHERE id_group = {int:id_group}
LIMIT 1',
array(
'min_posts' => -1,
'id_group' => $_REQUEST['group'],
)
);
// Doesn't exist?
if ($smcFunc['db_num_rows']($request) == 0)
fatal_lang_error('membergroup_does_not_exist', false);
$context['group'] = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
// Fix the membergroup icons.
$context['group']['icons'] = explode('#', $context['group']['icons']);
$context['group']['icons'] = !empty($context['group']['icons'][0]) && !empty($context['group']['icons'][1]) ? str_repeat('<img src="' . $settings['images_url'] . '/membericons/' . $context['group']['icons'][1] . '" alt="*">', $context['group']['icons'][0]) : '';
$context['group']['can_moderate'] = allowedTo('manage_membergroups') && (allowedTo('admin_forum') || $context['group']['group_type'] != 1);
$context['linktree'][] = array(
'url' => $scripturl . '?action=groups;sa=members;group=' . $context['group']['id'],
'name' => $context['group']['name'],
);
$context['can_send_email'] = allowedTo('moderate_forum');
// Load all the group moderators, for fun.
$request = $smcFunc['db_query']('', '
SELECT mem.id_member, mem.real_name
FROM {db_prefix}group_moderators AS mods
INNER JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
WHERE mods.id_group = {int:id_group}',
array(
'id_group' => $_REQUEST['group'],
)
);
$context['group']['moderators'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$context['group']['moderators'][] = array(
'id' => $row['id_member'],
'name' => $row['real_name']
);
if ($user_info['id'] == $row['id_member'] && $context['group']['group_type'] != 1)
$context['group']['can_moderate'] = true;
}
$smcFunc['db_free_result']($request);
// If this group is hidden then it can only "exists" if the user can moderate it!
if ($context['group']['hidden'] && !$context['group']['can_moderate'])
fatal_lang_error('membergroup_does_not_exist', false);
// You can only assign membership if you are the moderator and/or can manage groups!
if (!$context['group']['can_moderate'])
$context['group']['assignable'] = 0;
// Non-admins cannot assign admins.
elseif ($context['group']['id'] == 1 && !allowedTo('admin_forum'))
$context['group']['assignable'] = 0;
// Removing member from group?
if (isset($_POST['remove']) && !empty($_REQUEST['rem']) && is_array($_REQUEST['rem']) && $context['group']['assignable'])
{
checkSession();
validateToken('mod-mgm');
// Only proven admins can remove admins.
if ($context['group']['id'] == 1)
validateSession();
// Make sure we're dealing with integers only.
foreach ($_REQUEST['rem'] as $key => $group)
$_REQUEST['rem'][$key] = (int) $group;
require_once($sourcedir . '/Subs-Membergroups.php');
removeMembersFromGroups($_REQUEST['rem'], $_REQUEST['group'], true);
}
// Must be adding new members to the group...
elseif (isset($_REQUEST['add']) && (!empty($_REQUEST['toAdd']) || !empty($_REQUEST['member_add'])) && $context['group']['assignable'])
{
// Demand an admin password before adding new admins -- every time, no matter what.
if ($context['group']['id'] == 1)
validateSession('admin', true);
checkSession();
validateToken('mod-mgm');
$member_query = array();
$member_parameters = array();
// Get all the members to be added... taking into account names can be quoted ;)
$_REQUEST['toAdd'] = strtr($smcFunc['htmlspecialchars']($_REQUEST['toAdd'], ENT_QUOTES), array('&quot;' => '"'));
preg_match_all('~"([^"]+)"~', $_REQUEST['toAdd'], $matches);
$member_names = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_REQUEST['toAdd']))));
foreach ($member_names as $index => $member_name)
{
$member_names[$index] = trim($smcFunc['strtolower']($member_names[$index]));
if (strlen($member_names[$index]) == 0)
unset($member_names[$index]);
}
// Any passed by ID?
$member_ids = array();
if (!empty($_REQUEST['member_add']))
foreach ($_REQUEST['member_add'] as $id)
if ($id > 0)
$member_ids[] = (int) $id;
// Construct the query pelements.
if (!empty($member_ids))
{
$member_query[] = 'id_member IN ({array_int:member_ids})';
$member_parameters['member_ids'] = $member_ids;
}
if (!empty($member_names))
{
$member_query[] = 'LOWER(member_name) IN ({array_string:member_names})';
$member_query[] = 'LOWER(real_name) IN ({array_string:member_names})';
$member_parameters['member_names'] = $member_names;
}
$members = array();
if (!empty($member_query))
{
$request = $smcFunc['db_query']('', '
SELECT id_member
FROM {db_prefix}members
WHERE (' . implode(' OR ', $member_query) . ')
AND id_group != {int:id_group}
AND FIND_IN_SET({int:id_group}, additional_groups) = 0',
array_merge($member_parameters, array(
'id_group' => $_REQUEST['group'],
))
);
while ($row = $smcFunc['db_fetch_assoc']($request))
$members[] = $row['id_member'];
$smcFunc['db_free_result']($request);
}
// @todo Add $_POST['additional'] to templates!
// Do the updates...
if (!empty($members))
{
require_once($sourcedir . '/Subs-Membergroups.php');
addMembersToGroup($members, $_REQUEST['group'], isset($_POST['additional']) || $context['group']['hidden'] ? 'only_additional' : 'auto', true);
}
}
// Sort out the sorting!
$sort_methods = array(
'name' => 'real_name',
'email' => 'email_address',
'active' => 'last_login',
'registered' => 'date_registered',
'posts' => 'posts',
);
// They didn't pick one, default to by name..
if (!isset($_REQUEST['sort']) || !isset($sort_methods[$_REQUEST['sort']]))
{
$context['sort_by'] = 'name';
$querySort = 'real_name';
}
// Otherwise default to ascending.
else
{
$context['sort_by'] = $_REQUEST['sort'];
$querySort = $sort_methods[$_REQUEST['sort']];
}
$context['sort_direction'] = isset($_REQUEST['desc']) ? 'down' : 'up';
// The where on the query is interesting. Non-moderators should only see people who are in this group as primary.
if ($context['group']['can_moderate'])
$where = $context['group']['is_post_group'] ? 'id_post_group = {int:group}' : 'id_group = {int:group} OR FIND_IN_SET({int:group}, additional_groups) != 0';
else
$where = $context['group']['is_post_group'] ? 'id_post_group = {int:group}' : 'id_group = {int:group}';
// Count members of the group.
$request = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}members
WHERE ' . $where,
array(
'group' => $_REQUEST['group'],
)
);
list ($context['total_members']) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
// Create the page index.
$context['page_index'] = constructPageIndex($scripturl . '?action=' . ($context['group']['can_moderate'] ? 'moderate;area=viewgroups' : 'groups') . ';sa=members;group=' . $_REQUEST['group'] . ';sort=' . $context['sort_by'] . (isset($_REQUEST['desc']) ? ';desc' : ''), $_REQUEST['start'], $context['total_members'], $modSettings['defaultMaxMembers']);
$context['total_members'] = comma_format($context['total_members']);
$context['start'] = $_REQUEST['start'];
$context['can_moderate_forum'] = allowedTo('moderate_forum');
// Load up all members of this group.
$request = $smcFunc['db_query']('', '
SELECT id_member, member_name, real_name, email_address, member_ip, date_registered, last_login,
posts, is_activated, real_name
FROM {db_prefix}members
WHERE ' . $where . '
ORDER BY ' . $querySort . ' ' . ($context['sort_direction'] == 'down' ? 'DESC' : 'ASC') . '
LIMIT {int:start}, {int:max}',
array(
'group' => $_REQUEST['group'],
'start' => $context['start'],
'max' => $modSettings['defaultMaxMembers'],
)
);
$context['members'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$row['member_ip'] = inet_dtop($row['member_ip']);
$last_online = empty($row['last_login']) ? $txt['never'] : timeformat($row['last_login']);
// Italicize the online note if they aren't activated.
if ($row['is_activated'] % 10 != 1)
$last_online = '<em title="' . $txt['not_activated'] . '">' . $last_online . '</em>';
$context['members'][] = array(
'id' => $row['id_member'],
'name' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
'email' => $row['email_address'],
'ip' => '<a href="' . $scripturl . '?action=trackip;searchip=' . $row['member_ip'] . '">' . $row['member_ip'] . '</a>',
'registered' => timeformat($row['date_registered']),
'last_online' => $last_online,
'posts' => comma_format($row['posts']),
'is_activated' => $row['is_activated'] % 10 == 1,
);
}
$smcFunc['db_free_result']($request);
// Select the template.
$context['sub_template'] = 'group_members';
$context['page_title'] = $txt['membergroups_members_title'] . ': ' . $context['group']['name'];
createToken('mod-mgm');
if ($context['group']['assignable'])
loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
}
/**
* Show and manage all group requests.
*/
function GroupRequests()
{
global $txt, $context, $scripturl, $user_info, $sourcedir, $smcFunc, $modSettings;
// Set up the template stuff...
$context['page_title'] = $txt['mc_group_requests'];
$context['sub_template'] = 'show_list';
// Verify we can be here.
if ($user_info['mod_cache']['gq'] == '0=1')
isAllowedTo('manage_membergroups');
// Normally, we act normally...
$where = ($user_info['mod_cache']['gq'] == '1=1' || $user_info['mod_cache']['gq'] == '0=1' ? $user_info['mod_cache']['gq'] : 'lgr.' . $user_info['mod_cache']['gq']);
if (isset($_GET['closed']))
$where .= ' AND lgr.status != {int:status_open}';
else
$where .= ' AND lgr.status = {int:status_open}';
$where_parameters = array(
'status_open' => 0,
);
// We've submitted?
if (isset($_POST[$context['session_var']]) && !empty($_POST['groupr']) && !empty($_POST['req_action']))
{
checkSession();
validateToken('mod-gr');
// Clean the values.
foreach ($_POST['groupr'] as $k => $request)
$_POST['groupr'][$k] = (int) $request;
$log_changes = array();
// If we are giving a reason (And why shouldn't we?), then we don't actually do much.
if ($_POST['req_action'] == 'reason')
{
// Different sub template...
$context['sub_template'] = 'group_request_reason';
// And a limitation. We don't care that the page number bit makes no sense, as we don't need it!
$where .= ' AND lgr.id_request IN ({array_int:request_ids})';
$where_parameters['request_ids'] = $_POST['groupr'];
$context['group_requests'] = list_getGroupRequests(0, $modSettings['defaultMaxListItems'], 'lgr.id_request', $where, $where_parameters);
// Need to make another token for this.
createToken('mod-gr');
// Let obExit etc sort things out.
obExit();
}
// Otherwise we do something!
else
{
$request = $smcFunc['db_query']('', '
SELECT lgr.id_request
FROM {db_prefix}log_group_requests AS lgr
WHERE ' . $where . '
AND lgr.id_request IN ({array_int:request_list})',
array(
'request_list' => $_POST['groupr'],
'status_open' => 0,
)
);
$request_list = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
if (!isset($log_changes[$row['id_request']]))
$log_changes[$row['id_request']] = array(
'id_request' => $row['id_request'],
'status' => $_POST['req_action'] == 'approve' ? 1 : 2, // 1 = approved, 2 = rejected
'id_member_acted' => $user_info['id'],
'member_name_acted' => $user_info['name'],
'time_acted' => time(),
'act_reason' => $_POST['req_action'] != 'approve' && !empty($_POST['groupreason']) && !empty($_POST['groupreason'][$row['id_request']]) ? $smcFunc['htmlspecialchars']($_POST['groupreason'][$row['id_request']], ENT_QUOTES) : '',
);
$request_list[] = $row['id_request'];
}
$smcFunc['db_free_result']($request);
// Add a background task to handle notifying people of this request
$data = $smcFunc['json_encode'](array('member_id' => $user_info['id'], 'member_ip' => $user_info['ip'], 'request_list' => $request_list, 'status' => $_POST['req_action'], 'reason' => isset($_POST['groupreason']) ? $_POST['groupreason'] : '', 'time' => time()));
$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
array('$sourcedir/tasks/GroupAct-Notify.php', 'GroupAct_Notify_Background', $data, 0), array()
);
// Some changes to log?
if (!empty($log_changes))
{
foreach ($log_changes as $id_request => $details)
{
$smcFunc['db_query']('', '
UPDATE {db_prefix}log_group_requests
SET status = {int:status},
id_member_acted = {int:id_member_acted},
member_name_acted = {string:member_name_acted},
time_acted = {int:time_acted},
act_reason = {string:act_reason}
WHERE id_request = {int:id_request}',
$details
);
}
}
}
}
// We're going to want this for making our list.
require_once($sourcedir . '/Subs-List.php');
// This is all the information required for a group listing.
$listOptions = array(
'id' => 'group_request_list',
'width' => '100%',
'items_per_page' => $modSettings['defaultMaxListItems'],
'no_items_label' => $txt['mc_groupr_none_found'],
'base_href' => $scripturl . '?action=groups;sa=requests',
'default_sort_col' => 'member',
'get_items' => array(
'function' => 'list_getGroupRequests',
'params' => array(
$where,
$where_parameters,
),
),
'get_count' => array(
'function' => 'list_getGroupRequestCount',
'params' => array(
$where,
$where_parameters,
),
),
'columns' => array(
'member' => array(
'header' => array(
'value' => $txt['mc_groupr_member'],
),
'data' => array(
'db' => 'member_link',
),
'sort' => array(
'default' => 'mem.member_name',
'reverse' => 'mem.member_name DESC',
),
),
'group' => array(
'header' => array(
'value' => $txt['mc_groupr_group'],
),
'data' => array(
'db' => 'group_link',
),
'sort' => array(
'default' => 'mg.group_name',
'reverse' => 'mg.group_name DESC',
),
),
'reason' => array(
'header' => array(
'value' => $txt['mc_groupr_reason'],
),
'data' => array(
'db' => 'reason',
),
),
'date' => array(
'header' => array(
'value' => $txt['date'],
'style' => 'width: 18%; white-space:nowrap;',
),
'data' => array(
'db' => 'time_submitted',
),
),
'action' => array(
'header' => array(
'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
'style' => 'width: 4%;',
'class' => 'centercol',
),
'data' => array(
'sprintf' => array(
'format' => '<input type="checkbox" name="groupr[]" value="%1$d">',
'params' => array(
'id' => false,
),
),
'class' => 'centercol',
),
),
),
'form' => array(
'href' => $scripturl . '?action=groups;sa=requests',
'include_sort' => true,
'include_start' => true,
'hidden_fields' => array(
$context['session_var'] => $context['session_id'],
),
'token' => 'mod-gr',
),
'additional_rows' => array(
array(
'position' => 'bottom_of_list',
'value' => '
<select id="req_action" name="req_action" onchange="if (this.value != 0 &amp;&amp; (this.value == \'reason\' || confirm(\'' . $txt['mc_groupr_warning'] . '\'))) this.form.submit();">
<option value="0">' . $txt['with_selected'] . ':</option>
<option value="0" disabled>---------------------</option>
<option value="approve">' . $txt['mc_groupr_approve'] . '</option>
<option value="reject">' . $txt['mc_groupr_reject'] . '</option>
<option value="reason">' . $txt['mc_groupr_reject_w_reason'] . '</option>
</select>
<input type="submit" name="go" value="' . $txt['go'] . '" onclick="var sel = document.getElementById(\'req_action\'); if (sel.value != 0 &amp;&amp; sel.value != \'reason\' &amp;&amp; !confirm(\'' . $txt['mc_groupr_warning'] . '\')) return false;" class="button">',
'class' => 'floatright',
),
),
);
if (isset($_GET['closed']))
{
// Closed requests don't require interaction.
unset($listOptions['columns']['action'], $listOptions['form'], $listOptions['additional_rows'][0]);
$listOptions['base_href'] .= 'closed';
}
// Create the request list.
createToken('mod-gr');
createList($listOptions);
$context['default_list'] = 'group_request_list';
$context[$context['moderation_menu_name']]['tab_data'] = array(
'title' => $txt['mc_group_requests'],
);
}
/**
* Callback function for createList().
*
* @param string $where The WHERE clause for the query
* @param array $where_parameters The parameters for the WHERE clause
* @return int The number of group requests
*/
function list_getGroupRequestCount($where, $where_parameters)
{
global $smcFunc;
$request = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}log_group_requests AS lgr
WHERE ' . $where,
array_merge($where_parameters, array(
))
);
list ($totalRequests) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
return $totalRequests;
}
/**
* Callback function for createList()
*
* @param int $start The result to start with
* @param int $items_per_page The number of items per page
* @param string $sort An SQL sort expression (column/direction)
* @param string $where Data for the WHERE clause
* @param string $where_parameters Parameter values to be inserted into the WHERE clause
* @return array An array of group requests
* Each group request has:
* 'id'
* 'member_link'
* 'group_link'
* 'reason'
* 'time_submitted'
*/
function list_getGroupRequests($start, $items_per_page, $sort, $where, $where_parameters)
{
global $smcFunc, $scripturl, $txt;
$request = $smcFunc['db_query']('', '
SELECT
lgr.id_request, lgr.id_member, lgr.id_group, lgr.time_applied, lgr.reason,
lgr.status, lgr.id_member_acted, lgr.member_name_acted, lgr.time_acted, lgr.act_reason,
mem.member_name, mg.group_name, mg.online_color, mem.real_name
FROM {db_prefix}log_group_requests AS lgr
INNER JOIN {db_prefix}members AS mem ON (mem.id_member = lgr.id_member)
INNER JOIN {db_prefix}membergroups AS mg ON (mg.id_group = lgr.id_group)
WHERE ' . $where . '
ORDER BY {raw:sort}
LIMIT {int:start}, {int:max}',
array_merge($where_parameters, array(
'sort' => $sort,
'start' => $start,
'max' => $items_per_page,
))
);
$group_requests = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
if (empty($row['reason']))
$reason = '<em>(' . $txt['mc_groupr_no_reason'] . ')</em>';
else
$reason = censorText($row['reason']);
if (isset($_GET['closed']))
{
if ($row['status'] == 1)
$reason .= '<br><br><strong>' . $txt['mc_groupr_approved'] . '</strong>';
elseif ($row['status'] == 2)
$reason .= '<br><br><strong>' . $txt['mc_groupr_rejected'] . '</strong>';
$reason .= ' (' . timeformat($row['time_acted']) . ')';
if (!empty($row['act_reason']))
$reason .= '<br><br>' . censorText($row['act_reason']);
}
$group_requests[] = array(
'id' => $row['id_request'],
'member_link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
'group_link' => '<span style="color: ' . $row['online_color'] . '">' . $row['group_name'] . '</span>',
'reason' => $reason,
'time_submitted' => timeformat($row['time_applied']),
);
}
$smcFunc['db_free_result']($request);
return $group_requests;
}
?>

147
Sources/Help.php Normal file
View file

@ -0,0 +1,147 @@
<?php
/**
* This file has the important job of taking care of help messages and the help center.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Redirect to the user help ;).
* It loads information needed for the help section.
* It is accessed by ?action=help.
*
* Uses Help template and Manual language file.
*/
function ShowHelp()
{
loadTemplate('Help');
loadLanguage('Manual');
$subActions = array(
'index' => 'HelpIndex',
);
// CRUD $subActions as needed.
call_integration_hook('integrate_manage_help', array(&$subActions));
$sa = isset($_GET['sa'], $subActions[$_GET['sa']]) ? $_GET['sa'] : 'index';
call_helper($subActions[$sa]);
}
/**
* The main page for the Help section
*/
function HelpIndex()
{
global $scripturl, $context, $txt;
// We need to know where our wiki is.
$context['wiki_url'] = 'https://wiki.simplemachines.org/smf';
$context['wiki_prefix'] = 'SMF2.1:';
$context['canonical_url'] = $scripturl . '?action=help';
// Sections were are going to link...
$context['manual_sections'] = array(
'registering' => 'Registering',
'logging_in' => 'Logging_In',
'profile' => 'Profile',
'search' => 'Search',
'posting' => 'Posting',
'bbc' => 'Bulletin_board_code',
'personal_messages' => 'Personal_messages',
'memberlist' => 'Memberlist',
'calendar' => 'Calendar',
'features' => 'Features',
);
// Build the link tree.
$context['linktree'][] = array(
'url' => $scripturl . '?action=help',
'name' => $txt['help'],
);
// Lastly, some minor template stuff.
$context['page_title'] = $txt['manual_smf_user_help'];
$context['sub_template'] = 'manual';
}
/**
* Show some of the more detailed help to give the admin an idea...
* It shows a popup for administrative or user help.
* It uses the help parameter to decide what string to display and where to get
* the string from. ($helptxt or $txt?)
* It is accessed via ?action=helpadmin;help=?.
*
* Uses ManagePermissions language file, if the help starts with permissionhelp.
* @uses template_popup() with no layers.
*/
function ShowAdminHelp()
{
global $txt, $helptxt, $context, $scripturl, $boarddir, $boardurl;
if (!isset($_GET['help']) || !is_string($_GET['help']))
fatal_lang_error('no_access', false);
if (!isset($helptxt))
$helptxt = array();
// Load the admin help language file and template.
loadLanguage('Help');
// Permission specific help?
if (isset($_GET['help']) && substr($_GET['help'], 0, 14) == 'permissionhelp')
loadLanguage('ManagePermissions');
loadTemplate('Help');
// Allow mods to load their own language file here
call_integration_hook('integrate_helpadmin');
// What help string should be used?
if (isset($helptxt[$_GET['help']]))
$context['help_text'] = $helptxt[$_GET['help']];
elseif (isset($txt[$_GET['help']]))
$context['help_text'] = $txt[$_GET['help']];
else
fatal_lang_error('not_found', false, array(), 404);
switch ($_GET['help']) {
case 'cal_short_months':
$context['help_text'] = sprintf($context['help_text'], $txt['months_short'][1], $txt['months_titles'][1]);
break;
case 'cal_short_days':
$context['help_text'] = sprintf($context['help_text'], $txt['days_short'][1], $txt['days'][1]);
break;
case 'cron_is_real_cron':
$context['help_text'] = sprintf($context['help_text'], allowedTo('admin_forum') ? $boarddir : '[' . $txt['hidden'] . ']', $boardurl);
break;
case 'queryless_urls':
$context['help_text'] = sprintf($context['help_text'], (isset($_SERVER['SERVER_SOFTWARE']) && (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false || strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false) ? $helptxt['queryless_urls_supported'] : $helptxt['queryless_urls_unsupported']));
break;
}
// Does this text contain a link that we should fill in?
if (preg_match('~%([0-9]+\$)?s\?~', $context['help_text'], $match))
$context['help_text'] = sprintf($context['help_text'], $scripturl, $context['session_id'], $context['session_var']);
// Set the page title to something relevant.
$context['page_title'] = $context['forum_name'] . ' - ' . $txt['help'];
// Don't show any template layers, just the popup sub template.
$context['template_layers'] = array();
$context['sub_template'] = 'popup';
}
?>

715
Sources/Likes.php Normal file
View file

@ -0,0 +1,715 @@
<?php
/**
* This file contains liking posts and displaying the list of who liked a post.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Class Likes
*/
class Likes
{
/**
* @var boolean Know if a request comes from an ajax call or not, depends on $_GET['js'] been set.
*/
protected $_js = false;
/**
* @var string If filled, its value will contain a string matching a key on a language var $txt[$this->_error]
*/
protected $_error = false;
/**
* @var string The unique type to like, needs to be unique and it needs to be no longer than 6 characters, only numbers and letters are allowed.
*/
protected $_type = '';
/**
* @var string A generic string used if you need to pass any extra info. It gets set via $_GET['extra'].
*/
protected $_extra = false;
/**
* @var integer a valid ID to identify your like content.
*/
protected $_content = 0;
/**
* @var integer The number of times your content has been liked.
*/
protected $_numLikes = 0;
/**
* @var boolean If the current user has already liked this content.
*/
protected $_alreadyLiked = false;
/**
* @var array $_validLikes mostly used for external integration, needs to be filled as an array with the following keys:
* => 'can_like' boolean|string whether or not the current user can actually like your content.
* for can_like: Return a boolean true if the user can, otherwise return a string, the string will be used as key in a regular $txt language error var. The code assumes you already loaded your language file. If no value is returned or the $txt var isn't set, the code will use a generic error message.
* => 'redirect' string To add support for non JS users, It is highly encouraged to set a valid URL to redirect the user to, if you don't provide any, the code will redirect the user to the main page. The code only performs a light check to see if the redirect is valid so be extra careful while building it.
* => 'type' string 6 letters or numbers. The unique identifier for your content, the code doesn't check for duplicate entries, if there are 2 or more exact hook calls, the code will take the first registered one so make sure you provide a unique identifier. Must match with what you sent in $_GET['ltype'].
* => 'flush_cache' boolean this is optional, it tells the code to reset your like content's cache entry after a new entry has been inserted.
* => 'callback' callable optional, useful if you don't want to issue a separate hook for updating your data, it is called immediately after the data was inserted or deleted and before the actual hook. Uses call_helper(); so the same format for your function/method can be applied here.
* => 'json' boolean optional defaults to false, if true the Like class will return a json object as response instead of HTML.
*/
protected $_validLikes = array(
'can_like' => false,
'redirect' => '',
'type' => '',
'flush_cache' => '',
'callback' => false,
'json' => false,
);
/**
* @var array The current user info ($user_info).
*/
protected $_user;
/**
* @var integer The topic ID, used for liking messages.
*/
protected $_idTopic = 0;
/**
* @var boolean to know if response(); will be executed as normal. If this is set to false it indicates the method already solved its own way to send back a response.
*/
protected $_setResponse = true;
/**
* Likes::__construct()
*
* Sets the basic data needed for the rest of the process.
*/
public function __construct()
{
global $db_show_debug;
$this->_type = isset($_GET['ltype']) ? $_GET['ltype'] : '';
$this->_content = isset($_GET['like']) ? (int) $_GET['like'] : 0;
$this->_js = isset($_GET['js']) ? true : false;
$this->_sa = isset($_GET['sa']) ? $_GET['sa'] : 'like';
$this->_extra = isset($_GET['extra']) ? $_GET['extra'] : false;
// We do not want to output debug information here.
if ($this->_js)
$db_show_debug = false;
}
/**
* Likes::call()
*
* The main handler. Verifies permissions (whether the user can see the content in question), dispatch different method for different sub-actions.
* Accessed from index.php?action=likes
*/
public function call()
{
global $context;
$this->_user = $context['user'];
// Make sure the user can see and like your content.
$this->check();
$subActions = array(
'like',
'view',
'delete',
'insert',
'_count',
);
// So at this point, whatever type of like the user supplied and the item of content in question,
// we know it exists, now we need to figure out what we're doing with that.
if (in_array($this->_sa, $subActions) && !is_string($this->_error))
{
// To avoid ambiguity, turn the property to a normal var.
$call = $this->_sa;
// Guest can only view likes.
if ($call != 'view')
is_not_guest();
checkSession('get');
// Call the appropriate method.
$this->$call();
}
// else An error message.
$this->response();
}
/**
* Likes::get()
*
* A simple getter for all protected properties.
* Accessed from index.php?action=likes
*
* @param string $property The name of the property to get.
* @return mixed Either return the property or false if there isn't a property with that name.
*/
public function get($property = '')
{
// All properties inside Likes are protected, thus, an underscore is used.
$property = '_' . $property;
return property_exists($this, $property) ? $this->$property : false;
}
/**
* Likes::check()
*
* Performs basic checks on the data provided, checks for a valid msg like.
* Calls integrate_valid_likes hook for retrieving all the data needed and apply checks based on the data provided.
*/
protected function check()
{
global $smcFunc, $modSettings;
// This feature is currently disable.
if (empty($modSettings['enable_likes']))
return $this->_error = 'like_disable';
// Zerothly, they did indicate some kind of content to like, right?
preg_match('~^([a-z0-9\-\_]{1,6})~i', $this->_type, $matches);
$this->_type = isset($matches[1]) ? $matches[1] : '';
if ($this->_type == '' || $this->_content <= 0)
return $this->_error = 'cannot_';
// First we need to verify if the user can see the type of content or not. This is set up to be extensible,
// so we'll check for the one type we do know about, and if it's not that, we'll defer to any hooks.
if ($this->_type == 'msg')
{
// So we're doing something off a like. We need to verify that it exists, and that the current user can see it.
// Fortunately for messages, this is quite easy to do - and we'll get the topic id while we're at it, because
// we need this later for other things.
$request = $smcFunc['db_query']('', '
SELECT m.id_topic, m.id_member
FROM {db_prefix}messages AS m
WHERE {query_see_message_board}
AND m.id_msg = {int:msg}',
array(
'msg' => $this->_content,
)
);
if ($smcFunc['db_num_rows']($request) == 1)
list ($this->_idTopic, $topicOwner) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
if (empty($this->_idTopic))
return $this->_error = 'cannot_';
// So we know what topic it's in and more importantly we know the user can see it.
// If we're not viewing, we need some info set up.
$this->_validLikes['type'] = 'msg';
$this->_validLikes['flush_cache'] = 'likes_topic_' . $this->_idTopic . '_' . $this->_user['id'];
$this->_validLikes['redirect'] = 'topic=' . $this->_idTopic . '.msg' . $this->_content . '#msg' . $this->_content;
$this->_validLikes['can_like'] = ($this->_user['id'] == $topicOwner ? 'cannot_like_content' : (allowedTo('likes_like') ? true : 'cannot_like_content'));
}
else
{
// Modders: This will give you whatever the user offers up in terms of liking, e.g. $this->_type=msg, $this->_content=1
// When you hook this, check $this->_type first. If it is not something your mod worries about, return false.
// Otherwise, fill an array according to the docs for $this->_validLikes. Determine (however you need to) that the user can see and can_like the relevant liked content (and it exists) Remember that users can't like their own content.
// If the user can like it, you MUST return your type in the 'type' key back.
// See also issueLike() for further notes.
$can_like = call_integration_hook('integrate_valid_likes', array($this->_type, $this->_content, $this->_sa, $this->_js, $this->_extra));
$found = false;
if (!empty($can_like))
{
$can_like = (array) $can_like;
foreach ($can_like as $result)
{
if ($result !== false)
{
// Match the type with what we already have.
if (!isset($result['type']) || $result['type'] != $this->_type)
return $this->_error = 'not_valid_like_type';
// Fill out the rest.
$this->_type = $result['type'];
$this->_validLikes = array_merge($this->_validLikes, $result);
$found = true;
break;
}
}
}
if (!$found)
return $this->_error = 'cannot_';
}
// Does the user can like this? Viewing a list of likes doesn't require this permission.
if ($this->_sa != 'view' && isset($this->_validLikes['can_like']) && is_string($this->_validLikes['can_like']))
return $this->_error = $this->_validLikes['can_like'];
}
/**
* Likes::delete()
*
* Deletes an entry from user_likes table, needs 3 properties: $_content, $_type and $_user['id'].
*/
protected function delete()
{
global $smcFunc;
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}user_likes
WHERE content_id = {int:like_content}
AND content_type = {string:like_type}
AND id_member = {int:id_member}',
array(
'like_content' => $this->_content,
'like_type' => $this->_type,
'id_member' => $this->_user['id'],
)
);
// Are we calling this directly? if so, set a proper data for the response. Do note that __METHOD__ returns both the class name and the function name.
if ($this->_sa == __FUNCTION__)
$this->_data = __FUNCTION__;
// Check to see if there is an unread alert to delete as well...
$result = $smcFunc['db_query']('', '
SELECT id_alert, id_member FROM {db_prefix}user_alerts
WHERE content_id = {int:like_content}
AND content_type = {string:like_type}
AND id_member_started = {int:id_member_started}
AND content_action = {string:content_action}
AND is_read = {int:unread}',
array(
'like_content' => $this->_content,
'like_type' => $this->_type,
'id_member_started' => $this->_user['id'],
'content_action' => 'like',
'unread' => 0,
)
);
// Found one?
if ($smcFunc['db_num_rows']($result) == 1)
{
list($alert, $member) = $smcFunc['db_fetch_row']($result);
// Delete it
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}user_alerts
WHERE id_alert = {int:alert}',
array(
'alert' => $alert,
)
);
// Decrement counter for member who received the like
updateMemberData($member, array('alerts' => '-'));
}
}
/**
* Likes::insert()
*
* Inserts a new entry on user_likes table. Creates a background task for the inserted entry.
*/
protected function insert()
{
global $smcFunc;
// Any last minute changes? Temporarily turn the passed properties to normal vars to prevent unexpected behaviour with other methods using these properties.
$type = $this->_type;
$content = $this->_content;
$user = $this->_user;
$time = time();
call_integration_hook('integrate_issue_like_before', array(&$type, &$content, &$user, &$time));
// Insert the like.
$smcFunc['db_insert']('insert',
'{db_prefix}user_likes',
array('content_id' => 'int', 'content_type' => 'string-6', 'id_member' => 'int', 'like_time' => 'int'),
array($content, $type, $user['id'], $time),
array('content_id', 'content_type', 'id_member')
);
// Add a background task to process sending alerts.
// Mod author, you can add your own background task for your own custom like event using the "integrate_issue_like" hook or your callback, both are immediately called after this.
if ($this->_type == 'msg')
$smcFunc['db_insert']('insert',
'{db_prefix}background_tasks',
array('task_file' => 'string', 'task_class' => 'string', 'task_data' => 'string', 'claimed_time' => 'int'),
array('$sourcedir/tasks/Likes-Notify.php', 'Likes_Notify_Background', $smcFunc['json_encode'](array(
'content_id' => $content,
'content_type' => $type,
'sender_id' => $user['id'],
'sender_name' => $user['name'],
'time' => $time,
)), 0),
array('id_task')
);
// Are we calling this directly? if so, set a proper data for the response. Do note that __METHOD__ returns both the class name and the function name.
if ($this->_sa == __FUNCTION__)
$this->_data = __FUNCTION__;
}
/**
* Likes::_count()
*
* Sets $_numLikes with the actual number of likes your content has, needs two properties: $_content and $_view. When called directly it will return the number of likes as response.
*/
protected function _count()
{
global $smcFunc;
$request = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}user_likes
WHERE content_id = {int:like_content}
AND content_type = {string:like_type}',
array(
'like_content' => $this->_content,
'like_type' => $this->_type,
)
);
list ($this->_numLikes) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
// If you want to call this directly, fill out _data property too.
if ($this->_sa == __FUNCTION__)
$this->_data = $this->_numLikes;
}
/**
* Likes::like()
*
* Performs a like action, either like or unlike. Counts the total of likes and calls a hook after the event.
*/
protected function like()
{
global $smcFunc;
// Safety first!
if (empty($this->_type) || empty($this->_content))
return $this->_error = 'cannot_';
// Do we already like this?
$request = $smcFunc['db_query']('', '
SELECT content_id, content_type, id_member
FROM {db_prefix}user_likes
WHERE content_id = {int:like_content}
AND content_type = {string:like_type}
AND id_member = {int:id_member}',
array(
'like_content' => $this->_content,
'like_type' => $this->_type,
'id_member' => $this->_user['id'],
)
);
$this->_alreadyLiked = (bool) $smcFunc['db_num_rows']($request) != 0;
$smcFunc['db_free_result']($request);
if ($this->_alreadyLiked)
$this->delete();
else
$this->insert();
// Now, how many people like this content now? We *could* just +1 / -1 the relevant container but that has proven to become unstable.
$this->_count();
// Update the likes count for messages.
if ($this->_type == 'msg')
$this->msgIssueLike();
// Any callbacks?
elseif (!empty($this->_validLikes['callback']))
{
$call = call_helper($this->_validLikes['callback'], true);
if (!empty($call))
call_user_func_array($call, array($this));
}
// Sometimes there might be other things that need updating after we do this like.
call_integration_hook('integrate_issue_like', array($this));
// Now some clean up. This is provided here for any like handlers that want to do any cache flushing.
// This way a like handler doesn't need to explicitly declare anything in integrate_issue_like, but do so
// in integrate_valid_likes where it absolutely has to exist.
if (!empty($this->_validLikes['flush_cache']))
cache_put_data($this->_validLikes['flush_cache'], null);
// All done, start building the data to pass as response.
$this->_data = array(
'id_topic' => !empty($this->_idTopic) ? $this->_idTopic : 0,
'id_content' => $this->_content,
'count' => $this->_numLikes,
'can_like' => $this->_validLikes['can_like'],
'already_liked' => empty($this->_alreadyLiked),
'type' => $this->_type,
);
}
/**
* Likes::msgIssueLike()
*
* Partly it indicates how it's supposed to work and partly it deals with updating the count of likes
* attached to this message now.
*/
function msgIssueLike()
{
global $smcFunc;
if ($this->_type !== 'msg')
return;
$smcFunc['db_query']('', '
UPDATE {db_prefix}messages
SET likes = {int:num_likes}
WHERE id_msg = {int:id_msg}',
array(
'id_msg' => $this->_content,
'num_likes' => $this->_numLikes,
)
);
// Note that we could just as easily have cleared the cache here, or set up the redirection address
// but if your liked content doesn't need to do anything other than have the record in smf_user_likes,
// there's no point in creating another function unnecessarily.
}
/**
* Likes::view()
*
* This is for viewing the people who liked a thing.
* Accessed from index.php?action=likes;view and should generally load in a popup.
* We use a template for this in case themers want to style it.
*/
function view()
{
global $smcFunc, $txt, $context, $memberContext;
// Firstly, load what we need. We already know we can see this, so that's something.
$context['likers'] = array();
$request = $smcFunc['db_query']('', '
SELECT id_member, like_time
FROM {db_prefix}user_likes
WHERE content_id = {int:like_content}
AND content_type = {string:like_type}
ORDER BY like_time DESC',
array(
'like_content' => $this->_content,
'like_type' => $this->_type,
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
$context['likers'][$row['id_member']] = array('timestamp' => $row['like_time']);
// Now to get member data, including avatars and so on.
$members = array_keys($context['likers']);
$loaded = loadMemberData($members);
if (count($loaded) != count($members))
{
$members = array_diff($members, $loaded);
foreach ($members as $not_loaded)
unset ($context['likers'][$not_loaded]);
}
foreach ($context['likers'] as $liker => $dummy)
{
$loaded = loadMemberContext($liker);
if (!$loaded)
{
unset ($context['likers'][$liker]);
continue;
}
$context['likers'][$liker]['profile'] = &$memberContext[$liker];
$context['likers'][$liker]['time'] = !empty($dummy['timestamp']) ? timeformat($dummy['timestamp']) : '';
}
$count = count($context['likers']);
$title_base = isset($txt['likes_' . $count]) ? 'likes_' . $count : 'likes_n';
$context['page_title'] = strip_tags(sprintf($txt[$title_base], '', comma_format($count)));
// Lastly, setting up for display.
loadTemplate('Likes');
loadLanguage('Help'); // For the close window button.
$context['template_layers'] = array();
$context['sub_template'] = 'popup';
// We already took care of our response so there is no need to bother with respond();
$this->_setResponse = false;
}
/**
* Likes::response()
*
* Checks if the user can use JavaScript and acts accordingly.
* Calls the appropriate sub-template for each method
* Handles error messages.
*/
protected function response()
{
global $context, $txt;
// Don't do anything if someone else has already take care of the response.
if (!$this->_setResponse)
return;
// Want a json response huh?
if ($this->_validLikes['json'])
return $this->jsonResponse();
// Set everything up for display.
loadTemplate('Likes');
$context['template_layers'] = array();
// If there are any errors, process them first.
if ($this->_error)
{
// If this is a generic error, set it up good.
if ($this->_error == 'cannot_')
$this->_error = $this->_sa == 'view' ? 'cannot_view_likes' : 'cannot_like_content';
// Is this request coming from an ajax call?
if ($this->_js)
{
$context['sub_template'] = 'generic';
$context['data'] = isset($txt[$this->_error]) ? $txt[$this->_error] : $txt['like_error'];
}
// Nope? then just do a redirect to whatever URL was provided.
else
redirectexit(!empty($this->_validLikes['redirect']) ? $this->_validLikes['redirect'] . ';error=' . $this->_error : '');
return;
}
// A like operation.
else
{
// Not an ajax request so send the user back to the previous location or the main page.
if (!$this->_js)
redirectexit(!empty($this->_validLikes['redirect']) ? $this->_validLikes['redirect'] : '');
// These fine gentlemen all share the same template.
$generic = array('delete', 'insert', '_count');
if (in_array($this->_sa, $generic))
{
$context['sub_template'] = 'generic';
$context['data'] = isset($txt['like_' . $this->_data]) ? $txt['like_' . $this->_data] : $this->_data;
}
// Directly pass the current called sub-action and the data generated by its associated Method.
else
{
$context['sub_template'] = $this->_sa;
$context['data'] = $this->_data;
}
}
}
/**
* Outputs a JSON-encoded response
*/
protected function jsonResponse()
{
global $smcFunc;
$print = array(
'data' => $this->_data,
);
// If there is an error, send it.
if ($this->_error)
{
if ($this->_error == 'cannot_')
$this->_error = $this->_sa == 'view' ? 'cannot_view_likes' : 'cannot_like_content';
$print['error'] = $this->_error;
}
// Do you want to add something at the very last minute?
call_integration_hook('integrate_likes_json_response', array(&$print));
// Print the data.
smf_serverResponse($smcFunc['json_encode']($print));
die;
}
}
/**
* What's this? I dunno, what are you talking about? Never seen this before, nope. No sir.
*/
function BookOfUnknown()
{
global $context, $scripturl;
echo '<!DOCTYPE html>
<html', $context['right_to_left'] ? ' dir="rtl"' : '', '>
<head>
<title>The Book of Unknown, ', @$_GET['verse'] == '2:18' ? '2:18' : '4:16', '</title>
<style>
em
{
font-size: 1.3em;
line-height: 0;
}
</style>
</head>
<body style="background-color: #444455; color: white; font-style: italic; font-family: serif;">
<div style="margin-top: 12%; font-size: 1.1em; line-height: 1.4; text-align: center;">';
if (!isset($_GET['verse']) || ($_GET['verse'] != '2:18' && $_GET['verse'] != '22:1-2'))
$_GET['verse'] = '4:16';
if ($_GET['verse'] == '2:18')
echo '
Woe, it was that his name wasn\'t <em>known</em>, that he came in mystery, and was recognized by none.&nbsp;And it became to be in those days <em>something</em>.&nbsp; Something not yet <em id="unknown" name="[Unknown]">unknown</em> to mankind.&nbsp; And thus what was to be known the <em>secret project</em> began into its existence.&nbsp; Henceforth the opposition was only <em>weary</em> and <em>fearful</em>, for now their match was at arms against them.';
elseif ($_GET['verse'] == '4:16')
echo '
And it came to pass that the <em>unbelievers</em> dwindled in number and saw rise of many <em>proselytizers</em>, and the opposition found fear in the face of the <em>x</em> and the <em>j</em> while those who stood with the <em>something</em> grew stronger and came together.&nbsp; Still, this was only the <em>beginning</em>, and what lay in the future was <em id="unknown" name="[Unknown]">unknown</em> to all, even those on the right side.';
elseif ($_GET['verse'] == '22:1-2')
echo '
<p>Now <em>behold</em>, that which was once the secret project was <em id="unknown" name="[Unknown]">unknown</em> no longer.&nbsp; Alas, it needed more than <em>only one</em>, but yet even thought otherwise.&nbsp; It became that the opposition <em>rumored</em> and lied, but still to no avail.&nbsp; Their match, though not <em>perfect</em>, had them outdone.</p>
<p style="margin: 2ex 1ex 0 1ex; font-size: 1.05em; line-height: 1.5; text-align: center;">Let it continue.&nbsp; <em>The end</em>.</p>';
echo '
</div>
<div style="margin-top: 2ex; font-size: 2em; text-align: right;">';
if ($_GET['verse'] == '2:18')
echo '
from <span style="font-family: Georgia, serif;"><strong><a href="', $scripturl, '?action=about:unknown;verse=4:16" style="color: white; text-decoration: none; cursor: text;">The Book of Unknown</a></strong>, 2:18</span>';
elseif ($_GET['verse'] == '4:16')
echo '
from <span style="font-family: Georgia, serif;"><strong><a href="', $scripturl, '?action=about:unknown;verse=22:1-2" style="color: white; text-decoration: none; cursor: text;">The Book of Unknown</a></strong>, 4:16</span>';
elseif ($_GET['verse'] == '22:1-2')
echo '
from <span style="font-family: Georgia, serif;"><strong>The Book of Unknown</strong>, 22:1-2</span>';
echo '
</div>
</body>
</html>';
obExit(false);
}
?>

4055
Sources/Load.php Normal file

File diff suppressed because it is too large Load diff

964
Sources/LogInOut.php Normal file
View file

@ -0,0 +1,964 @@
<?php
/**
* This file is concerned pretty entirely, as you see from its name, with
* logging in and out members, and the validation of that.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Ask them for their login information. (shows a page for the user to type
* in their username and password.)
* It caches the referring URL in $_SESSION['login_url'].
* It is accessed from ?action=login.
*
* Uses Login template and language file with the login sub-template.
*/
function Login()
{
global $txt, $context, $scripturl, $user_info;
// You are already logged in, go take a tour of the boards
if (!empty($user_info['id']))
{
// This came from a valid hashed return url. Or something that knows our secrets...
if (!empty($_REQUEST['return_hash']) && !empty($_REQUEST['return_to']) && hash_hmac('sha1', un_htmlspecialchars($_REQUEST['return_to']), get_auth_secret()) == $_REQUEST['return_hash'])
redirectexit(un_htmlspecialchars($_REQUEST['return_to']));
else
redirectexit();
}
// We need to load the Login template/language file.
loadLanguage('Login');
loadTemplate('Login');
$context['sub_template'] = 'login';
/* This is true when:
* We have a valid header indicating a JQXHR request. This is not sent during a cross domain request.
* OR we have found:
* 1. valid cors host
* 2. A header indicating a SMF request
* 3. The url has a ajax in either the GET or POST
* These are not intended for security, but ensuring the request is intended for a JQXHR response.
*/
if (
(
!empty($_SERVER['HTTP_X_REQUESTED_WITH'])
&& $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
)
||
(
!empty($context['valid_cors_found'])
&& !empty($_SERVER['HTTP_X_SMF_AJAX'])
&& isset($_REQUEST['ajax'])
)
)
{
$context['from_ajax'] = true;
$context['template_layers'] = array();
}
// Get the template ready.... not really much else to do.
$context['page_title'] = $txt['login'];
$context['default_username'] = &$_REQUEST['u'];
$context['default_password'] = '';
$context['never_expire'] = false;
// Add the login chain to the link tree.
$context['linktree'][] = array(
'url' => $scripturl . '?action=login',
'name' => $txt['login'],
);
// Set the login URL - will be used when the login process is done (but careful not to send us to an attachment).
if (isset($_SESSION['old_url']) && strpos($_SESSION['old_url'], 'dlattach') === false && preg_match('~(board|topic)[=,]~', $_SESSION['old_url']) != 0)
$_SESSION['login_url'] = $_SESSION['old_url'];
// This came from a valid hashed return url. Or something that knows our secrets...
elseif (!empty($_REQUEST['return_hash']) && !empty($_REQUEST['return_to']) && hash_hmac('sha1', un_htmlspecialchars($_REQUEST['return_to']), get_auth_secret()) == $_REQUEST['return_hash'])
$_SESSION['login_url'] = un_htmlspecialchars($_REQUEST['return_to']);
elseif (isset($_SESSION['login_url']) && strpos($_SESSION['login_url'], 'dlattach') !== false)
unset($_SESSION['login_url']);
// Create a one time token.
createToken('login');
}
/**
* Actually logs you in.
* What it does:
* - checks credentials and checks that login was successful.
* - it employs protection against a specific IP or user trying to brute force
* a login to an account.
* - upgrades password encryption on login, if necessary.
* - after successful login, redirects you to $_SESSION['login_url'].
* - accessed from ?action=login2, by forms.
* On error, uses the same templates Login() uses.
*/
function Login2()
{
global $txt, $scripturl, $user_info, $user_settings, $smcFunc;
global $cookiename, $modSettings, $context, $sourcedir, $maintenance;
// Check to ensure we're forcing SSL for authentication
if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
fatal_lang_error('login_ssl_required', false);
// Load cookie authentication stuff.
require_once($sourcedir . '/Subs-Auth.php');
/* This is true when:
* We have a valid header indicating a JQXHR request. This is not sent during a cross domain request.
* OR we have found:
* 1. valid cors host
* 2. A header indicating a SMF request
* 3. The url has a ajax in either the GET or POST
* These are not intended for security, but ensuring the request is intended for a JQXHR response.
*/
if (
(
!empty($_SERVER['HTTP_X_REQUESTED_WITH'])
&& $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
)
||
(
!empty($context['valid_cors_found'])
&& !empty($_SERVER['HTTP_X_SMF_AJAX'])
&& isset($_REQUEST['ajax'])
)
)
{
$context['from_ajax'] = true;
$context['template_layers'] = array();
}
if (isset($_GET['sa']) && $_GET['sa'] == 'salt' && !$user_info['is_guest'])
{
// First check for 2.1 json-format cookie in $_COOKIE
if (isset($_COOKIE[$cookiename]) && preg_match('~^{"0":\d+,"1":"[0-9a-f]*","2":\d+~', $_COOKIE[$cookiename]) === 1)
list (,, $timeout) = $smcFunc['json_decode']($_COOKIE[$cookiename], true);
// Try checking for 2.1 json-format cookie in $_SESSION
elseif (isset($_SESSION['login_' . $cookiename]) && preg_match('~^{"0":\d+,"1":"[0-9a-f]*","2":\d+~', $_SESSION['login_' . $cookiename]) === 1)
list (,, $timeout) = $smcFunc['json_decode']($_SESSION['login_' . $cookiename]);
// Next, try checking for 2.0 serialized string cookie in $_COOKIE
elseif (isset($_COOKIE[$cookiename]) && preg_match('~^a:[34]:\{i:0;i:\d+;i:1;s:(0|40):"([a-fA-F0-9]{40})?";i:2;[id]:\d+;~', $_COOKIE[$cookiename]) === 1)
list (,, $timeout) = safe_unserialize($_COOKIE[$cookiename]);
// Last, see if you need to fall back on checking for 2.0 serialized string cookie in $_SESSION
elseif (isset($_SESSION['login_' . $cookiename]) && preg_match('~^a:[34]:\{i:0;i:\d+;i:1;s:(0|40):"([a-fA-F0-9]{40})?";i:2;[id]:\d+;~', $_SESSION['login_' . $cookiename]) === 1)
list (,, $timeout) = safe_unserialize($_SESSION['login_' . $cookiename]);
else
{
loadLanguage('Errors');
trigger_error($txt['login_no_session_cookie'], E_USER_ERROR);
}
$user_settings['password_salt'] = bin2hex($smcFunc['random_bytes'](16));
updateMemberData($user_info['id'], array('password_salt' => $user_settings['password_salt']));
// Preserve the 2FA cookie?
if (!empty($modSettings['tfa_mode']) && !empty($_COOKIE[$cookiename . '_tfa']))
{
list (,, $exp) = $smcFunc['json_decode']($_COOKIE[$cookiename . '_tfa'], true);
setTFACookie((int) $exp - time(), $user_info['password_salt'], hash_salt($user_settings['tfa_backup'], $user_settings['password_salt']));
}
setLoginCookie((int) $timeout - time(), $user_info['id'], hash_salt($user_settings['passwd'], $user_settings['password_salt']));
redirectexit('action=login2;sa=check;member=' . $user_info['id'], $context['server']['needs_login_fix']);
}
// Double check the cookie...
elseif (isset($_GET['sa']) && $_GET['sa'] == 'check')
{
// Strike! You're outta there!
if ($_GET['member'] != $user_info['id'])
fatal_lang_error('login_cookie_error', false);
$user_info['can_mod'] = allowedTo('access_mod_center') || (!$user_info['is_guest'] && ($user_info['mod_cache']['gq'] != '0=1' || $user_info['mod_cache']['bq'] != '0=1' || ($modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']))));
// Some whitelisting for login_url...
if (empty($_SESSION['login_url']))
redirectexit(empty($user_settings['tfa_secret']) ? '' : 'action=logintfa');
elseif (!empty($_SESSION['login_url']) && (strpos($_SESSION['login_url'], 'http://') === false && strpos($_SESSION['login_url'], 'https://') === false))
{
unset($_SESSION['login_url']);
redirectexit(empty($user_settings['tfa_secret']) ? '' : 'action=logintfa');
}
elseif (!empty($user_settings['tfa_secret']))
{
redirectexit('action=logintfa');
}
else
{
// Best not to clutter the session data too much...
$temp = $_SESSION['login_url'];
unset($_SESSION['login_url']);
redirectexit($temp);
}
}
// Beyond this point you are assumed to be a guest trying to login.
if (!$user_info['is_guest'])
redirectexit();
// Are you guessing with a script?
checkSession();
validateToken('login');
spamProtection('login');
// Set the login_url if it's not already set (but careful not to send us to an attachment).
if ((empty($_SESSION['login_url']) && isset($_SESSION['old_url']) && strpos($_SESSION['old_url'], 'dlattach') === false && preg_match('~(board|topic)[=,]~', $_SESSION['old_url']) != 0) || (isset($_GET['quicklogin']) && isset($_SESSION['old_url']) && strpos($_SESSION['old_url'], 'login') === false))
$_SESSION['login_url'] = $_SESSION['old_url'];
// Been guessing a lot, haven't we?
if (isset($_SESSION['failed_login']) && $_SESSION['failed_login'] >= $modSettings['failed_login_threshold'] * 3)
fatal_lang_error('login_threshold_fail', 'login');
// Set up the cookie length. (if it's invalid, just fall through and use the default.)
if (isset($_POST['cookieneverexp']) || (!empty($_POST['cookielength']) && $_POST['cookielength'] == -1))
$modSettings['cookieTime'] = 3153600;
elseif (!empty($_POST['cookielength']) && ($_POST['cookielength'] >= 1 && $_POST['cookielength'] <= 3153600))
$modSettings['cookieTime'] = (int) $_POST['cookielength'];
loadLanguage('Login');
// Load the template stuff.
loadTemplate('Login');
$context['sub_template'] = 'login';
// Create a one time token.
createToken('login');
// Set up the default/fallback stuff.
$context['default_username'] = isset($_POST['user']) ? preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars']($_POST['user'])) : '';
$context['default_password'] = '';
$context['never_expire'] = $modSettings['cookieTime'] <= 525600;
$context['login_errors'] = array($txt['error_occured']);
$context['page_title'] = $txt['login'];
// Add the login chain to the link tree.
$context['linktree'][] = array(
'url' => $scripturl . '?action=login',
'name' => $txt['login'],
);
// You forgot to type your username, dummy!
if (!isset($_POST['user']) || $_POST['user'] == '')
{
$context['login_errors'] = array($txt['need_username']);
return;
}
// Hmm... maybe 'admin' will login with no password. Uhh... NO!
if (!isset($_POST['passwrd']) || $_POST['passwrd'] == '')
{
$context['login_errors'] = array($txt['no_password']);
return;
}
// No funky symbols either.
if (preg_match('~[<>&"\'=\\\]~', preg_replace('~(&#(\\d{1,7}|x[0-9a-fA-F]{1,6});)~', '', $_POST['user'])) != 0)
{
$context['login_errors'] = array($txt['error_invalid_characters_username']);
return;
}
// And if it's too long, trim it back.
if ($smcFunc['strlen']($_POST['user']) > 80)
{
$_POST['user'] = $smcFunc['substr']($_POST['user'], 0, 79);
$context['default_username'] = preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars']($_POST['user']));
}
// Are we using any sort of integration to validate the login?
if (in_array('retry', call_integration_hook('integrate_validate_login', array($_POST['user'], isset($_POST['passwrd']) ? $_POST['passwrd'] : null, $modSettings['cookieTime'])), true))
{
$context['login_errors'] = array($txt['incorrect_password']);
return;
}
// Load the data up!
$request = $smcFunc['db_query']('', '
SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
passwd_flood, tfa_secret
FROM {db_prefix}members
WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name) = LOWER({string:user_name})' : 'member_name = {string:user_name}') . '
LIMIT 1',
array(
'user_name' => $smcFunc['db_case_sensitive'] ? strtolower($_POST['user']) : $_POST['user'],
)
);
// Probably mistyped or their email, try it as an email address. (member_name first, though!)
if ($smcFunc['db_num_rows']($request) == 0 && strpos($_POST['user'], '@') !== false)
{
$smcFunc['db_free_result']($request);
$request = $smcFunc['db_query']('', '
SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
passwd_flood, tfa_secret
FROM {db_prefix}members
WHERE email_address = {string:user_name}
LIMIT 1',
array(
'user_name' => $_POST['user'],
)
);
}
// Let them try again, it didn't match anything...
if ($smcFunc['db_num_rows']($request) == 0)
{
$context['login_errors'] = array($txt['username_no_exist']);
return;
}
$user_settings = $smcFunc['db_fetch_assoc']($request);
$smcFunc['db_free_result']($request);
// Bad password! Thought you could fool the database?!
if (!hash_verify_password($user_settings['member_name'], un_htmlspecialchars($_POST['passwrd']), $user_settings['passwd']))
{
// Let's be cautious, no hacking please. thanx.
validatePasswordFlood($user_settings['id_member'], $user_settings['member_name'], $user_settings['passwd_flood']);
// Maybe we were too hasty... let's try some other authentication methods.
$other_passwords = array();
// None of the below cases will be used most of the time (because the salt is normally set.)
if (!empty($modSettings['enable_password_conversion']) && $user_settings['password_salt'] == '')
{
// YaBB SE, Discus, MD5 (used a lot), SHA-1 (used some), SMF 1.0.x, IkonBoard, and none at all.
$other_passwords[] = crypt($_POST['passwrd'], substr($_POST['passwrd'], 0, 2));
$other_passwords[] = crypt($_POST['passwrd'], substr($user_settings['passwd'], 0, 2));
$other_passwords[] = md5($_POST['passwrd']);
$other_passwords[] = sha1($_POST['passwrd']);
$other_passwords[] = md5_hmac($_POST['passwrd'], strtolower($user_settings['member_name']));
$other_passwords[] = md5($_POST['passwrd'] . strtolower($user_settings['member_name']));
$other_passwords[] = md5(md5($_POST['passwrd']));
$other_passwords[] = $_POST['passwrd'];
$other_passwords[] = crypt($_POST['passwrd'], $user_settings['passwd']);
// This one is a strange one... MyPHP, crypt() on the MD5 hash.
$other_passwords[] = crypt(md5($_POST['passwrd']), md5($_POST['passwrd']));
// Snitz style - SHA-256. Technically, this is a downgrade, but most PHP configurations don't support sha256 anyway.
if (strlen($user_settings['passwd']) == 64 && function_exists('mhash') && defined('MHASH_SHA256'))
$other_passwords[] = bin2hex(mhash(MHASH_SHA256, $_POST['passwrd']));
// phpBB3 users new hashing. We now support it as well ;).
$other_passwords[] = phpBB3_password_check($_POST['passwrd'], $user_settings['passwd']);
// APBoard 2 Login Method.
$other_passwords[] = md5(crypt($_POST['passwrd'], 'CRYPT_MD5'));
}
// If the salt is set let's try some other options
elseif (!empty($modSettings['enable_password_conversion']) && $user_settings['password_salt'] != '')
{
// PHPBB 3 check this function exists in PHP 5.5 or higher
if (function_exists('password_verify'))
$other_passwords[] = password_verify($_POST['passwrd'],$user_settings['password_salt']);
// PHP-Fusion
$other_passwords[] = hash_hmac('sha256', $_POST['passwrd'], $user_settings['password_salt']);
// MyBB
$other_passwords[] = md5(md5($user_settings['password_salt']) . md5($_POST['passwrd']));
}
// The hash should be 40 if it's SHA-1, so we're safe with more here too.
elseif (!empty($modSettings['enable_password_conversion']) && strlen($user_settings['passwd']) == 32)
{
// vBulletin 3 style hashing? Let's welcome them with open arms \o/.
$other_passwords[] = md5(md5($_POST['passwrd']) . stripslashes($user_settings['password_salt']));
// Hmm.. p'raps it's Invision 2 style?
$other_passwords[] = md5(md5($user_settings['password_salt']) . md5($_POST['passwrd']));
// Some common md5 ones.
$other_passwords[] = md5($user_settings['password_salt'] . $_POST['passwrd']);
$other_passwords[] = md5($_POST['passwrd'] . $user_settings['password_salt']);
}
elseif (strlen($user_settings['passwd']) == 40)
{
// Maybe they are using a hash from before the password fix.
// This is also valid for SMF 1.1 to 2.0 style of hashing, changed to bcrypt in SMF 2.1
$other_passwords[] = sha1(strtolower($user_settings['member_name']) . un_htmlspecialchars($_POST['passwrd']));
// BurningBoard3 style of hashing.
if (!empty($modSettings['enable_password_conversion']))
$other_passwords[] = sha1($user_settings['password_salt'] . sha1($user_settings['password_salt'] . sha1($_POST['passwrd'])));
// PunBB
$other_passwords[] = sha1($user_settings['password_salt'] . sha1($_POST['passwrd']));
// Perhaps we converted to UTF-8 and have a valid password being hashed differently.
if ($context['character_set'] == 'UTF-8' && !empty($modSettings['previousCharacterSet']) && $modSettings['previousCharacterSet'] != 'utf8')
{
// Try iconv first, for no particular reason.
if (function_exists('iconv'))
$other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', $modSettings['previousCharacterSet'], $user_settings['member_name'])) . un_htmlspecialchars(iconv('UTF-8', $modSettings['previousCharacterSet'], $_POST['passwrd'])));
// Say it aint so, iconv failed!
if (empty($other_passwords['iconv']) && function_exists('mb_convert_encoding'))
$other_passwords[] = sha1(strtolower(mb_convert_encoding($user_settings['member_name'], 'UTF-8', $modSettings['previousCharacterSet'])) . un_htmlspecialchars(mb_convert_encoding($_POST['passwrd'], 'UTF-8', $modSettings['previousCharacterSet'])));
}
}
// SMF's sha1 function can give a funny result on Linux (Not our fault!). If we've now got the real one let the old one be valid!
if (stripos(PHP_OS, 'win') !== 0 && strlen($user_settings['passwd']) < hash_length())
{
require_once($sourcedir . '/Subs-Compat.php');
$other_passwords[] = sha1_smf(strtolower($user_settings['member_name']) . un_htmlspecialchars($_POST['passwrd']));
}
// Allows mods to easily extend the $other_passwords array
call_integration_hook('integrate_other_passwords', array(&$other_passwords));
// Whichever encryption it was using, let's make it use SMF's now ;).
if (in_array($user_settings['passwd'], $other_passwords))
{
$user_settings['passwd'] = hash_password($user_settings['member_name'], un_htmlspecialchars($_POST['passwrd']));
$user_settings['password_salt'] = bin2hex($smcFunc['random_bytes'](16));
// Update the password and set up the hash.
updateMemberData($user_settings['id_member'], array('passwd' => $user_settings['passwd'], 'password_salt' => $user_settings['password_salt'], 'passwd_flood' => ''));
}
// Okay, they for sure didn't enter the password!
else
{
// They've messed up again - keep a count to see if they need a hand.
$_SESSION['failed_login'] = isset($_SESSION['failed_login']) ? ($_SESSION['failed_login'] + 1) : 1;
// Hmm... don't remember it, do you? Here, try the password reminder ;).
if ($_SESSION['failed_login'] >= $modSettings['failed_login_threshold'])
redirectexit('action=reminder');
// We'll give you another chance...
else
{
// Log an error so we know that it didn't go well in the error log.
log_error($txt['incorrect_password'] . ' - <span class="remove">' . $user_settings['member_name'] . '</span>', 'user');
$context['login_errors'] = array($txt['incorrect_password']);
return;
}
}
}
elseif (!empty($user_settings['passwd_flood']))
{
// Let's be sure they weren't a little hacker.
validatePasswordFlood($user_settings['id_member'], $user_settings['member_name'], $user_settings['passwd_flood'], true);
// If we got here then we can reset the flood counter.
updateMemberData($user_settings['id_member'], array('passwd_flood' => ''));
}
// Correct password, but they've got no salt; fix it!
if (strlen($user_settings['password_salt']) < 32)
{
$user_settings['password_salt'] = bin2hex($smcFunc['random_bytes'](16));
updateMemberData($user_settings['id_member'], array('password_salt' => $user_settings['password_salt']));
}
// Check their activation status.
if (!checkActivation())
return;
DoLogin();
}
/**
* Allows the user to enter their Two-Factor Authentication code
*/
function LoginTFA()
{
global $sourcedir, $txt, $context, $user_info, $modSettings, $scripturl;
if (!$user_info['is_guest'] || empty($context['tfa_member']) || empty($modSettings['tfa_mode']))
fatal_lang_error('no_access', false);
loadLanguage('Profile');
require_once($sourcedir . '/Class-TOTP.php');
$member = $context['tfa_member'];
// Prevent replay attacks by limiting at least 2 minutes before they can log in again via 2FA
if (time() - $member['last_login'] < 120)
fatal_lang_error('tfa_wait', false);
$totp = new \TOTP\Auth($member['tfa_secret']);
$totp->setRange(1);
/* This is true when:
* We have a valid header indicating a JQXHR request. This is not sent during a cross domain request.
* OR we have found:
* 1. valid cors host
* 2. A header indicating a SMF request
* 3. The url has a ajax in either the GET or POST
* These are not intended for security, but ensuring the request is intended for a JQXHR response.
*/
if (
(
!empty($_SERVER['HTTP_X_REQUESTED_WITH'])
&& $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
)
||
(
!empty($context['valid_cors_found'])
&& !empty($_SERVER['HTTP_X_SMF_AJAX'])
&& isset($_REQUEST['ajax'])
)
)
{
$context['from_ajax'] = true;
$context['template_layers'] = array();
}
if (!empty($_POST['tfa_code']) && empty($_POST['tfa_backup']))
{
// Check to ensure we're forcing SSL for authentication
if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
fatal_lang_error('login_ssl_required', false);
$code = $_POST['tfa_code'];
if (strlen($code) == $totp->getCodeLength() && $totp->validateCode($code))
{
updateMemberData($member['id_member'], array('last_login' => time()));
setTFACookie(3153600, $member['id_member'], hash_salt($member['tfa_backup'], $member['password_salt']));
redirectexit();
}
else
{
validatePasswordFlood($member['id_member'], $member['member_name'], $member['passwd_flood'], false, true);
$context['tfa_error'] = true;
$context['tfa_value'] = $_POST['tfa_code'];
}
}
elseif (!empty($_POST['tfa_backup']))
{
// Check to ensure we're forcing SSL for authentication
if (!empty($modSettings['force_ssl']) && empty($maintenance) && !httpsOn())
fatal_lang_error('login_ssl_required', false);
$backup = $_POST['tfa_backup'];
if (hash_verify_password($member['member_name'], $backup, $member['tfa_backup']))
{
// Get rid of their current TFA settings
updateMemberData($member['id_member'], array(
'tfa_secret' => '',
'tfa_backup' => '',
'last_login' => time(),
));
setTFACookie(3153600, $member['id_member'], hash_salt($member['tfa_backup'], $member['password_salt']));
redirectexit('action=profile;area=tfasetup;backup');
}
else
{
validatePasswordFlood($member['id_member'], $member['member_name'], $member['passwd_flood'], false, true);
$context['tfa_backup_error'] = true;
$context['tfa_value'] = $_POST['tfa_code'];
$context['tfa_backup_value'] = $_POST['tfa_backup'];
}
}
loadTemplate('Login');
$context['sub_template'] = 'login_tfa';
$context['page_title'] = $txt['login'];
$context['tfa_url'] = $scripturl . '?action=logintfa';
}
/**
* Check activation status of the current user.
*/
function checkActivation()
{
global $context, $txt, $scripturl, $user_settings, $modSettings;
if (!isset($context['login_errors']))
$context['login_errors'] = array();
// What is the true activation status of this account?
$activation_status = $user_settings['is_activated'] > 10 ? $user_settings['is_activated'] - 10 : $user_settings['is_activated'];
// Check if the account is activated - COPPA first...
if ($activation_status == 5)
{
$context['login_errors'][] = $txt['coppa_no_consent'] . ' <a href="' . $scripturl . '?action=coppa;member=' . $user_settings['id_member'] . '">' . $txt['coppa_need_more_details'] . '</a>';
return false;
}
// Awaiting approval still?
elseif ($activation_status == 3)
fatal_lang_error('still_awaiting_approval', 'user');
// Awaiting deletion, changed their mind?
elseif ($activation_status == 4)
{
if (isset($_REQUEST['undelete']))
{
updateMemberData($user_settings['id_member'], array('is_activated' => 1));
updateSettings(array('unapprovedMembers' => ($modSettings['unapprovedMembers'] > 0 ? $modSettings['unapprovedMembers'] - 1 : 0)));
}
else
{
$context['disable_login_hashing'] = true;
$context['login_errors'][] = $txt['awaiting_delete_account'];
$context['login_show_undelete'] = true;
return false;
}
}
// Standard activation?
elseif ($activation_status != 1)
{
log_error($txt['activate_not_completed1'] . ' - <span class="remove">' . $user_settings['member_name'] . '</span>', 'user');
$context['login_errors'][] = $txt['activate_not_completed1'] . ' <a href="' . $scripturl . '?action=activate;sa=resend;u=' . $user_settings['id_member'] . '">' . $txt['activate_not_completed2'] . '</a>';
return false;
}
return true;
}
/**
* Perform the logging in. (set cookie, call hooks, etc)
*/
function DoLogin()
{
global $user_info, $user_settings, $smcFunc;
global $maintenance, $modSettings, $context, $sourcedir;
// Load cookie authentication stuff.
require_once($sourcedir . '/Subs-Auth.php');
// Call login integration functions.
call_integration_hook('integrate_login', array($user_settings['member_name'], null, $modSettings['cookieTime']));
// Get ready to set the cookie...
$user_info['id'] = $user_settings['id_member'];
// Bam! Cookie set. A session too, just in case.
setLoginCookie(60 * $modSettings['cookieTime'], $user_settings['id_member'], hash_salt($user_settings['passwd'], $user_settings['password_salt']));
// Reset the login threshold.
if (isset($_SESSION['failed_login']))
unset($_SESSION['failed_login']);
$user_info['is_guest'] = false;
$user_settings['additional_groups'] = explode(',', $user_settings['additional_groups']);
$user_info['is_admin'] = $user_settings['id_group'] == 1 || in_array(1, $user_settings['additional_groups']);
// Are you banned?
is_not_banned(true);
// Don't stick the language or theme after this point.
unset($_SESSION['language'], $_SESSION['id_theme']);
// First login?
$request = $smcFunc['db_query']('', '
SELECT last_login
FROM {db_prefix}members
WHERE id_member = {int:id_member}
AND last_login = 0',
array(
'id_member' => $user_info['id'],
)
);
if ($smcFunc['db_num_rows']($request) == 1)
$_SESSION['first_login'] = true;
else
unset($_SESSION['first_login']);
$smcFunc['db_free_result']($request);
// You've logged in, haven't you?
$update = array('member_ip' => $user_info['ip'], 'member_ip2' => $_SERVER['BAN_CHECK_IP']);
if (empty($user_settings['tfa_secret']))
$update['last_login'] = time();
updateMemberData($user_info['id'], $update);
// Get rid of the online entry for that old guest....
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_online
WHERE session = {string:session}',
array(
'session' => 'ip' . $user_info['ip'],
)
);
$_SESSION['log_time'] = 0;
// Log this entry, only if we have it enabled.
if (!empty($modSettings['loginHistoryDays']))
$smcFunc['db_insert']('insert',
'{db_prefix}member_logins',
array(
'id_member' => 'int', 'time' => 'int', 'ip' => 'inet', 'ip2' => 'inet',
),
array(
$user_info['id'], time(), $user_info['ip'], $user_info['ip2']
),
array(
'id_member', 'time'
)
);
// Just log you back out if it's in maintenance mode and you AREN'T an admin.
if (empty($maintenance) || allowedTo('admin_forum'))
redirectexit('action=login2;sa=check;member=' . $user_info['id'], $context['server']['needs_login_fix']);
else
redirectexit('action=logout;' . $context['session_var'] . '=' . $context['session_id'], $context['server']['needs_login_fix']);
}
/**
* Logs the current user out of their account.
* It requires that the session hash is sent as well, to prevent automatic logouts by images or javascript.
* It redirects back to $_SESSION['logout_url'], if it exists.
* It is accessed via ?action=logout;session_var=...
*
* @param bool $internal If true, it doesn't check the session
* @param bool $redirect Whether or not to redirect the user after they log out
*/
function Logout($internal = false, $redirect = true)
{
global $sourcedir, $user_info, $user_settings, $context, $smcFunc, $cookiename, $modSettings;
// They decided to cancel a logout?
if (!$internal && isset($_POST['cancel']) && isset($_GET[$context['session_var']]))
redirectexit(!empty($_SESSION['logout_return']) ? $_SESSION['logout_return'] : '');
// Prompt to logout?
elseif (!$internal && !isset($_GET[$context['session_var']]))
{
loadLanguage('Login');
loadTemplate('Login');
$context['sub_template'] = 'logout';
// This came from a valid hashed return url. Or something that knows our secrets...
if (!empty($_REQUEST['return_hash']) && !empty($_REQUEST['return_to']) && hash_hmac('sha1', un_htmlspecialchars($_REQUEST['return_to']), get_auth_secret()) == $_REQUEST['return_hash'])
{
$_SESSION['logout_url'] = un_htmlspecialchars($_REQUEST['return_to']);
$_SESSION['logout_return'] = $_SESSION['logout_url'];
}
// Setup the return address.
elseif (isset($_SESSION['old_url']))
$_SESSION['logout_return'] = $_SESSION['old_url'];
// Don't go any further.
return;
}
// Make sure they aren't being auto-logged out.
elseif (!$internal && isset($_GET[$context['session_var']]))
checkSession('get');
require_once($sourcedir . '/Subs-Auth.php');
if (isset($_SESSION['pack_ftp']))
$_SESSION['pack_ftp'] = null;
// It won't be first login anymore.
unset($_SESSION['first_login']);
// Just ensure they aren't a guest!
if (!$user_info['is_guest'])
{
// Pass the logout information to integrations.
call_integration_hook('integrate_logout', array($user_settings['member_name']));
// If you log out, you aren't online anymore :P.
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_online
WHERE id_member = {int:current_member}',
array(
'current_member' => $user_info['id'],
)
);
}
$_SESSION['log_time'] = 0;
// Empty the cookie! (set it in the past, and for id_member = 0)
setLoginCookie(-3600, 0);
// And some other housekeeping while we're at it.
$salt = bin2hex($smcFunc['random_bytes'](16));
if (!empty($user_info['id']))
updateMemberData($user_info['id'], array('password_salt' => $salt));
if (!empty($modSettings['tfa_mode']) && !empty($user_info['id']) && !empty($_COOKIE[$cookiename . '_tfa']))
{
list (,, $exp) = $smcFunc['json_decode']($_COOKIE[$cookiename . '_tfa'], true);
setTFACookie((int) $exp - time(), $salt, hash_salt($user_settings['tfa_backup'], $salt));
}
session_destroy();
// Off to the merry board index we go!
if ($redirect)
{
if (empty($_SESSION['logout_url']))
redirectexit('', $context['server']['needs_login_fix']);
elseif (!empty($_SESSION['logout_url']) && (strpos($_SESSION['logout_url'], 'http://') === false && strpos($_SESSION['logout_url'], 'https://') === false))
{
unset ($_SESSION['logout_url']);
redirectexit();
}
else
{
$temp = $_SESSION['logout_url'];
unset($_SESSION['logout_url']);
redirectexit($temp, $context['server']['needs_login_fix']);
}
}
}
/**
* MD5 Encryption used for older passwords. (SMF 1.0.x/YaBB SE 1.5.x hashing)
*
* @param string $data The data
* @param string $key The key
* @return string The HMAC MD5 of data with key
*/
function md5_hmac($data, $key)
{
$key = str_pad(strlen($key) <= 64 ? $key : pack('H*', md5($key)), 64, chr(0x00));
return md5(($key ^ str_repeat(chr(0x5c), 64)) . pack('H*', md5(($key ^ str_repeat(chr(0x36), 64)) . $data)));
}
/**
* Custom encryption for phpBB3 based passwords.
*
* @param string $passwd The raw (unhashed) password
* @param string $passwd_hash The hashed password
* @return string The hashed version of $passwd
*/
function phpBB3_password_check($passwd, $passwd_hash)
{
// Too long or too short?
if (strlen($passwd_hash) != 34)
return;
// Range of characters allowed.
$range = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
// Tests
$strpos = strpos($range, $passwd_hash[3]);
$count = 1 << $strpos;
$salt = substr($passwd_hash, 4, 8);
$hash = md5($salt . $passwd, true);
for (; $count != 0; --$count)
$hash = md5($hash . $passwd, true);
$output = substr($passwd_hash, 0, 12);
$i = 0;
while ($i < 16)
{
$value = ord($hash[$i++]);
$output .= $range[$value & 0x3f];
if ($i < 16)
$value |= ord($hash[$i]) << 8;
$output .= $range[($value >> 6) & 0x3f];
if ($i++ >= 16)
break;
if ($i < 16)
$value |= ord($hash[$i]) << 16;
$output .= $range[($value >> 12) & 0x3f];
if ($i++ >= 16)
break;
$output .= $range[($value >> 18) & 0x3f];
}
// Return now.
return $output;
}
/**
* This protects against brute force attacks on a member's password.
* Importantly, even if the password was right we DON'T TELL THEM!
*
* @param int $id_member The ID of the member
* @param string $member_name The name of the member.
* @param bool|string $password_flood_value False if we don't have a flood value, otherwise a string with a timestamp and number of tries separated by a |
* @param bool $was_correct Whether or not the password was correct
* @param bool $tfa Whether we're validating for two-factor authentication
*/
function validatePasswordFlood($id_member, $member_name, $password_flood_value = false, $was_correct = false, $tfa = false)
{
global $cookiename, $sourcedir;
// As this is only brute protection, we allow 5 attempts every 10 seconds.
// Destroy any session or cookie data about this member, as they validated wrong.
// Only if they're not validating for 2FA
if (!$tfa)
{
require_once($sourcedir . '/Subs-Auth.php');
setLoginCookie(-3600, 0);
if (isset($_SESSION['login_' . $cookiename]))
unset($_SESSION['login_' . $cookiename]);
}
// We need a member!
if (!$id_member)
{
// Redirect back!
redirectexit();
// Probably not needed, but still make sure...
fatal_lang_error('no_access', false);
}
// Right, have we got a flood value?
if ($password_flood_value !== false)
@list ($time_stamp, $number_tries) = explode('|', $password_flood_value);
// Timestamp or number of tries invalid?
if (empty($number_tries) || empty($time_stamp))
{
$number_tries = 0;
$time_stamp = time();
}
// They've failed logging in already
if (!empty($number_tries))
{
// Give them less chances if they failed before
$number_tries = $time_stamp < time() - 20 ? 2 : $number_tries;
// They are trying too fast, make them wait longer
if ($time_stamp < time() - 10)
$time_stamp = time();
}
$number_tries++;
// Broken the law?
if ($number_tries > 5)
fatal_lang_error('login_threshold_brute_fail', 'login', [$member_name]);
// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
updateMemberData($id_member, array('passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries));
}
?>

573
Sources/Logging.php Normal file
View file

@ -0,0 +1,573 @@
<?php
/**
* This file concerns itself with logging, whether in the database or files.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.4
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Put this user in the online log.
*
* @param bool $force Whether to force logging the data
*/
function writeLog($force = false)
{
global $user_info, $user_settings, $context, $modSettings, $settings, $topic, $board, $smcFunc, $sourcedir, $cache_enable;
// If we are showing who is viewing a topic, let's see if we are, and force an update if so - to make it accurate.
if (!empty($settings['display_who_viewing']) && ($topic || $board))
{
// Take the opposite approach!
$force = true;
// Don't update for every page - this isn't wholly accurate but who cares.
if ($topic)
{
if (isset($_SESSION['last_topic_id']) && $_SESSION['last_topic_id'] == $topic)
$force = false;
$_SESSION['last_topic_id'] = $topic;
}
}
// Are they a spider we should be tracking? Mode = 1 gets tracked on its spider check...
if (!empty($user_info['possibly_robot']) && !empty($modSettings['spider_mode']) && $modSettings['spider_mode'] > 1)
{
require_once($sourcedir . '/ManageSearchEngines.php');
logSpider();
}
// Don't mark them as online more than every so often.
if (!empty($_SESSION['log_time']) && $_SESSION['log_time'] >= (time() - 8) && !$force)
return;
if (!empty($modSettings['who_enabled']))
{
$encoded_get = truncate_array($_GET) + array('USER_AGENT' => mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 128));
// In the case of a dlattach action, session_var may not be set.
if (!isset($context['session_var']))
$context['session_var'] = $_SESSION['session_var'];
unset($encoded_get['sesc'], $encoded_get[$context['session_var']]);
$encoded_get = $smcFunc['json_encode']($encoded_get);
// Sometimes folks mess with USER_AGENT & $_GET data, so one last check to avoid 'data too long' errors
if (mb_strlen($encoded_get) > 2048)
$encoded_get = '';
}
else
$encoded_get = '';
// Guests use 0, members use their session ID.
$session_id = $user_info['is_guest'] ? 'ip' . $user_info['ip'] : session_id();
// Grab the last all-of-SMF-specific log_online deletion time.
$do_delete = cache_get_data('log_online-update', 30) < time() - 30;
// If the last click wasn't a long time ago, and there was a last click...
if (!empty($_SESSION['log_time']) && $_SESSION['log_time'] >= time() - $modSettings['lastActive'] * 20)
{
if ($do_delete)
{
$smcFunc['db_query']('delete_log_online_interval', '
DELETE FROM {db_prefix}log_online
WHERE log_time < {int:log_time}
AND session != {string:session}',
array(
'log_time' => time() - $modSettings['lastActive'] * 60,
'session' => $session_id,
)
);
// Cache when we did it last.
cache_put_data('log_online-update', time(), 30);
}
$smcFunc['db_query']('', '
UPDATE {db_prefix}log_online
SET log_time = {int:log_time}, ip = {inet:ip}, url = {string:url}
WHERE session = {string:session}',
array(
'log_time' => time(),
'ip' => $user_info['ip'],
'url' => $encoded_get,
'session' => $session_id,
)
);
// Guess it got deleted.
if ($smcFunc['db_affected_rows']() == 0)
$_SESSION['log_time'] = 0;
}
else
$_SESSION['log_time'] = 0;
// Otherwise, we have to delete and insert.
if (empty($_SESSION['log_time']))
{
if ($do_delete || !empty($user_info['id']))
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_online
WHERE ' . ($do_delete ? 'log_time < {int:log_time}' : '') . ($do_delete && !empty($user_info['id']) ? ' OR ' : '') . (empty($user_info['id']) ? '' : 'id_member = {int:current_member}'),
array(
'current_member' => $user_info['id'],
'log_time' => time() - $modSettings['lastActive'] * 60,
)
);
$smcFunc['db_insert']($do_delete ? 'ignore' : 'replace',
'{db_prefix}log_online',
array('session' => 'string', 'id_member' => 'int', 'id_spider' => 'int', 'log_time' => 'int', 'ip' => 'inet', 'url' => 'string'),
array($session_id, $user_info['id'], empty($_SESSION['id_robot']) ? 0 : $_SESSION['id_robot'], time(), $user_info['ip'], $encoded_get),
array('session')
);
}
// Mark your session as being logged.
$_SESSION['log_time'] = time();
// Well, they are online now.
if (empty($_SESSION['timeOnlineUpdated']))
$_SESSION['timeOnlineUpdated'] = time();
// Set their login time, if not already done within the last minute.
if (SMF != 'SSI' && !empty($user_info['last_login']) && $user_info['last_login'] < time() - 60 && (!isset($_REQUEST['action']) || !in_array($_REQUEST['action'], array('.xml', 'login2', 'logintfa'))))
{
// Don't count longer than 15 minutes.
if (time() - $_SESSION['timeOnlineUpdated'] > 60 * 15)
$_SESSION['timeOnlineUpdated'] = time();
$user_settings['total_time_logged_in'] += time() - $_SESSION['timeOnlineUpdated'];
updateMemberData($user_info['id'], array('last_login' => time(), 'member_ip' => $user_info['ip'], 'member_ip2' => $_SERVER['BAN_CHECK_IP'], 'total_time_logged_in' => $user_settings['total_time_logged_in']));
if (!empty($cache_enable) && $cache_enable >= 2)
cache_put_data('user_settings-' . $user_info['id'], $user_settings, 60);
$user_info['total_time_logged_in'] += time() - $_SESSION['timeOnlineUpdated'];
$_SESSION['timeOnlineUpdated'] = time();
}
}
/**
* Logs the last database error into a file.
* Attempts to use the backup file first, to store the last database error
* and only update db_last_error.php if the first was successful.
*/
function logLastDatabaseError()
{
global $boarddir, $cachedir;
// Make a note of the last modified time in case someone does this before us
$last_db_error_change = @filemtime($cachedir . '/db_last_error.php');
// save the old file before we do anything
$file = $cachedir . '/db_last_error.php';
$dberror_backup_fail = !@is_writable($cachedir . '/db_last_error_bak.php') || !@copy($file, $cachedir . '/db_last_error_bak.php');
$dberror_backup_fail = !$dberror_backup_fail ? (!file_exists($cachedir . '/db_last_error_bak.php') || filesize($cachedir . '/db_last_error_bak.php') === 0) : $dberror_backup_fail;
clearstatcache();
if (filemtime($cachedir . '/db_last_error.php') === $last_db_error_change)
{
// Write the change
$write_db_change = '<' . '?' . "php\n" . '$db_last_error = ' . time() . ';' . "\n" . '?' . '>';
$written_bytes = file_put_contents($cachedir . '/db_last_error.php', $write_db_change, LOCK_EX);
// survey says ...
if ($written_bytes !== strlen($write_db_change) && !$dberror_backup_fail)
{
// Oops. maybe we have no more disk space left, or some other troubles, troubles...
// Copy the file back and run for your life!
@copy($cachedir . '/db_last_error_bak.php', $cachedir . '/db_last_error.php');
}
else
{
@touch($boarddir . '/' . 'Settings.php');
return true;
}
}
return false;
}
/**
* This function shows the debug information tracked when $db_show_debug = true
* in Settings.php
*/
function displayDebug()
{
global $context, $scripturl, $boarddir, $sourcedir, $cachedir, $settings, $modSettings;
global $db_cache, $db_count, $cache_misses, $cache_count_misses, $db_show_debug, $cache_count, $cache_hits, $smcFunc, $txt, $cache_enable;
// Add to Settings.php if you want to show the debugging information.
if (!isset($db_show_debug) || $db_show_debug !== true || (isset($_GET['action']) && $_GET['action'] == 'viewquery'))
return;
if (empty($_SESSION['view_queries']))
$_SESSION['view_queries'] = 0;
if (empty($context['debug']['language_files']))
$context['debug']['language_files'] = array();
if (empty($context['debug']['sheets']))
$context['debug']['sheets'] = array();
$files = get_included_files();
$total_size = 0;
for ($i = 0, $n = count($files); $i < $n; $i++)
{
if (file_exists($files[$i]))
$total_size += filesize($files[$i]);
$files[$i] = strtr($files[$i], array($boarddir => '.', $sourcedir => '(Sources)', $cachedir => '(Cache)', $settings['actual_theme_dir'] => '(Current Theme)'));
}
$warnings = 0;
if (!empty($db_cache))
{
foreach ($db_cache as $q => $query_data)
{
if (!empty($query_data['w']))
$warnings += count($query_data['w']);
}
$_SESSION['debug'] = &$db_cache;
}
// Gotta have valid HTML ;).
$temp = ob_get_contents();
ob_clean();
echo preg_replace('~</body>\s*</html>~', '', $temp), '
<div class="smalltext" style="text-align: left; margin: 1ex;">
', $txt['debug_browser'], $context['browser_body_id'], ' <em>(', implode('</em>, <em>', array_reverse(array_keys($context['browser'], true))), ')</em><br>
', $txt['debug_templates'], count($context['debug']['templates']), ': <em>', implode('</em>, <em>', $context['debug']['templates']), '</em>.<br>
', $txt['debug_subtemplates'], count($context['debug']['sub_templates']), ': <em>', implode('</em>, <em>', $context['debug']['sub_templates']), '</em>.<br>
', $txt['debug_language_files'], count($context['debug']['language_files']), ': <em>', implode('</em>, <em>', $context['debug']['language_files']), '</em>.<br>
', $txt['debug_stylesheets'], count($context['debug']['sheets']), ': <em>', implode('</em>, <em>', $context['debug']['sheets']), '</em>.<br>
', $txt['debug_hooks'], empty($context['debug']['hooks']) ? 0 : count($context['debug']['hooks']) . ' (<a href="javascript:void(0);" onclick="document.getElementById(\'debug_hooks\').style.display = \'inline\'; this.style.display = \'none\'; return false;">', $txt['debug_show'], '</a><span id="debug_hooks" style="display: none;"><em>' . implode('</em>, <em>', $context['debug']['hooks']), '</em></span>)', '<br>
', (isset($context['debug']['instances']) ? ($txt['debug_instances'] . (empty($context['debug']['instances']) ? 0 : count($context['debug']['instances'])) . ' (<a href="javascript:void(0);" onclick="document.getElementById(\'debug_instances\').style.display = \'inline\'; this.style.display = \'none\'; return false;">' . $txt['debug_show'] . '</a><span id="debug_instances" style="display: none;"><em>' . implode('</em>, <em>', array_keys($context['debug']['instances'])) . '</em></span>)' . '<br>') : ''), '
', $txt['debug_files_included'], count($files), ' - ', round($total_size / 1024), $txt['debug_kb'], ' (<a href="javascript:void(0);" onclick="document.getElementById(\'debug_include_info\').style.display = \'inline\'; this.style.display = \'none\'; return false;">', $txt['debug_show'], '</a><span id="debug_include_info" style="display: none;"><em>', implode('</em>, <em>', $files), '</em></span>)<br>';
if (function_exists('memory_get_peak_usage'))
echo $txt['debug_memory_use'], ceil(memory_get_peak_usage() / 1024), $txt['debug_kb'], '<br>';
// What tokens are active?
if (isset($_SESSION['token']))
echo $txt['debug_tokens'] . '<em>' . implode(',</em> <em>', array_keys($_SESSION['token'])), '</em>.<br>';
if (!empty($cache_enable) && !empty($cache_hits))
{
$missed_entries = array();
$entries = array();
$total_t = 0;
$total_s = 0;
foreach ($cache_hits as $cache_hit)
{
$entries[] = $cache_hit['d'] . ' ' . $cache_hit['k'] . ': ' . sprintf($txt['debug_cache_seconds_bytes'], comma_format($cache_hit['t'], 5), $cache_hit['s']);
$total_t += $cache_hit['t'];
$total_s += $cache_hit['s'];
}
if (!isset($cache_misses))
$cache_misses = array();
foreach ($cache_misses as $missed)
$missed_entries[] = $missed['d'] . ' ' . $missed['k'];
echo '
', $txt['debug_cache_hits'], $cache_count, ': ', sprintf($txt['debug_cache_seconds_bytes_total'], comma_format($total_t, 5), comma_format($total_s)), ' (<a href="javascript:void(0);" onclick="document.getElementById(\'debug_cache_info\').style.display = \'inline\'; this.style.display = \'none\'; return false;">', $txt['debug_show'], '</a><span id="debug_cache_info" style="display: none;"><em>', implode('</em>, <em>', $entries), '</em></span>)<br>
', $txt['debug_cache_misses'], $cache_count_misses, ': (<a href="javascript:void(0);" onclick="document.getElementById(\'debug_cache_misses_info\').style.display = \'inline\'; this.style.display = \'none\'; return false;">', $txt['debug_show'], '</a><span id="debug_cache_misses_info" style="display: none;"><em>', implode('</em>, <em>', $missed_entries), '</em></span>)<br>';
}
echo '
<a href="', $scripturl, '?action=viewquery" target="_blank" rel="noopener">', $warnings == 0 ? sprintf($txt['debug_queries_used'], (int) $db_count) : sprintf($txt['debug_queries_used_and_warnings'], (int) $db_count, $warnings), '</a><br>
<br>';
if ($_SESSION['view_queries'] == 1 && !empty($db_cache))
foreach ($db_cache as $q => $query_data)
{
$is_select = strpos(trim($query_data['q']), 'SELECT') === 0 || preg_match('~^INSERT(?: IGNORE)? INTO \w+(?:\s+\([^)]+\))?\s+SELECT .+$~s', trim($query_data['q'])) != 0 || strpos(trim($query_data['q']), 'WITH') === 0;
// Temporary tables created in earlier queries are not explainable.
if ($is_select)
{
foreach (array('log_topics_unread', 'topics_posted_in', 'tmp_log_search_topics', 'tmp_log_search_messages') as $tmp)
if (strpos(trim($query_data['q']), $tmp) !== false)
{
$is_select = false;
break;
}
}
// But actual creation of the temporary tables are.
elseif (preg_match('~^CREATE TEMPORARY TABLE .+?SELECT .+$~s', trim($query_data['q'])) != 0)
$is_select = true;
// Make the filenames look a bit better.
if (isset($query_data['f']))
$query_data['f'] = preg_replace('~^' . preg_quote($boarddir, '~') . '~', '...', $query_data['f']);
echo '
<strong>', $is_select ? '<a href="' . $scripturl . '?action=viewquery;qq=' . ($q + 1) . '#qq' . $q . '" target="_blank" rel="noopener" style="text-decoration: none;">' : '', nl2br(str_replace("\t", '&nbsp;&nbsp;&nbsp;', $smcFunc['htmlspecialchars'](ltrim($query_data['q'], "\n\r")))) . ($is_select ? '</a></strong>' : '</strong>') . '<br>
&nbsp;&nbsp;&nbsp;';
if (!empty($query_data['f']) && !empty($query_data['l']))
echo sprintf($txt['debug_query_in_line'], $query_data['f'], $query_data['l']);
if (isset($query_data['s'], $query_data['t']) && isset($txt['debug_query_which_took_at']))
echo sprintf($txt['debug_query_which_took_at'], round($query_data['t'], 8), round($query_data['s'], 8)) . '<br>';
elseif (isset($query_data['t']))
echo sprintf($txt['debug_query_which_took'], round($query_data['t'], 8)) . '<br>';
echo '
<br>';
}
echo '
<a href="' . $scripturl . '?action=viewquery;sa=hide">', $txt['debug_' . (empty($_SESSION['view_queries']) ? 'show' : 'hide') . '_queries'], '</a>
</div></body></html>';
}
/**
* Track Statistics.
* Caches statistics changes, and flushes them if you pass nothing.
* If '+' is used as a value, it will be incremented.
* It does not actually commit the changes until the end of the page view.
* It depends on the trackStats setting.
*
* @param array $stats An array of data
* @return bool Whether or not the info was updated successfully
*/
function trackStats($stats = array())
{
global $modSettings, $smcFunc;
static $cache_stats = array();
if (empty($modSettings['trackStats']))
return false;
if (!empty($stats))
return $cache_stats = array_merge($cache_stats, $stats);
elseif (empty($cache_stats))
return false;
$setStringUpdate = '';
$insert_keys = array();
$date = smf_strftime('%Y-%m-%d', time());
$update_parameters = array(
'current_date' => $date,
);
foreach ($cache_stats as $field => $change)
{
$setStringUpdate .= '
' . $field . ' = ' . ($change === '+' ? $field . ' + 1' : '{int:' . $field . '}') . ',';
if ($change === '+')
$cache_stats[$field] = 1;
else
$update_parameters[$field] = $change;
$insert_keys[$field] = 'int';
}
$smcFunc['db_query']('', '
UPDATE {db_prefix}log_activity
SET' . substr($setStringUpdate, 0, -1) . '
WHERE date = {date:current_date}',
$update_parameters
);
if ($smcFunc['db_affected_rows']() == 0)
{
$smcFunc['db_insert']('ignore',
'{db_prefix}log_activity',
array_merge($insert_keys, array('date' => 'date')),
array_merge($cache_stats, array($date)),
array('date')
);
}
// Don't do this again.
$cache_stats = array();
return true;
}
/**
* This function logs an action to the database. It is a
* thin wrapper around {@link logActions()}.
*
* @example logAction('remove', array('starter' => $id_member_started));
*
* @param string $action A code for the report; a list of such strings
* can be found in Modlog.{language}.php (modlog_ac_ strings)
* @param array $extra An associated array of parameters for the
* item being logged. Typically this will include 'topic' for the topic's id.
* @param string $log_type A string reflecting the type of log.
*
* @return int The ID of the row containing the logged data
*/
function logAction($action, array $extra = array(), $log_type = 'moderate')
{
return logActions(array(array(
'action' => $action,
'log_type' => $log_type,
'extra' => $extra,
)));
}
/**
* Log changes to the forum, such as moderation events or administrative
* changes. This behaves just like {@link logAction()} in SMF 2.0, except
* that it is designed to log multiple actions at once.
*
* SMF uses three log types:
*
* - `user` for actions executed that aren't related to
* moderation (e.g. signature or other changes from the profile);
* - `moderate` for moderation actions (e.g. topic changes);
* - `admin` for administrative actions.
*
* @param array $logs An array of log data
*
* @return int The last logged ID
*/
function logActions(array $logs)
{
global $modSettings, $user_info, $smcFunc, $sourcedir, $txt;
$inserts = array();
$log_types = array(
'moderate' => 1,
'user' => 2,
'admin' => 3,
);
$always_log = array('agreement_accepted', 'policy_accepted', 'agreement_updated', 'policy_updated');
call_integration_hook('integrate_log_types', array(&$log_types, &$always_log));
foreach ($logs as $log)
{
if (!isset($log_types[$log['log_type']]) && (empty($modSettings[$log['log_type'] . 'log_enabled']) || !in_array($log['action'], $always_log)))
continue;
if (!is_array($log['extra']))
{
loadLanguage('Errors');
trigger_error(sprintf($txt['logActions_not_array'], $log['action']), E_USER_NOTICE);
}
// Pull out the parts we want to store separately, but also make sure that the data is proper
if (isset($log['extra']['topic']))
{
if (!is_numeric($log['extra']['topic']))
{
loadLanguage('Errors');
trigger_error($txt['logActions_topic_not_numeric'], E_USER_NOTICE);
}
$topic_id = empty($log['extra']['topic']) ? 0 : (int) $log['extra']['topic'];
unset($log['extra']['topic']);
}
else
$topic_id = 0;
if (isset($log['extra']['message']))
{
if (!is_numeric($log['extra']['message']))
{
loadLanguage('Errors');
trigger_error($txt['logActions_message_not_numeric'], E_USER_NOTICE);
}
$msg_id = empty($log['extra']['message']) ? 0 : (int) $log['extra']['message'];
unset($log['extra']['message']);
}
else
$msg_id = 0;
// @todo cache this?
// Is there an associated report on this?
if (in_array($log['action'], array('move', 'remove', 'split', 'merge')))
{
$request = $smcFunc['db_query']('', '
SELECT id_report
FROM {db_prefix}log_reported
WHERE {raw:column_name} = {int:reported}
LIMIT 1',
array(
'column_name' => !empty($msg_id) ? 'id_msg' : 'id_topic',
'reported' => !empty($msg_id) ? $msg_id : $topic_id,
)
);
// Alright, if we get any result back, update open reports.
if ($smcFunc['db_num_rows']($request) > 0)
{
require_once($sourcedir . '/Subs-ReportedContent.php');
updateSettings(array('last_mod_report_action' => time()));
recountOpenReports('posts');
}
$smcFunc['db_free_result']($request);
}
if (isset($log['extra']['member']) && !is_numeric($log['extra']['member']))
{
loadLanguage('Errors');
trigger_error($txt['logActions_member_not_numeric'], E_USER_NOTICE);
}
if (isset($log['extra']['board']))
{
if (!is_numeric($log['extra']['board']))
{
loadLanguage('Errors');
trigger_error($txt['logActions_board_not_numeric'], E_USER_NOTICE);
}
$board_id = empty($log['extra']['board']) ? 0 : (int) $log['extra']['board'];
unset($log['extra']['board']);
}
else
$board_id = 0;
if (isset($log['extra']['board_to']))
{
if (!is_numeric($log['extra']['board_to']))
{
loadLanguage('Errors');
trigger_error($txt['logActions_board_to_not_numeric'], E_USER_NOTICE);
}
if (empty($board_id))
{
$board_id = empty($log['extra']['board_to']) ? 0 : (int) $log['extra']['board_to'];
unset($log['extra']['board_to']);
}
}
if (isset($log['extra']['member_affected']))
$memID = $log['extra']['member_affected'];
else
$memID = $user_info['id'];
if (isset($user_info['ip']))
$memIP = $user_info['ip'];
else
$memIP = 'null';
$inserts[] = array(
time(), $log_types[$log['log_type']], $memID, $memIP, $log['action'],
$board_id, $topic_id, $msg_id, $smcFunc['json_encode']($log['extra']),
);
}
$id_action = $smcFunc['db_insert']('',
'{db_prefix}log_actions',
array(
'log_time' => 'int', 'id_log' => 'int', 'id_member' => 'int', 'ip' => 'inet', 'action' => 'string',
'id_board' => 'int', 'id_topic' => 'int', 'id_msg' => 'int', 'extra' => 'string-65534',
),
$inserts,
array('id_action'),
1
);
return $id_action;
}
?>

File diff suppressed because it is too large Load diff

2448
Sources/ManageBans.php Normal file

File diff suppressed because it is too large Load diff

909
Sources/ManageBoards.php Normal file
View file

@ -0,0 +1,909 @@
<?php
/**
* Manage and maintain the boards and categories of the forum.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* The main dispatcher; doesn't do anything, just delegates.
* This is the main entry point for all the manageboards admin screens.
* Called by ?action=admin;area=manageboards.
* It checks the permissions, based on the sub-action, and calls a function based on the sub-action.
*
* Uses ManageBoards language file.
*/
function ManageBoards()
{
global $context, $txt;
// Everything's gonna need this.
loadLanguage('ManageBoards');
// Format: 'sub-action' => array('function', 'permission')
$subActions = array(
'board' => array('EditBoard', 'manage_boards'),
'board2' => array('EditBoard2', 'manage_boards'),
'cat' => array('EditCategory', 'manage_boards'),
'cat2' => array('EditCategory2', 'manage_boards'),
'main' => array('ManageBoardsMain', 'manage_boards'),
'move' => array('ManageBoardsMain', 'manage_boards'),
'newcat' => array('EditCategory', 'manage_boards'),
'newboard' => array('EditBoard', 'manage_boards'),
'settings' => array('EditBoardSettings', 'admin_forum'),
);
// Create the tabs for the template.
$context[$context['admin_menu_name']]['tab_data'] = array(
'title' => $txt['boards_and_cats'],
'help' => 'manage_boards',
'description' => $txt['boards_and_cats_desc'],
'tabs' => array(
'main' => array(
),
'newcat' => array(
),
'settings' => array(
'description' => $txt['mboards_settings_desc'],
),
),
);
call_integration_hook('integrate_manage_boards', array(&$subActions));
// Default to sub action 'main' or 'settings' depending on permissions.
$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : (allowedTo('manage_boards') ? 'main' : 'settings');
// Have you got the proper permissions?
isAllowedTo($subActions[$_REQUEST['sa']][1]);
call_helper($subActions[$_REQUEST['sa']][0]);
}
/**
* The main control panel thing, the screen showing all boards and categories.
* Called by ?action=admin;area=manageboards or ?action=admin;area=manageboards;sa=move.
* Requires manage_boards permission.
* It also handles the interface for moving boards.
*
* Uses ManageBoards template, main sub-template.
*/
function ManageBoardsMain()
{
global $txt, $context, $cat_tree, $boards, $boardList, $scripturl, $sourcedir, $smcFunc;
loadTemplate('ManageBoards');
require_once($sourcedir . '/Subs-Boards.php');
if (isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'move' && in_array($_REQUEST['move_to'], array('child', 'before', 'after', 'top')))
{
checkSession('get');
validateToken('admin-bm-' . (int) $_REQUEST['src_board'], 'request');
if ($_REQUEST['move_to'] === 'top')
$boardOptions = array(
'move_to' => $_REQUEST['move_to'],
'target_category' => (int) $_REQUEST['target_cat'],
'move_first_child' => true,
);
else
$boardOptions = array(
'move_to' => $_REQUEST['move_to'],
'target_board' => (int) $_REQUEST['target_board'],
'move_first_child' => true,
);
modifyBoard((int) $_REQUEST['src_board'], $boardOptions);
}
getBoardTree();
$context['move_board'] = !empty($_REQUEST['move']) && isset($boards[(int) $_REQUEST['move']]) ? (int) $_REQUEST['move'] : 0;
$context['categories'] = array();
foreach ($cat_tree as $catid => $tree)
{
$context['categories'][$catid] = array(
'name' => &$tree['node']['name'],
'id' => &$tree['node']['id'],
'boards' => array()
);
$move_cat = !empty($context['move_board']) && $boards[$context['move_board']]['category'] == $catid;
foreach ($boardList[$catid] as $boardid)
{
$context['categories'][$catid]['boards'][$boardid] = array(
'id' => &$boards[$boardid]['id'],
'name' => &$boards[$boardid]['name'],
'description' => &$boards[$boardid]['description'],
'child_level' => &$boards[$boardid]['level'],
'move' => $move_cat && ($boardid == $context['move_board'] || isChildOf($boardid, $context['move_board'])),
'permission_profile' => &$boards[$boardid]['profile'],
'is_redirect' => !empty($boards[$boardid]['redirect']),
);
}
}
if (!empty($context['move_board']))
{
createToken('admin-bm-' . $context['move_board'], 'request');
$context['move_title'] = sprintf($txt['mboards_select_destination'], $smcFunc['htmlspecialchars']($boards[$context['move_board']]['name']));
foreach ($cat_tree as $catid => $tree)
{
$prev_child_level = 0;
$prev_board = 0;
$stack = array();
// Just a shortcut, this is the same for all the urls
$security = $context['session_var'] . '=' . $context['session_id'] . ';' . $context['admin-bm-' . $context['move_board'] . '_token_var'] . '=' . $context['admin-bm-' . $context['move_board'] . '_token'];
foreach ($boardList[$catid] as $boardid)
{
if (!isset($context['categories'][$catid]['move_link']))
$context['categories'][$catid]['move_link'] = array(
'child_level' => 0,
'label' => $txt['mboards_order_before'] . ' \'' . $smcFunc['htmlspecialchars']($boards[$boardid]['name']) . '\'',
'href' => $scripturl . '?action=admin;area=manageboards;sa=move;src_board=' . $context['move_board'] . ';target_board=' . $boardid . ';move_to=before;' . $security,
);
if (!$context['categories'][$catid]['boards'][$boardid]['move'])
$context['categories'][$catid]['boards'][$boardid]['move_links'] = array(
array(
'child_level' => $boards[$boardid]['level'],
'label' => $txt['mboards_order_after'] . '\'' . $smcFunc['htmlspecialchars']($boards[$boardid]['name']) . '\'',
'href' => $scripturl . '?action=admin;area=manageboards;sa=move;src_board=' . $context['move_board'] . ';target_board=' . $boardid . ';move_to=after;' . $security,
'class' => $boards[$boardid]['level'] > 0 ? 'above' : 'below',
),
array(
'child_level' => $boards[$boardid]['level'] + 1,
'label' => $txt['mboards_order_child_of'] . ' \'' . $smcFunc['htmlspecialchars']($boards[$boardid]['name']) . '\'',
'href' => $scripturl . '?action=admin;area=manageboards;sa=move;src_board=' . $context['move_board'] . ';target_board=' . $boardid . ';move_to=child;' . $security,
'class' => 'here',
),
);
$difference = $boards[$boardid]['level'] - $prev_child_level;
if ($difference == 1)
array_push($stack, !empty($context['categories'][$catid]['boards'][$prev_board]['move_links']) ? array_shift($context['categories'][$catid]['boards'][$prev_board]['move_links']) : null);
elseif ($difference < 0)
{
if (empty($context['categories'][$catid]['boards'][$prev_board]['move_links']))
$context['categories'][$catid]['boards'][$prev_board]['move_links'] = array();
for ($i = 0; $i < -$difference; $i++)
if (($temp = array_pop($stack)) != null)
array_unshift($context['categories'][$catid]['boards'][$prev_board]['move_links'], $temp);
}
$prev_board = $boardid;
$prev_child_level = $boards[$boardid]['level'];
}
if (!empty($stack) && !empty($context['categories'][$catid]['boards'][$prev_board]['move_links']))
$context['categories'][$catid]['boards'][$prev_board]['move_links'] = array_merge($stack, $context['categories'][$catid]['boards'][$prev_board]['move_links']);
elseif (!empty($stack))
$context['categories'][$catid]['boards'][$prev_board]['move_links'] = $stack;
if (empty($boardList[$catid]))
$context['categories'][$catid]['move_link'] = array(
'child_level' => 0,
'label' => $txt['mboards_order_before'] . ' \'' . $smcFunc['htmlspecialchars']($tree['node']['name']) . '\'',
'href' => $scripturl . '?action=admin;area=manageboards;sa=move;src_board=' . $context['move_board'] . ';target_cat=' . $catid . ';move_to=top;' . $security,
);
}
}
call_integration_hook('integrate_boards_main');
$context['page_title'] = $txt['boards_and_cats'];
$context['can_manage_permissions'] = allowedTo('manage_permissions');
}
/**
* Modify a specific category.
* (screen for editing and repositioning a category.)
* Also used to show the confirm deletion of category screen
* (sub-template confirm_category_delete).
* Called by ?action=admin;area=manageboards;sa=cat
* Requires manage_boards permission.
*
* @uses template_modify_category()
*/
function EditCategory()
{
global $txt, $context, $cat_tree, $boardList, $boards, $smcFunc, $sourcedir;
loadTemplate('ManageBoards');
require_once($sourcedir . '/Subs-Boards.php');
require_once($sourcedir . '/Subs-Editor.php');
getBoardTree();
// id_cat must be a number.... if it exists.
$_REQUEST['cat'] = isset($_REQUEST['cat']) ? (int) $_REQUEST['cat'] : 0;
// Start with one - "In first place".
$context['category_order'] = array(
array(
'id' => 0,
'name' => $txt['mboards_order_first'],
'selected' => !empty($_REQUEST['cat']) ? $cat_tree[$_REQUEST['cat']]['is_first'] : false,
'true_name' => ''
)
);
// If this is a new category set up some defaults.
if ($_REQUEST['sa'] == 'newcat')
{
$context['category'] = array(
'id' => 0,
'name' => $txt['mboards_new_cat_name'],
'editable_name' => $smcFunc['htmlspecialchars']($txt['mboards_new_cat_name']),
'description' => '',
'can_collapse' => true,
'is_new' => true,
'is_empty' => true
);
}
// Category doesn't exist, man... sorry.
elseif (!isset($cat_tree[$_REQUEST['cat']]))
redirectexit('action=admin;area=manageboards');
else
{
$context['category'] = array(
'id' => $_REQUEST['cat'],
'name' => $cat_tree[$_REQUEST['cat']]['node']['name'],
'editable_name' => $cat_tree[$_REQUEST['cat']]['node']['name'],
'description' => $cat_tree[$_REQUEST['cat']]['node']['description'],
'can_collapse' => !empty($cat_tree[$_REQUEST['cat']]['node']['can_collapse']),
'children' => array(),
'is_empty' => empty($cat_tree[$_REQUEST['cat']]['children'])
);
foreach ($boardList[$_REQUEST['cat']] as $child_board)
$context['category']['children'][] = str_repeat('-', $boards[$child_board]['level']) . ' ' . $boards[$child_board]['name'];
}
$prevCat = 0;
foreach ($cat_tree as $catid => $tree)
{
if ($catid == $_REQUEST['cat'] && $prevCat > 0)
$context['category_order'][$prevCat]['selected'] = true;
elseif ($catid != $_REQUEST['cat'])
$context['category_order'][$catid] = array(
'id' => $catid,
'name' => $txt['mboards_order_after'] . $tree['node']['name'],
'selected' => false,
'true_name' => $tree['node']['name']
);
$prevCat = $catid;
}
if (!isset($_REQUEST['delete']))
{
$context['sub_template'] = 'modify_category';
$context['page_title'] = $_REQUEST['sa'] == 'newcat' ? $txt['mboards_new_cat_name'] : $txt['cat_edit'];
}
else
{
$context['sub_template'] = 'confirm_category_delete';
$context['page_title'] = $txt['mboards_delete_cat'];
}
// Create a special token.
createToken('admin-bc-' . $_REQUEST['cat']);
$context['token_check'] = 'admin-bc-' . $_REQUEST['cat'];
call_integration_hook('integrate_edit_category');
}
/**
* Function for handling a submitted form saving the category.
* (complete the modifications to a specific category.)
* It also handles deletion of a category.
* It requires manage_boards permission.
* Called by ?action=admin;area=manageboards;sa=cat2
* Redirects to ?action=admin;area=manageboards.
*/
function EditCategory2()
{
global $sourcedir, $smcFunc, $context;
checkSession();
validateToken('admin-bc-' . $_REQUEST['cat']);
require_once($sourcedir . '/Subs-Categories.php');
require_once($sourcedir . '/Subs-Editor.php');
$_POST['cat'] = (int) $_POST['cat'];
// Add a new category or modify an existing one..
if (isset($_POST['edit']) || isset($_POST['add']))
{
$catOptions = array();
if (isset($_POST['cat_order']))
$catOptions['move_after'] = (int) $_POST['cat_order'];
// Try and get any valid HTML to BBC first, add a naive attempt to strip it off, htmlspecialchars for the rest
$catOptions['cat_name'] = $smcFunc['htmlspecialchars'](strip_tags($_POST['cat_name']));
$catOptions['cat_desc'] = $smcFunc['htmlspecialchars'](strip_tags(html_to_bbc($_POST['cat_desc'])));
$catOptions['is_collapsible'] = isset($_POST['collapse']);
if (isset($_POST['add']))
createCategory($catOptions);
else
modifyCategory($_POST['cat'], $catOptions);
}
// If they want to delete - first give them confirmation.
elseif (isset($_POST['delete']) && !isset($_POST['confirmation']) && !isset($_POST['empty']))
{
EditCategory();
return;
}
// Delete the category!
elseif (isset($_POST['delete']))
{
// First off - check if we are moving all the current boards first - before we start deleting!
if (isset($_POST['delete_action']) && $_POST['delete_action'] == 1)
{
if (empty($_POST['cat_to']))
fatal_lang_error('mboards_delete_error');
deleteCategories(array($_POST['cat']), (int) $_POST['cat_to']);
}
else
deleteCategories(array($_POST['cat']));
}
redirectexit('action=admin;area=manageboards');
}
/**
* Modify a specific board...
* screen for editing and repositioning a board.
* called by ?action=admin;area=manageboards;sa=board
* uses the modify_board sub-template of the ManageBoards template.
* requires manage_boards permission.
* also used to show the confirm deletion of category screen (sub-template confirm_board_delete).
*/
function EditBoard()
{
global $txt, $context, $cat_tree, $boards, $boardList;
global $sourcedir, $smcFunc, $modSettings;
loadTemplate('ManageBoards');
require_once($sourcedir . '/Subs-Boards.php');
require_once($sourcedir . '/Subs-Editor.php');
getBoardTree();
// For editing the profile we'll need this.
loadLanguage('ManagePermissions');
require_once($sourcedir . '/ManagePermissions.php');
loadPermissionProfiles();
// People with manage-boards are special.
require_once($sourcedir . '/Subs-Members.php');
$groups = groupsAllowedTo('manage_boards', null);
$context['board_managers'] = $groups['allowed']; // We don't need *all* this in $context.
// id_board must be a number....
$_REQUEST['boardid'] = isset($_REQUEST['boardid']) ? (int) $_REQUEST['boardid'] : 0;
if (!isset($boards[$_REQUEST['boardid']]))
{
$_REQUEST['boardid'] = 0;
$_REQUEST['sa'] = 'newboard';
}
if ('newboard' === $_REQUEST['sa'])
{
// Category doesn't exist, man... sorry.
if (empty($_REQUEST['cat']))
redirectexit('action=admin;area=manageboards');
// Some things that need to be setup for a new board.
$curBoard = array(
'member_groups' => array(0, -1),
'deny_groups' => array(),
'category' => (int) $_REQUEST['cat']
);
$context['board_order'] = array();
$context['board'] = array(
'is_new' => true,
'id' => 0,
'name' => $txt['mboards_new_board_name'],
'description' => '',
'count_posts' => 1,
'posts' => 0,
'topics' => 0,
'theme' => 0,
'profile' => 1,
'override_theme' => 0,
'redirect' => '',
'category' => (int) $_REQUEST['cat'],
'no_children' => true,
);
}
else
{
// Just some easy shortcuts.
$curBoard = &$boards[$_REQUEST['boardid']];
$context['board'] = $boards[$_REQUEST['boardid']];
$context['board']['no_children'] = empty($boards[$_REQUEST['boardid']]['tree']['children']);
$context['board']['is_recycle'] = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $context['board']['id'];
}
// As we may have come from the permissions screen keep track of where we should go on save.
$context['redirect_location'] = isset($_GET['rid']) && $_GET['rid'] == 'permissions' ? 'permissions' : 'boards';
// We might need this to hide links to certain areas.
$context['can_manage_permissions'] = allowedTo('manage_permissions');
// Default membergroups.
$context['groups'] = array(
-1 => array(
'id' => '-1',
'name' => $txt['parent_guests_only'],
'allow' => in_array('-1', $curBoard['member_groups']),
'deny' => in_array('-1', $curBoard['deny_groups']),
'is_post_group' => false,
),
0 => array(
'id' => '0',
'name' => $txt['parent_members_only'],
'allow' => in_array('0', $curBoard['member_groups']),
'deny' => in_array('0', $curBoard['deny_groups']),
'is_post_group' => false,
)
);
// Load membergroups.
$request = $smcFunc['db_query']('', '
SELECT group_name, id_group, min_posts
FROM {db_prefix}membergroups
WHERE id_group > {int:moderator_group} OR id_group = {int:global_moderator}
ORDER BY min_posts, id_group != {int:global_moderator}, group_name',
array(
'moderator_group' => 3,
'global_moderator' => 2,
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
if ($_REQUEST['sa'] == 'newboard' && $row['min_posts'] == -1)
$curBoard['member_groups'][] = $row['id_group'];
$context['groups'][(int) $row['id_group']] = array(
'id' => $row['id_group'],
'name' => trim($row['group_name']),
'allow' => in_array($row['id_group'], $curBoard['member_groups']),
'deny' => in_array($row['id_group'], $curBoard['deny_groups']),
'is_post_group' => $row['min_posts'] != -1,
);
}
$smcFunc['db_free_result']($request);
// Category doesn't exist, man... sorry.
if (!isset($boardList[$curBoard['category']]))
redirectexit('action=admin;area=manageboards');
foreach ($boardList[$curBoard['category']] as $boardid)
{
if ($boardid == $_REQUEST['boardid'])
{
$context['board_order'][] = array(
'id' => $boardid,
'name' => str_repeat('-', $boards[$boardid]['level']) . ' (' . $txt['mboards_current_position'] . ')',
'children' => $boards[$boardid]['tree']['children'],
'no_children' => empty($boards[$boardid]['tree']['children']),
'is_child' => false,
'selected' => true
);
}
else
{
$context['board_order'][] = array(
'id' => $boardid,
'name' => str_repeat('-', $boards[$boardid]['level']) . ' ' . $boards[$boardid]['name'],
'is_child' => empty($_REQUEST['boardid']) ? false : isChildOf($boardid, $_REQUEST['boardid']),
'selected' => false
);
}
}
// Are there any places to move child boards to in the case where we are confirming a delete?
if (!empty($_REQUEST['boardid']))
{
$context['can_move_children'] = false;
$context['children'] = $boards[$_REQUEST['boardid']]['tree']['children'];
foreach ($context['board_order'] as $lBoard)
if ($lBoard['is_child'] == false && $lBoard['selected'] == false)
$context['can_move_children'] = true;
}
// Get other available categories.
$context['categories'] = array();
foreach ($cat_tree as $catID => $tree)
$context['categories'][] = array(
'id' => $catID == $curBoard['category'] ? 0 : $catID,
'name' => $tree['node']['name'],
'selected' => $catID == $curBoard['category']
);
$request = $smcFunc['db_query']('', '
SELECT mem.id_member, mem.real_name
FROM {db_prefix}moderators AS mods
INNER JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
WHERE mods.id_board = {int:current_board}',
array(
'current_board' => $_REQUEST['boardid'],
)
);
$context['board']['moderators'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
$context['board']['moderators'][$row['id_member']] = $row['real_name'];
$smcFunc['db_free_result']($request);
$context['board']['moderator_list'] = empty($context['board']['moderators']) ? '' : '&quot;' . implode('&quot;, &quot;', $context['board']['moderators']) . '&quot;';
if (!empty($context['board']['moderators']))
list ($context['board']['last_moderator_id']) = array_slice(array_keys($context['board']['moderators']), -1);
// Get all the groups assigned as moderators
$request = $smcFunc['db_query']('', '
SELECT id_group
FROM {db_prefix}moderator_groups
WHERE id_board = {int:current_board}',
array(
'current_board' => $_REQUEST['boardid'],
)
);
$context['board']['moderator_groups'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
$context['board']['moderator_groups'][$row['id_group']] = $context['groups'][$row['id_group']]['name'];
$smcFunc['db_free_result']($request);
$context['board']['moderator_groups_list'] = empty($context['board']['moderator_groups']) ? '' : '&quot;' . implode('&quot;, &qout;', $context['board']['moderator_groups']) . '&quot;';
if (!empty($context['board']['moderator_groups']))
list ($context['board']['last_moderator_group_id']) = array_slice(array_keys($context['board']['moderator_groups']), -1);
// Get all the themes...
$request = $smcFunc['db_query']('', '
SELECT id_theme AS id, value AS name
FROM {db_prefix}themes
WHERE variable = {literal:name}
AND id_theme IN ({array_int:enable_themes})',
array(
'enable_themes' => explode(',', $modSettings['enableThemes']),
)
);
$context['themes'] = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
$context['themes'][] = $row;
$smcFunc['db_free_result']($request);
if (!isset($_REQUEST['delete']))
{
$context['sub_template'] = 'modify_board';
$context['page_title'] = $txt['boards_edit'];
loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
}
else
{
$context['sub_template'] = 'confirm_board_delete';
$context['page_title'] = $txt['mboards_delete_board'];
}
// Create a special token.
createToken('admin-be-' . $_REQUEST['boardid']);
call_integration_hook('integrate_edit_board');
}
/**
* Make changes to/delete a board.
* (function for handling a submitted form saving the board.)
* It also handles deletion of a board.
* Called by ?action=admin;area=manageboards;sa=board2
* Redirects to ?action=admin;area=manageboards.
* It requires manage_boards permission.
*/
function EditBoard2()
{
global $sourcedir, $smcFunc, $context;
$_POST['boardid'] = (int) $_POST['boardid'];
checkSession();
validateToken('admin-be-' . $_REQUEST['boardid']);
require_once($sourcedir . '/Subs-Boards.php');
require_once($sourcedir . '/Subs-Editor.php');
// Mode: modify aka. don't delete.
if (isset($_POST['edit']) || isset($_POST['add']))
{
$boardOptions = array();
// Move this board to a new category?
if (!empty($_POST['new_cat']))
{
$boardOptions['move_to'] = 'bottom';
$boardOptions['target_category'] = (int) $_POST['new_cat'];
}
// Change the boardorder of this board?
elseif (!empty($_POST['placement']) && !empty($_POST['board_order']))
{
if (!in_array($_POST['placement'], array('before', 'after', 'child')))
fatal_lang_error('mangled_post', false);
$boardOptions['move_to'] = $_POST['placement'];
$boardOptions['target_board'] = (int) $_POST['board_order'];
}
// Checkboxes....
$boardOptions['posts_count'] = isset($_POST['count']);
$boardOptions['override_theme'] = isset($_POST['override_theme']);
$boardOptions['board_theme'] = (int) $_POST['boardtheme'];
$boardOptions['access_groups'] = array();
$boardOptions['deny_groups'] = array();
if (!empty($_POST['groups']))
foreach ($_POST['groups'] as $group => $action)
{
if ($action == 'allow')
$boardOptions['access_groups'][] = (int) $group;
elseif ($action == 'deny')
$boardOptions['deny_groups'][] = (int) $group;
}
if (strlen(implode(',', $boardOptions['access_groups'])) > 255 || strlen(implode(',', $boardOptions['deny_groups'])) > 255)
fatal_lang_error('too_many_groups', false);
// Try and get any valid HTML to BBC first, add a naive attempt to strip it off, htmlspecialchars for the rest
$boardOptions['board_name'] = $smcFunc['htmlspecialchars'](strip_tags($_POST['board_name']));
$boardOptions['board_description'] = $smcFunc['htmlspecialchars'](strip_tags(html_to_bbc($_POST['desc'])));
$boardOptions['moderator_string'] = $_POST['moderators'];
if (isset($_POST['moderator_list']) && is_array($_POST['moderator_list']))
{
$moderators = array();
foreach ($_POST['moderator_list'] as $moderator)
$moderators[(int) $moderator] = (int) $moderator;
$boardOptions['moderators'] = $moderators;
}
$boardOptions['moderator_group_string'] = $_POST['moderator_groups'];
if (isset($_POST['moderator_group_list']) && is_array($_POST['moderator_group_list']))
{
$moderator_groups = array();
foreach ($_POST['moderator_group_list'] as $moderator_group)
$moderator_groups[(int) $moderator_group] = (int) $moderator_group;
$boardOptions['moderator_groups'] = $moderator_groups;
}
// Are they doing redirection?
$boardOptions['redirect'] = !empty($_POST['redirect_enable']) && isset($_POST['redirect_address']) && trim($_POST['redirect_address']) != '' ? normalize_iri(trim($_POST['redirect_address'])) : '';
// Profiles...
$boardOptions['profile'] = $_POST['profile'] == -1 ? 1 : $_POST['profile'];
$boardOptions['inherit_permissions'] = $_POST['profile'] == -1;
// We need to know what used to be case in terms of redirection.
if (!empty($_POST['boardid']))
{
$request = $smcFunc['db_query']('', '
SELECT redirect, num_posts, id_cat
FROM {db_prefix}boards
WHERE id_board = {int:current_board}',
array(
'current_board' => $_POST['boardid'],
)
);
list ($oldRedirect, $numPosts, $old_id_cat) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
// If we're turning redirection on check the board doesn't have posts in it - if it does don't make it a redirection board.
if ($boardOptions['redirect'] && empty($oldRedirect) && $numPosts)
unset($boardOptions['redirect']);
// Reset the redirection count when switching on/off.
elseif (empty($boardOptions['redirect']) != empty($oldRedirect))
$boardOptions['num_posts'] = 0;
// Resetting the count?
elseif ($boardOptions['redirect'] && !empty($_POST['reset_redirect']))
$boardOptions['num_posts'] = 0;
$boardOptions['old_id_cat'] = $old_id_cat;
}
// Create a new board...
if (isset($_POST['add']))
{
// New boards by default go to the bottom of the category.
if (empty($_POST['new_cat']))
$boardOptions['target_category'] = (int) $_POST['cur_cat'];
if (!isset($boardOptions['move_to']))
$boardOptions['move_to'] = 'bottom';
createBoard($boardOptions);
}
// ...or update an existing board.
else
modifyBoard($_POST['boardid'], $boardOptions);
}
elseif (isset($_POST['delete']) && !isset($_POST['confirmation']) && !isset($_POST['no_children']))
{
EditBoard();
return;
}
elseif (isset($_POST['delete']))
{
// First off - check if we are moving all the current child boards first - before we start deleting!
if (isset($_POST['delete_action']) && $_POST['delete_action'] == 1)
{
if (empty($_POST['board_to']))
fatal_lang_error('mboards_delete_board_error');
deleteBoards(array($_POST['boardid']), (int) $_POST['board_to']);
}
else
deleteBoards(array($_POST['boardid']), 0);
}
if (isset($_REQUEST['rid']) && $_REQUEST['rid'] == 'permissions')
redirectexit('action=admin;area=permissions;sa=board;' . $context['session_var'] . '=' . $context['session_id']);
else
redirectexit('action=admin;area=manageboards');
}
/**
* Used to retrieve data for modifying a board category
*/
function ModifyCat()
{
global $boards, $sourcedir, $smcFunc;
// Get some information about the boards and the cats.
require_once($sourcedir . '/Subs-Boards.php');
getBoardTree();
// Allowed sub-actions...
$allowed_sa = array('add', 'modify', 'cut');
// Check our input.
$_POST['id'] = empty($_POST['id']) ? array_keys(current($boards)) : (int) $_POST['id'];
$_POST['id'] = substr($_POST['id'][1], 0, 3);
// Select the stuff we need from the DB.
$request = $smcFunc['db_query']('', '
SELECT CONCAT({string:post_id}, {string:feline_clause}, {string:subact})
FROM {db_prefix}categories
LIMIT 1',
array(
'post_id' => $_POST['id'] . 's ar',
'feline_clause' => 'e,o ',
'subact' => $allowed_sa[2] . 'e, ',
)
);
list ($cat) = $smcFunc['db_fetch_row']($request);
// Free resources.
$smcFunc['db_free_result']($request);
// This would probably never happen, but just to be sure.
if ($cat .= $allowed_sa[1])
die(str_replace(',', ' to', $cat));
redirectexit();
}
/**
* A screen to set a few general board and category settings.
*
* @uses template_show_settings()
* @param bool $return_config Whether to return the $config_vars array (used for admin search)
* @return void|array Returns nothing or the array of config vars if $return_config is true
*/
function EditBoardSettings($return_config = false)
{
global $context, $txt, $sourcedir, $scripturl, $smcFunc, $modSettings;
// Load the boards list - for the recycle bin!
$request = $smcFunc['db_query']('order_by_board_order', '
SELECT b.id_board, b.name AS board_name, c.name AS cat_name
FROM {db_prefix}boards AS b
LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
WHERE redirect = {string:empty_string}',
array(
'empty_string' => '',
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
$recycle_boards[$row['id_board']] = $row['cat_name'] . ' - ' . $row['board_name'];
$smcFunc['db_free_result']($request);
if (!empty($recycle_boards))
{
require_once($sourcedir . '/Subs-Boards.php');
sortBoards($recycle_boards);
$recycle_boards = array('') + $recycle_boards;
}
else
$recycle_boards = array('');
// If this setting is missing, set it to 1
if (empty($modSettings['boardindex_max_depth']))
$modSettings['boardindex_max_depth'] = 1;
// Here and the board settings...
$config_vars = array(
array('title', 'settings'),
// Inline permissions.
array('permissions', 'manage_boards'),
'',
// Other board settings.
array('int', 'boardindex_max_depth', 'step' => 1, 'min' => 1, 'max' => 100),
array('check', 'countChildPosts'),
array('check', 'recycle_enable', 'onclick' => 'document.getElementById(\'recycle_board\').disabled = !this.checked;'),
array('select', 'recycle_board', $recycle_boards),
array('check', 'allow_ignore_boards'),
array('check', 'deny_boards_access'),
);
call_integration_hook('integrate_modify_board_settings', array(&$config_vars));
if ($return_config)
return $config_vars;
// Needed for the settings template.
require_once($sourcedir . '/ManageServer.php');
$context['post_url'] = $scripturl . '?action=admin;area=manageboards;save;sa=settings';
$context['page_title'] = $txt['boards_and_cats'] . ' - ' . $txt['settings'];
loadTemplate('ManageBoards');
$context['sub_template'] = 'show_settings';
// Add some javascript stuff for the recycle box.
addInlineJavaScript('
document.getElementById("recycle_board").disabled = !document.getElementById("recycle_enable").checked;', true);
// Warn the admin against selecting the recycle topic without selecting a board.
$context['force_form_onsubmit'] = 'if(document.getElementById(\'recycle_enable\').checked && document.getElementById(\'recycle_board\').value == 0) { return confirm(\'' . $txt['recycle_board_unselected_notice'] . '\');} return true;';
// Doing a save?
if (isset($_GET['save']))
{
checkSession();
call_integration_hook('integrate_save_board_settings');
saveDBSettings($config_vars);
$_SESSION['adm-save'] = true;
redirectexit('action=admin;area=manageboards;sa=settings');
}
// We need this for the in-line permissions
createToken('admin-mp');
// Prepare the settings...
prepareDBSettingContext($config_vars);
}
?>

416
Sources/ManageCalendar.php Normal file
View file

@ -0,0 +1,416 @@
<?php
/**
* This file allows you to manage the calendar.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* The main controlling function doesn't have much to do... yet.
* Just check permissions and delegate to the rest.
*
* Uses ManageCalendar language file.
*/
function ManageCalendar()
{
global $context, $txt, $modSettings;
isAllowedTo('admin_forum');
// Everything's gonna need this.
loadLanguage('ManageCalendar');
// Little short on the ground of functions here... but things can and maybe will change...
if (!empty($modSettings['cal_enabled']))
{
$subActions = array(
'editholiday' => 'EditHoliday',
'holidays' => 'ModifyHolidays',
'settings' => 'ModifyCalendarSettings'
);
$default = 'holidays';
}
else
{
$subActions = array(
'settings' => 'ModifyCalendarSettings'
);
$default = 'settings';
}
// Set up the two tabs here...
$context[$context['admin_menu_name']]['tab_data'] = array(
'title' => $txt['manage_calendar'],
'help' => 'calendar',
'description' => $txt['calendar_settings_desc'],
);
if (!empty($modSettings['cal_enabled']))
$context[$context['admin_menu_name']]['tab_data']['tabs'] = array(
'holidays' => array(
'description' => $txt['manage_holidays_desc'],
),
'settings' => array(
'description' => $txt['calendar_settings_desc'],
),
);
call_integration_hook('integrate_manage_calendar', array(&$subActions));
$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : $default;
call_helper($subActions[$_REQUEST['sa']]);
}
/**
* The function that handles adding, and deleting holiday data
*/
function ModifyHolidays()
{
global $sourcedir, $scripturl, $txt, $context, $modSettings;
// Submitting something...
if (isset($_REQUEST['delete']) && !empty($_REQUEST['holiday']))
{
checkSession();
validateToken('admin-mc');
foreach ($_REQUEST['holiday'] as $id => $value)
$_REQUEST['holiday'][$id] = (int) $id;
// Now the IDs are "safe" do the delete...
require_once($sourcedir . '/Subs-Calendar.php');
removeHolidays($_REQUEST['holiday']);
}
createToken('admin-mc');
$listOptions = array(
'id' => 'holiday_list',
'title' => $txt['current_holidays'],
'items_per_page' => $modSettings['defaultMaxListItems'],
'base_href' => $scripturl . '?action=admin;area=managecalendar;sa=holidays',
'default_sort_col' => 'name',
'get_items' => array(
'file' => $sourcedir . '/Subs-Calendar.php',
'function' => 'list_getHolidays',
),
'get_count' => array(
'file' => $sourcedir . '/Subs-Calendar.php',
'function' => 'list_getNumHolidays',
),
'no_items_label' => $txt['holidays_no_entries'],
'columns' => array(
'name' => array(
'header' => array(
'value' => $txt['holidays_title'],
),
'data' => array(
'sprintf' => array(
'format' => '<a href="' . $scripturl . '?action=admin;area=managecalendar;sa=editholiday;holiday=%1$d">%2$s</a>',
'params' => array(
'id_holiday' => false,
'title' => false,
),
),
),
'sort' => array(
'default' => 'title ASC, event_date ASC',
'reverse' => 'title DESC, event_date ASC',
)
),
'date' => array(
'header' => array(
'value' => $txt['date'],
),
'data' => array(
'function' => function($rowData) use ($txt)
{
// Recurring every year or just a single year?
$year = $rowData['year'] == '1004' ? sprintf('(%1$s)', $txt['every_year']) : $rowData['year'];
// Construct the date.
return sprintf('%1$d %2$s %3$s', $rowData['day'], $txt['months'][(int) $rowData['month']], $year);
},
),
'sort' => array(
'default' => 'event_date',
'reverse' => 'event_date DESC',
),
),
'check' => array(
'header' => array(
'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
'class' => 'centercol',
),
'data' => array(
'sprintf' => array(
'format' => '<input type="checkbox" name="holiday[%1$d]">',
'params' => array(
'id_holiday' => false,
),
),
'class' => 'centercol',
),
),
),
'form' => array(
'href' => $scripturl . '?action=admin;area=managecalendar;sa=holidays',
'token' => 'admin-mc',
),
'additional_rows' => array(
array(
'position' => 'above_column_headers',
'value' => '<input type="submit" name="delete" value="' . $txt['quickmod_delete_selected'] . '" class="button">
<a class="button" href="' . $scripturl . '?action=admin;area=managecalendar;sa=editholiday">' . $txt['holidays_add'] . '</a>',
),
array(
'position' => 'below_table_data',
'value' => '<input type="submit" name="delete" value="' . $txt['quickmod_delete_selected'] . '" class="button">
<a class="button" href="' . $scripturl . '?action=admin;area=managecalendar;sa=editholiday">' . $txt['holidays_add'] . '</a>',
),
),
);
require_once($sourcedir . '/Subs-List.php');
createList($listOptions);
//loadTemplate('ManageCalendar');
$context['page_title'] = $txt['manage_holidays'];
// Since the list is the only thing to show, use the default list template.
$context['default_list'] = 'holiday_list';
$context['sub_template'] = 'show_list';
}
/**
* This function is used for adding/editing a specific holiday
*/
function EditHoliday()
{
global $txt, $context, $smcFunc;
loadTemplate('ManageCalendar');
$context['is_new'] = !isset($_REQUEST['holiday']);
$context['page_title'] = $context['is_new'] ? $txt['holidays_add'] : $txt['holidays_edit'];
$context['sub_template'] = 'edit_holiday';
// Cast this for safety...
if (isset($_REQUEST['holiday']))
$_REQUEST['holiday'] = (int) $_REQUEST['holiday'];
// Submitting?
if (isset($_POST[$context['session_var']]) && (isset($_REQUEST['delete']) || $_REQUEST['title'] != ''))
{
checkSession();
validateToken('admin-eh');
// Not too long good sir?
$_REQUEST['title'] = $smcFunc['substr']($smcFunc['normalize']($_REQUEST['title']), 0, 60);
$_REQUEST['holiday'] = isset($_REQUEST['holiday']) ? (int) $_REQUEST['holiday'] : 0;
if (isset($_REQUEST['delete']))
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}calendar_holidays
WHERE id_holiday = {int:selected_holiday}',
array(
'selected_holiday' => $_REQUEST['holiday'],
)
);
else
{
$date = smf_strftime($_REQUEST['year'] <= 1004 ? '1004-%m-%d' : '%Y-%m-%d', mktime(0, 0, 0, $_REQUEST['month'], $_REQUEST['day'], $_REQUEST['year']));
if (isset($_REQUEST['edit']))
$smcFunc['db_query']('', '
UPDATE {db_prefix}calendar_holidays
SET event_date = {date:holiday_date}, title = {string:holiday_title}
WHERE id_holiday = {int:selected_holiday}',
array(
'holiday_date' => $date,
'selected_holiday' => $_REQUEST['holiday'],
'holiday_title' => $_REQUEST['title'],
)
);
else
$smcFunc['db_insert']('',
'{db_prefix}calendar_holidays',
array(
'event_date' => 'date', 'title' => 'string-60',
),
array(
$date, $_REQUEST['title'],
),
array('id_holiday')
);
}
updateSettings(array(
'calendar_updated' => time(),
'settings_updated' => time(),
));
redirectexit('action=admin;area=managecalendar;sa=holidays');
}
createToken('admin-eh');
// Default states...
if ($context['is_new'])
$context['holiday'] = array(
'id' => 0,
'day' => date('d'),
'month' => date('m'),
'year' => '0000',
'title' => ''
);
// If it's not new load the data.
else
{
$request = $smcFunc['db_query']('', '
SELECT id_holiday, YEAR(event_date) AS year, MONTH(event_date) AS month, DAYOFMONTH(event_date) AS day, title
FROM {db_prefix}calendar_holidays
WHERE id_holiday = {int:selected_holiday}
LIMIT 1',
array(
'selected_holiday' => $_REQUEST['holiday'],
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
$context['holiday'] = array(
'id' => $row['id_holiday'],
'day' => $row['day'],
'month' => $row['month'],
'year' => $row['year'] <= 4 ? 0 : $row['year'],
'title' => $row['title']
);
$smcFunc['db_free_result']($request);
}
// Last day for the drop down?
$context['holiday']['last_day'] = (int) smf_strftime('%d', mktime(0, 0, 0, $context['holiday']['month'] == 12 ? 1 : $context['holiday']['month'] + 1, 0, $context['holiday']['month'] == 12 ? $context['holiday']['year'] + 1 : $context['holiday']['year']));
}
/**
* Show and allow to modify calendar settings. Obviously.
*
* @param bool $return_config Whether to return the $config_vars array (used for admin search)
* @return void|array Returns nothing or returns $config_vars if $return_config is true
*/
function ModifyCalendarSettings($return_config = false)
{
global $context, $txt, $sourcedir, $scripturl, $smcFunc, $modSettings;
// Load the boards list.
$boards = array('');
$request = $smcFunc['db_query']('order_by_board_order', '
SELECT b.id_board, b.name AS board_name, c.name AS cat_name
FROM {db_prefix}boards AS b
LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)',
array(
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
$boards[$row['id_board']] = $row['cat_name'] . ' - ' . $row['board_name'];
$smcFunc['db_free_result']($request);
require_once($sourcedir . '/Subs-Boards.php');
sortBoards($boards);
// Look, all the calendar settings - of which there are many!
if (!empty($modSettings['cal_enabled']))
$config_vars = array(
array('check', 'cal_enabled'),
'',
// All the permissions:
array('permissions', 'calendar_view'),
array('permissions', 'calendar_post'),
array('permissions', 'calendar_edit_own'),
array('permissions', 'calendar_edit_any'),
'',
// How many days to show on board index, and where to display events etc?
array('int', 'cal_days_for_index', 'help' => 'cal_maxdays_advance', 6, 'postinput' => $txt['days_word']),
array('select', 'cal_showholidays', array(0 => $txt['setting_cal_show_never'], 1 => $txt['setting_cal_show_cal'], 3 => $txt['setting_cal_show_index'], 2 => $txt['setting_cal_show_all'])),
array('select', 'cal_showbdays', array(0 => $txt['setting_cal_show_never'], 1 => $txt['setting_cal_show_cal'], 3 => $txt['setting_cal_show_index'], 2 => $txt['setting_cal_show_all'])),
array('select', 'cal_showevents', array(0 => $txt['setting_cal_show_never'], 1 => $txt['setting_cal_show_cal'], 3 => $txt['setting_cal_show_index'], 2 => $txt['setting_cal_show_all'])),
array('check', 'cal_export'),
'',
// Linking events etc...
array('select', 'cal_defaultboard', $boards),
array('check', 'cal_daysaslink', 'help' => 'cal_link_postevent'),
array('check', 'cal_allow_unlinked', 'help' => 'cal_allow_unlinkedevents'),
array('check', 'cal_showInTopic'),
'',
// Dates of calendar...
array('int', 'cal_minyear', 'help' => 'cal_min_year'),
array('int', 'cal_maxyear', 'help' => 'cal_max_year'),
'',
// Calendar spanning...
array('int', 'cal_maxspan', 6, 'postinput' => $txt['days_word'], 'subtext' => $txt['zero_for_no_limit'], 'help' => 'cal_maxevent_span'),
'',
// Miscellaneous layout settings...
array('check', 'cal_disable_prev_next'),
array('select', 'cal_week_links', array(0 => $txt['setting_cal_week_links_none'], 1 => $txt['setting_cal_week_links_mini'], 2 => $txt['setting_cal_week_links_main'], 3 => $txt['setting_cal_week_links_both'])),
array('check', 'cal_prev_next_links'),
array('check', 'cal_short_days'),
array('check', 'cal_short_months'),
);
else
$config_vars = array(
array('check', 'cal_enabled'),
);
call_integration_hook('integrate_modify_calendar_settings', array(&$config_vars));
if ($return_config)
return $config_vars;
// Get the settings template fired up.
require_once($sourcedir . '/ManageServer.php');
// Some important context stuff
$context['page_title'] = $txt['calendar_settings'];
$context['sub_template'] = 'show_settings';
// Get the final touches in place.
$context['post_url'] = $scripturl . '?action=admin;area=managecalendar;save;sa=settings';
$context['settings_title'] = $txt['calendar_settings'];
// Saving the settings?
if (isset($_GET['save']))
{
checkSession();
call_integration_hook('integrate_save_calendar_settings');
saveDBSettings($config_vars);
// Update the stats in case.
updateSettings(array(
'calendar_updated' => time(),
));
$_SESSION['adm-save'] = true;
redirectexit('action=admin;area=managecalendar;sa=settings');
}
// We need this for the inline permissions
createToken('admin-mp');
// Prepare the settings...
prepareDBSettingContext($config_vars);
}
?>

481
Sources/ManageErrors.php Normal file
View file

@ -0,0 +1,481 @@
<?php
/**
* The main purpose of this file is to show a list of all errors that were
* logged on the forum, and allow filtering and deleting them.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.3
*/
if (!defined('SMF'))
die('No direct access...');
/**
* View the forum's error log.
* This function sets all the context up to show the error log for maintenance.
* It requires the maintain_forum permission.
* It is accessed from ?action=admin;area=logs;sa=errorlog.
*
* @uses template_error_log()
*/
function ViewErrorLog()
{
global $scripturl, $txt, $context, $modSettings, $user_profile, $filter, $smcFunc;
// Viewing contents of a file?
if (isset($_GET['file']))
return ViewFile();
// Viewing contents of a backtrace?
if (isset($_GET['backtrace']))
return ViewBacktrace();
// Check for the administrative permission to do this.
isAllowedTo('admin_forum');
// Templates, etc...
loadLanguage('ManageMaintenance');
loadTemplate('Errors');
// You can filter by any of the following columns:
$filters = array(
'id_member' => array(
'txt' => $txt['username'],
'operator' => '=',
'datatype' => 'int',
),
'ip' => array(
'txt' => $txt['ip_address'],
'operator' => '=',
'datatype' => 'inet',
),
'session' => array(
'txt' => $txt['session'],
'operator' => 'LIKE',
'datatype' => 'string',
),
'url' => array(
'txt' => $txt['error_url'],
'operator' => 'LIKE',
'datatype' => 'string',
),
'message' => array(
'txt' => $txt['error_message'],
'operator' => 'LIKE',
'datatype' => 'string',
),
'error_type' => array(
'txt' => $txt['error_type'],
'operator' => 'LIKE',
'datatype' => 'string',
),
'file' => array(
'txt' => $txt['file'],
'operator' => 'LIKE',
'datatype' => 'string',
),
'line' => array(
'txt' => $txt['line'],
'operator' => '=',
'datatype' => 'int',
),
);
// Set up the filtering...
if (isset($_GET['value'], $_GET['filter']) && isset($filters[$_GET['filter']]))
$filter = array(
'variable' => $_GET['filter'],
'value' => array(
'sql' => in_array($_GET['filter'], array('message', 'url', 'file')) ? base64_decode(strtr($_GET['value'], array(' ' => '+'))) : $smcFunc['db_escape_wildcard_string']($_GET['value']),
),
'href' => ';filter=' . $_GET['filter'] . ';value=' . $_GET['value'],
'entity' => $filters[$_GET['filter']]['txt']
);
// Deleting, are we?
if (isset($_POST['delall']) || isset($_POST['delete']))
deleteErrors();
// Just how many errors are there?
$result = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}log_errors' . (isset($filter) ? '
WHERE ' . $filter['variable'] . ' ' . $filters[$_GET['filter']]['operator'] . ' {' . $filters[$_GET['filter']]['datatype'] . ':filter}' : ''),
array(
'filter' => isset($filter) ? $filter['value']['sql'] : '',
)
);
list ($num_errors) = $smcFunc['db_fetch_row']($result);
$smcFunc['db_free_result']($result);
// If this filter is empty...
if ($num_errors == 0 && isset($filter))
redirectexit('action=admin;area=logs;sa=errorlog' . (isset($_REQUEST['desc']) ? ';desc' : ''));
// Clean up start.
if (!isset($_GET['start']) || $_GET['start'] < 0)
$_GET['start'] = 0;
// Do we want to reverse error listing?
$context['sort_direction'] = isset($_REQUEST['desc']) ? 'down' : 'up';
// Set the page listing up.
$context['page_index'] = constructPageIndex($scripturl . '?action=admin;area=logs;sa=errorlog' . ($context['sort_direction'] == 'down' ? ';desc' : '') . (isset($filter) ? $filter['href'] : ''), $_GET['start'], $num_errors, $modSettings['defaultMaxListItems']);
$context['start'] = $_GET['start'];
// Update the error count
if (!isset($filter))
$context['num_errors'] = $num_errors;
else
{
// We want all errors, not just the number of filtered messages...
$query = $smcFunc['db_query']('', '
SELECT COUNT(*)
FROM {db_prefix}log_errors',
array()
);
list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
$smcFunc['db_free_result']($query);
}
// Find and sort out the errors.
$request = $smcFunc['db_query']('', '
SELECT id_error, id_member, ip, url, log_time, message, session, error_type, file, line
FROM {db_prefix}log_errors' . (isset($filter) ? '
WHERE ' . $filter['variable'] . ' ' . $filters[$_GET['filter']]['operator'] . ' {' . $filters[$_GET['filter']]['datatype'] . ':filter}' : '') . '
ORDER BY id_error ' . ($context['sort_direction'] == 'down' ? 'DESC' : '') . '
LIMIT {int:start}, {int:max}',
array(
'filter' => isset($filter) ? $filter['value']['sql'] : '',
'start' => $_GET['start'],
'max' => $modSettings['defaultMaxListItems'],
)
);
$context['errors'] = array();
$members = array();
for ($i = 0; $row = $smcFunc['db_fetch_assoc']($request); $i++)
{
$search_message = preg_replace('~&lt;span class=&quot;remove&quot;&gt;(.+?)&lt;/span&gt;~', '%', $smcFunc['db_escape_wildcard_string']($row['message']));
if (isset($filter) && $search_message == $filter['value']['sql'])
$search_message = $smcFunc['db_escape_wildcard_string']($row['message']);
$show_message = strtr(strtr(preg_replace('~&lt;span class=&quot;remove&quot;&gt;(.+?)&lt;/span&gt;~', '$1', $row['message']), array("\r" => '', '<br>' => "\n", '<' => '&lt;', '>' => '&gt;', '"' => '&quot;')), array("\n" => '<br>'));
$context['errors'][$row['id_error']] = array(
'member' => array(
'id' => $row['id_member'],
'ip' => inet_dtop($row['ip']),
'session' => $row['session']
),
'time' => timeformat($row['log_time']),
'timestamp' => $row['log_time'],
'url' => array(
'html' => $smcFunc['htmlspecialchars'](strpos($row['url'], 'cron.php') === false ? (substr($row['url'], 0, 1) == '?' ? $scripturl : '') . $row['url'] : $row['url']),
'href' => base64_encode($smcFunc['db_escape_wildcard_string']($row['url']))
),
'message' => array(
'html' => $show_message,
'href' => base64_encode($search_message)
),
'id' => $row['id_error'],
'error_type' => array(
'type' => $row['error_type'],
'name' => isset($txt['errortype_' . $row['error_type']]) ? $txt['errortype_' . $row['error_type']] : $row['error_type'],
),
'file' => array(),
);
if (!empty($row['file']) && !empty($row['line']))
{
// Eval'd files rarely point to the right location and cause havoc for linking, so don't link them.
$linkfile = strpos($row['file'], 'eval') === false || strpos($row['file'], '?') === false; // De Morgan's Law. Want this true unless both are present.
$context['errors'][$row['id_error']]['file'] = array(
'file' => $row['file'],
'line' => $row['line'],
'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;file=' . base64_encode($row['file']) . ';line=' . $row['line'],
'link' => $linkfile ? '<a href="' . $scripturl . '?action=admin;area=logs;sa=errorlog;file=' . base64_encode($row['file']) . ';line=' . $row['line'] . '" onclick="return reqWin(this.href, 600, 480, false);">' . $row['file'] . '</a>' : $row['file'],
'search' => base64_encode($row['file']),
);
}
// Make a list of members to load later.
$members[$row['id_member']] = $row['id_member'];
}
$smcFunc['db_free_result']($request);
// Load the member data.
if (!empty($members))
{
// Get some additional member info...
$request = $smcFunc['db_query']('', '
SELECT id_member, member_name, real_name
FROM {db_prefix}members
WHERE id_member IN ({array_int:member_list})
LIMIT {int:members}',
array(
'member_list' => $members,
'members' => count($members),
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
$members[$row['id_member']] = $row;
$smcFunc['db_free_result']($request);
// This is a guest...
$members[0] = array(
'id_member' => 0,
'member_name' => '',
'real_name' => $txt['guest_title']
);
// Go through each error and tack the data on.
foreach ($context['errors'] as $id => $dummy)
{
$memID = $context['errors'][$id]['member']['id'];
$context['errors'][$id]['member']['username'] = $members[$memID]['member_name'];
$context['errors'][$id]['member']['name'] = $members[$memID]['real_name'];
$context['errors'][$id]['member']['href'] = empty($memID) ? '' : $scripturl . '?action=profile;u=' . $memID;
$context['errors'][$id]['member']['link'] = empty($memID) ? $txt['guest_title'] : '<a href="' . $scripturl . '?action=profile;u=' . $memID . '">' . $context['errors'][$id]['member']['name'] . '</a>';
}
}
// Filtering anything?
if (isset($filter))
{
$context['filter'] = &$filter;
// Set the filtering context.
if ($filter['variable'] == 'id_member')
{
$id = $filter['value']['sql'];
loadMemberData($id, false, 'minimal');
$context['filter']['value']['html'] = '<a href="' . $scripturl . '?action=profile;u=' . $id . '">' . (isset($user_profile[$id]['real_name']) ? $user_profile[$id]['real_name'] : $txt['guest']) . '</a>';
}
elseif ($filter['variable'] == 'url')
$context['filter']['value']['html'] = '\'' . strtr($smcFunc['htmlspecialchars']((substr($filter['value']['sql'], 0, 1) == '?' ? $scripturl : '') . $filter['value']['sql']), array('\_' => '_')) . '\'';
elseif ($filter['variable'] == 'message')
{
$context['filter']['value']['html'] = '\'' . strtr($smcFunc['htmlspecialchars']($filter['value']['sql']), array("\n" => '<br>', '&lt;br /&gt;' => '<br>', "\t" => '&nbsp;&nbsp;&nbsp;', '\_' => '_', '\\%' => '%', '\\\\' => '\\')) . '\'';
$context['filter']['value']['html'] = preg_replace('~&amp;lt;span class=&amp;quot;remove&amp;quot;&amp;gt;(.+?)&amp;lt;/span&amp;gt;~', '$1', $context['filter']['value']['html']);
}
elseif ($filter['variable'] == 'error_type')
{
$context['filter']['value']['html'] = '\'' . strtr($smcFunc['htmlspecialchars']($filter['value']['sql']), array("\n" => '<br>', '&lt;br /&gt;' => '<br>', "\t" => '&nbsp;&nbsp;&nbsp;', '\_' => '_', '\\%' => '%', '\\\\' => '\\')) . '\'';
}
else
$context['filter']['value']['html'] = &$filter['value']['sql'];
}
$context['error_types'] = array();
$context['error_types']['all'] = array(
'label' => $txt['errortype_all'],
'error_type' => 'all',
'description' => isset($txt['errortype_all_desc']) ? $txt['errortype_all_desc'] : '',
'url' => $scripturl . '?action=admin;area=logs;sa=errorlog' . ($context['sort_direction'] == 'down' ? ';desc' : ''),
'is_selected' => empty($filter),
);
$sum = 0;
// What type of errors do we have and how many do we have?
$request = $smcFunc['db_query']('', '
SELECT error_type, COUNT(*) AS num_errors
FROM {db_prefix}log_errors
GROUP BY error_type
ORDER BY error_type = {string:critical_type} DESC, error_type ASC',
array(
'critical_type' => 'critical',
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// Total errors so far?
$sum += $row['num_errors'];
$context['error_types'][$sum] = array(
'label' => (isset($txt['errortype_' . $row['error_type']]) ? $txt['errortype_' . $row['error_type']] : $row['error_type']) . ' (' . $row['num_errors'] . ')',
'error_type' => $row['error_type'],
'description' => isset($txt['errortype_' . $row['error_type'] . '_desc']) ? $txt['errortype_' . $row['error_type'] . '_desc'] : '',
'url' => $scripturl . '?action=admin;area=logs;sa=errorlog' . ($context['sort_direction'] == 'down' ? ';desc' : '') . ';filter=error_type;value=' . $row['error_type'],
'is_selected' => isset($filter) && $filter['value']['sql'] == $smcFunc['db_escape_wildcard_string']($row['error_type']),
);
}
$smcFunc['db_free_result']($request);
// Update the all errors tab with the total number of errors
$context['error_types']['all']['label'] .= ' (' . $sum . ')';
// Finally, work out what is the last tab!
if (isset($context['error_types'][$sum]))
$context['error_types'][$sum]['is_last'] = true;
else
$context['error_types']['all']['is_last'] = true;
// And this is pretty basic ;).
$context['page_title'] = $txt['errorlog'];
$context['has_filter'] = isset($filter);
$context['sub_template'] = 'error_log';
createToken('admin-el');
}
/**
* Delete all or some of the errors in the error log.
* It applies any necessary filters to deletion.
* This should only be called by ViewErrorLog().
* It attempts to TRUNCATE the table to reset the auto_increment.
* Redirects back to the error log when done.
*/
function deleteErrors()
{
global $filter, $smcFunc;
// Make sure the session exists and is correct; otherwise, might be a hacker.
checkSession();
validateToken('admin-el');
// Delete all or just some?
if (isset($_POST['delall']) && !isset($filter))
$smcFunc['db_query']('truncate_table', '
TRUNCATE {db_prefix}log_errors',
array(
)
);
// Deleting all with a filter?
elseif (isset($_POST['delall']) && isset($filter))
{
// ip need a different placeholder type
$filter_type = $filter['variable'] == 'ip'? 'inet' : 'string';
$filter_op = $filter['variable'] == 'ip'? '=' : 'LIKE';
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_errors
WHERE ' . $filter['variable'] . ' ' . $filter_op . ' {' . $filter_type . ':filter}',
array(
'filter' => $filter['value']['sql'],
)
);
}
// Just specific errors?
elseif (!empty($_POST['delete']))
{
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}log_errors
WHERE id_error IN ({array_int:error_list})',
array(
'error_list' => array_unique($_POST['delete']),
)
);
// Go back to where we were.
redirectexit('action=admin;area=logs;sa=errorlog' . (isset($_REQUEST['desc']) ? ';desc' : '') . ';start=' . $_GET['start'] . (isset($filter) ? ';filter=' . $_GET['filter'] . ';value=' . $_GET['value'] : ''));
}
// Back to the error log!
redirectexit('action=admin;area=logs;sa=errorlog' . (isset($_REQUEST['desc']) ? ';desc' : ''));
}
/**
* View a file specified in $_REQUEST['file'], with php highlighting on it
* Preconditions:
* - file must be readable,
* - full file path must be base64 encoded,
* - user must have admin_forum permission.
* The line number number is specified by $_REQUEST['line']...
* The function will try to get the 20 lines before and after the specified line.
*/
function ViewFile()
{
global $context, $boarddir, $sourcedir, $cachedir, $smcFunc;
// Check for the administrative permission to do this.
isAllowedTo('admin_forum');
// Decode the file and get the line
$file = realpath(base64_decode($_REQUEST['file']));
$real_board = realpath($boarddir);
$real_source = realpath($sourcedir);
$real_cache = realpath($cachedir);
$basename = strtolower(basename($file));
$ext = strrchr($basename, '.');
$line = isset($_REQUEST['line']) ? (int) $_REQUEST['line'] : 0;
// Make sure the file we are looking for is one they are allowed to look at
if ($ext != '.php' || (strpos($file, $real_board) === false && strpos($file, $real_source) === false) || ($basename == 'settings.php' || $basename == 'settings_bak.php') || strpos($file, $real_cache) !== false || !is_readable($file))
fatal_lang_error('error_bad_file', true, array($smcFunc['htmlspecialchars']($file)));
// get the min and max lines
$min = $line - 20 <= 0 ? 1 : $line - 20;
$max = $line + 21; // One additional line to make everything work out correctly
if ($max <= 0 || $min >= $max)
fatal_lang_error('error_bad_line');
$file_data = explode('<br />', highlight_php_code($smcFunc['htmlspecialchars'](implode('', file($file)))));
// We don't want to slice off too many so lets make sure we stop at the last one
$max = min($max, max(array_keys($file_data)));
$file_data = array_slice($file_data, $min - 1, $max - $min);
$context['file_data'] = array(
'contents' => $file_data,
'min' => $min,
'target' => $line,
'file' => strtr($file, array('"' => '\\"')),
);
loadTemplate('Errors');
$context['template_layers'] = array();
$context['sub_template'] = 'show_file';
}
/**
* View a backtrace specified in $_REQUEST['backtrace'], with php highlighting on it
* Preconditions:
* - user must have admin_forum permission.
*/
function ViewBacktrace()
{
global $context, $smcFunc, $scripturl;
// Check for the administrative permission to do this.
isAllowedTo('admin_forum');
$id_error = (int) $_REQUEST['backtrace'];
$request = $smcFunc['db_query']('', '
SELECT backtrace, error_type, message, file, line, url
FROM {db_prefix}log_errors
WHERE id_error = {int:id_error}',
array(
'id_error' => $id_error,
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
$context['error_info'] = $row;
$context['error_info']['url'] = $scripturl . $row['url'];
$context['error_info']['backtrace'] = $smcFunc['json_decode']($row['backtrace']);
}
$smcFunc['db_free_result']($request);
loadCSSFile('admin.css', array(), 'smf_admin');
loadTemplate('Errors');
loadLanguage('ManageMaintenance');
$context['template_layers'] = array();
$context['sub_template'] = 'show_backtrace';
}
?>

1763
Sources/ManageLanguages.php Normal file

File diff suppressed because it is too large Load diff

526
Sources/ManageMail.php Normal file
View file

@ -0,0 +1,526 @@
<?php
/**
* This file is all about mail, how we love it so. In particular it handles the admin side of
* mail configuration, as well as reviewing the mail queue - if enabled.
*
* @todo refactor as controller-model.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/
if (!defined('SMF'))
die('No direct access...');
/**
* Main dispatcher. This function checks permissions and passes control through to the relevant section.
*/
function ManageMail()
{
global $context, $txt, $sourcedir;
// You need to be an admin to edit settings!
isAllowedTo('admin_forum');
loadLanguage('Help');
loadLanguage('ManageMail');
// We'll need the utility functions from here.
require_once($sourcedir . '/ManageServer.php');
$context['page_title'] = $txt['mailqueue_title'];
$context['sub_template'] = 'show_settings';
$subActions = array(
'browse' => 'BrowseMailQueue',
'clear' => 'ClearMailQueue',
'settings' => 'ModifyMailSettings',
'test' => 'TestMailSend',
);
call_integration_hook('integrate_manage_mail', array(&$subActions));
// By default we want to browse
$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'browse';
$context['sub_action'] = $_REQUEST['sa'];
// Load up all the tabs...
$context[$context['admin_menu_name']]['tab_data'] = array(
'title' => $txt['mailqueue_title'],
'help' => '',
'description' => $txt['mailqueue_desc'],
);
// Call the right function for this sub-action.
call_helper($subActions[$_REQUEST['sa']]);
}
/**
* Display the mail queue...
*/
function BrowseMailQueue()
{
global $scripturl, $context, $txt, $smcFunc;
global $sourcedir, $modSettings;
// First, are we deleting something from the queue?
if (isset($_REQUEST['delete']))
{
checkSession();
$smcFunc['db_query']('', '
DELETE FROM {db_prefix}mail_queue
WHERE id_mail IN ({array_int:mail_ids})',
array(
'mail_ids' => $_REQUEST['delete'],
)
);
}
// How many items do we have?
$request = $smcFunc['db_query']('', '
SELECT COUNT(*) AS queue_size, MIN(time_sent) AS oldest
FROM {db_prefix}mail_queue',
array(
)
);
list ($mailQueueSize, $mailOldest) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
$context['oldest_mail'] = empty($mailOldest) ? $txt['mailqueue_oldest_not_available'] : time_since(time() - $mailOldest);
$context['mail_queue_size'] = comma_format($mailQueueSize);
$listOptions = array(
'id' => 'mail_queue',
'title' => $txt['mailqueue_browse'],
'items_per_page' => $modSettings['defaultMaxListItems'],
'base_href' => $scripturl . '?action=admin;area=mailqueue',
'default_sort_col' => 'age',
'no_items_label' => $txt['mailqueue_no_items'],
'get_items' => array(
'function' => 'list_getMailQueue',
),
'get_count' => array(
'function' => 'list_getMailQueueSize',
),
'columns' => array(
'subject' => array(
'header' => array(
'value' => $txt['mailqueue_subject'],
),
'data' => array(
'function' => function($rowData) use ($smcFunc)
{
return $smcFunc['strlen']($rowData['subject']) > 50 ? sprintf('%1$s...', $smcFunc['htmlspecialchars']($smcFunc['substr']($rowData['subject'], 0, 47))) : $smcFunc['htmlspecialchars']($rowData['subject']);
},
'class' => 'smalltext',
),
'sort' => array(
'default' => 'subject',
'reverse' => 'subject DESC',
),
),
'recipient' => array(
'header' => array(
'value' => $txt['mailqueue_recipient'],
),
'data' => array(
'sprintf' => array(
'format' => '<a href="mailto:%1$s">%1$s</a>',
'params' => array(
'recipient' => true,
),
),
'class' => 'smalltext',
),
'sort' => array(
'default' => 'recipient',
'reverse' => 'recipient DESC',
),
),
'priority' => array(
'header' => array(
'value' => $txt['mailqueue_priority'],
),
'data' => array(
'function' => function($rowData) use ($txt)
{
// We probably have a text label with your priority.
$txtKey = sprintf('mq_mpriority_%1$s', $rowData['priority']);
// But if not, revert to priority 0.
return isset($txt[$txtKey]) ? $txt[$txtKey] : $txt['mq_mpriority_1'];
},
'class' => 'smalltext',
),
'sort' => array(
'default' => 'priority',
'reverse' => 'priority DESC',
),
),
'age' => array(
'header' => array(
'value' => $txt['mailqueue_age'],
),
'data' => array(
'function' => function($rowData)
{
return time_since(time() - $rowData['time_sent']);
},
'class' => 'smalltext',
),
'sort' => array(
'default' => 'time_sent',
'reverse' => 'time_sent DESC',
),
),
'check' => array(
'header' => array(
'value' => '<input type="checkbox" onclick="invertAll(this, this.form);">',
),
'data' => array(
'function' => function($rowData)
{
return '<input type="checkbox" name="delete[]" value="' . $rowData['id_mail'] . '">';
},
'class' => 'smalltext',
),
),
),
'form' => array(
'href' => $scripturl . '?action=admin;area=mailqueue',
'include_start' => true,
'include_sort' => true,
),
'additional_rows' => array(
array(
'position' => 'top_of_list',
'value' => '<input type="submit" name="delete_redirects" value="' . $txt['quickmod_delete_selected'] . '" data-confirm="' . $txt['quickmod_confirm'] . '" class="button you_sure"><a class="button you_sure" href="' . $scripturl . '?action=admin;area=mailqueue;sa=clear;' . $context['session_var'] . '=' . $context['session_id'] . '" data-confirm="' . $txt['mailqueue_clear_list_warning'] . '">' . $txt['mailqueue_clear_list'] . '</a> ',
),
array(
'position' => 'bottom_of_list',
'value' => '<input type="submit" name="delete_redirects" value="' . $txt['quickmod_delete_selected'] . '" data-confirm="' . $txt['quickmod_confirm'] . '" class="button you_sure"><a class="button you_sure" href="' . $scripturl . '?action=admin;area=mailqueue;sa=clear;' . $context['session_var'] . '=' . $context['session_id'] . '" data-confirm="' . $txt['mailqueue_clear_list_warning'] . '">' . $txt['mailqueue_clear_list'] . '</a> ',
),
),
);
require_once($sourcedir . '/Subs-List.php');
createList($listOptions);
loadTemplate('ManageMail');
$context['sub_template'] = 'browse';
}
/**
* This function grabs the mail queue items from the database, according to the params given.
* Callback for $listOptions['get_items'] in BrowseMailQueue()
*
* @param int $start The item to start with (for pagination purposes)
* @param int $items_per_page How many items to show on each page
* @param string $sort A string indicating how to sort the results
* @return array An array with info about the mail queue items
*/
function list_getMailQueue($start, $items_per_page, $sort)
{
global $smcFunc, $txt;
$request = $smcFunc['db_query']('', '
SELECT
id_mail, time_sent, recipient, priority, private, subject
FROM {db_prefix}mail_queue
ORDER BY {raw:sort}
LIMIT {int:start}, {int:items_per_page}',
array(
'start' => $start,
'sort' => $sort,
'items_per_page' => $items_per_page,
)
);
$mails = array();
while ($row = $smcFunc['db_fetch_assoc']($request))
{
// Private PM/email subjects and similar shouldn't be shown in the mailbox area.
if (!empty($row['private']))
$row['subject'] = $txt['personal_message'];
$mails[] = $row;
}
$smcFunc['db_free_result']($request);
return $mails;
}
/**
* Returns the total count of items in the mail queue.
* Callback for $listOptions['get_count'] in BrowseMailQueue
*
* @return int The total number of mail queue items
*/
function list_getMailQueueSize()
{
global $smcFunc;
// How many items do we have?
$request = $smcFunc['db_query']('', '
SELECT COUNT(*) AS queue_size
FROM {db_prefix}mail_queue',
array(
)
);
list ($mailQueueSize) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
return $mailQueueSize;
}
/**
* Allows to view and modify the mail settings.
*
* @param bool $return_config Whether to return the $config_vars array (used for admin search)
* @return void|array Returns nothing or returns the $config_vars array if $return_config is true
*/
function ModifyMailSettings($return_config = false)
{
global $txt, $scripturl, $context, $modSettings, $txtBirthdayEmails;
loadLanguage('EmailTemplates');
$body = $txtBirthdayEmails[(empty($modSettings['birthday_email']) ? 'happy_birthday' : $modSettings['birthday_email']) . '_body'];
$subject = $txtBirthdayEmails[(empty($modSettings['birthday_email']) ? 'happy_birthday' : $modSettings['birthday_email']) . '_subject'];
$emails = array();
$processedBirthdayEmails = array();
foreach ($txtBirthdayEmails as $key => $value)
{
$index = substr($key, 0, strrpos($key, '_'));
$element = substr($key, strrpos($key, '_') + 1);
$processedBirthdayEmails[$index][$element] = $value;
}
foreach ($processedBirthdayEmails as $index => $dummy)
$emails[$index] = $index;
$config_vars = array(
// Mail queue stuff, this rocks ;)
array('int', 'mail_limit', 'subtext' => $txt['zero_to_disable']),
array('int', 'mail_quantity'),
'',
// SMTP stuff.
array('select', 'mail_type', array($txt['mail_type_default'], 'SMTP', 'SMTP - STARTTLS')),
array('text', 'smtp_host'),
array('text', 'smtp_port'),
array('text', 'smtp_username'),
array('password', 'smtp_password'),
'',
array('select', 'birthday_email', $emails, 'value' => array('subject' => $subject, 'body' => $body), 'javascript' => 'onchange="fetch_birthday_preview()"'),
'birthday_subject' => array('var_message', 'birthday_subject', 'var_message' => $processedBirthdayEmails[empty($modSettings['birthday_email']) ? 'happy_birthday' : $modSettings['birthday_email']]['subject'], 'disabled' => true, 'size' => strlen($subject) + 3),
'birthday_body' => array('var_message', 'birthday_body', 'var_message' => nl2br($body), 'disabled' => true, 'size' => ceil(strlen($body) / 25)),
);
call_integration_hook('integrate_modify_mail_settings', array(&$config_vars));
if ($return_config)
return $config_vars;
// Saving?
if (isset($_GET['save']))
{
// Make the SMTP password a little harder to see in a backup etc.
if (!empty($_POST['smtp_password'][1]))
{
$_POST['smtp_password'][0] = base64_encode($_POST['smtp_password'][0]);
$_POST['smtp_password'][1] = base64_encode($_POST['smtp_password'][1]);
}
checkSession();
// We don't want to save the subject and body previews.
unset($config_vars['birthday_subject'], $config_vars['birthday_body']);
call_integration_hook('integrate_save_mail_settings');
saveDBSettings($config_vars);
redirectexit('action=admin;area=mailqueue;sa=settings');
}
$context['post_url'] = $scripturl . '?action=admin;area=mailqueue;save;sa=settings';
$context['settings_title'] = $txt['mailqueue_settings'];
prepareDBSettingContext($config_vars);
$context['settings_insert_above'] = '
<script>
var bDay = {';
$i = 0;
foreach ($processedBirthdayEmails as $index => $email)
{
$is_last = ++$i == count($processedBirthdayEmails);
$context['settings_insert_above'] .= '
' . $index . ': {
subject: ' . JavaScriptEscape($email['subject']) . ',
body: ' . JavaScriptEscape(nl2br($email['body'])) . '
}' . (!$is_last ? ',' : '');
}
$context['settings_insert_above'] .= '
};
function fetch_birthday_preview()
{
var index = document.getElementById(\'birthday_email\').value;
document.getElementById(\'birthday_subject\').innerHTML = bDay[index].subject;
document.getElementById(\'birthday_body\').innerHTML = bDay[index].body;
}
</script>';
}
/**
* This function clears the mail queue of all emails, and at the end redirects to browse.
*/
function ClearMailQueue()
{
global $sourcedir, $smcFunc;
checkSession('get');
// This is certainly needed!
require_once($sourcedir . '/ScheduledTasks.php');
// If we don't yet have the total to clear, find it.
if (!isset($_GET['te']))
{
// How many items do we have?
$request = $smcFunc['db_query']('', '
SELECT COUNT(*) AS queue_size
FROM {db_prefix}mail_queue',
array(
)
);
list ($_GET['te']) = $smcFunc['db_fetch_row']($request);
$smcFunc['db_free_result']($request);
}
else
$_GET['te'] = (int) $_GET['te'];
$_GET['sent'] = isset($_GET['sent']) ? (int) $_GET['sent'] : 0;
// Send 50 at a time, then go for a break...
while (ReduceMailQueue(50, true, true) === true)
{
// Sent another 50.
$_GET['sent'] += 50;
pauseMailQueueClear();
}
return BrowseMailQueue();
}
/**
* Used for pausing the mail queue.
*/
function pauseMailQueueClear()
{
global $context, $txt;
// Try get more time...
@set_time_limit(600);
if (function_exists('apache_reset_timeout'))
@apache_reset_timeout();
// Have we already used our maximum time?
if ((time() - TIME_START) < 5)
return;
$context['continue_get_data'] = '?action=admin;area=mailqueue;sa=clear;te=' . $_GET['te'] . ';sent=' . $_GET['sent'] . ';' . $context['session_var'] . '=' . $context['session_id'];
$context['page_title'] = $txt['not_done_title'];
$context['continue_post_data'] = '';
$context['continue_countdown'] = '2';
$context['sub_template'] = 'not_done';
// Keep browse selected.
$context['selected'] = 'browse';
// What percent through are we?
$context['continue_percent'] = round(($_GET['sent'] / $_GET['te']) * 100, 1);
// Never more than 100%!
$context['continue_percent'] = min($context['continue_percent'], 100);
obExit();
}
/**
* Test mail sending ability.
*
*/
function TestMailSend()
{
global $scripturl, $context, $sourcedir, $user_info, $smcFunc;
loadLanguage('ManageMail');
loadTemplate('ManageMail');
$context['sub_template'] = 'mailtest';
$context['base_url'] = $scripturl . '?action=admin;area=mailqueue;sa=test';
$context['post_url'] = $context['base_url'] . ';save';
// Sending the test message now.
if (isset($_GET['save']))
{
require_once($sourcedir . '/Subs-Post.php');
// Send to the current user, no options.
$to = $user_info['email'];
$subject = $smcFunc['htmlspecialchars']($_POST['subject']);
$message = $smcFunc['htmlspecialchars']($_POST['message']);
$result = sendmail($to, $subject, $message, null, null, false, 0);
redirectexit($context['base_url'] . ';result=' . ($result ? 'success' : 'failure'));
}
// The result.
if (isset($_GET['result']))
$context['result'] = ($_GET['result'] == 'success' ? 'success' : 'failure');
}
/**
* Little utility function to calculate how long ago a time was.
*
* @param int $time_diff The time difference, in seconds
* @return string A string indicating how many days, hours, minutes or seconds (depending on $time_diff)
*/
function time_since($time_diff)
{
global $txt;
if ($time_diff < 0)
$time_diff = 0;
// Just do a bit of an if fest...
if ($time_diff > 86400)
{
$days = round($time_diff / 86400, 1);
return sprintf($days == 1 ? $txt['mq_day'] : $txt['mq_days'], $time_diff / 86400);
}
// Hours?
elseif ($time_diff > 3600)
{
$hours = round($time_diff / 3600, 1);
return sprintf($hours == 1 ? $txt['mq_hour'] : $txt['mq_hours'], $hours);
}
// Minutes?
elseif ($time_diff > 60)
{
$minutes = (int) ($time_diff / 60);
return sprintf($minutes == 1 ? $txt['mq_minute'] : $txt['mq_minutes'], $minutes);
}
// Otherwise must be second
else
return sprintf($time_diff == 1 ? $txt['mq_second'] : $txt['mq_seconds'], $time_diff);
}
?>

Some files were not shown because too many files have changed in this diff Show more