refactor: remove legacy files and update logo in the project (#2024)
Some checks are pending
Deploy Documentation to GitHub Pages / Build Documentation (push) Waiting to run
Deploy Documentation to GitHub Pages / Deploy to GitHub Pages (push) Blocked by required conditions

- Deleted outdated legacy files including PHP scripts and documentation to streamline the project structure.
- Introduced a new logo SVG file while removing the old logo, ensuring a fresh visual identity for the project.
- Updated the app logo component to reflect the new branding.

These changes aim to modernize the project and enhance its overall presentation.
This commit is contained in:
Yury Pikhtarev 2025-07-01 23:10:28 +04:00 committed by GitHub
parent ccf2400450
commit 7f79ad5217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 38 additions and 8298 deletions

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="603px" height="685px" viewBox="0 0 603 685" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
<title>hexagon/logos/blue/logos-hexagon-blue</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0%" y1="0%" x2="98.8868215%" y2="97.9173437%" id="linearGradient-1">
<stop stop-color="#FFF690" offset="0%"></stop>
<stop stop-color="#F73265" offset="100%"></stop>
</linearGradient>
<linearGradient x1="3.31094268%" y1="3.27848249%" x2="96.3504029%" y2="95.405792%" id="linearGradient-2">
<stop stop-color="#90F1FF" offset="0%"></stop>
<stop stop-color="#5889E2" offset="63.1111018%"></stop>
<stop stop-color="#3255F7" offset="100%"></stop>
</linearGradient>
<path d="M299.000683,3.7151298 L560.496431,154.769267 L560.496431,154.769267 C568.228676,159.235832 572.991495,167.487448 572.991495,176.417052 L572.991495,478.548125 L572.991495,478.548125 C572.991495,487.477729 568.228676,495.729345 560.496431,500.19591 L299.000683,651.250047 L299.000683,651.250047 C291.263148,655.719667 281.728347,655.719667 273.990812,651.250047 L12.4950645,500.19591 L12.4950645,500.19591 C4.76281902,495.729345 2.34479103e-13,487.477729 2.13162821e-13,478.548125 L8.52651283e-14,176.417052 L1.13686838e-13,176.417052 C9.23705556e-14,167.487448 4.76281902,159.235832 12.4950645,154.769267 L273.990812,3.7151298 L273.990812,3.7151298 C281.728347,-0.754490431 291.263148,-0.754490431 299.000683,3.7151298 Z" id="path-3"></path>
<linearGradient x1="9.43044958%" y1="-1.96493998%" x2="92.6401197%" y2="103.463939%" id="linearGradient-5">
<stop stop-color="#6B6FF6" offset="0%"></stop>
<stop stop-color="#463ECE" offset="63.6965346%"></stop>
<stop stop-color="#2525B1" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Logos-hexagon-" transform="translate(-1394.000000, -1179.000000)">
<g id="blue-hex" transform="translate(1409.000000, 1194.000000)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<use id="hexagon" fill="url(#linearGradient-2)" xlink:href="#path-3"></use>
<path d="M35.6474524,252.011052 C57.5792716,248.936263 73.5006627,246.936786 83.4116256,246.012623 C85.3483447,245.83203 93.5371578,260.863118 107.978065,291.105887 C105.585122,265.157924 100.947006,240.335538 94.0637153,216.638728 C87.180425,192.941918 79.3422018,173.038673 70.5490457,156.928991 C87.4778221,169.993303 102.28373,184.042325 114.966768,199.076057 C133.991326,221.626655 146.215857,240.097062 150.524568,239.179957 C174.371981,234.104053 288.708722,205.850827 493.534794,154.420279 L493.534793,154.420279 C494.494688,154.179256 495.486516,154.676533 495.867628,155.589903 L619.500024,451.886805 L602.369475,479.569058 C564.419303,575.284459 544.273883,629.355903 541.933213,641.783391 C539.592543,654.210878 545.635807,675.013029 560.063006,704.189842 C523.018243,685.238792 495.212531,669.468619 476.645872,656.879326 C458.079212,644.290032 435.741013,626.059666 409.631273,602.188228 C405.966733,591.806549 403.394451,583.110518 401.914426,576.100135 C400.501307,569.406665 398.360528,559.192058 395.492088,545.456313 L395.492088,545.456313 C395.226816,544.186041 394.478074,543.06866 393.404087,542.340296 C385.443102,536.941267 379.706378,533.394853 376.193916,531.701054 C373.020556,530.17078 364.938681,524.983896 345.087223,519.981246 C332.411458,516.7869 323.406519,516.652869 318.072405,519.579152 L318.072405,519.579152 C317.640435,519.81613 317.31114,520.20422 317.147659,520.669012 L308.992849,543.853801 L308.992849,543.853801 C308.698801,544.689805 307.889655,545.232614 307.004588,545.18761 C270.042109,543.308142 242.155663,543.424767 223.345249,545.537486 C219.109154,546.013269 202.688596,544.667744 174.083243,546.743522 C155.013007,548.127375 138.07571,549.786593 123.271353,551.721178 L118.809238,561.731591 L77.0200202,537.740979 L87.8210762,514.452203 L87.8210762,514.452203 C88.518817,512.947764 90.1765855,512.139506 91.7915654,512.51636 L119.138093,518.897647 C136.551056,515.564583 152.595231,512.671095 167.270617,510.217183 C181.946003,507.763271 199.64439,505.507123 220.365777,503.448739 C215.316545,482.887324 215.229057,462.529137 220.103312,442.374177 C224.977567,422.219217 231.315081,398.523411 239.115852,371.286759 C221.621355,394.225102 210.65524,413.24527 206.217505,428.347263 C202.145687,442.20401 199.958233,452.962045 199.655142,460.621369 L199.655142,460.621369 C199.611622,461.721142 198.687679,462.579072 197.587701,462.541097 L106.237337,459.387391 L106.237337,459.387391 C105.329728,459.356058 104.557159,458.71715 104.356016,457.831555 C93.9445572,411.991836 84.8381737,377.818415 77.0368653,355.311293 C69.2916535,332.966011 54.9838285,299.475177 34.1133904,254.83879 L34.1133901,254.83879 C33.6455452,253.838192 34.0774264,252.647785 35.0780239,252.17994 C35.2583697,252.095616 35.4502951,252.038693 35.6474524,252.011052 Z" id="Combined-Shape-Copy" fill="url(#linearGradient-5)" mask="url(#mask-4)"></path>
<path d="M116.582959,519.883342 C119.761882,522.308731 122.567273,526.175894 124.999134,531.484832 C127.430996,536.79377 128.909309,542.311044 129.434074,548.036655 C129.73578,539.570689 129.449088,533.143889 128.573997,528.756255 C127.698906,524.368621 126.05691,520.474513 123.648009,517.07393 L116.582959,519.883342 Z" id="sh-hooves" fill="#2C21A0" mask="url(#mask-4)" transform="translate(123.066530, 532.555292) rotate(11.000000) translate(-123.066530, -532.555292) "></path>
<path d="M316.569498,522.22129 C323.489134,496.262444 328.890477,477.943871 332.773526,467.265572 C336.656575,456.587272 343.082322,442.415045 352.050767,424.74889 C343.258067,449.09874 337.094046,469.65694 333.558704,486.423491 C330.023362,503.190043 328.114151,513.536824 327.831072,517.463836 C324.951591,517.671974 322.888557,518.039353 321.641973,518.565973 C320.395388,519.092593 318.704563,520.311032 316.569498,522.22129 Z" id="sh-legs" fill="#2C21A0" mask="url(#mask-4)"></path>
<path d="M396.438203,586.255751 C405.641322,559.576572 412.845211,539.870761 418.049868,527.138317 C419.711645,523.07302 421.349402,519.645761 422.963139,516.85654 L426.467276,521.486323 C425.843333,523.530421 425.297768,525.226927 424.83058,526.575841 C421.646838,535.768287 419.216279,548.401403 417.538904,564.475188 C412.769203,567.026606 409.254076,569.469009 406.993523,571.802396 C404.73297,574.135784 401.21453,578.953569 396.438203,586.255751 Z" id="sh-belly" fill="#2C21A0" mask="url(#mask-4)" transform="translate(411.452740, 551.556145) rotate(97.000000) translate(-411.452740, -551.556145) "></path>
</g>
<g id="NAME" transform="translate(350.000000, -8.000000)"></g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7 KiB

37
docs/static/img/logos.svg vendored Normal file
View file

@ -0,0 +1,37 @@
<svg width="603" height="685" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="3.311%" y1="3.278%" x2="96.35%" y2="95.406%" id="b">
<stop stop-color="#90F1FF" offset="0%"/>
<stop stop-color="#5889E2" offset="63.111%"/>
<stop stop-color="#3255F7" offset="100%"/>
</linearGradient>
<linearGradient x1="9.43%" y1="-1.965%" x2="92.64%" y2="103.464%" id="c">
<stop stop-color="#6B6FF6" offset="0%"/>
<stop stop-color="#463ECE" offset="63.697%"/>
<stop stop-color="#2525B1" offset="100%"/>
</linearGradient>
<path
d="M299 3.715 560.497 154.77a25 25 0 0 1 12.495 21.648v302.131a25 25 0 0 1-12.495 21.648L299.001 651.25a25 25 0 0 1-25.01 0L12.495 500.196A25 25 0 0 1 0 478.548v-302.13a25 25 0 0 1 12.495-21.649L273.991 3.715a25 25 0 0 1 25.01 0z"
id="a"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(15 15)">
<mask id="d" fill="#fff">
<use xlink:href="#a"/>
</mask>
<use fill="url(#b)" xlink:href="#a"/>
<path
d="M35.647 252.011c21.932-3.075 37.854-5.074 47.765-5.998 1.936-.181 10.125 14.85 24.566 45.093-2.393-25.948-7.031-50.77-13.914-74.467-6.884-23.697-14.722-43.6-23.515-59.71 16.929 13.064 31.735 27.113 44.418 42.147 19.024 22.55 31.249 41.021 35.558 40.104 23.847-5.076 138.184-33.33 343.01-84.76a2 2 0 0 1 2.333 1.17L619.5 451.887l-17.13 27.682c-37.95 95.715-58.096 149.787-60.437 162.214-2.34 12.428 3.703 33.23 18.13 62.407-37.045-18.951-64.85-34.721-83.417-47.31-18.567-12.59-40.905-30.82-67.015-54.692-3.664-10.381-6.237-19.077-7.717-26.088-1.413-6.693-3.553-16.908-6.422-30.644a5 5 0 0 0-2.088-3.116c-7.96-5.399-13.698-8.945-17.21-10.639-3.173-1.53-11.255-6.717-31.107-11.72-12.676-3.194-21.68-3.328-27.015-.402a2 2 0 0 0-.924 1.09l-8.155 23.185a2 2 0 0 1-1.988 1.334c-36.963-1.88-64.85-1.763-83.66.35-4.236.475-20.656-.87-49.262 1.206-19.07 1.383-36.007 3.043-50.812 4.977l-4.462 10.01-41.789-23.99 10.801-23.289a3.5 3.5 0 0 1 3.97-1.936l27.347 6.382c17.413-3.333 33.457-6.227 48.133-8.68 14.675-2.455 32.373-4.71 53.095-6.77-5.05-20.56-5.137-40.919-.263-61.074 4.875-20.155 11.212-43.85 19.013-71.087-17.495 22.938-28.46 41.958-32.898 57.06-4.072 13.857-6.26 24.615-6.563 32.274a2 2 0 0 1-2.067 1.92l-91.35-3.154a2 2 0 0 1-1.882-1.555c-10.411-45.84-19.518-80.014-27.32-102.52-7.744-22.346-22.052-55.837-42.923-100.473a2 2 0 0 1 1.534-2.828z"
fill="url(#c)" mask="url(#d)"/>
<path
d="M116.583 519.883c3.179 2.426 5.984 6.293 8.416 11.602 2.432 5.309 3.91 10.826 4.435 16.552.302-8.466.015-14.893-.86-19.28-.875-4.388-2.517-8.282-4.926-11.683l-7.065 2.81z"
fill="#2C21A0" mask="url(#d)" transform="rotate(11 123.067 532.555)"/>
<path
d="M316.57 522.221c6.92-25.959 12.32-44.277 16.204-54.955 3.883-10.679 10.308-24.851 19.277-42.517-8.793 24.35-14.957 44.908-18.492 61.674-3.536 16.767-5.445 27.114-5.728 31.04-2.88.209-4.942.576-6.189 1.103-1.247.527-2.937 1.745-5.073 3.655z"
fill="#2C21A0" mask="url(#d)"/>
<path
d="M396.438 586.256c9.203-26.68 16.407-46.385 21.612-59.118 1.662-4.065 3.3-7.492 4.913-10.281l3.504 4.63c-.624 2.043-1.17 3.74-1.636 5.089-3.184 9.192-5.615 21.825-7.292 37.9-4.77 2.55-8.285 4.993-10.545 7.326-2.261 2.334-5.78 7.152-10.556 14.454z"
fill="#2C21A0" mask="url(#d)" transform="rotate(97 411.453 551.556)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,30 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_ROOT', './../');
define('IN_ADMIN', true);
require dirname(__DIR__) . '/common.php';
require ATTACH_DIR . '/attachment_mod.php';
require ATTACH_DIR . '/includes/functions_admin.php';
$user->session_start();
if (IS_GUEST) {
redirect(LOGIN_URL . '?redirect=admin/index.php');
}
if (!IS_ADMIN) {
bb_die($lang['NOT_ADMIN']);
}
if (!$userdata['session_admin']) {
$redirect = url_arg($_SERVER['REQUEST_URI'], 'admin', 1);
redirect(LOGIN_URL . "?redirect=$redirect");
}

View file

@ -1,45 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_SCRIPT', 'ajax');
define('IN_AJAX', true);
require __DIR__ . '/common.php';
// Init Ajax class
$ajax = new TorrentPier\Ajax();
$ajax->init();
// Init userdata
$user->session_start();
// Load actions required modules
switch ($ajax->action) {
case 'view_post':
case 'posts':
case 'post_mod_comment':
require INC_DIR . '/bbcode.php';
break;
case 'view_torrent':
case 'mod_action':
case 'change_tor_status':
case 'change_torrent':
case 'passkey':
require ATTACH_DIR . '/attachment_mod.php';
break;
}
$ajax->exec();
/**
* @deprecated ajax_common
* Dirty class removed from here since 2.2.0
* To add new actions see at src/Ajax.php
*/

View file

@ -1,13 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('START_CRON', true);
define('BB_ROOT', __DIR__ . '/');
require __DIR__ . '/common.php';

View file

@ -1,51 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_SCRIPT', 'info');
require __DIR__ . '/common.php';
// Start session management
$user->session_start();
$info = [];
$htmlDir = LANG_DIR . 'html/';
$show = isset($_REQUEST['show']) ? (string)$_REQUEST['show'] : '';
switch ($show) {
case 'advert':
$info['title'] = $lang['ADVERT'];
$info['src'] = 'advert.html';
break;
case 'copyright_holders':
$info['title'] = $lang['COPYRIGHT_HOLDERS'];
$info['src'] = 'copyright_holders.html';
break;
case 'user_agreement':
$info['title'] = $lang['USER_AGREEMENT'];
$info['src'] = 'user_agreement.html';
break;
default:
case 'not_found':
$info['title'] = $lang['NOT_FOUND'];
$info['src'] = 'not_found.html';
break;
}
$require = is_file($htmlDir . $info['src']) ? ($htmlDir . $info['src']) : false;
$template->assign_vars([
'PAGE_TITLE' => mb_strtoupper($info['title'], DEFAULT_CHARSET),
'REQUIRE' => $require ? file_get_contents($require) : $lang['NOT_FOUND'],
]);
print_page('info.tpl', 'simple');

View file

@ -1,77 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
require ATTACH_DIR . '/includes/functions_includes.php';
require ATTACH_DIR . '/includes/functions_attach.php';
require ATTACH_DIR . '/includes/functions_delete.php';
require ATTACH_DIR . '/includes/functions_thumbs.php';
if (defined('ATTACH_INSTALL')) {
return;
}
/**
* wrapper function for determining the correct language directory
*/
function attach_mod_get_lang($language_file)
{
global $attach_config;
$file = LANG_ROOT_DIR . '/' . config()->get('default_lang') . '/' . $language_file . '.php';
if (file_exists($file)) {
return config()->get('default_lang');
}
$file = LANG_ROOT_DIR . '/' . $attach_config['board_lang'] . '/' . $language_file . '.php';
if (file_exists($file)) {
return $attach_config['board_lang'];
}
bb_die('Attachment mod language file does not exist: language/' . $attach_config['board_lang'] . '/' . $language_file . '.php');
}
/**
* Get attachment mod configuration
*/
function get_config()
{
$attach_config = [];
$sql = 'SELECT * FROM ' . BB_ATTACH_CONFIG;
if (!($result = DB()->sql_query($sql))) {
bb_die('Could not query attachment information');
}
while ($row = DB()->sql_fetchrow($result)) {
$attach_config[$row['config_name']] = trim($row['config_value']);
}
// We assign the original default board language here, because it gets overwritten later with the users default language
$attach_config['board_lang'] = trim(config()->get('default_lang'));
return $attach_config;
}
// Get Attachment Config
$attach_config = [];
if (!$attach_config = CACHE('bb_cache')->get('attach_config')) {
$attach_config = get_config();
CACHE('bb_cache')->set('attach_config', $attach_config, 86400);
}
include ATTACH_DIR . '/displaying.php';
include ATTACH_DIR . '/posting_attachments.php';
$upload_dir = $attach_config['upload_dir'];

View file

@ -1,389 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
/**
* All Attachment Functions needed everywhere
*/
/**
* A simple dectobase64 function
*/
function base64_pack($number)
{
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+-';
$base = strlen($chars);
if ($number > 4096) {
return;
}
if ($number < $base) {
return $chars[$number];
}
$hexval = '';
while ($number > 0) {
$remainder = $number % $base;
if ($remainder < $base) {
$hexval = $chars[$remainder] . $hexval;
}
$number = floor($number / $base);
}
return $hexval;
}
/**
* base64todec function
*/
function base64_unpack($string)
{
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+-';
$base = strlen($chars);
$length = strlen($string);
$number = 0;
for ($i = 1; $i <= $length; $i++) {
$pos = $length - $i;
$operand = strpos($chars, (string)$string[$pos]);
$exponent = $base ** ($i - 1);
$decValue = $operand * $exponent;
$number += $decValue;
}
return $number;
}
/**
* Per Forum based Extension Group Permissions (Encode Number) -> Theoretically up to 158 Forums saveable. :)
* We are using a base of 64, but splitting it to one-char and two-char numbers. :)
*/
function auth_pack($auth_array)
{
$one_char_encoding = '#';
$two_char_encoding = '.';
$one_char = $two_char = false;
$auth_cache = '';
foreach ($auth_array as $i => $iValue) {
$val = base64_pack((int)$auth_array[$i]);
if (strlen($val) == 1 && !$one_char) {
$auth_cache .= $one_char_encoding;
$one_char = true;
} elseif (strlen($val) == 2 && !$two_char) {
$auth_cache .= $two_char_encoding;
$two_char = true;
}
$auth_cache .= $val;
}
return $auth_cache;
}
/**
* Reverse the auth_pack process
*/
function auth_unpack($auth_cache)
{
$one_char_encoding = '#';
$two_char_encoding = '.';
$auth = [];
$auth_len = 1;
for ($pos = 0, $posMax = strlen($auth_cache); $pos < $posMax; $pos += $auth_len) {
$forum_auth = $auth_cache[$pos];
if ($forum_auth == $one_char_encoding) {
$auth_len = 1;
continue;
}
if ($forum_auth == $two_char_encoding) {
$auth_len = 2;
$pos--;
continue;
}
$forum_auth = substr($auth_cache, $pos, $auth_len);
$forum_id = base64_unpack($forum_auth);
$auth[] = (int)$forum_id;
}
return $auth;
}
/**
* Used for determining if Forum ID is authed, please use this Function on all Posting Screens
*/
function is_forum_authed($auth_cache, $check_forum_id)
{
$one_char_encoding = '#';
$two_char_encoding = '.';
if (trim($auth_cache) == '') {
return true;
}
$auth = [];
$auth_len = 1;
for ($pos = 0, $posMax = strlen($auth_cache); $pos < $posMax; $pos += $auth_len) {
$forum_auth = $auth_cache[$pos];
if ($forum_auth == $one_char_encoding) {
$auth_len = 1;
continue;
}
if ($forum_auth == $two_char_encoding) {
$auth_len = 2;
$pos--;
continue;
}
$forum_auth = substr($auth_cache, $pos, $auth_len);
$forum_id = (int)base64_unpack($forum_auth);
if ($forum_id == $check_forum_id) {
return true;
}
}
return false;
}
/**
* Deletes an Attachment
*/
function unlink_attach($filename, $mode = false)
{
global $upload_dir, $attach_config;
$filename = basename($filename);
if ($mode == MODE_THUMBNAIL) {
$filename = $upload_dir . '/' . THUMB_DIR . '/t_' . $filename;
} else {
$filename = $upload_dir . '/' . $filename;
}
return @unlink($filename);
}
/**
* Physical Filename stored already ?
*/
function physical_filename_already_stored($filename)
{
if ($filename == '') {
return false;
}
$filename = basename($filename);
$sql = 'SELECT attach_id
FROM ' . BB_ATTACHMENTS_DESC . "
WHERE physical_filename = '" . DB()->escape($filename) . "'
LIMIT 1";
if (!($result = DB()->sql_query($sql))) {
bb_die('Could not get attachment information for filename: ' . htmlspecialchars($filename));
}
$num_rows = DB()->num_rows($result);
DB()->sql_freeresult($result);
return $num_rows != 0;
}
/**
* get all attachments from a post (could be an post array too)
*/
function get_attachments_from_post($post_id_array)
{
global $attach_config;
$attachments = [];
if (!is_array($post_id_array)) {
if (empty($post_id_array)) {
return $attachments;
}
$post_id = (int)$post_id_array;
$post_id_array = [];
$post_id_array[] = $post_id;
}
$post_id_array = implode(', ', array_map('\intval', $post_id_array));
if ($post_id_array == '') {
return $attachments;
}
$display_order = ((int)$attach_config['display_order'] == 0) ? 'DESC' : 'ASC';
$sql = 'SELECT a.post_id, d.*
FROM ' . BB_ATTACHMENTS . ' a, ' . BB_ATTACHMENTS_DESC . " d
WHERE a.post_id IN ($post_id_array)
AND a.attach_id = d.attach_id
ORDER BY d.filetime $display_order";
if (!($result = DB()->sql_query($sql))) {
bb_die('Could not get attachment informations for post number ' . $post_id_array);
}
$num_rows = DB()->num_rows($result);
$attachments = DB()->sql_fetchrowset($result);
DB()->sql_freeresult($result);
if ($num_rows == 0) {
return [];
}
return $attachments;
}
/**
* Get allowed Extensions and their respective Values
*/
function get_extension_informations()
{
return $GLOBALS['datastore']->get('attach_extensions');
}
//
// Sync Topic
//
function attachment_sync_topic($topics)
{
if (is_array($topics)) {
$topics = implode(',', $topics);
}
$posts_without_attach = $topics_without_attach = [];
// Check orphan post_attachment markers
$sql = "SELECT p.post_id
FROM " . BB_POSTS . " p
LEFT JOIN " . BB_ATTACHMENTS . " a USING(post_id)
WHERE p.topic_id IN($topics)
AND p.post_attachment = 1
AND a.post_id IS NULL";
if ($rowset = DB()->fetch_rowset($sql)) {
foreach ($rowset as $row) {
$posts_without_attach[] = $row['post_id'];
}
if ($posts_sql = implode(',', $posts_without_attach)) {
DB()->query("UPDATE " . BB_POSTS . " SET post_attachment = 0 WHERE post_id IN($posts_sql)");
}
}
// Update missing topic_attachment markers
DB()->query("
UPDATE " . BB_TOPICS . " t, " . BB_POSTS . " p SET
t.topic_attachment = 1
WHERE p.topic_id IN($topics)
AND p.post_attachment = 1
AND p.topic_id = t.topic_id
");
// Fix orphan topic_attachment markers
$sql = "SELECT t.topic_id
FROM " . BB_POSTS . " p, " . BB_TOPICS . " t
WHERE t.topic_id = p.topic_id
AND t.topic_id IN($topics)
AND t.topic_attachment = 1
GROUP BY p.topic_id
HAVING SUM(p.post_attachment) = 0";
if ($rowset = DB()->fetch_rowset($sql)) {
foreach ($rowset as $row) {
$topics_without_attach[] = $row['topic_id'];
}
if ($topics_sql = implode(',', $topics_without_attach)) {
DB()->query("UPDATE " . BB_TOPICS . " SET topic_attachment = 0 WHERE topic_id IN($topics_sql)");
}
}
}
/**
* _set_var
*
* Set variable, used by {@link get_var the get_var function}
*
* @private
*/
function _set_var(&$result, $var, $type, $multibyte = false)
{
settype($var, $type);
$result = $var;
if ($type == 'string') {
$result = trim(str_replace(["\r\n", "\r", '\xFF'], ["\n", "\n", ' '], $result));
// 2.0.x is doing addslashes on all variables
$result = stripslashes($result);
if ($multibyte) {
$result = preg_replace('#&amp;(\#[0-9]+;)#', '&\1', $result);
}
}
}
/**
* Used to get passed variable
*
* @param $var_name
* @param $default
* @param bool $multibyte
* @return array|string
*/
function get_var($var_name, $default, $multibyte = false)
{
$type = null;
if (!isset($_REQUEST[$var_name]) ||
(is_array($_REQUEST[$var_name]) && !is_array($default)) ||
(is_array($default) && !is_array($_REQUEST[$var_name]))) {
return (is_array($default)) ? [] : $default;
}
$var = $_REQUEST[$var_name];
if (!is_array($default)) {
$type = gettype($default);
$key_type = null;
} else {
foreach ($default as $key_type => $type) {
$key_type = gettype($key_type);
$type = gettype($type);
}
}
if (is_array($var)) {
$_var = $var;
$var = [];
foreach ($_var as $k => $v) {
if (is_array($v)) {
foreach ($v as $_k => $_v) {
_set_var($k, $k, $key_type);
_set_var($_k, $_k, $key_type);
_set_var($var[$k][$_k], $_v, $type, $multibyte);
}
} else {
_set_var($k, $k, $key_type);
_set_var($var[$k], $v, $type, $multibyte);
}
}
} else {
_set_var($var, $var, $type, $multibyte);
}
return $var;
}

View file

@ -1,156 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
/**
* Called from admin_users.php and admin_groups.php in order to process Quota Settings (admin/admin_users.php:admin/admin_groups.php)
*/
function attachment_quota_settings($admin_mode, $mode, $submit = false)
{
global $template, $lang, $attach_config;
$this_userdata = [];
if ($attach_config['upload_dir'][0] == '/' || ($attach_config['upload_dir'][0] != '/' && $attach_config['upload_dir'][1] == ':')) {
$upload_dir = $attach_config['upload_dir'];
} else {
$upload_dir = BB_ROOT . $attach_config['upload_dir'];
}
include ATTACH_DIR . '/includes/functions_selects.php';
if (!function_exists("process_quota_settings")) {
include ATTACH_DIR . '/includes/functions_admin.php';
}
$user_id = 0;
if ($admin_mode == 'user') {
// We overwrite submit here... to be sure
$submit = isset($_POST['submit']);
if (!$submit && $mode != 'save') {
$user_id = get_var(POST_USERS_URL, 0);
$u_name = get_var('username', '');
if (!$user_id && !$u_name) {
bb_die($lang['NO_USER_ID_SPECIFIED']);
}
if ($user_id) {
$this_userdata['user_id'] = $user_id;
} else {
// Get userdata is handling the sanitizing of username
$this_userdata = get_userdata($_POST['username'], true);
}
$user_id = (int)$this_userdata['user_id'];
} else {
$user_id = get_var('id', 0);
if (!$user_id) {
bb_die($lang['NO_USER_ID_SPECIFIED']);
}
}
}
if ($admin_mode == 'user' && !$submit && $mode != 'save') {
// Show the contents
$sql = 'SELECT quota_limit_id, quota_type FROM ' . BB_QUOTA . ' WHERE user_id = ' . (int)$user_id;
if (!($result = DB()->sql_query($sql))) {
bb_die('Unable to get quota settings #1');
}
$pm_quota = $upload_quota = 0;
if ($row = DB()->sql_fetchrow($result)) {
do {
if ($row['quota_type'] == QUOTA_UPLOAD_LIMIT) {
$upload_quota = $row['quota_limit_id'];
} elseif ($row['quota_type'] == QUOTA_PM_LIMIT) {
$pm_quota = $row['quota_limit_id'];
}
} while ($row = DB()->sql_fetchrow($result));
} else {
// Set Default Quota Limit
$upload_quota = $attach_config['default_upload_quota'];
$pm_quota = $attach_config['default_pm_quota'];
}
DB()->sql_freeresult($result);
$template->assign_vars([
'S_SELECT_UPLOAD_QUOTA' => quota_limit_select('user_upload_quota', $upload_quota),
'S_SELECT_PM_QUOTA' => quota_limit_select('user_pm_quota', $pm_quota)
]);
}
if ($admin_mode == 'user' && $submit && @$_POST['delete_user']) {
process_quota_settings($admin_mode, $user_id, QUOTA_UPLOAD_LIMIT, 0);
process_quota_settings($admin_mode, $user_id, QUOTA_PM_LIMIT, 0);
} elseif ($admin_mode == 'user' && $submit && $mode == 'save') {
// Get the contents
$upload_quota = get_var('user_upload_quota', 0);
$pm_quota = get_var('user_pm_quota', 0);
process_quota_settings($admin_mode, $user_id, QUOTA_UPLOAD_LIMIT, $upload_quota);
process_quota_settings($admin_mode, $user_id, QUOTA_PM_LIMIT, $pm_quota);
}
if ($admin_mode == 'group' && $mode == 'newgroup') {
return;
}
if ($admin_mode == 'group' && !$submit && isset($_POST['edit'])) {
// Get group id again
$group_id = get_var(POST_GROUPS_URL, 0);
// Show the contents
$sql = 'SELECT quota_limit_id, quota_type FROM ' . BB_QUOTA . ' WHERE group_id = ' . (int)$group_id;
if (!($result = DB()->sql_query($sql))) {
bb_die('Unable to get quota settings #2');
}
$pm_quota = $upload_quota = 0;
if ($row = DB()->sql_fetchrow($result)) {
do {
if ($row['quota_type'] == QUOTA_UPLOAD_LIMIT) {
$upload_quota = $row['quota_limit_id'];
} elseif ($row['quota_type'] == QUOTA_PM_LIMIT) {
$pm_quota = $row['quota_limit_id'];
}
} while ($row = DB()->sql_fetchrow($result));
} else {
// Set Default Quota Limit
$upload_quota = $attach_config['default_upload_quota'];
$pm_quota = $attach_config['default_pm_quota'];
}
DB()->sql_freeresult($result);
$template->assign_vars([
'S_SELECT_UPLOAD_QUOTA' => quota_limit_select('group_upload_quota', $upload_quota),
'S_SELECT_PM_QUOTA' => quota_limit_select('group_pm_quota', $pm_quota)
]);
}
if ($admin_mode == 'group' && $submit && isset($_POST['group_delete'])) {
$group_id = get_var(POST_GROUPS_URL, 0);
process_quota_settings($admin_mode, $group_id, QUOTA_UPLOAD_LIMIT, 0);
process_quota_settings($admin_mode, $group_id, QUOTA_PM_LIMIT, 0);
} elseif ($admin_mode == 'group' && $submit) {
$group_id = get_var(POST_GROUPS_URL, 0);
// Get the contents
$upload_quota = get_var('group_upload_quota', 0);
$pm_quota = get_var('group_pm_quota', 0);
process_quota_settings($admin_mode, $group_id, QUOTA_UPLOAD_LIMIT, $upload_quota);
process_quota_settings($admin_mode, $group_id, QUOTA_PM_LIMIT, $pm_quota);
}
}

View file

@ -1,57 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
/**
* Create thumbnail
*
* @param string $source
* @param string $newFile
* @param string $mimeType
* @return bool
* @throws Exception
*/
function createThumbnail(string $source, string $newFile, string $mimeType): bool
{
global $attach_config;
// Check for source image existence
if (!$source = realpath($source)) {
return false;
}
// Checks the max allowed filesize
$min_filesize = (int)$attach_config['img_min_thumb_filesize'];
if (!filesize($source) || filesize($source) <= $min_filesize) {
return false;
}
// Making the thumbnail image
try {
$image = new \claviska\SimpleImage();
$image
->fromFile($source)
->autoOrient()
->resize(150)
->toFile($newFile, $mimeType, ['quality' => 85]);
} catch (Exception $e) {
// Handle errors
throw new Exception($e->getMessage());
}
// Check the thumbnail existence after creating
if (!is_file($newFile)) {
return false;
}
return true;
}

View file

@ -1,23 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
/**
* Entry Point
*/
function execute_posting_attachment_handling()
{
global $attachment_mod;
$attachment_mod['posting'] = new TorrentPier\Legacy\AttachPosting();
$attachment_mod['posting']->posting_attachment_mod();
}

View file

@ -1,41 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
bb_log(date('H:i:s - ') . getmypid() . ' --x- SELECT jobs' . LOG_LF, CRON_LOG_DIR . '/cron_check');
// Get cron jobs
$cron_jobs = DB()->fetch_rowset("
SELECT * FROM " . BB_CRON . "
WHERE cron_active = 1
AND next_run <= NOW()
ORDER BY run_order
");
// Run cron jobs
if ($cron_jobs) {
bb_log(date('H:i:s - ') . getmypid() . ' --x- RUN jobs' . LOG_LF, CRON_LOG_DIR . '/cron_check');
foreach ($cron_jobs as $job) {
if ($job['disable_board']) {
TorrentPier\Helpers\CronHelper::disableBoard();
break;
}
}
require(CRON_DIR . 'cron_run.php');
// Update cron_last_check
bb_update_config(['cron_last_check' => TIMENOW + 10]);
} else {
bb_log(date('H:i:s - ') . getmypid() . ' --x- no active jobs found ----------------------------------------------' . LOG_LF, CRON_LOG_DIR . '/cron_check');
}

View file

@ -1,124 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
define('IN_CRON', true);
// Set SESSION vars (optimized for InnoDB)
DB()->query("
SET SESSION
bulk_insert_buffer_size = 8*1024*1024
, join_buffer_size = 4*1024*1024
, read_buffer_size = 4*1024*1024
, read_rnd_buffer_size = 8*1024*1024
, sort_buffer_size = 4*1024*1024
, tmp_table_size = 80*1024*1024
, group_concat_max_len = 1*1024*1024
");
// Restore vars at shutdown
DB()->add_shutdown_query("
SET SESSION
bulk_insert_buffer_size = DEFAULT
, join_buffer_size = DEFAULT
, read_buffer_size = DEFAULT
, read_rnd_buffer_size = DEFAULT
, sort_buffer_size = DEFAULT
, tmp_table_size = DEFAULT
, group_concat_max_len = DEFAULT
");
// $cron_jobs obtained in cron_check.php
foreach ($cron_jobs as $job) {
$job_script = CRON_JOB_DIR . basename($job['cron_script']);
if (is_file($job_script)) {
$cron_start_time = utime();
$cron_runtime_log = [];
$cron_write_log = (CRON_LOG_ENABLED && (CRON_FORCE_LOG || $job['log_enabled'] >= 1));
$cron_sql_log_file = CRON_LOG_DIR . '/SQL-' . basename($job['cron_script']);
if ($cron_write_log) {
$msg = [];
$msg[] = 'start';
$msg[] = date('m-d');
$msg[] = date('H:i:s');
$msg[] = sprintf('%05d', getmypid());
$msg[] = $job['cron_title'];
$msg = implode(LOG_SEPR, $msg);
bb_log($msg . LOG_LF, CRON_LOG_DIR . '/' . CRON_LOG_FILE);
}
if ($job['log_sql_queries']) {
DB()->log_next_query(100000, $cron_sql_log_file);
}
set_time_limit(600);
require($job_script);
if ($job['log_sql_queries']) {
DB()->log_next_query(0);
bb_log(LOG_LF, $cron_sql_log_file);
}
if ($cron_write_log) {
$msg = [];
$msg[] = ' end';
$msg[] = date('m-d');
$msg[] = date('H:i:s');
$msg[] = sprintf('%05d', getmypid());
$msg[] = round(utime() - $cron_start_time) . '/' . round(utime() - TIMESTART) . ' sec';
$msg = implode(LOG_SEPR, $msg);
$msg .= LOG_LF . '------=-------=----------=------=-------=----------';
bb_log($msg . LOG_LF, CRON_LOG_DIR . '/' . CRON_LOG_FILE);
if (is_array($cron_runtime_log)) {
$runtime_log_file = ($job['log_file']) ?: $job['cron_script'];
$cron_runtime_log[] = '';
bb_log($cron_runtime_log, CRON_LOG_DIR . '/' . basename($runtime_log_file));
}
}
DB()->query("
UPDATE " . BB_CRON . " SET
last_run = NOW(),
run_counter = run_counter + 1,
next_run =
CASE
WHEN schedule = 'hourly' THEN
DATE_ADD(NOW(), INTERVAL 1 HOUR)
WHEN schedule = 'daily' THEN
DATE_ADD(DATE_ADD(CURDATE(), INTERVAL 1 DAY), INTERVAL TIME_TO_SEC(run_time) SECOND)
WHEN schedule = 'weekly' THEN
DATE_ADD(
DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(NOW()) DAY), INTERVAL 7 DAY),
INTERVAL CONCAT(ROUND(run_day-1), ' ', run_time) DAY_SECOND)
WHEN schedule = 'monthly' THEN
DATE_ADD(
DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFMONTH(NOW())-1 DAY), INTERVAL 1 MONTH),
INTERVAL CONCAT(ROUND(run_day-1), ' ', run_time) DAY_SECOND)
ELSE
DATE_ADD(NOW(), INTERVAL TIME_TO_SEC(run_interval) SECOND)
END
WHERE cron_id = {$job['cron_id']}
LIMIT 1
");
if (utime() - TIMESTART > 600) {
// so that daily scripts do not block interval scripts for a long time
return;
}
} else {
$cron_err_msg = "Can not run \"{$job['cron_title']}\" : file \"$job_script\" not found" . LOG_LF;
bb_log($cron_err_msg, 'cron_error');
}
}

View file

@ -1,19 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$search_results_expire = TIMENOW - 3 * 3600;
DB()->query("
DELETE FROM " . BB_SEARCH . "
WHERE search_time < $search_results_expire
");

View file

@ -1,14 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$datastore->update('cat_forums');

View file

@ -1,14 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$datastore->update('stats');

View file

@ -1,54 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$user_session_expire_time = TIMENOW - (int)config()->get('user_session_duration');
$admin_session_expire_time = TIMENOW - (int)config()->get('admin_session_duration');
$user_session_gc_time = $user_session_expire_time - (int)config()->get('user_session_gc_ttl');
$admin_session_gc_time = $admin_session_expire_time;
// ############################ Tables LOCKED ################################
DB()->lock([
BB_USERS . ' u',
BB_SESSIONS . ' s'
]);
// Update user's session time
DB()->query("
UPDATE
" . BB_USERS . " u,
" . BB_SESSIONS . " s
SET
u.user_session_time = IF(u.user_session_time < s.session_time, s.session_time, u.user_session_time)
WHERE
u.user_id = s.session_user_id
AND s.session_user_id != " . GUEST_UID . "
AND (
(s.session_time < $user_session_expire_time AND s.session_admin = 0)
OR
(s.session_time < $admin_session_expire_time AND s.session_admin != 0)
)
");
DB()->unlock();
// ############################ Tables UNLOCKED ##############################
// Delete staled sessions
DB()->query("
DELETE s
FROM " . BB_SESSIONS . " s
WHERE
(s.session_time < $user_session_gc_time AND s.session_admin = 0)
OR
(s.session_time < $admin_session_gc_time AND s.session_admin != 0)
");

View file

@ -1,21 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$extensions = DB()->fetch_rowset("
SELECT e.extension, g.cat_id, g.download_mode, g.upload_icon, g.allow_group FROM
" . BB_EXTENSIONS . " e,
" . BB_EXTENSION_GROUPS . " g
WHERE e.group_id = g.group_id
");
$this->store('attach_extensions', $extensions);

View file

@ -1,21 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$sql = "SELECT * FROM " . BB_BANLIST;
$bans = [];
foreach (DB()->fetch_rowset($sql) as $row) {
$bans[$row['ban_userid']] = $row;
}
$this->store('ban_list', $bans);

View file

@ -1,21 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$sql = "SELECT * FROM " . BB_WORDS;
$words = [];
foreach (DB()->fetch_rowset($sql) as $row) {
$words[$row['word_id']] = $row;
}
$this->store('censor', $words);

View file

@ -1,21 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$sql = "SELECT * FROM " . BB_RANKS;
$ranks = [];
foreach (DB()->fetch_rowset($sql) as $row) {
$ranks[$row['rank_id']] = $row;
}
$this->store('ranks', $ranks);

View file

@ -1,25 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
$smilies = [];
$rowset = DB()->fetch_rowset("SELECT * FROM " . BB_SMILIES);
sort($rowset);
foreach ($rowset as $smile) {
$smilies['orig'][] = '#(?<=^|\W)' . preg_quote($smile['code'], '#') . '(?=$|\W)#';
$smilies['repl'][] = ' <img class="smile" src="' . config()->get('smilies_path') . '/' . $smile['smile_url'] . '" alt="' . $smile['code'] . '" title="' . $smile['emoticon'] . '" align="absmiddle" border="0" />';
$smilies['smile'][] = $smile;
}
$this->store('smile_replacements', $smilies);

View file

@ -1,161 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
/**
* Remove target file
*
* @param string $file Path to file
* @param bool $withoutOutput Hide output
*/
function removeFile(string $file, bool $withoutOutput = false): void
{
if (unlink($file)) {
if ($withoutOutput === false) {
echo "- File removed: $file" . PHP_EOL;
}
} else {
if ($withoutOutput === false) {
echo "- File cannot be removed: $file" . PHP_EOL;
}
exit;
}
}
/**
* Remove folder (recursively)
*
* @param string $dir Path to folder
* @param bool $withoutOutput Hide output
*/
function removeDir(string $dir, bool $withoutOutput = false): void
{
$it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file) {
if ($file->isDir()) {
removeDir($file->getPathname(), $withoutOutput);
} else {
removeFile($file->getPathname(), $withoutOutput);
}
}
if (rmdir($dir)) {
if ($withoutOutput === false) {
echo "- Folder removed: $dir" . PHP_EOL;
}
} else {
if ($withoutOutput === false) {
echo "- Folder cannot be removed: $dir" . PHP_EOL;
}
exit;
}
}
/**
* Colored console output
*
* @param string $str
* @param string $type
* @return void
*/
function out(string $str, string $type = ''): void
{
echo match ($type) {
'error' => "\033[31m$str \033[0m\n",
'success' => "\033[32m$str \033[0m\n",
'warning' => "\033[33m$str \033[0m\n",
'info' => "\033[36m$str \033[0m\n",
'debug' => "\033[90m$str \033[0m\n",
default => "$str\n",
};
}
/**
* Run process with realtime output
*
* @param string $cmd
* @param string|null $input
* @return int
*/
function runProcess(string $cmd, ?string $input = null): int
{
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($cmd, $descriptorSpec, $pipes);
if (!is_resource($process)) {
out('- Could not start subprocess', 'error');
return -1;
}
// Write input if provided
if ($input !== null) {
fwrite($pipes[0], $input);
fclose($pipes[0]);
}
// Read and print output in real-time
while (!feof($pipes[1])) {
echo stream_get_contents($pipes[1], 1);
flush(); // Flush output buffer for immediate display
}
// Read and print error output
while (!feof($pipes[2])) {
echo stream_get_contents($pipes[2], 1);
flush();
}
fclose($pipes[1]);
fclose($pipes[2]);
return proc_close($process);
}
/**
* Setting permissions recursively
*
* @param string $dir
* @param int $dirPermissions
* @param int $filePermissions
* @return void
*/
function chmod_r(string $dir, int $dirPermissions, int $filePermissions): void
{
$dp = opendir($dir);
while ($file = readdir($dp)) {
if (($file == '.') || ($file == '..')) {
continue;
}
$fullPath = realpath($dir . '/' . $file);
if (is_dir($fullPath)) {
out("- Directory: $fullPath");
chmod($fullPath, $dirPermissions);
chmod_r($fullPath, $dirPermissions, $filePermissions);
} elseif (is_file($fullPath)) {
// out("- File: $fullPath");
chmod($fullPath, $filePermissions);
} else {
out("- Cannot find target path: $fullPath", 'error');
return;
}
}
closedir($dp);
}

View file

@ -1,96 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
if (!defined('BB_ROOT')) {
die(basename(__FILE__));
}
?>
<style>
.sqlLog {
clear: both;
font-family: Courier, monospace;
font-size: 12px;
white-space: nowrap;
background: #F5F5F5;
border: 1px solid #BBC0C8;
overflow: auto;
width: 98%;
margin: 0 auto;
padding: 2px 4px;
}
.sqlLogTitle {
font-weight: bold;
color: #444444;
font-size: 11px;
font-family: Verdana, Arial, Helvetica, sans-serif;
padding-bottom: 2px;
}
.sqlLogRow {
background-color: #F5F5F5;
padding-bottom: 1px;
border: solid #F5F5F5;
border-width: 0 0 1px 0;
cursor: pointer;
}
.sqlLogRow:hover {
border-color: #8B0000;
}
.sqlLogWrapped {
white-space: normal;
overflow: visible;
}
.sqlExplain {
color: #B50000;
font-size: 13px;
cursor: inherit !important;
}
.sqlHighlight {
background: #FFE4E1;
}
</style>
<?php
if (!empty($_COOKIE['explain'])) {
// Get all database server instances from the new DatabaseFactory
$server_names = \TorrentPier\Database\DatabaseFactory::getServerNames();
foreach ($server_names as $srv_name) {
try {
$db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
if (!empty($db_obj->do_explain)) {
$db_obj->explain('display');
}
} catch (\Exception $e) {
// Skip if server not available
}
}
}
$sql_log = !empty($_COOKIE['sql_log']) ? dev()->getSqlDebugLog() : false;
if ($sql_log) {
echo '<div class="sqlLog" id="sqlLog">' . $sql_log . '</div><!-- / sqlLog --><br clear="all" />';
}
?>
<script type="text/javascript">
function fixSqlLog() {
if ($("#sqlLog").height() > 400) {
$("#sqlLog").height(400);
}
}
$(document).ready(fixSqlLog);
</script>

View file

@ -1 +0,0 @@
{MESSAGE}

View file

@ -1,3 +0,0 @@
<div id="infobox-body">
<p>File not found</p>
</div><!--/infobox-body-->

View file

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class RemoveDemoMode extends AbstractMigration
{
/**
* Migrate Up.
*/
public function up(): void
{
// Delete the demo_mode.php cron job from bb_cron table
$this->table('bb_cron')
->getAdapter()
->execute("DELETE FROM bb_cron WHERE cron_script = 'demo_mode.php'");
}
/**
* Migrate Down.
*/
public function down(): void
{
// Restore the demo_mode.php cron job to bb_cron table
$this->table('bb_cron')->insert([
'cron_active' => 1,
'cron_title' => 'Demo mode',
'cron_script' => 'demo_mode.php',
'schedule' => 'daily',
'run_day' => null,
'run_time' => '05:00:00',
'run_order' => 255,
'last_run' => '1900-01-01 00:00:00',
'next_run' => '1900-01-01 00:00:00',
'run_interval' => null,
'log_enabled' => 1,
'log_file' => 'demo_mode_cron',
'log_sql_queries' => 1,
'disable_board' => 1,
'run_counter' => 0
])->save();
}
}

View file

@ -1,62 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
define('BB_SCRIPT', 'profile');
define('IN_PROFILE', true);
require __DIR__ . '/common.php';
// Start session management
$user->session_start();
set_die_append_msg();
$mode = request_var('mode', 'viewprofile');
switch ($mode) {
case 'viewprofile':
require UCP_DIR . '/viewprofile.php';
break;
case 'register':
case 'editprofile':
if (IS_GUEST && $mode == 'editprofile') {
login_redirect();
}
require UCP_DIR . '/register.php';
break;
case 'sendpassword':
require UCP_DIR . '/sendpasswd.php';
break;
case 'activate':
require UCP_DIR . '/activate.php';
break;
case 'email':
require UCP_DIR . '/email.php';
break;
case 'bonus':
if (IS_GUEST) {
login_redirect();
}
require UCP_DIR . '/bonus.php';
break;
case 'watch':
if (IS_GUEST) {
login_redirect();
}
require UCP_DIR . '/topic_watch.php';
break;
default:
bb_die('Invalid mode');
}

View file

@ -1,473 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Cache;
use Nette\Caching\Cache;
use Nette\Caching\Storage;
use Nette\Caching\Storages\FileStorage;
use Nette\Caching\Storages\MemcachedStorage;
use Nette\Caching\Storages\MemoryStorage;
use Nette\Caching\Storages\SQLiteStorage;
use TorrentPier\Dev;
/**
* Unified Cache Manager using Nette Caching internally
* Maintains backward compatibility with Legacy Cache and Datastore APIs
*
* @package TorrentPier\Cache
*/
class CacheManager
{
/**
* Singleton instances of cache managers
* @var array
*/
private static array $instances = [];
/**
* Nette Cache instance
* @var Cache
*/
private Cache $cache;
/**
* Storage instance
* @var Storage
*/
private Storage $storage;
/**
* Cache prefix
* @var string
*/
public string $prefix;
/**
* Engine type
* @var string
*/
public string $engine;
/**
* Currently in usage (for backward compatibility)
* @var bool
*/
public bool $used = true;
/**
* Debug properties for backward compatibility
*/
public int $num_queries = 0;
public float $sql_starttime = 0;
public float $sql_inittime = 0;
public float $sql_timetotal = 0;
public float $cur_query_time = 0;
public array $dbg = [];
public int $dbg_id = 0;
public bool $dbg_enabled = false;
public ?string $cur_query = null;
/**
* Constructor
*
* @param string $namespace
* @param Storage $storage Pre-built storage instance from UnifiedCacheSystem
* @param array $config
*/
private function __construct(string $namespace, Storage $storage, array $config)
{
$this->storage = $storage;
$this->prefix = $config['prefix'] ?? 'tp_';
$this->engine = $config['engine'] ?? 'Unknown';
// Create Nette Cache instance with namespace
$this->cache = new Cache($this->storage, $namespace);
// Enable debug if allowed
$this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
* Get singleton instance (called by UnifiedCacheSystem)
*
* @param string $namespace
* @param Storage $storage Pre-built storage instance
* @param array $config
* @return self
*/
public static function getInstance(string $namespace, Storage $storage, array $config): self
{
$key = $namespace . '_' . md5(serialize($config));
if (!isset(self::$instances[$key])) {
self::$instances[$key] = new self($namespace, $storage, $config);
}
return self::$instances[$key];
}
/**
* Cache get method (Legacy Cache API)
*
* @param string $name
* @return mixed
*/
public function get(string $name): mixed
{
$key = $this->prefix . $name;
$this->cur_query = "cache->get('$key')";
$this->debug('start');
$result = $this->cache->load($key);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
// Convert null to false for backward compatibility with legacy cache system
return $result ?? false;
}
/**
* Cache set method (Legacy Cache API)
*
* @param string $name
* @param mixed $value
* @param int $ttl
* @return bool
*/
public function set(string $name, mixed $value, int $ttl = 604800): bool
{
$key = $this->prefix . $name;
$this->cur_query = "cache->set('$key')";
$this->debug('start');
$dependencies = [];
if ($ttl > 0) {
$dependencies[Cache::Expire] = $ttl . ' seconds';
}
try {
$this->cache->save($key, $value, $dependencies);
$result = true;
} catch (\Exception $e) {
$result = false;
}
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
return $result;
}
/**
* Cache remove method (Legacy Cache API)
*
* @param string|null $name
* @return bool
*/
public function rm(?string $name = null): bool
{
if ($name === null) {
// Remove all items in this namespace
$this->cur_query = "cache->clean(all)";
$this->debug('start');
$this->cache->clean([Cache::All => true]);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
return true;
}
$key = $this->prefix . $name;
$this->cur_query = "cache->remove('$key')";
$this->debug('start');
$this->cache->remove($key);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
return true;
}
/**
* Advanced Nette Caching methods
*/
/**
* Load with callback (Nette native method)
*
* @param string $key
* @param callable|null $callback
* @param array $dependencies
* @return mixed
*/
public function load(string $key, ?callable $callback = null, array $dependencies = []): mixed
{
$fullKey = $this->prefix . $key;
$this->cur_query = "cache->load('$fullKey')";
$this->debug('start');
$result = $this->cache->load($fullKey, $callback, $dependencies);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
// Convert null to false for backward compatibility, but only if no callback was provided
// When callback is provided, null indicates the callback was executed and returned null
return ($result === null && $callback === null) ? false : $result;
}
/**
* Save with dependencies
*
* @param string $key
* @param mixed $value
* @param array $dependencies
* @return void
*/
public function save(string $key, mixed $value, array $dependencies = []): void
{
$fullKey = $this->prefix . $key;
$this->cur_query = "cache->save('$fullKey')";
$this->debug('start');
$this->cache->save($fullKey, $value, $dependencies);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
}
/**
* Clean cache by criteria
*
* @param array $conditions
* @return void
*/
public function clean(array $conditions = []): void
{
$this->cur_query = "cache->clean(" . json_encode($conditions) . ")";
$this->debug('start');
$this->cache->clean($conditions);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
}
/**
* Bulk load
*
* @param array $keys
* @param callable|null $callback
* @return array
*/
public function bulkLoad(array $keys, ?callable $callback = null): array
{
$prefixedKeys = array_map(fn($key) => $this->prefix . $key, $keys);
$this->cur_query = "cache->bulkLoad(" . count($keys) . " keys)";
$this->debug('start');
$result = $this->cache->bulkLoad($prefixedKeys, $callback);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
return $result;
}
/**
* Memoize function call
*
* @param callable $function
* @param mixed ...$args
* @return mixed
*/
public function call(callable $function, ...$args): mixed
{
$this->cur_query = "cache->call(" . (is_string($function) ? $function : 'callable') . ")";
$this->debug('start');
$result = $this->cache->call($function, ...$args);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
return $result;
}
/**
* Wrap function for memoization
*
* @param callable $function
* @return callable
*/
public function wrap(callable $function): callable
{
return $this->cache->wrap($function);
}
/**
* Capture output
*
* @param string $key
* @return \Nette\Caching\OutputHelper|null
*/
public function capture(string $key): ?\Nette\Caching\OutputHelper
{
$fullKey = $this->prefix . $key;
return $this->cache->capture($fullKey);
}
/**
* Remove specific key
*
* @param string $key
* @return void
*/
public function remove(string $key): void
{
$fullKey = $this->prefix . $key;
$this->cur_query = "cache->remove('$fullKey')";
$this->debug('start');
$this->cache->remove($fullKey);
$this->debug('stop');
$this->cur_query = null;
$this->num_queries++;
}
/**
* Debug method (backward compatibility)
*
* @param string $mode
* @param string|null $cur_query
* @return void
*/
public function debug(string $mode, ?string $cur_query = null): void
{
if (!$this->dbg_enabled) {
return;
}
$id =& $this->dbg_id;
$dbg =& $this->dbg[$id];
switch ($mode) {
case 'start':
$this->sql_starttime = utime();
$dbg['sql'] = dev()->formatShortQuery($cur_query ?? $this->cur_query);
$dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
$dbg['time'] = '';
break;
case 'stop':
$this->cur_query_time = utime() - $this->sql_starttime;
$this->sql_timetotal += $this->cur_query_time;
$dbg['time'] = $this->cur_query_time;
$id++;
break;
default:
bb_simple_die('[Cache] Incorrect debug mode');
break;
}
}
/**
* Find caller source (backward compatibility)
*
* @param string $mode
* @return string
*/
public function debug_find_source(string $mode = 'all'): string
{
if (!SQL_PREPEND_SRC) {
return 'src disabled';
}
foreach (debug_backtrace() as $trace) {
if (!empty($trace['file']) && $trace['file'] !== __FILE__) {
switch ($mode) {
case 'file':
return $trace['file'];
case 'line':
return (string)$trace['line'];
case 'all':
default:
return hide_bb_path($trace['file']) . '(' . $trace['line'] . ')';
}
}
}
return 'src not found';
}
/**
* Get storage instance (for advanced usage)
*
* @return Storage
*/
public function getStorage(): Storage
{
return $this->storage;
}
/**
* Get Nette Cache instance (for advanced usage)
*
* @return Cache
*/
public function getCache(): Cache
{
return $this->cache;
}
/**
* Magic property getter for backward compatibility
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
// Handle legacy properties that don't exist in unified system
if ($name === 'db') {
// Legacy cache systems sometimes had a 'db' property for database storage
// Our unified system doesn't use separate database connections for cache
// Return an object with empty debug arrays for compatibility
return (object)[
'dbg' => [],
'engine' => $this->engine,
'sql_timetotal' => 0
];
}
throw new \InvalidArgumentException("Property '$name' not found in CacheManager");
}
}

View file

@ -1,470 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Cache;
use Nette\Caching\Cache;
use Nette\Caching\Storage;
use TorrentPier\Dev;
/**
* Datastore Manager using unified CacheManager internally
* Maintains backward compatibility with Legacy Datastore API
*
* @package TorrentPier\Cache
*/
class DatastoreManager
{
/**
* Singleton instance
* @var self|null
*/
private static ?self $instance = null;
/**
* Unified cache manager instance
* @var CacheManager
*/
private CacheManager $cacheManager;
/**
* Директория с builder-скриптами (внутри INC_DIR)
*/
public string $ds_dir = 'datastore';
/**
* Готовая к употреблению data
* array('title' => data)
*/
public array $data = [];
/**
* Список элементов, которые будут извлечены из хранилища при первом же запросе get()
* до этого момента они ставятся в очередь $queued_items для дальнейшего извлечения _fetch()'ем
* всех элементов одним запросом
* array('title1', 'title2'...)
*/
public array $queued_items = [];
/**
* 'title' => 'builder script name' inside "includes/datastore" dir
*/
public array $known_items = [
'cat_forums' => 'build_cat_forums.php',
'censor' => 'build_censor.php',
'check_updates' => 'build_check_updates.php',
'jumpbox' => 'build_cat_forums.php',
'viewtopic_forum_select' => 'build_cat_forums.php',
'latest_news' => 'build_cat_forums.php',
'network_news' => 'build_cat_forums.php',
'ads' => 'build_cat_forums.php',
'moderators' => 'build_moderators.php',
'stats' => 'build_stats.php',
'ranks' => 'build_ranks.php',
'ban_list' => 'build_bans.php',
'attach_extensions' => 'build_attach_extensions.php',
'smile_replacements' => 'build_smilies.php',
];
/**
* Engine type (for backward compatibility)
* @var string
*/
public string $engine;
/**
* Debug properties (delegated to CacheManager)
*/
public int $num_queries = 0;
public float $sql_starttime = 0;
public float $sql_inittime = 0;
public float $sql_timetotal = 0;
public float $cur_query_time = 0;
public array $dbg = [];
public int $dbg_id = 0;
public bool $dbg_enabled = false;
public ?string $cur_query = null;
/**
* Constructor
*
* @param Storage $storage Pre-built storage instance from UnifiedCacheSystem
* @param array $config
*/
private function __construct(Storage $storage, array $config)
{
// Create unified cache manager for datastore with pre-built storage
$this->cacheManager = CacheManager::getInstance('datastore', $storage, $config);
$this->engine = $this->cacheManager->engine;
$this->dbg_enabled = dev()->checkSqlDebugAllowed();
}
/**
* Get singleton instance
*
* @param Storage $storage Pre-built storage instance
* @param array $config
* @return self
*/
public static function getInstance(Storage $storage, array $config): self
{
if (self::$instance === null) {
self::$instance = new self($storage, $config);
}
return self::$instance;
}
/**
* Enqueue items for batch loading
*
* @param array $items
* @return void
*/
public function enqueue(array $items): void
{
foreach ($items as $item) {
if (!in_array($item, $this->queued_items) && !isset($this->data[$item])) {
$this->queued_items[] = $item;
}
}
}
/**
* Get datastore item
*
* @param string $title
* @return mixed
*/
public function &get(string $title): mixed
{
if (!isset($this->data[$title])) {
$this->enqueue([$title]);
$this->_fetch();
}
return $this->data[$title];
}
/**
* Store data into datastore
*
* @param string $item_name
* @param mixed $item_data
* @return bool
*/
public function store(string $item_name, mixed $item_data): bool
{
$this->data[$item_name] = $item_data;
// Use cache manager with permanent storage (no TTL)
$dependencies = [
// No time expiration for datastore items - they persist until manually updated
];
try {
$this->cacheManager->save($item_name, $item_data, $dependencies);
$this->_updateDebugCounters();
return true;
} catch (\Exception $e) {
$this->_updateDebugCounters();
return false;
}
}
/**
* Remove data from memory cache
*
* @param array|string $items
* @return void
*/
public function rm(array|string $items): void
{
foreach ((array)$items as $item) {
unset($this->data[$item]);
}
}
/**
* Update datastore items
*
* @param array|string $items
* @return void
*/
public function update(array|string $items): void
{
if ($items == 'all') {
$items = array_keys(array_unique($this->known_items));
}
foreach ((array)$items as $item) {
$this->_build_item($item);
}
}
/**
* Clean datastore cache (for admin purposes)
*
* @return void
*/
public function clean(): void
{
foreach ($this->known_items as $title => $script_name) {
$this->cacheManager->remove($title);
}
$this->_updateDebugCounters();
}
/**
* Fetch items from store
*
* @return void
*/
public function _fetch(): void
{
$this->_fetch_from_store();
foreach ($this->queued_items as $title) {
// Only rebuild items that had true cache misses, not cached false/null values
if (!isset($this->data[$title]) || $this->data[$title] === '__CACHE_MISS__') {
$this->_build_item($title);
}
}
$this->queued_items = [];
}
/**
* Fetch items from cache store
*
* @return void
* @throws \Exception
*/
public function _fetch_from_store(): void
{
if (!$items = $this->queued_items) {
$src = $this->_debug_find_caller('enqueue');
throw new \Exception("Datastore: no items queued for fetching [$src]");
}
// Use bulk loading for efficiency
$keys = $items;
$results = $this->cacheManager->bulkLoad($keys);
foreach ($items as $item) {
$fullKey = $this->cacheManager->prefix . $item;
// Distinguish between cache miss (null) and cached false value
if (array_key_exists($fullKey, $results)) {
// Item exists in cache (even if the value is null/false)
$this->data[$item] = $results[$fullKey];
} else {
// True cache miss - item not found in cache at all
// Use a special sentinel value to mark as "needs building"
$this->data[$item] = '__CACHE_MISS__';
}
}
$this->_updateDebugCounters();
}
/**
* Build item using builder script
*
* @param string $title
* @return void
* @throws \Exception
*/
public function _build_item(string $title): void
{
if (!isset($this->known_items[$title])) {
throw new \Exception("Unknown datastore item: $title");
}
$file = INC_DIR . '/' . $this->ds_dir . '/' . $this->known_items[$title];
if (!file_exists($file)) {
throw new \Exception("Datastore builder script not found: $file");
}
require $file;
}
/**
* Find debug caller (backward compatibility)
*
* @param string $function_name
* @return string
*/
public function _debug_find_caller(string $function_name): string
{
foreach (debug_backtrace() as $trace) {
if (isset($trace['function']) && $trace['function'] === $function_name) {
return hide_bb_path($trace['file']) . '(' . $trace['line'] . ')';
}
}
return 'caller not found';
}
/**
* Update debug counters from cache manager
*
* @return void
*/
private function _updateDebugCounters(): void
{
$this->num_queries = $this->cacheManager->num_queries;
$this->sql_timetotal = $this->cacheManager->sql_timetotal;
$this->dbg = $this->cacheManager->dbg;
$this->dbg_id = $this->cacheManager->dbg_id;
}
/**
* Advanced Nette caching methods (extended functionality)
*/
/**
* Load with dependencies
*
* @param string $key
* @param callable|null $callback
* @param array $dependencies
* @return mixed
*/
public function load(string $key, ?callable $callback = null, array $dependencies = []): mixed
{
return $this->cacheManager->load($key, $callback, $dependencies);
}
/**
* Save with dependencies
*
* @param string $key
* @param mixed $value
* @param array $dependencies
* @return void
*/
public function save(string $key, mixed $value, array $dependencies = []): void
{
$this->cacheManager->save($key, $value, $dependencies);
$this->_updateDebugCounters();
}
/**
* Clean by criteria
*
* @param array $conditions
* @return void
*/
public function cleanByCriteria(array $conditions = []): void
{
$this->cacheManager->clean($conditions);
$this->_updateDebugCounters();
}
/**
* Clean by tags
*
* @param array $tags
* @return void
*/
public function cleanByTags(array $tags): void
{
$this->cacheManager->clean([Cache::Tags => $tags]);
$this->_updateDebugCounters();
}
/**
* Get cache manager instance (for advanced usage)
*
* @return CacheManager
*/
public function getCacheManager(): CacheManager
{
return $this->cacheManager;
}
/**
* Get engine name
*
* @return string
*/
public function getEngine(): string
{
return $this->engine;
}
/**
* Check if storage supports tags
*
* @return bool
*/
public function supportsTags(): bool
{
return $this->cacheManager->getStorage() instanceof \Nette\Caching\Storages\IJournal;
}
/**
* Magic method to delegate unknown method calls to cache manager
*
* @param string $method
* @param array $args
* @return mixed
*/
public function __call(string $method, array $args): mixed
{
if (method_exists($this->cacheManager, $method)) {
$result = $this->cacheManager->$method(...$args);
$this->_updateDebugCounters();
return $result;
}
throw new \BadMethodCallException("Method '$method' not found in DatastoreManager or CacheManager");
}
/**
* Magic property getter to delegate to cache manager
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
if (property_exists($this->cacheManager, $name)) {
return $this->cacheManager->$name;
}
// Handle legacy properties that don't exist in unified system
if ($name === 'db') {
// Legacy cache systems sometimes had a 'db' property for database storage
// Our unified system doesn't use separate database connections for cache
// Return an object with empty debug arrays for compatibility
return (object)[
'dbg' => [],
'engine' => $this->engine,
'sql_timetotal' => 0
];
}
throw new \InvalidArgumentException("Property '$name' not found");
}
/**
* Magic property setter to delegate to cache manager
*
* @param string $name
* @param mixed $value
* @return void
*/
public function __set(string $name, mixed $value): void
{
if (property_exists($this->cacheManager, $name)) {
$this->cacheManager->$name = $value;
} else {
throw new \InvalidArgumentException("Property '$name' not found");
}
}
}

View file

@ -1,454 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Cache;
use Nette\Caching\Storage;
use Nette\Caching\Storages\FileStorage;
use Nette\Caching\Storages\MemcachedStorage;
use Nette\Caching\Storages\MemoryStorage;
use Nette\Caching\Storages\SQLiteStorage;
/**
* Unified Cache System using Nette Caching
* Replaces Legacy Caches class and provides both cache and datastore functionality
*
* @package TorrentPier\Cache
*/
class UnifiedCacheSystem
{
/**
* Singleton instance
* @var self|null
*/
private static ?self $instance = null;
/**
* Configuration
* @var array
*/
private array $cfg;
/**
* Cache manager instances
* @var array
*/
private array $managers = [];
/**
* References to cache managers (for backward compatibility)
* @var array
*/
private array $ref = [];
/**
* Datastore manager instance
* @var DatastoreManager|null
*/
private ?DatastoreManager $datastore = null;
/**
* Stub cache manager for non-configured caches
* @var CacheManager|null
*/
private ?CacheManager $stub = null;
/**
* Get singleton instance
*
* @param array|null $cfg
* @return self
*/
public static function getInstance(?array $cfg = null): self
{
if (self::$instance === null) {
if ($cfg === null) {
throw new \InvalidArgumentException('Configuration must be provided on first initialization');
}
self::$instance = new self($cfg);
}
return self::$instance;
}
/**
* Constructor
*
* @param array $cfg
*/
private function __construct(array $cfg)
{
$this->cfg = $cfg['cache'] ?? [];
// Create stub cache manager
$stubStorage = new MemoryStorage();
$stubConfig = [
'engine' => 'Memory',
'prefix' => $this->cfg['prefix'] ?? 'tp_'
];
$this->stub = CacheManager::getInstance('__stub', $stubStorage, $stubConfig);
}
/**
* Get cache manager instance (backward compatible with CACHE() function)
*
* @param string $cache_name
* @return CacheManager
*/
public function get_cache_obj(string $cache_name): CacheManager
{
if (!isset($this->ref[$cache_name])) {
if (!$engine_cfg = $this->cfg['engines'][$cache_name] ?? null) {
// Return stub for non-configured caches
$this->ref[$cache_name] = $this->stub;
} else {
$cache_type = $engine_cfg[0] ?? 'file';
if (!isset($this->managers[$cache_name])) {
// Build storage and config directly
$storage = $this->_buildStorage($cache_type, $cache_name);
$config = [
'engine' => $this->_getEngineType($cache_type),
'prefix' => $this->cfg['prefix'] ?? 'tp_'
];
$this->managers[$cache_name] = CacheManager::getInstance($cache_name, $storage, $config);
}
$this->ref[$cache_name] = $this->managers[$cache_name];
}
}
return $this->ref[$cache_name];
}
/**
* Get datastore manager instance
*
* @param string $datastore_type
* @return DatastoreManager
*/
public function getDatastore(string $datastore_type = 'file'): DatastoreManager
{
if ($this->datastore === null) {
// Build storage and config for datastore
$storage = $this->_buildDatastoreStorage($datastore_type);
$config = [
'engine' => $this->_getEngineType($datastore_type),
'prefix' => $this->cfg['prefix'] ?? 'tp_'
];
$this->datastore = DatastoreManager::getInstance($storage, $config);
}
return $this->datastore;
}
/**
* Build storage instance directly (eliminates redundancy with CacheManager)
*
* @param string $cache_type
* @param string $cache_name
* @return Storage
*/
private function _buildStorage(string $cache_type, string $cache_name): Storage
{
switch ($cache_type) {
case 'file':
case 'filecache':
case 'apcu':
case 'redis':
// Some deprecated cache types will fall back to file storage
$dir = rtrim($this->cfg['db_dir'] ?? sys_get_temp_dir() . '/cache/', '/') . '/' . $cache_name . '/';
// Create directory automatically using TorrentPier's bb_mkdir function
if (!is_dir($dir) && !bb_mkdir($dir)) {
throw new \RuntimeException("Failed to create cache directory: $dir");
}
return new FileStorage($dir);
case 'sqlite':
$dbFile = rtrim($this->cfg['db_dir'] ?? sys_get_temp_dir() . '/cache/', '/') . '/' . $cache_name . '.db';
// Create parent directory for SQLite file
$dbDir = dirname($dbFile);
if (!is_dir($dbDir) && !bb_mkdir($dbDir)) {
throw new \RuntimeException("Failed to create cache directory for SQLite: $dbDir");
}
return new SQLiteStorage($dbFile);
case 'memory':
return new MemoryStorage();
case 'memcached':
$memcachedConfig = $this->cfg['memcached'] ?? ['host' => '127.0.0.1', 'port' => 11211];
$host = $memcachedConfig['host'] ?? '127.0.0.1';
$port = $memcachedConfig['port'] ?? 11211;
return new MemcachedStorage("{$host}:{$port}");
default:
// Fallback to file storage
$dir = rtrim($this->cfg['db_dir'] ?? sys_get_temp_dir() . '/cache/', '/') . '/' . $cache_name . '/';
// Create directory automatically using TorrentPier's bb_mkdir function
if (!is_dir($dir) && !bb_mkdir($dir)) {
throw new \RuntimeException("Failed to create cache directory: $dir");
}
return new FileStorage($dir);
}
}
/**
* Get engine type name for debugging
*
* @param string $cache_type
* @return string
*/
private function _getEngineType(string $cache_type): string
{
return match ($cache_type) {
'sqlite' => 'SQLite',
'memory' => 'Memory',
'memcached' => 'Memcached',
default => 'File',
};
}
/**
* Build datastore storage instance
*
* @param string $datastore_type
* @return Storage
*/
private function _buildDatastoreStorage(string $datastore_type): Storage
{
switch ($datastore_type) {
case 'file':
case 'filecache':
case 'apcu':
case 'redis':
// Some deprecated cache types will fall back to file storage
$dir = rtrim($this->cfg['db_dir'] ?? sys_get_temp_dir() . '/cache/', '/') . '/datastore/';
// Create directory automatically using TorrentPier's bb_mkdir function
if (!is_dir($dir) && !bb_mkdir($dir)) {
throw new \RuntimeException("Failed to create datastore directory: $dir");
}
return new FileStorage($dir);
case 'sqlite':
$dbFile = rtrim($this->cfg['db_dir'] ?? sys_get_temp_dir() . '/cache/', '/') . '/datastore.db';
// Create parent directory for SQLite file
$dbDir = dirname($dbFile);
if (!is_dir($dbDir) && !bb_mkdir($dbDir)) {
throw new \RuntimeException("Failed to create datastore directory for SQLite: $dbDir");
}
return new SQLiteStorage($dbFile);
case 'memory':
return new MemoryStorage();
case 'memcached':
$memcachedConfig = $this->cfg['memcached'] ?? ['host' => '127.0.0.1', 'port' => 11211];
$host = $memcachedConfig['host'] ?? '127.0.0.1';
$port = $memcachedConfig['port'] ?? 11211;
return new MemcachedStorage("{$host}:{$port}");
default:
// Fallback to file storage
$dir = rtrim($this->cfg['db_dir'] ?? sys_get_temp_dir() . '/cache/', '/') . '/datastore/';
// Create directory automatically using TorrentPier's bb_mkdir function
if (!is_dir($dir) && !bb_mkdir($dir)) {
throw new \RuntimeException("Failed to create datastore directory: $dir");
}
return new FileStorage($dir);
}
}
/**
* Get all cache managers (for debugging)
*
* @return array
*/
public function getAllCacheManagers(): array
{
return $this->managers;
}
/**
* Get configuration
*
* @return array
*/
public function getConfig(): array
{
return $this->cfg;
}
/**
* Clear all caches
*
* @return void
*/
public function clearAll(): void
{
foreach ($this->managers as $manager) {
$manager->rm(); // Clear all items in namespace
}
if ($this->datastore) {
$this->datastore->clean();
}
}
/**
* Get cache statistics
*
* @return array
*/
public function getStatistics(): array
{
$stats = [
'total_managers' => count($this->managers),
'managers' => []
];
foreach ($this->managers as $name => $manager) {
$stats['managers'][$name] = [
'engine' => $manager->engine,
'num_queries' => $manager->num_queries,
'total_time' => $manager->sql_timetotal,
'debug_enabled' => $manager->dbg_enabled
];
}
if ($this->datastore) {
$stats['datastore'] = [
'engine' => $this->datastore->engine,
'num_queries' => $this->datastore->num_queries,
'total_time' => $this->datastore->sql_timetotal,
'queued_items' => count($this->datastore->queued_items),
'loaded_items' => count($this->datastore->data)
];
}
return $stats;
}
/**
* Magic method for backward compatibility
* Allows access to legacy properties like ->obj
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
switch ($name) {
case 'obj':
// Return array of cache objects for backward compatibility
$obj = ['__stub' => $this->stub];
foreach ($this->managers as $cache_name => $manager) {
$obj[$cache_name] = $manager;
}
return $obj;
case 'cfg':
return $this->cfg;
case 'ref':
return $this->ref;
default:
throw new \InvalidArgumentException("Property '$name' not found");
}
}
/**
* Create cache manager with advanced Nette features
*
* @param string $namespace
* @param array $config
* @return CacheManager
*/
public function createAdvancedCache(string $namespace, array $config = []): CacheManager
{
$fullConfig = array_merge($this->cfg, $config);
$fullConfig['prefix'] = $fullConfig['prefix'] ?? 'tp_';
// Build storage for the advanced cache
$storageType = $config['storage_type'] ?? 'file';
$storage = $this->_buildStorage($storageType, $namespace);
$managerConfig = [
'engine' => $this->_getEngineType($storageType),
'prefix' => $fullConfig['prefix']
];
return CacheManager::getInstance($namespace, $storage, $managerConfig);
}
/**
* Create cache with file dependencies
*
* @param string $namespace
* @param array $files
* @return CacheManager
*/
public function createFileBasedCache(string $namespace, array $files = []): CacheManager
{
$cache = $this->createAdvancedCache($namespace);
// Example usage:
// $value = $cache->load('key', function() use ($files) {
// return expensive_computation();
// }, [Cache::Files => $files]);
return $cache;
}
/**
* Create cache with tags support
*
* @param string $namespace
* @return CacheManager
*/
public function createTaggedCache(string $namespace): CacheManager
{
// Use SQLite storage which supports tags via journal
$storage = $this->_buildStorage('sqlite', $namespace);
$config = [
'engine' => 'SQLite',
'prefix' => $this->cfg['prefix'] ?? 'tp_'
];
return CacheManager::getInstance($namespace, $storage, $config);
}
/**
* Prevent cloning of the singleton instance
*/
private function __clone()
{
}
/**
* Prevent unserialization of the singleton instance
*/
public function __wakeup()
{
throw new \Exception("Cannot unserialize a singleton.");
}
}

View file

@ -1,38 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Captcha;
/**
* Interface CaptchaInterface
* @package TorrentPier\Captcha
*/
interface CaptchaInterface
{
/**
* Constructor
*
* @param array $settings
*/
public function __construct(array $settings);
/**
* Returns captcha widget
*
* @return string
*/
public function get(): string;
/**
* Checking captcha answer
*
* @return bool
*/
public function check(): bool;
}

View file

@ -1,182 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier;
/**
* Configuration management class
*
* Encapsulates the global $bb_cfg array and provides methods to access configuration values
*/
class Config
{
private static ?Config $instance = null;
private array $config = [];
private function __construct(array $config = [])
{
$this->config = $config;
}
/**
* Get the singleton instance of Config
*/
public static function getInstance(array $config = []): Config
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* Initialize the config with the global $bb_cfg array
*/
public static function init(array $bb_cfg): Config
{
self::$instance = new self($bb_cfg);
return self::$instance;
}
/**
* Get a configuration value by key
* Supports dot notation for nested arrays (e.g., 'db.host')
*/
public function get(string $key, mixed $default = null): mixed
{
if (str_contains($key, '.')) {
return $this->getNestedValue($key, $default);
}
return $this->config[$key] ?? $default;
}
/**
* Set a configuration value by key
* Supports dot notation for nested arrays
*/
public function set(string $key, mixed $value): void
{
if (str_contains($key, '.')) {
$this->setNestedValue($key, $value);
} else {
$this->config[$key] = $value;
}
}
/**
* Check if a configuration key exists
* Supports dot notation for nested arrays
*/
public function has(string $key): bool
{
if (str_contains($key, '.')) {
return $this->getNestedValue($key) !== null;
}
return array_key_exists($key, $this->config);
}
/**
* Get all configuration values
*/
public function all(): array
{
return $this->config;
}
/**
* Get a nested value using dot notation
*/
private function getNestedValue(string $key, mixed $default = null): mixed
{
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (!is_array($value) || !array_key_exists($k, $value)) {
return $default;
}
$value = $value[$k];
}
return $value;
}
/**
* Set a nested value using dot notation
*/
private function setNestedValue(string $key, mixed $value): void
{
$keys = explode('.', $key);
$target = &$this->config;
foreach ($keys as $k) {
if (!isset($target[$k]) || !is_array($target[$k])) {
$target[$k] = [];
}
$target = &$target[$k];
}
$target = $value;
}
/**
* Merge additional configuration values
*/
public function merge(array $config): void
{
$this->config = array_merge_recursive($this->config, $config);
}
/**
* Get a section of the configuration
*/
public function getSection(string $section): array
{
return $this->config[$section] ?? [];
}
/**
* Magic method to allow property access
*/
public function __get(string $key): mixed
{
return $this->get($key);
}
/**
* Magic method to allow property setting
*/
public function __set(string $key, mixed $value): void
{
$this->set($key, $value);
}
/**
* Magic method to check if property exists
*/
public function __isset(string $key): bool
{
return $this->has($key);
}
/**
* Prevent cloning of the singleton instance
*/
private function __clone() {}
/**
* Prevent unserialization of the singleton instance
*/
public function __wakeup()
{
throw new \Exception("Cannot unserialize a singleton.");
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,569 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Database;
/**
* Database Debug functionality extracted from Database class
* Handles all debug logging, timing, and query explanation features
*/
class DatabaseDebugger
{
private Database $db;
// Debug configuration
public bool $dbg_enabled = false;
public bool $do_explain = false;
public float $slow_time = 3.0;
// Timing and statistics
public float $sql_starttime = 0;
public float $cur_query_time = 0;
// Debug storage
public array $dbg = [];
public int $dbg_id = 0;
public array $legacy_queries = []; // Track queries that needed legacy compatibility fixes
// Explain functionality
public string $explain_hold = '';
public string $explain_out = '';
// Nette Explorer tracking
public bool $is_nette_explorer_query = false;
public function __construct(Database $db)
{
$this->db = $db;
// Initialize debug settings more safely
$this->initializeDebugSettings();
$this->slow_time = defined('SQL_SLOW_QUERY_TIME') ? SQL_SLOW_QUERY_TIME : 3;
}
/**
* Initialize debug settings exactly like the original Database class
*/
private function initializeDebugSettings(): void
{
// Use the EXACT same logic as the original DB class
$this->dbg_enabled = (dev()->checkSqlDebugAllowed() || !empty($_COOKIE['explain']));
$this->do_explain = ($this->dbg_enabled && !empty($_COOKIE['explain']));
}
/**
* Store debug info
*/
public function debug(string $mode): void
{
$id =& $this->dbg_id;
$dbg =& $this->dbg[$id];
if ($mode === 'start') {
// Always update timing if required constants are defined
if (defined('SQL_CALC_QUERY_TIME') && SQL_CALC_QUERY_TIME || defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES) {
$this->sql_starttime = microtime(true);
$this->db->sql_starttime = $this->sql_starttime; // Update main Database object too
}
if ($this->dbg_enabled) {
$currentQuery = $this->db->cur_query ?? '';
$dbg['sql'] = preg_replace('#^(\s*)(/\*)(.*)(\*/)(\s*)#', '', $currentQuery);
// Also check SQL syntax to detect Nette Explorer queries
if (!$this->is_nette_explorer_query && $this->detectNetteExplorerBySqlSyntax($dbg['sql'])) {
$this->markAsNetteExplorerQuery();
}
$dbg['src'] = $this->debug_find_source();
$dbg['file'] = $this->debug_find_source('file');
$dbg['line'] = $this->debug_find_source('line');
$dbg['time'] = '';
$dbg['info'] = '';
$dbg['mem_before'] = function_exists('sys') ? sys('mem') : 0;
}
if ($this->do_explain) {
$this->explain('start');
}
} elseif ($mode === 'stop') {
if (defined('SQL_CALC_QUERY_TIME') && SQL_CALC_QUERY_TIME || defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES) {
$this->cur_query_time = microtime(true) - $this->sql_starttime;
$this->db->sql_timetotal += $this->cur_query_time;
$this->db->DBS['sql_timetotal'] += $this->cur_query_time;
if (defined('SQL_LOG_SLOW_QUERIES') && SQL_LOG_SLOW_QUERIES && $this->cur_query_time > $this->slow_time) {
$this->log_slow_query();
}
}
if ($this->dbg_enabled) {
$dbg['time'] = $this->cur_query_time > 0 ? $this->cur_query_time : (microtime(true) - $this->sql_starttime);
$dbg['info'] = $this->db->query_info();
$dbg['mem_after'] = function_exists('sys') ? sys('mem') : 0;
// Add Nette Explorer marker to debug info for panel display
if ($this->is_nette_explorer_query && !str_contains($dbg['info'], '[Nette Explorer]')) {
// Store both plain text and HTML versions
$dbg['info_plain'] = $dbg['info'] . ' [Nette Explorer]';
$dbg['info'] .= ' <span style="color: #28a745; font-weight: bold; background: #d4edda; padding: 2px 6px; border-radius: 3px; font-size: 11px;">[Nette Explorer]</span>';
$dbg['is_nette_explorer'] = true;
} else {
$dbg['info_plain'] = $dbg['info'];
$dbg['is_nette_explorer'] = false;
}
$id++;
}
if ($this->do_explain) {
$this->explain('stop');
}
// Check for logging
if ($this->db->DBS['log_counter'] && $this->db->inited) {
$this->log_query($this->db->DBS['log_file']);
$this->db->DBS['log_counter']--;
}
// Reset Nette Explorer flag after query completion
$this->resetNetteExplorerFlag();
}
// Update timing in main Database object
$this->db->cur_query_time = $this->cur_query_time;
}
/**
* Find source of database call
*/
public function debug_find_source(string $mode = 'all'): string
{
if (!defined('SQL_PREPEND_SRC') || !SQL_PREPEND_SRC) {
return 'src disabled';
}
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Check if this is a Nette Explorer query by examining the call stack
$isNetteExplorer = $this->detectNetteExplorerInTrace($trace);
if ($isNetteExplorer) {
$this->markAsNetteExplorerQuery();
}
// Find first non-DB call (skip Database.php, DebugSelection.php, and DatabaseDebugger.php)
foreach ($trace as $frame) {
if (isset($frame['file']) &&
!str_contains($frame['file'], 'Database/Database.php') &&
!str_contains($frame['file'], 'Database/DebugSelection.php') &&
!str_contains($frame['file'], 'Database/DatabaseDebugger.php')) {
switch ($mode) {
case 'file':
return $frame['file'];
case 'line':
return (string)($frame['line'] ?? '?');
case 'all':
default:
$file = function_exists('hide_bb_path') ? hide_bb_path($frame['file']) : basename($frame['file']);
$line = $frame['line'] ?? '?';
return "$file($line)";
}
}
}
return 'src not found';
}
/**
* Detect if the current query comes from Nette Explorer by examining the call stack
*/
public function detectNetteExplorerInTrace(array $trace): bool
{
foreach ($trace as $frame) {
if (isset($frame['class'])) {
// Check for Nette Database classes in the call stack
if (str_contains($frame['class'], 'Nette\\Database\\') ||
str_contains($frame['class'], 'TorrentPier\\Database\\DebugSelection')) {
return true;
}
}
if (isset($frame['file'])) {
// Check for Nette Database files or our DebugSelection
if (str_contains($frame['file'], 'vendor/nette/database/') ||
str_contains($frame['file'], 'Database/DebugSelection.php')) {
return true;
}
}
}
return false;
}
/**
* Detect if SQL query syntax suggests it came from Nette Explorer
*/
public function detectNetteExplorerBySqlSyntax(string $sql): bool
{
// Nette Database typically generates SQL with these characteristics:
// 1. Backticks around column/table names
// 2. Parentheses around WHERE conditions like (column = value)
// 3. Specific patterns like IN (value) instead of IN (value)
$nettePatterns = [
'/`[a-zA-Z0-9_]+`/', // Backticks around identifiers
'/WHERE\s*\([^)]+\)/', // Parentheses around WHERE conditions
'/SELECT\s+`[^`]+`.*FROM\s+`[^`]+`/', // SELECT with backticked columns and tables
];
foreach ($nettePatterns as $pattern) {
if (preg_match($pattern, $sql)) {
return true;
}
}
return false;
}
/**
* Prepare for logging
*/
public function log_next_query(int $queries_count = 1, string $log_file = 'sql_queries'): void
{
$this->db->DBS['log_file'] = $log_file;
$this->db->DBS['log_counter'] = $queries_count;
}
/**
* Log query
*/
public function log_query(string $log_file = 'sql_queries'): void
{
if (!function_exists('bb_log') || !function_exists('dev')) {
return;
}
$q_time = ($this->cur_query_time >= 10) ? round($this->cur_query_time, 0) : sprintf('%.3f', $this->cur_query_time);
$msg = [];
$msg[] = round($this->sql_starttime);
$msg[] = date('m-d H:i:s', (int)$this->sql_starttime);
$msg[] = sprintf('%-6s', $q_time);
$msg[] = sprintf('%05d', getmypid());
$msg[] = $this->db->db_server;
$msg[] = function_exists('dev') ? dev()->formatShortQuery($this->db->cur_query) : $this->db->cur_query;
$msg = implode(defined('LOG_SEPR') ? LOG_SEPR : ' | ', $msg);
$msg .= ($info = $this->db->query_info()) ? ' # ' . $info : '';
$msg .= ' # ' . $this->debug_find_source() . ' ';
$msg .= defined('IN_CRON') ? 'cron' : basename($_SERVER['REQUEST_URI'] ?? '');
bb_log($msg . (defined('LOG_LF') ? LOG_LF : "\n"), $log_file);
}
/**
* Log slow query
*/
public function log_slow_query(string $log_file = 'sql_slow_bb'): void
{
if (!defined('IN_FIRST_SLOW_QUERY') && function_exists('CACHE')) {
$cache = CACHE('bb_cache');
if ($cache && $cache->get('dont_log_slow_query')) {
return;
}
}
$this->log_query($log_file);
}
/**
* Log error
*
* NOTE: This method logs detailed information to FILES only (error_log, bb_log).
* Log files are not accessible to regular users, so detailed information is safe here.
* User-facing error display is handled separately with proper security checks.
*/
public function log_error(?\Exception $exception = null): void
{
$error_details = [];
$error_msg = '';
if ($exception) {
// Use the actual exception information which is more reliable
$error_msg = "Database Error: " . $exception->getMessage();
$error_code = $exception->getCode();
if ($error_code) {
$error_msg = "Database Error ({$error_code}): " . $exception->getMessage();
}
// Collect detailed error information
$error_details[] = "Exception: " . get_class($exception);
$error_details[] = "Message: " . $exception->getMessage();
$error_details[] = "Code: " . $exception->getCode();
$error_details[] = "File: " . $exception->getFile() . ":" . $exception->getLine();
// Add PDO-specific details if it's a PDO exception
if ($exception instanceof \PDOException) {
$error_details[] = "PDO Error Info: " . json_encode($exception->errorInfo ?? []);
}
} else {
// Fallback to PDO error state (legacy behavior)
$error = $this->db->sql_error();
// Only log if there's an actual error (not 00000 which means "no error")
if (!$error['code'] || $error['code'] === '00000' || !$error['message']) {
return; // Don't log empty or "no error" states
}
$error_msg = "Database Error ({$error['code']}): " . $error['message'];
$error_details[] = "PDO Error Code: " . $error['code'];
$error_details[] = "PDO Error Message: " . $error['message'];
}
// Add comprehensive context for debugging
$error_details[] = "Query: " . ($this->db->cur_query ?: 'None');
$error_details[] = "Source: " . $this->debug_find_source();
$error_details[] = "Database: " . ($this->db->selected_db ?: 'None');
$error_details[] = "Server: " . $this->db->db_server;
$error_details[] = "Timestamp: " . date('Y-m-d H:i:s');
$error_details[] = "Request URI: " . ($_SERVER['REQUEST_URI'] ?? 'CLI');
$error_details[] = "User IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'Unknown');
// Check connection status
try {
if ($this->db->connection) {
$error_details[] = "Connection Status: Active";
$pdo = $this->db->connection->getPdo();
$error_details[] = "PDO Connection: " . ($pdo ? 'Available' : 'Null');
if ($pdo) {
$errorInfo = $pdo->errorInfo();
$error_details[] = "Current PDO Error Info: " . json_encode($errorInfo);
}
} else {
$error_details[] = "Connection Status: No connection";
}
} catch (\Exception $e) {
$error_details[] = "Connection Check Failed: " . $e->getMessage();
}
// Build comprehensive log message
$log_message = $error_msg . "\n" . implode("\n", $error_details);
// Log to both error_log and TorrentPier's logging system
error_log($error_msg);
// Use TorrentPier's bb_log for better file management and organization
if (function_exists('bb_log')) {
bb_log($log_message, 'database_errors');
}
// Also log to PHP error log for immediate access
error_log("DETAILED: " . $log_message);
}
/**
* Log legacy query that needed automatic compatibility fix
*/
public function logLegacyQuery(string $query, string $error): void
{
$legacy_entry = [
'query' => $query,
'error' => $error,
'source' => $this->debug_find_source(),
'file' => $this->debug_find_source('file'),
'line' => $this->debug_find_source('line'),
'time' => microtime(true)
];
$this->legacy_queries[] = $legacy_entry;
// Mark the CURRENT debug entry as legacy instead of creating a new one
if ($this->dbg_enabled && !empty($this->dbg)) {
// Find the most recent debug entry (the one that just executed and failed)
$current_id = $this->dbg_id - 1;
if (isset($this->dbg[$current_id])) {
// Mark the existing entry as legacy
$this->dbg[$current_id]['is_legacy_query'] = true;
// Update the info to show it was automatically fixed
$original_info = $this->dbg[$current_id]['info'] ?? '';
$original_info_plain = $this->dbg[$current_id]['info_plain'] ?? $original_info;
$this->dbg[$current_id]['info'] = 'LEGACY COMPATIBILITY FIX APPLIED - ' . $original_info;
$this->dbg[$current_id]['info_plain'] = 'LEGACY COMPATIBILITY FIX APPLIED - ' . $original_info_plain;
}
}
// Log to file for permanent record
$msg = 'LEGACY QUERY DETECTED - NEEDS FIXING' . LOG_LF;
$msg .= 'Query: ' . $query . LOG_LF;
$msg .= 'Error: ' . $error . LOG_LF;
$msg .= 'Source: ' . $legacy_entry['source'] . LOG_LF;
$msg .= 'Time: ' . date('Y-m-d H:i:s', (int)$legacy_entry['time']) . LOG_LF;
bb_log($msg, 'legacy_queries', false);
}
/**
* Set slow query marker
*/
public function expect_slow_query(int $ignoring_time = 60, int $new_priority = 10): void
{
if (function_exists('CACHE')) {
$cache = CACHE('bb_cache');
if ($old_priority = $cache->get('dont_log_slow_query')) {
if ($old_priority > $new_priority) {
return;
}
}
if (!defined('IN_FIRST_SLOW_QUERY')) {
define('IN_FIRST_SLOW_QUERY', true);
}
$cache->set('dont_log_slow_query', $new_priority, $ignoring_time);
}
}
/**
* Explain queries - maintains compatibility with legacy SqlDb
*/
public function explain($mode, $html_table = '', array $row = []): mixed
{
if (!$this->do_explain) {
return false;
}
$query = $this->db->cur_query ?? '';
// Remove comments
$query = preg_replace('#(\s*)(/\*)(.*)(\*/)(\s*)#', '', $query);
switch ($mode) {
case 'start':
$this->explain_hold = '';
if (preg_match('#UPDATE ([a-z0-9_]+).*?WHERE(.*)/#', $query, $m)) {
$query = "SELECT * FROM $m[1] WHERE $m[2]";
} elseif (preg_match('#DELETE FROM ([a-z0-9_]+).*?WHERE(.*)#s', $query, $m)) {
$query = "SELECT * FROM $m[1] WHERE $m[2]";
}
if (str_starts_with($query, "SELECT")) {
$html_table = false;
try {
$result = $this->db->connection->query("EXPLAIN $query");
while ($row = $result->fetch()) {
// Convert row to array regardless of type
$rowArray = (array)$row;
$html_table = $this->explain('add_explain_row', $html_table, $rowArray);
}
} catch (\Exception $e) {
// Skip if explain fails
}
if ($html_table) {
$this->explain_hold .= '</table>';
}
}
break;
case 'stop':
if (!$this->explain_hold) {
break;
}
$id = $this->dbg_id - 1;
$htid = 'expl-' . spl_object_hash($this->db->connection) . '-' . $id;
$dbg = $this->dbg[$id] ?? [];
// Ensure required keys exist with defaults
$dbg = array_merge([
'time' => $this->cur_query_time ?? 0,
'sql' => $this->db->cur_query ?? '',
'query' => $this->db->cur_query ?? '',
'src' => $this->debug_find_source(),
'trace' => $this->debug_find_source() // Backup for compatibility
], $dbg);
$this->explain_out .= '
<table width="98%" cellpadding="0" cellspacing="0" class="bodyline row2 bCenter" style="border-bottom: 0;">
<tr>
<th style="height: 22px;" align="left">&nbsp;' . ($dbg['src'] ?? $dbg['trace']) . '&nbsp; [' . sprintf('%.3f', $dbg['time']) . ' s]&nbsp; <i>' . $this->db->query_info() . '</i></th>
<th class="copyElement" data-clipboard-target="#' . $htid . '" style="height: 22px;" align="right" title="Copy to clipboard">' . "[{$this->db->engine}] {$this->db->db_server}.{$this->db->selected_db}" . ' :: Query #' . ($this->db->num_queries + 1) . '&nbsp;</th>
</tr>
<tr><td colspan="2">' . $this->explain_hold . '</td></tr>
</table>
<div class="sqlLog"><div id="' . $htid . '" class="sqlLogRow sqlExplain" style="padding: 0;">' . (function_exists('dev') ? dev()->formatShortQuery($dbg['sql'] ?? $dbg['query'], true) : htmlspecialchars($dbg['sql'] ?? $dbg['query'])) . '&nbsp;&nbsp;</div></div>
<br />';
break;
case 'add_explain_row':
if (!$html_table && $row) {
$html_table = true;
$this->explain_hold .= '<table width="100%" cellpadding="3" cellspacing="1" class="bodyline" style="border-width: 0;"><tr>';
foreach (array_keys($row) as $val) {
$this->explain_hold .= '<td class="row3 gensmall" align="center"><b>' . htmlspecialchars($val) . '</b></td>';
}
$this->explain_hold .= '</tr>';
}
$this->explain_hold .= '<tr>';
foreach (array_values($row) as $i => $val) {
$class = !($i % 2) ? 'row1' : 'row2';
$this->explain_hold .= '<td class="' . $class . ' gen">' . str_replace(["{$this->db->selected_db}.", ',', ';'], ['', ', ', ';<br />'], htmlspecialchars($val ?? '')) . '</td>';
}
$this->explain_hold .= '</tr>';
return $html_table;
case 'display':
echo '<a name="explain"></a><div class="med">' . $this->explain_out . '</div>';
break;
}
return false;
}
/**
* Get debug statistics for display
*/
public function getDebugStats(): array
{
return [
'num_queries' => count($this->dbg),
'sql_timetotal' => $this->db->sql_timetotal,
'queries' => $this->dbg,
'explain_out' => $this->explain_out
];
}
/**
* Clear debug data
*/
public function clearDebugData(): void
{
$this->dbg = [];
$this->dbg_id = 0;
$this->explain_hold = '';
$this->explain_out = '';
}
/**
* Mark next query as coming from Nette Explorer
*/
public function markAsNetteExplorerQuery(): void
{
$this->is_nette_explorer_query = true;
}
/**
* Reset Nette Explorer query flag
*/
public function resetNetteExplorerFlag(): void
{
$this->is_nette_explorer_query = false;
}
}

View file

@ -1,101 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Database;
/**
* Database Factory - maintains compatibility with existing DB() function calls
*
* This factory completely replaces the legacy SqlDb/Dbs system with the new
* Nette Database implementation while maintaining full backward compatibility.
*/
class DatabaseFactory
{
private static array $instances = [];
private static array $server_configs = [];
private static array $server_aliases = [];
/**
* Initialize the factory with database configuration
*/
public static function init(array $db_config, array $db_aliases = []): void
{
self::$server_configs = $db_config;
self::$server_aliases = $db_aliases;
}
/**
* Get database instance (maintains compatibility with existing DB() calls)
*/
public static function getInstance(string $srv_name_or_alias = 'db'): Database
{
$srv_name = self::resolveSrvName($srv_name_or_alias);
if (!isset(self::$instances[$srv_name])) {
// Get configuration for this server
$cfg_values = self::$server_configs[$srv_name] ?? null;
if (!$cfg_values) {
throw new \RuntimeException("Database configuration not found for server: $srv_name");
}
self::$instances[$srv_name] = Database::getInstance($cfg_values, $srv_name);
}
return self::$instances[$srv_name];
}
/**
* Resolve server name using alias system
*/
private static function resolveSrvName(string $name): string
{
// Check if it's an alias
if (isset(self::$server_aliases[$name])) {
return self::$server_aliases[$name];
}
// Check if it's a direct server name
if (isset(self::$server_configs[$name])) {
return $name;
}
// Default to 'db'
return 'db';
}
/**
* Check if a specific database server is configured
*/
public static function hasServer(string $srv_name): bool
{
return isset(self::$server_configs[$srv_name]);
}
/**
* Get all configured server names
*/
public static function getServerNames(): array
{
return array_keys(self::$server_configs);
}
/**
* Clear all cached instances (useful for testing)
*/
public static function clearInstances(): void
{
foreach (self::$instances as $instance) {
if (method_exists($instance, 'close')) {
$instance->close();
}
}
self::$instances = [];
Database::destroyInstances();
}
}

View file

@ -1,295 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Database;
use Nette\Database\Table\Selection;
use ReflectionClass;
use Exception;
/**
* DebugSelection - Wraps Nette Database Selection to provide debug logging and explain functionality
*/
class DebugSelection
{
private Selection $selection;
private Database $db;
public function __construct(Selection $selection, Database $db)
{
$this->selection = $selection;
$this->db = $db;
}
/**
* Magic method to delegate calls to the wrapped Selection
*/
public function __call(string $name, array $arguments)
{
$result = call_user_func_array([$this->selection, $name], $arguments);
// If result is another Selection, wrap it too
if ($result instanceof Selection) {
return new self($result, $this->db);
}
return $result;
}
/**
* Magic method to delegate property access
*/
public function __get(string $name)
{
return $this->selection->$name;
}
/**
* Magic method to delegate property setting
*/
public function __set(string $name, $value): void
{
$this->selection->$name = $value;
}
/**
* Magic method to check if property is set
*/
public function __isset(string $name): bool
{
return isset($this->selection->$name);
}
/**
* Log query execution for debug panel
*/
private function logQuery(string $method, array $arguments): void
{
if (!defined('SQL_DEBUG') || !SQL_DEBUG) {
return;
}
// Use the actual SQL with substituted parameters for both logging and EXPLAIN
$sql = $this->generateSqlForLogging($method, $arguments, false);
// Mark this query as coming from Nette Explorer
$this->db->debugger->markAsNetteExplorerQuery();
// Set the query for debug logging
$this->db->cur_query = $sql;
$this->db->debug('start');
}
/**
* Complete query logging after execution
*/
private function completeQueryLogging(): void
{
if (!defined('SQL_DEBUG') || !SQL_DEBUG) {
return;
}
// Note: explain('stop') is automatically called by debug('stop') when do_explain is true
$this->db->debug('stop');
}
/**
* Generate SQL representation for logging and EXPLAIN
*/
private function generateSqlForLogging(string $method, array $arguments, bool $useRawSQL = true): string
{
// For SELECT operations, try to get the SQL from Nette
if (in_array($method, ['fetch', 'fetchAll', 'count'], true)) {
$sql = $useRawSQL ? $this->getSqlFromSelection() : $this->getSqlFromSelection(true);
// Modify the SQL based on the method
switch ($method) {
case 'fetch':
// If it doesn't already have LIMIT, add it
if (!preg_match('/LIMIT\s+\d+/i', $sql)) {
$sql .= ' LIMIT 1';
}
return $sql;
case 'count':
// Replace SELECT * with SELECT COUNT(*)
return preg_replace('/^SELECT\s+\*/i', 'SELECT COUNT(*)', $sql);
case 'fetchAll':
default:
return $sql;
}
}
// For INSERT/UPDATE/DELETE, generate appropriate SQL
$tableName = $this->selection->getName();
return match ($method) {
'insert' => $this->generateInsertSql($tableName, $arguments),
'update' => $this->generateUpdateSql($tableName, $arguments, $useRawSQL),
'delete' => $this->generateDeleteSql($tableName, $useRawSQL),
default => "-- Explorer method: {$method} on {$tableName}"
};
}
/**
* Generate INSERT SQL statement
*/
private function generateInsertSql(string $tableName, array $arguments): string
{
if (!isset($arguments[0]) || !is_array($arguments[0])) {
return "INSERT INTO {$tableName} (...) VALUES (...)";
}
$data = $arguments[0];
$columns = implode(', ', array_keys($data));
$values = implode(', ', array_map(
static fn($v) => is_string($v) ? "'$v'" : $v,
array_values($data)
));
return "INSERT INTO {$tableName} ({$columns}) VALUES ({$values})";
}
/**
* Generate UPDATE SQL statement
*/
private function generateUpdateSql(string $tableName, array $arguments, bool $useRawSQL): string
{
$setPairs = [];
if (isset($arguments[0]) && is_array($arguments[0])) {
foreach ($arguments[0] as $key => $value) {
$setPairs[] = "{$key} = " . (is_string($value) ? "'$value'" : $value);
}
}
$setClause = !empty($setPairs) ? implode(', ', $setPairs) : '...';
$sql = $this->getSqlFromSelection(!$useRawSQL);
// Extract WHERE clause from the SQL
if (preg_match('/WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT|\s+GROUP\s+BY|$)/i', $sql, $matches)) {
return "UPDATE {$tableName} SET {$setClause} WHERE " . trim($matches[1]);
}
return "UPDATE {$tableName} SET {$setClause}";
}
/**
* Generate DELETE SQL statement
*/
private function generateDeleteSql(string $tableName, bool $useRawSQL): string
{
$sql = $this->getSqlFromSelection(!$useRawSQL);
// Extract WHERE clause from the SQL
if (preg_match('/WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT|\s+GROUP\s+BY|$)/i', $sql, $matches)) {
return "DELETE FROM {$tableName} WHERE " . trim($matches[1]);
}
return "DELETE FROM {$tableName}";
}
/**
* Get SQL from Nette Selection with optional parameter substitution
*/
private function getSqlFromSelection(bool $replaceParameters = false): string
{
try {
$reflectionClass = new ReflectionClass($this->selection);
$sql = '';
// Try getSql() method first
if ($reflectionClass->hasMethod('getSql')) {
$getSqlMethod = $reflectionClass->getMethod('getSql');
$getSqlMethod->setAccessible(true);
$sql = $getSqlMethod->invoke($this->selection);
} else {
// Try __toString() method as fallback
$sql = (string)$this->selection;
}
// For EXPLAIN to work, we need to replace ? with actual values
if ($replaceParameters) {
$sql = preg_replace('/\?/', '1', $sql);
}
return $sql;
} catch (Exception $e) {
// Fall back to simple representation
return "SELECT * FROM " . $this->selection->getName() . " WHERE 1=1";
}
}
// Delegate common Selection methods with logging
public function where(...$args): self
{
return new self($this->selection->where(...$args), $this->db);
}
public function order(...$args): self
{
return new self($this->selection->order(...$args), $this->db);
}
public function select(...$args): self
{
return new self($this->selection->select(...$args), $this->db);
}
public function limit(...$args): self
{
return new self($this->selection->limit(...$args), $this->db);
}
public function fetch()
{
$this->logQuery('fetch', []);
$result = $this->selection->fetch();
$this->completeQueryLogging();
return $result;
}
public function fetchAll(): array
{
$this->logQuery('fetchAll', []);
$result = $this->selection->fetchAll();
$this->completeQueryLogging();
return $result;
}
public function insert($data)
{
$this->logQuery('insert', [$data]);
$result = $this->selection->insert($data);
$this->completeQueryLogging();
return $result;
}
public function update($data): int
{
$this->logQuery('update', [$data]);
$result = $this->selection->update($data);
$this->completeQueryLogging();
return $result;
}
public function delete(): int
{
$this->logQuery('delete', []);
$result = $this->selection->delete();
$this->completeQueryLogging();
return $result;
}
public function count(): int
{
$this->logQuery('count', []);
$result = $this->selection->count();
$this->completeQueryLogging();
return $result;
}
}

View file

@ -1,305 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Database;
/**
* Migration Status Manager
*
* Provides read-only access to database migration status information
* for the admin panel. Uses TorrentPier's database singleton and config system.
*/
class MigrationStatus
{
private string $migrationTable;
private string $migrationPath;
private array $initialMigrations = [
'20250619000001',
'20250619000002'
];
public function __construct()
{
$this->migrationTable = BB_MIGRATIONS;
$this->migrationPath = BB_ROOT . 'migrations';
}
/**
* Get complete migration status information
*
* @return array Migration status data including applied/pending migrations
*/
public function getMigrationStatus(): array
{
try {
// Check if migration table exists using Nette Database Explorer
$tableExists = $this->checkMigrationTableExists();
$setupStatus = $this->getSetupStatus();
if (!$tableExists) {
return [
'table_exists' => false,
'current_version' => null,
'applied_migrations' => [],
'pending_migrations' => $this->getAvailableMigrations(),
'setup_status' => $setupStatus,
'requires_setup' => $setupStatus['needs_setup']
];
}
// Get applied migrations using Nette Database Explorer
$appliedMigrations = DB()->query("
SELECT version, migration_name, start_time, end_time
FROM {$this->migrationTable}
ORDER BY version ASC
")->fetchAll();
// Convert Nette Result objects to arrays
$appliedMigrationsArray = [];
foreach ($appliedMigrations as $migration) {
$appliedMigrationsArray[] = [
'version' => $migration->version,
'migration_name' => $migration->migration_name,
'start_time' => $migration->start_time,
'end_time' => $migration->end_time
];
}
// Get current version (latest applied)
$currentVersion = null;
if (!empty($appliedMigrationsArray)) {
$currentVersion = end($appliedMigrationsArray)['version'];
}
// Get pending migrations
$availableMigrations = $this->getAvailableMigrations();
$appliedVersions = array_column($appliedMigrationsArray, 'version');
$pendingMigrations = array_filter($availableMigrations, function ($migration) use ($appliedVersions) {
return !in_array($migration['version'], $appliedVersions);
});
return [
'table_exists' => true,
'current_version' => $currentVersion,
'applied_migrations' => $appliedMigrationsArray,
'pending_migrations' => array_values($pendingMigrations),
'setup_status' => $setupStatus,
'requires_setup' => $setupStatus['needs_setup']
];
} catch (\Exception $e) {
bb_die('Error checking migration status: ' . $e->getMessage());
}
}
/**
* Determine setup status for existing installations
*
* @return array Setup status information
*/
private function getSetupStatus(): array
{
try {
// Check if core TorrentPier tables exist (indicates existing installation)
$coreTablesExist = $this->checkCoreTablesExist();
$migrationTableExists = $this->checkMigrationTableExists();
if (!$coreTablesExist) {
// Fresh installation
return [
'type' => 'fresh',
'needs_setup' => false,
'message' => 'Fresh installation - migrations will run normally',
'action_required' => false
];
}
if (!$migrationTableExists) {
// Existing installation without migration system
return [
'type' => 'existing_needs_setup',
'needs_setup' => true,
'message' => 'Existing installation detected - migration setup required',
'action_required' => true,
'instructions' => 'Mark initial migrations as applied using --fake flag'
];
}
// Check if initial migrations are marked as applied
$initialMigrationsApplied = $this->checkInitialMigrationsApplied();
if (!$initialMigrationsApplied) {
return [
'type' => 'existing_partial_setup',
'needs_setup' => true,
'message' => 'Migration table exists but initial migrations not marked as applied',
'action_required' => true,
'instructions' => 'Run: php vendor/bin/phinx migrate --fake --target=20250619000002'
];
}
// Fully set up
return [
'type' => 'fully_setup',
'needs_setup' => false,
'message' => 'Migration system fully configured',
'action_required' => false
];
} catch (\Exception $e) {
return [
'type' => 'error',
'needs_setup' => false,
'message' => 'Error detecting setup status: ' . $e->getMessage(),
'action_required' => false
];
}
}
/**
* Check if core TorrentPier tables exist
*
* @return bool True if core tables exist
*/
private function checkCoreTablesExist(): bool
{
try {
$coreTable = 'bb_users'; // Key table that should exist in any TorrentPier installation
$escapedTable = DB()->escape($coreTable);
$result = DB()->query("
SELECT COUNT(*) as table_count
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = '{$escapedTable}'
")->fetch();
return $result && $result->table_count > 0;
} catch (\Exception $e) {
return false;
}
}
/**
* Check if initial migrations are marked as applied
*
* @return bool True if initial migrations are applied
*/
private function checkInitialMigrationsApplied(): bool
{
try {
$initialMigrationsCSV = implode(',', $this->initialMigrations);
$result = DB()->query("
SELECT COUNT(*) as migration_count
FROM {$this->migrationTable}
WHERE version IN ($initialMigrationsCSV)
")->fetch();
return $result && $result->migration_count >= count($this->initialMigrations);
} catch (\Exception $e) {
return false;
}
}
/**
* Check if migration table exists
*
* @return bool True if table exists, false otherwise
*/
private function checkMigrationTableExists(): bool
{
try {
// Using simple query without parameters to avoid issues
$escapedTable = DB()->escape($this->migrationTable);
$result = DB()->query("
SELECT COUNT(*) as table_count
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = '{$escapedTable}'
")->fetch();
return $result && $result->table_count > 0;
} catch (\Exception $e) {
return false;
}
}
/**
* Get available migrations from filesystem
*
* @return array List of available migration files
*/
private function getAvailableMigrations(): array
{
$migrations = [];
if (is_dir($this->migrationPath)) {
$files = glob($this->migrationPath . '/*.php');
foreach ($files as $file) {
$filename = basename($file);
if (preg_match('/^(\d+)_(.+)\.php$/', $filename, $matches)) {
$migrations[] = [
'version' => $matches[1],
'name' => $matches[2],
'filename' => $filename,
'file_path' => $file
];
}
}
}
// Sort by version
usort($migrations, function ($a, $b) {
return strcmp($a['version'], $b['version']);
});
return $migrations;
}
/**
* Get database schema information
*
* @return array Database statistics and information
*/
public function getSchemaInfo(): array
{
try {
// Get database name using Nette Database Explorer
$dbInfo = DB()->query("SELECT DATABASE() as db_name")->fetch();
// Get table count using Nette Database Explorer
$tableInfo = DB()->query("
SELECT COUNT(*) as table_count
FROM information_schema.tables
WHERE table_schema = DATABASE()
")->fetch();
// Get database size using Nette Database Explorer
$sizeInfo = DB()->query("
SELECT
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) as size_mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
")->fetch();
return [
'database_name' => $dbInfo->db_name ?? 'Unknown',
'table_count' => $tableInfo->table_count ?? 0,
'size_mb' => $sizeInfo->size_mb ?? 0
];
} catch (\Exception $e) {
return [
'database_name' => 'Unknown',
'table_count' => 0,
'size_mb' => 0,
'error' => $e->getMessage()
];
}
}
}

View file

@ -1,442 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier;
use Bugsnag\Client;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\BrowserConsoleHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Whoops\Handler\PlainTextHandler;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use jacklul\MonologTelegramHandler\TelegramHandler;
use jacklul\MonologTelegramHandler\TelegramFormatter;
use Exception;
/**
* Development and Debugging System
*
* Singleton class that provides development and debugging functionality
* including error handling, SQL logging, and debugging utilities.
*/
class Dev
{
private static ?Dev $instance = null;
/**
* Whoops instance
*
* @var Run
*/
private Run $whoops;
/**
* Initialize debugging system
*/
private function __construct()
{
$this->whoops = new Run;
if ($this->isDebugEnabled()) {
$this->getWhoopsOnPage();
} else {
$this->getWhoopsPlaceholder();
}
$this->getWhoopsLogger();
if (!$this->isDevelopmentEnvironment()) {
$this->getTelegramSender();
$this->getBugsnag();
}
$this->whoops->register();
}
/**
* Get the singleton instance of Dev
*/
public static function getInstance(): Dev
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initialize the dev system (for compatibility)
*/
public static function init(): Dev
{
return self::getInstance();
}
/**
* [Whoops] Bugsnag handler
*
* @return void
*/
private function getBugsnag(): void
{
if (!config()->get('bugsnag.enabled')) {
return;
}
$bugsnag = Client::make(config()->get('bugsnag.api_key'));
$this->whoops->pushHandler(function ($e) use ($bugsnag) {
$bugsnag->notifyException($e);
});
}
/**
* [Whoops] Telegram handler
*
* @return void
*/
private function getTelegramSender(): void
{
if (!config()->get('telegram_sender.enabled')) {
return;
}
$telegramSender = new PlainTextHandler();
$telegramSender->loggerOnly(true);
$telegramSender->setLogger((new Logger(
APP_NAME,
[(new TelegramHandler(config()->get('telegram_sender.token'), (int)config()->get('telegram_sender.chat_id'), timeout: (int)config()->get('telegram_sender.timeout')))
->setFormatter(new TelegramFormatter())]
)));
$this->whoops->pushHandler($telegramSender);
}
/**
* [Whoops] On page handler (in debug)
*
* @return void
*/
private function getWhoopsOnPage(): void
{
/**
* Show errors on page with enhanced database information
*/
$prettyPageHandler = new \TorrentPier\Whoops\EnhancedPrettyPageHandler();
foreach (config()->get('whoops.blacklist', []) as $key => $secrets) {
foreach ($secrets as $secret) {
$prettyPageHandler->blacklist($key, $secret);
}
}
$this->whoops->pushHandler($prettyPageHandler);
/**
* Show log in browser console
*/
$loggingInConsole = new PlainTextHandler();
$loggingInConsole->loggerOnly(true);
$loggingInConsole->setLogger((new Logger(
APP_NAME,
[(new BrowserConsoleHandler())
->setFormatter((new LineFormatter(null, null, true)))]
)));
$this->whoops->pushHandler($loggingInConsole);
}
/**
* [Whoops] Logger handler
*
* @return void
*/
private function getWhoopsLogger(): void
{
if ((int)ini_get('log_errors') !== 1) {
return;
}
$loggingInFile = new PlainTextHandler();
$loggingInFile->loggerOnly(true);
$loggingInFile->setLogger((new Logger(
APP_NAME,
[(new StreamHandler(WHOOPS_LOG_FILE))
->setFormatter((new LineFormatter(null, null, true)))]
)));
$this->whoops->pushHandler($loggingInFile);
}
/**
* [Whoops] Placeholder handler (non debug)
*
* @return void
*/
private function getWhoopsPlaceholder(): void
{
$this->whoops->pushHandler(function ($e) {
echo config()->get('whoops.error_message');
echo "<hr/>Error: {$e->getMessage()}.";
});
}
/**
* Get SQL debug log (instance method)
*
* @return string
* @throws Exception
*/
public function getSqlLogInstance(): string
{
$log = '';
$totalLegacyQueries = 0;
// Check for legacy queries across all database instances
$server_names = \TorrentPier\Database\DatabaseFactory::getServerNames();
foreach ($server_names as $srv_name) {
try {
$db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
if (!empty($db_obj->debugger->legacy_queries)) {
$totalLegacyQueries += count($db_obj->debugger->legacy_queries);
}
} catch (\Exception $e) {
// Skip if server not available
}
}
// Add warning banner if legacy queries were detected
if ($totalLegacyQueries > 0) {
$log .= '<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 10px; margin-bottom: 10px; border-radius: 4px;">'
. '<strong>⚠️ Legacy Query Warning:</strong> '
. $totalLegacyQueries . ' quer' . ($totalLegacyQueries > 1 ? 'ies' : 'y') . ' with duplicate columns detected and automatically fixed. '
. 'These queries should be updated to explicitly select columns. '
. 'Check the legacy_queries.log file for details.'
. '</div>';
}
// Get debug information from new database system
foreach ($server_names as $srv_name) {
try {
$db_obj = \TorrentPier\Database\DatabaseFactory::getInstance($srv_name);
$log .= !empty($db_obj->dbg) ? $this->getSqlLogHtml($db_obj, "database: $srv_name [{$db_obj->engine}]") : '';
} catch (\Exception $e) {
// Skip if server not available
}
}
// Get cache system debug information
$cacheSystem = \TorrentPier\Cache\UnifiedCacheSystem::getInstance();
$cacheObjects = $cacheSystem->obj; // Uses magic __get method for backward compatibility
foreach ($cacheObjects as $cache_name => $cache_obj) {
if (!empty($cache_obj->db->dbg)) {
$log .= $this->getSqlLogHtml($cache_obj->db, "cache: $cache_name [{$cache_obj->db->engine}]");
} elseif (!empty($cache_obj->dbg)) {
$log .= $this->getSqlLogHtml($cache_obj, "cache: $cache_name [{$cache_obj->engine}]");
}
}
// Get datastore debug information
$datastore = datastore();
if (!empty($datastore->db->dbg)) {
$log .= $this->getSqlLogHtml($datastore->db, "cache: datastore [{$datastore->db->engine}]");
} elseif (!empty($datastore->dbg)) {
$log .= $this->getSqlLogHtml($datastore, "cache: datastore [{$datastore->engine}]");
}
return $log;
}
/**
* Sql debug status (instance method)
*
* @return bool
*/
public function sqlDebugAllowedInstance(): bool
{
return (SQL_DEBUG && DBG_USER && !empty($_COOKIE['sql_log']));
}
/**
* Get SQL query html log
*
* @param object $db_obj
* @param string $log_name
*
* @return string
* @throws Exception
*/
private function getSqlLogHtml(object $db_obj, string $log_name): string
{
$log = '';
foreach ($db_obj->dbg as $i => $dbg) {
$id = "sql_{$i}_" . random_int(0, mt_getrandmax());
$sql = $this->shortQueryInstance($dbg['sql'], true);
$time = sprintf('%.3f', $dbg['time']);
$perc = '[' . round($dbg['time'] * 100 / $db_obj->sql_timetotal) . '%]';
// Use plain text version for title attribute to avoid HTML issues
$info_plain = !empty($dbg['info_plain']) ? $dbg['info_plain'] . ' [' . $dbg['src'] . ']' : $dbg['src'];
$info = !empty($dbg['info']) ? $dbg['info'] . ' [' . $dbg['src'] . ']' : $dbg['src'];
// Check if this is a legacy query that needed compatibility fix
$isLegacyQuery = !empty($dbg['is_legacy_query']);
$rowClass = $isLegacyQuery ? 'sqlLogRow sqlLegacyRow' : 'sqlLogRow';
$rowStyle = $isLegacyQuery ? ' style="background-color: #ffe6e6; border-left: 4px solid #dc3545; color: #721c24;"' : '';
$legacyWarning = $isLegacyQuery ? '<span style="color: #dc3545; font-weight: bold; margin-right: 8px;">[LEGACY]</span>' : '';
$log .= '<div onclick="$(this).toggleClass(\'sqlHighlight\');" class="' . $rowClass . '" title="' . htmlspecialchars($info_plain) . '"' . $rowStyle . '>'
. $legacyWarning
. '<span style="letter-spacing: -1px;">' . $time . ' </span>'
. '<span class="copyElement" data-clipboard-target="#' . $id . '" title="Copy to clipboard" style="color: rgb(128,128,128); letter-spacing: -1px;">' . $perc . '</span>&nbsp;'
. '<span style="letter-spacing: 0;" id="' . $id . '">' . $sql . '</span>'
. '<span style="color: rgb(128,128,128);"> # ' . $info . ' </span>'
. '</div>';
}
return '<div class="sqlLogTitle">' . $log_name . '</div>' . $log;
}
/**
* Short query (instance method)
*
* @param string $sql
* @param bool $esc_html
* @return string
*/
public function shortQueryInstance(string $sql, bool $esc_html = false): string
{
$max_len = 100;
$sql = str_compact($sql);
if (!empty($_COOKIE['sql_log_full'])) {
if (mb_strlen($sql, DEFAULT_CHARSET) > $max_len) {
$sql = mb_substr($sql, 0, 50) . ' [...cut...] ' . mb_substr($sql, -50);
}
}
return $esc_html ? htmlCHR($sql, true) : $sql;
}
// Static methods for backward compatibility (proxy to instance methods)
/**
* Get SQL debug log (static)
*
* @return string
* @throws Exception
* @deprecated Use dev()->getSqlLog() instead
*/
public static function getSqlLog(): string
{
return self::getInstance()->getSqlLogInstance();
}
/**
* Sql debug status (static)
*
* @return bool
* @deprecated Use dev()->sqlDebugAllowed() instead
*/
public static function sqlDebugAllowed(): bool
{
return self::getInstance()->sqlDebugAllowedInstance();
}
/**
* Short query (static)
*
* @param string $sql
* @param bool $esc_html
* @return string
* @deprecated Use dev()->shortQuery() instead
*/
public static function shortQuery(string $sql, bool $esc_html = false): string
{
return self::getInstance()->shortQueryInstance($sql, $esc_html);
}
/**
* Get SQL debug log (for dev() singleton usage)
*
* @return string
* @throws Exception
*/
public function getSqlDebugLog(): string
{
return $this->getSqlLogInstance();
}
/**
* Check if SQL debugging is allowed (for dev() singleton usage)
*
* @return bool
*/
public function checkSqlDebugAllowed(): bool
{
return $this->sqlDebugAllowedInstance();
}
/**
* Format SQL query for display (for dev() singleton usage)
*
* @param string $sql
* @param bool $esc_html
* @return string
*/
public function formatShortQuery(string $sql, bool $esc_html = false): string
{
return $this->shortQueryInstance($sql, $esc_html);
}
/**
* Get Whoops instance
*
* @return Run
*/
public function getWhoops(): Run
{
return $this->whoops;
}
/**
* Check if debug mode is enabled
*
* @return bool
*/
public function isDebugEnabled(): bool
{
return DBG_USER;
}
/**
* Check if application is in development environment
*
* @return bool
*/
public function isDevelopmentEnvironment(): bool
{
return APP_ENV === 'development';
}
/**
* Prevent cloning of the singleton instance
*/
private function __clone()
{
}
/**
* Prevent unserialization of the singleton instance
*/
public function __wakeup()
{
throw new \Exception("Cannot unserialize a singleton.");
}
}

View file

@ -1,217 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier;
use Exception;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport\SendmailTransport;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
/**
* Class Emailer
* @package TorrentPier
*/
class Emailer
{
/** @var string message text */
private string $message;
/** @var string message subject */
private string $subject;
private ?Address $to = null;
private ?Address $reply = null;
/** @var array message template with the language */
private array $tpl_msg = [];
/** @var array variables to be substituted in message templates */
private array $vars = [];
/**
* Setting the message subject
*
* @param string $subject
*
* @return void
*/
public function set_subject(string $subject): void
{
$this->subject = $subject;
}
/**
* Set recipient address
*
* @param string $email recipient address
* @param string $name recipient name
*
* @return void
*/
public function set_to(string $email, string $name): void
{
$this->to = new Address($email, $name);
}
/**
* Setting an address for the response
*
* @param string $email recipient address
*
* @return void
*/
public function set_reply(string $email): void
{
$this->reply = new Address($email);
}
/**
* Setting the message template
*
* @param string $template_file
* @param string $template_lang
*
* @return void
*/
public function set_template(string $template_file, string $template_lang = ''): void
{
if (!$template_lang) {
$template_lang = config()->get('default_lang');
}
if (empty($this->tpl_msg[$template_lang . $template_file])) {
$tpl_file = LANG_ROOT_DIR . '/' . $template_lang . '/email/' . $template_file . '.html';
if (!is_file($tpl_file)) {
$tpl_file = LANG_ROOT_DIR . '/' . config()->get('default_lang') . '/email/' . $template_file . '.html';
if (!is_file($tpl_file)) {
throw new Exception('Could not find email template file: ' . $template_file);
}
}
if (!$fd = fopen($tpl_file, 'rb')) {
throw new Exception('Failed opening email template file: ' . $tpl_file);
}
$this->tpl_msg[$template_lang . $template_file] = fread($fd, filesize($tpl_file));
fclose($fd);
}
$this->message = $this->tpl_msg[$template_lang . $template_file];
}
/**
* Sending a message to recipients via Symfony Mailer
*
* @param string $email_format
*
* @return bool
* @throws Exception
*/
public function send(string $email_format = 'text/plain'): bool
{
global $lang;
if (!config()->get('emailer.enabled')) {
return false;
}
/** Replace vars and prepare message */
$this->message = preg_replace('#\{([a-z0-9\-_]*?)}#is', "$\\1", $this->message);
foreach ($this->vars as $key => $val) {
$this->message = preg_replace(sprintf('/\$\{?%s\}?/', $key), $val, $this->message);
}
$this->message = trim($this->message);
/** Set some variables */
$this->subject = !empty($this->subject) ? $this->subject : $lang['EMAILER_SUBJECT']['EMPTY'];
/** Prepare message */
if (config()->get('emailer.smtp.enabled')) {
if (!empty(config()->get('emailer.smtp.host'))) {
$sslType = config()->get('emailer.smtp.ssl_type');
if (empty($sslType)) {
$sslType = null;
}
/** @var EsmtpTransport $transport external SMTP with SSL */
$transport = (new EsmtpTransport(
config()->get('emailer.smtp.host'),
config()->get('emailer.smtp.port'),
$sslType
))
->setUsername(config()->get('emailer.smtp.username'))
->setPassword(config()->get('emailer.smtp.password'));
} else {
$transport = new EsmtpTransport('localhost', 25);
}
} else {
$transport = new SendmailTransport(config()->get('emailer.sendmail_command'));
}
$mailer = new Mailer($transport);
/** @var Email $message */
$message = (new Email())
->subject($this->subject)
->to($this->to)
->from(new Address(config()->get('board_email'), config()->get('board_email_sitename')))
->returnPath(new Address(config()->get('bounce_email')))
->replyTo($this->reply ?? new Address(config()->get('board_email')));
/**
* This non-standard header tells compliant autoresponders ("email holiday mode") to not
* reply to this message because it's an automated email
*/
$message->getHeaders()
->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');
switch ($email_format) {
case EMAIL_TYPE_HTML:
$message->html($this->message);
break;
case EMAIL_TYPE_TEXT:
$message->text($this->message);
break;
default:
throw new Exception('Unknown email format: ' . $email_format);
}
/** Send message */
try {
$mailer->send($message);
} catch (TransportExceptionInterface $e) {
bb_die('Failed sending email: ' . $e->getMessage());
}
return true;
}
/**
* Set message template variables
*
* @param $vars
*
* @return void
*/
public function assign_vars($vars): void
{
$this->vars = array_merge([
'BOARD_EMAIL' => config()->get('board_email'),
'SITENAME' => config()->get('board_email_sitename'),
'EMAIL_SIG' => !empty(config()->get('board_email_sig')) ? "-- \n" . config()->get('board_email_sig') : '',
], $vars);
}
}

View file

@ -1,116 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier;
use Closure;
use Dotenv\Repository\Adapter\PutenvAdapter;
use Dotenv\Repository\RepositoryBuilder;
use Dotenv\Repository\RepositoryInterface;
use PhpOption\Option;
/**
* Class Env
* @package TorrentPier
*/
class Env
{
/**
* Indicates if the putenv adapter is enabled.
*
* @var bool
*/
protected static bool $putenv = true;
/**
* The environment repository instance.
*
* @var RepositoryInterface|null
*/
protected static $repository;
/**
* Enable the putenv adapter.
*
* @return void
*/
public static function enablePutenv(): void
{
static::$putenv = true;
static::$repository = null;
}
/**
* Disable the putenv adapter.
*
* @return void
*/
public static function disablePutenv(): void
{
static::$putenv = false;
static::$repository = null;
}
/**
* Get the environment repository instance.
*
* @return RepositoryInterface|null
*/
public static function getRepository(): ?RepositoryInterface
{
if (static::$repository === null) {
$builder = RepositoryBuilder::createWithDefaultAdapters();
if (static::$putenv) {
$builder = $builder->addAdapter(PutenvAdapter::class);
}
static::$repository = $builder->immutable()->make();
}
return static::$repository;
}
/**
* Gets the value of an environment variable.
*
* @param string $key
* @param mixed|null $default
* @return mixed
*/
public static function get(string $key, mixed $default = null): mixed
{
return Option::fromValue(static::getRepository()->get($key))
->map(function ($value) {
switch (strtolower($value)) {
case 'true':
case '(true)':
return true;
case 'false':
case '(false)':
return false;
case 'empty':
case '(empty)':
return '';
case 'null':
case '(null)':
return null;
}
if (preg_match('/\A([\'"])(.*)\1\z/', $value, $matches)) {
return $matches[2];
}
return $value;
})
->getOrCall(fn () => $default instanceof Closure ? $default() : $default);
}
}

View file

@ -1,138 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Helpers;
/**
* Class CronHelper
* @package TorrentPier\Helpers
*/
class CronHelper
{
/**
* Checking whether cron scripts execution is enabled
*
* @return bool
*/
public static function isEnabled(): bool
{
return env('APP_CRON_ENABLED', true);
}
/**
* Unlock cron (time-dependent)
*
* @return void
*/
public static function releaseDeadlock(): void
{
if (is_file(CRON_RUNNING)) {
if (TIMENOW - filemtime(CRON_RUNNING) > 2400) {
self::enableBoard();
self::releaseLockFile();
}
}
}
/**
* Снятие блокировки крона (по файлу)
*
* @return void
*/
public static function releaseLockFile(): void
{
if (is_file(CRON_RUNNING)) {
rename(CRON_RUNNING, CRON_ALLOWED);
}
self::touchLockFile(CRON_ALLOWED);
}
/**
* Создание файла блокировки
*
* @param string $lock_file
* @return void
*/
public static function touchLockFile(string $lock_file): void
{
file_write('', $lock_file, replace_content: true);
}
/**
* Включение форума (при разблокировке крона)
*
* @return void
*/
public static function enableBoard(): void
{
if (is_file(BB_DISABLED)) {
rename(BB_DISABLED, BB_ENABLED);
}
}
/**
* Отключение форума (при блокировке крона)
*
* @return void
*/
public static function disableBoard(): void
{
if (is_file(BB_ENABLED)) {
rename(BB_ENABLED, BB_DISABLED);
}
}
/**
* Проверка наличия файла блокировки
*
* @return bool
*/
public static function hasFileLock(): bool
{
$lock_obtained = false;
if (is_file(CRON_ALLOWED)) {
$lock_obtained = rename(CRON_ALLOWED, CRON_RUNNING);
} elseif (is_file(CRON_RUNNING)) {
self::releaseDeadlock();
} elseif (!is_file(CRON_ALLOWED) && !is_file(CRON_RUNNING)) {
file_write('', CRON_ALLOWED);
$lock_obtained = rename(CRON_ALLOWED, CRON_RUNNING);
}
return $lock_obtained;
}
/**
* Отслеживание запуска задач
*
* @param string $mode
* @return void
*/
public static function trackRunning(string $mode): void
{
if (!defined('START_MARK')) {
define('START_MARK', TRIGGERS_DIR . '/cron_started_at_' . date('Y-m-d_H-i-s') . '_by_pid_' . getmypid());
}
switch ($mode) {
case 'start':
self::touchLockFile(CRON_RUNNING);
file_write('', START_MARK);
break;
case 'end':
if (is_file(START_MARK)) {
unlink(START_MARK);
}
break;
default:
bb_simple_die("Invalid cron track mode: $mode");
}
}
}

View file

@ -1,38 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Helpers;
/**
* Class IsHelper
* @package TorrentPier\Helpers
*/
class IsHelper
{
/**
* Return true if server have SSL
*
* @return bool
*/
public static function isHTTPS(): bool
{
if (
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
|| (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on')
|| (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443)
|| (isset($_SERVER['HTTP_X_FORWARDED_PORT']) && $_SERVER['HTTP_X_FORWARDED_PORT'] == 443)
|| (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] == 'https')
) {
return true;
}
return false;
}
}

View file

@ -1,51 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Helpers;
/**
* Class StringHelper
* @package TorrentPier\Helpers
*/
class StringHelper
{
/**
* Return true if $value contains numbers
*
* @param string $value
* @return bool
*/
public static function isContainsNums(string $value): bool
{
return preg_match('@[[:digit:]]@', $value);
}
/**
* Return true if $value contains letters (Uppercase included)
*
* @param string $value
* @param bool $uppercase
* @return bool
*/
public static function isContainsLetters(string $value, bool $uppercase = false): bool
{
return $uppercase ? preg_match('@[A-Z]@', $value) : preg_match('@[a-z]@', $value);
}
/**
* Return true if $value contains special symbols
*
* @param string $value
* @return bool
*/
public static function isContainsSpecSymbols(string $value): bool
{
return preg_match('@[[:punct:]]@', $value);
}
}

View file

@ -1,48 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Helpers;
/**
* Class VersionHelper
* @package TorrentPier\Helpers
*/
class VersionHelper
{
/**
* Version prefix
*
* @var string
*/
private const VERSION_PREFIX = 'v';
/**
* Returns version without prefix (v)
*
* @param string $version
* @return string
*/
public static function removerPrefix(string $version): string
{
$version = trim($version);
$version = mb_strtolower($version, DEFAULT_CHARSET);
return str_replace(self::VERSION_PREFIX, '', $version);
}
/**
* Returns version with prefix (v)
*
* @param string $version
* @return string
*/
public static function addPrefix(string $version): string
{
return self::VERSION_PREFIX . trim($version);
}
}

View file

@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
use Dotenv\Dotenv;
class Bootstrap
{
private static ?Container $container = null;
public static function init(string $rootPath, array $config = []): Container
{
if (self::$container !== null) {
return self::$container;
}
// Load environment variables
self::loadEnvironment($rootPath);
// Merge configuration
$config = self::loadConfiguration($rootPath, $config);
// Create and configure container
self::$container = ContainerFactory::create($config);
// Register container instance with itself
self::$container->getWrappedContainer()->set(Container::class, self::$container);
self::$container->getWrappedContainer()->set('container', self::$container);
return self::$container;
}
public static function getContainer(): ?Container
{
return self::$container;
}
public static function reset(): void
{
self::$container = null;
}
private static function loadEnvironment(string $rootPath): void
{
if (file_exists($rootPath . '/.env')) {
$dotenv = Dotenv::createImmutable($rootPath);
$dotenv->load();
}
}
private static function loadConfiguration(string $rootPath, array $config): array
{
// Load base configuration
$configPath = $rootPath . '/config';
// Container configuration
if (file_exists($configPath . '/container.php')) {
$containerConfig = require $configPath . '/container.php';
$config = array_merge($config, $containerConfig);
}
// Services configuration
if (file_exists($configPath . '/services.php')) {
$servicesConfig = require $configPath . '/services.php';
$config['definitions'] = array_merge(
$config['definitions'] ?? [],
$servicesConfig
);
}
// Database configuration
if (file_exists($configPath . '/database.php')) {
$config['database'] = require $configPath . '/database.php';
}
// Cache configuration
if (file_exists($configPath . '/cache.php')) {
$config['cache'] = require $configPath . '/cache.php';
}
// Environment from .env
$config['environment'] = $_ENV['APP_ENV'] ?? 'development';
$config['debug'] = filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN);
return $config;
}
}

View file

@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
use DI\Container as DIContainer;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
private DIContainer $container;
public function __construct(DIContainer $container)
{
$this->container = $container;
}
public function get(string $id): mixed
{
return $this->container->get($id);
}
public function has(string $id): bool
{
return $this->container->has($id);
}
public function make(string $name, array $parameters = []): mixed
{
return $this->container->make($name, $parameters);
}
public function call(callable $callable, array $parameters = []): mixed
{
return $this->container->call($callable, $parameters);
}
public function injectOn(object $instance): object
{
return $this->container->injectOn($instance);
}
public function getWrappedContainer(): DIContainer
{
return $this->container;
}
}

View file

@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
use DI\ContainerBuilder;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\ApplicationDefinitions;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\DomainDefinitions;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\InfrastructureDefinitions;
use TorrentPier\Infrastructure\DependencyInjection\Definitions\PresentationDefinitions;
class ContainerFactory
{
public static function create(array $config = []): Container
{
$builder = new ContainerBuilder();
// Configure container settings
self::configureContainer($builder, $config);
// Add definitions from all layers
self::addDefinitions($builder, $config);
// Build the container
$diContainer = $builder->build();
$container = new Container($diContainer);
// Register and boot service providers
self::registerProviders($container, $config);
return $container;
}
private static function configureContainer(ContainerBuilder $builder, array $config): void
{
// Enable compilation in production for better performance
$isProduction = ($config['environment'] ?? 'development') === 'production';
if ($isProduction) {
$builder->enableCompilation($config['compilation_dir'] ?? __DIR__ . '/../../../internal_data/cache/container');
$builder->writeProxiesToFile(true, $config['proxies_dir'] ?? __DIR__ . '/../../../internal_data/cache/proxies');
}
// Enable autowiring by default
$builder->useAutowiring($config['autowiring'] ?? true);
}
private static function addDefinitions(ContainerBuilder $builder, array $config): void
{
// Add config definitions first
if (isset($config['definitions'])) {
$builder->addDefinitions($config['definitions']);
}
// Add layer-specific definitions
$builder->addDefinitions(DomainDefinitions::getDefinitions());
$builder->addDefinitions(ApplicationDefinitions::getDefinitions());
$builder->addDefinitions(InfrastructureDefinitions::getDefinitions($config));
$builder->addDefinitions(PresentationDefinitions::getDefinitions());
// Add custom definition files if provided
if (isset($config['definition_files'])) {
foreach ($config['definition_files'] as $file) {
if (file_exists($file)) {
$builder->addDefinitions($file);
}
}
}
}
private static function registerProviders(Container $container, array $config): void
{
$providers = $config['providers'] ?? [];
// Instantiate providers
$instances = [];
foreach ($providers as $providerClass) {
if (class_exists($providerClass)) {
$provider = new $providerClass();
if ($provider instanceof ServiceProvider) {
$instances[] = $provider;
$provider->register($container);
}
}
}
// Boot all providers after registration
foreach ($instances as $provider) {
$provider->boot($container);
}
}
}

View file

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Psr\Container\ContainerInterface;
class ApplicationDefinitions
{
public static function getDefinitions(): array
{
return [
// Command Bus
// 'CommandBusInterface' => DI\factory(function (ContainerInterface $c) {
// return new CommandBus($c);
// }),
// Query Bus
// 'QueryBusInterface' => DI\factory(function (ContainerInterface $c) {
// return new QueryBus($c);
// }),
// Event Dispatcher
// 'EventDispatcherInterface' => DI\factory(function (ContainerInterface $c) {
// return new EventDispatcher();
// }),
// Application Services
// These typically orchestrate domain objects and handle use cases
// Forum Handlers
// 'TorrentPier\Application\Forum\Handler\CreatePostHandler' => DI\autowire(),
// 'TorrentPier\Application\Forum\Handler\GetThreadListHandler' => DI\autowire(),
// Tracker Handlers
// 'TorrentPier\Application\Tracker\Handler\RegisterTorrentHandler' => DI\autowire(),
// 'TorrentPier\Application\Tracker\Handler\ProcessAnnounceHandler' => DI\autowire(),
// User Handlers
// 'TorrentPier\Application\User\Handler\RegisterUserHandler' => DI\autowire(),
// 'TorrentPier\Application\User\Handler\AuthenticateUserHandler' => DI\autowire(),
];
}
}

View file

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Psr\Container\ContainerInterface;
class DomainDefinitions
{
public static function getDefinitions(): array
{
return [
// Domain services should not depend on infrastructure
// These are typically created by factories in the application layer
// Example domain service factory definitions:
// 'TorrentPier\Domain\Forum\Repository\ForumRepositoryInterface' => DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\ForumRepository');
// }),
// 'TorrentPier\Domain\Tracker\Repository\TorrentRepositoryInterface' => DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\TorrentRepository');
// }),
// 'TorrentPier\Domain\User\Repository\UserRepositoryInterface' => DI\factory(function (ContainerInterface $c) {
// return $c->get('TorrentPier\Infrastructure\Persistence\Repository\UserRepository');
// }),
];
}
}

View file

@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Nette\Caching\Cache;
use Nette\Caching\Storages\FileStorage;
use Nette\Caching\Storages\MemcachedStorage;
use Nette\Caching\Storages\SQLiteStorage;
use Nette\Database\Connection;
use Psr\Container\ContainerInterface;
class InfrastructureDefinitions
{
public static function getDefinitions(array $config = []): array
{
return [
// TODO: Add infrastructure service definitions as they are implemented
// Example: Database Connection (implement when Nette Database integration is ready)
// 'database.connection.default' => DI\factory(function () use ($config) {
// $dbConfig = $config['database'] ?? [];
// $dsn = sprintf(
// 'mysql:host=%s;port=%s;dbname=%s;charset=%s',
// $dbConfig['host'] ?? '127.0.0.1',
// $dbConfig['port'] ?? 3306,
// $dbConfig['database'] ?? 'tp',
// $dbConfig['charset'] ?? 'utf8mb4'
// );
//
// return new Connection(
// $dsn,
// $dbConfig['username'] ?? 'root',
// $dbConfig['password'] ?? ''
// );
// }),
// Example: Cache Storage (implement when cache infrastructure is ready)
// 'cache.storage' => DI\factory(function () use ($config) {
// $cacheConfig = $config['cache'] ?? [];
// $driver = $cacheConfig['driver'] ?? 'file';
//
// switch ($driver) {
// case 'memcached':
// $memcached = new \Memcached();
// $memcached->addServer(
// $cacheConfig['memcached']['host'] ?? '127.0.0.1',
// $cacheConfig['memcached']['port'] ?? 11211
// );
// return new MemcachedStorage($memcached);
//
// case 'sqlite':
// return new SQLiteStorage($cacheConfig['sqlite']['path'] ?? __DIR__ . '/../../../../internal_data/cache/cache.db');
//
// case 'file':
// default:
// return new FileStorage($cacheConfig['file']['path'] ?? __DIR__ . '/../../../../internal_data/cache');
// }
// }),
// Example: Repository Implementations (implement when repositories are created)
// 'TorrentPier\Infrastructure\Persistence\Repository\ForumRepository' => DI\autowire()
// ->constructorParameter('connection', DI\get('database.connection.default'))
// ->constructorParameter('cache', DI\get('cache.factory')),
// Example: Email Service (implement when email infrastructure is ready)
// 'EmailServiceInterface' => DI\factory(function (ContainerInterface $c) use ($config) {
// $emailConfig = $config['email'] ?? [];
// return new SmtpEmailService($emailConfig);
// }),
// Example: File Storage (implement when file storage abstraction is ready)
// 'FileStorageInterface' => DI\factory(function (ContainerInterface $c) use ($config) {
// $storageConfig = $config['storage'] ?? [];
// return match ($storageConfig['driver'] ?? 'local') {
// 's3' => new S3FileStorage($storageConfig['s3']),
// default => new LocalFileStorage($storageConfig['local']['path'] ?? __DIR__ . '/../../../../internal_data/uploads'),
// };
// }),
];
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection\Definitions;
use DI;
use Psr\Container\ContainerInterface;
class PresentationDefinitions
{
public static function getDefinitions(): array
{
return [
// HTTP Controllers
// Controllers are typically autowired with their dependencies
// Web Controllers
// 'TorrentPier\Presentation\Http\Controllers\Web\HomeController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Web\ForumController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Web\TrackerController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Web\UserController' => DI\autowire(),
// API Controllers
// 'TorrentPier\Presentation\Http\Controllers\Api\UserController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Api\TorrentController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Api\ForumController' => DI\autowire(),
// Admin Controllers
// 'TorrentPier\Presentation\Http\Controllers\Admin\DashboardController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\UserController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\ForumController' => DI\autowire(),
// 'TorrentPier\Presentation\Http\Controllers\Admin\TrackerController' => DI\autowire(),
// Middleware
// 'AuthenticationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthenticationMiddleware'),
// 'AuthorizationMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\AuthorizationMiddleware'),
// 'CorsMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\CorsMiddleware'),
// 'RateLimitMiddleware' => DI\autowire('TorrentPier\Presentation\Http\Middleware\RateLimitMiddleware'),
// CLI Commands
// 'TorrentPier\Presentation\Cli\Commands\CacheCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\MigrateCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\SeedCommand' => DI\autowire(),
// 'TorrentPier\Presentation\Cli\Commands\TrackerCommand' => DI\autowire(),
// View/Response Transformers
// 'JsonResponseTransformer' => DI\autowire('TorrentPier\Presentation\Http\Responses\JsonResponseTransformer'),
// 'HtmlResponseTransformer' => DI\autowire('TorrentPier\Presentation\Http\Responses\HtmlResponseTransformer'),
];
}
}

View file

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace TorrentPier\Infrastructure\DependencyInjection;
interface ServiceProvider
{
/**
* Register services in the container
*
* @param Container $container
* @return void
*/
public function register(Container $container): void;
/**
* Bootstrap services after all providers have been registered
*
* @param Container $container
* @return void
*/
public function boot(Container $container): void;
}

View file

@ -1,201 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Legacy\Admin;
/**
* Class Cron
* @package TorrentPier\Legacy\Admin
*/
class Cron
{
/**
* Run cron jobs
*
* @param string $jobs
*/
public static function run_jobs(string $jobs): void
{
/** @noinspection PhpUnusedLocalVariableInspection */
// bb_cfg deprecated, but kept for compatibility with non-adapted cron jobs
global $bb_cfg, $datastore;
\define('IN_CRON', true);
$sql = "SELECT cron_script FROM " . BB_CRON . " WHERE cron_id IN ($jobs)";
if (!$result = DB()->sql_query($sql)) {
bb_die('Could not obtain cron script');
}
while ($row = DB()->sql_fetchrow($result)) {
$job = $row['cron_script'];
$job_script = INC_DIR . '/cron/jobs/' . $job;
require($job_script);
}
DB()->query("
UPDATE " . BB_CRON . " SET
last_run = NOW(),
run_counter = run_counter + 1,
next_run =
CASE
WHEN schedule = 'hourly' THEN
DATE_ADD(NOW(), INTERVAL 1 HOUR)
WHEN schedule = 'daily' THEN
DATE_ADD(DATE_ADD(CURDATE(), INTERVAL 1 DAY), INTERVAL TIME_TO_SEC(run_time) SECOND)
WHEN schedule = 'weekly' THEN
DATE_ADD(
DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(NOW()) DAY), INTERVAL 7 DAY),
INTERVAL CONCAT(ROUND(run_day-1), ' ', run_time) DAY_SECOND)
WHEN schedule = 'monthly' THEN
DATE_ADD(
DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFMONTH(NOW())-1 DAY), INTERVAL 1 MONTH),
INTERVAL CONCAT(ROUND(run_day-1), ' ', run_time) DAY_SECOND)
ELSE
DATE_ADD(NOW(), INTERVAL TIME_TO_SEC(run_interval) SECOND)
END
WHERE cron_id IN ($jobs)
");
}
/**
* Delete cron jobs
*
* @param string $jobs
*/
public static function delete_jobs(string $jobs): void
{
DB()->query("DELETE FROM " . BB_CRON . " WHERE cron_id IN ($jobs)");
}
/**
* Toggle activity of cron jobs
*
* @param string $jobs
* @param string $cron_action
*/
public static function toggle_active(string $jobs, string $cron_action): void
{
$active = ($cron_action == 'disable') ? 0 : 1;
DB()->query("UPDATE " . BB_CRON . " SET cron_active = $active WHERE cron_id IN ($jobs)");
}
/**
* Validate cron admin post query
*
* @param array $cron_arr
*
* @return int|string
*/
public static function validate_cron_post(array $cron_arr)
{
$errors = 'Errors in: ';
$errnum = 0;
if (!$cron_arr['cron_title']) {
$errors .= 'cron title (empty value), ';
$errnum++;
}
if (!$cron_arr['cron_script']) {
$errors .= 'cron script (empty value), ';
$errnum++;
}
if ($errnum > 0) {
$result = $errors . ' total ' . $errnum . ' errors <br/> <a href="javascript:history.back(-1)">Back</a>';
} else {
$result = 1;
}
return $result;
}
/**
* Insert cron job to database
*
* @param array $cron_arr
*/
public static function insert_cron_job(array $cron_arr): void
{
$row = DB()->fetch_row("SELECT cron_title, cron_script FROM " . BB_CRON . " WHERE cron_title = '" . $_POST['cron_title'] . "' or cron_script = '" . $_POST['cron_script'] . "' ");
if ($row) {
global $lang;
if ($_POST['cron_script'] == $row['cron_script']) {
$langmode = $lang['SCRIPT_DUPLICATE'];
} else {
$langmode = $lang['TITLE_DUPLICATE'];
}
$message = $langmode . "<br /><br />" . sprintf($lang['CLICK_RETURN_JOBS_ADDED'], "<a href=\"javascript:history.back(-1)\">", "</a>") . "<br /><br />" . sprintf($lang['CLICK_RETURN_JOBS'], "<a href=\"admin_cron.php?mode=list\">", "</a>") . "<br /><br />" . sprintf($lang['CLICK_RETURN_ADMIN_INDEX'], "<a href=\"index.php?pane=right\">", "</a>");
bb_die($message);
}
$cron_active = $cron_arr['cron_active'];
$cron_title = $cron_arr['cron_title'];
$cron_script = $cron_arr['cron_script'];
$schedule = $cron_arr['schedule'];
$run_day = $cron_arr['run_day'];
$run_time = $cron_arr['run_time'];
$run_order = $cron_arr['run_order'];
$last_run = $cron_arr['last_run'];
$next_run = $cron_arr['next_run'];
$run_interval = $cron_arr['run_interval'];
$log_enabled = $cron_arr['log_enabled'];
$log_file = $cron_arr['log_file'];
$log_sql_queries = $cron_arr['log_sql_queries'];
$disable_board = $cron_arr['disable_board'];
$run_counter = $cron_arr['run_counter'];
DB()->query("INSERT INTO " . BB_CRON . " (cron_active, cron_title, cron_script, schedule, run_day, run_time, run_order, last_run, next_run, run_interval, log_enabled, log_file, log_sql_queries, disable_board, run_counter) VALUES (
$cron_active, '$cron_title', '$cron_script', '$schedule', '$run_day', '$run_time', '$run_order', '$last_run', '$next_run', '$run_interval', $log_enabled, '$log_file', $log_sql_queries, $disable_board, '$run_counter')");
}
/**
* Update cron job in database
*
* @param array $cron_arr
*/
public static function update_cron_job(array $cron_arr): void
{
$cron_id = $cron_arr['cron_id'];
$cron_active = $cron_arr['cron_active'];
$cron_title = DB()->escape($cron_arr['cron_title']);
$cron_script = DB()->escape($cron_arr['cron_script']);
$schedule = $cron_arr['schedule'];
$run_day = $cron_arr['run_day'];
$run_time = $cron_arr['run_time'];
$run_order = $cron_arr['run_order'];
$last_run = $cron_arr['last_run'];
$next_run = $cron_arr['next_run'];
$run_interval = $cron_arr['run_interval'];
$log_enabled = $cron_arr['log_enabled'];
$log_file = DB()->escape($cron_arr['log_file']);
$log_sql_queries = $cron_arr['log_sql_queries'];
$disable_board = $cron_arr['disable_board'];
$run_counter = $cron_arr['run_counter'];
DB()->query("UPDATE " . BB_CRON . " SET
cron_active = '$cron_active',
cron_title = '$cron_title',
cron_script = '$cron_script',
schedule = '$schedule',
run_day = '$run_day',
run_time = '$run_time',
run_order = '$run_order',
last_run = '$last_run',
next_run = '$next_run',
run_interval = '$run_interval',
log_enabled = '$log_enabled',
log_file = '$log_file',
log_sql_queries = '$log_sql_queries',
disable_board = '$disable_board',
run_counter = '$run_counter'
WHERE cron_id = $cron_id
");
}
}

View file

@ -1,141 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Legacy\Admin;
/**
* Class Torrent
* @package TorrentPier\Legacy\Admin
*/
class Torrent
{
/**
* Update boolean config table
*
* @param string $table_name
* @param string $key
* @param string $field_name
* @param string $field_def_val
*/
public static function update_table_bool($table_name, $key, $field_name, $field_def_val)
{
// Clear current status
$sql = "UPDATE $table_name
SET $field_name = $field_def_val
WHERE 1";
if (!$result = DB()->sql_query($sql)) {
bb_die('Could not update ' . $table_name);
}
if (isset($_POST[$field_name])) {
// Get new status
$in_sql = [];
foreach ($_POST[$field_name] as $i => $val) {
$in_sql[] = (int)$val;
}
// Update status
if ($in_sql = implode(',', $in_sql)) {
$sql = "UPDATE $table_name
SET $field_name = 1
WHERE $key IN($in_sql)";
if (!$result = DB()->sql_query($sql)) {
bb_die('Could not update ' . $table_name);
}
}
}
}
/**
* Assign config variables to template
*
* @param array $default_cfg
* @param array $cfg
*/
public static function set_tpl_vars($default_cfg, $cfg)
{
global $template;
foreach ($default_cfg as $config_name => $config_value) {
$template->assign_vars([strtoupper($config_name) => htmlspecialchars($cfg[$config_name])]);
}
}
/**
* Assign boolean config variables to template
*
* @param array $default_cfg
* @param array $cfg
*/
public static function set_tpl_vars_bool($default_cfg, $cfg)
{
global $template, $lang;
foreach ($default_cfg as $config_name => $config_value) {
// YES/NO 'checked'
$template->assign_vars([
strtoupper($config_name) . '_YES' => ($cfg[$config_name]) ? HTML_CHECKED : '',
strtoupper($config_name) . '_NO' => (!$cfg[$config_name]) ? HTML_CHECKED : '',
]);
// YES/NO lang vars
$template->assign_vars([
'L_' . strtoupper($config_name) . '_YES' => ($cfg[$config_name]) ? "<u>$lang[YES]</u>" : $lang['YES'],
'L_' . strtoupper($config_name) . '_NO' => (!$cfg[$config_name]) ? "<u>$lang[NO]</u>" : $lang['NO'],
]);
}
}
/**
* Assign language config variables to template
*
* @param array $default_cfg
*/
public static function set_tpl_vars_lang($default_cfg)
{
global $template, $lang;
foreach ($default_cfg as $config_name => $config_value) {
$template->assign_vars([
'L_' . strtoupper($config_name) => $lang[$config_name] ?? '',
'L_' . strtoupper($config_name) . '_EXPL' => $lang[$config_name . '_expl'] ?? '',
'L_' . strtoupper($config_name) . '_HEAD' => $lang[$config_name . '_head'] ?? '',
]);
}
}
/**
* Update config table
*
* @param string $table_name
* @param array $default_cfg
* @param array $cfg
* @param string $type
*/
public static function update_config_table($table_name, $default_cfg, $cfg, $type)
{
foreach ($default_cfg as $config_name => $config_value) {
if (isset($_POST[$config_name]) && $_POST[$config_name] != $cfg[$config_name]) {
if ($type == 'str') {
$config_value = $_POST[$config_name];
} elseif ($type == 'bool') {
$config_value = ($_POST[$config_name]) ? 1 : 0;
} elseif ($type == 'num') {
$config_value = abs((int)$_POST[$config_name]);
} else {
return;
}
bb_update_config([$config_name => $config_value], $table_name);
}
}
}
}

View file

@ -1,144 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier;
/**
* Class Sessions
* @package TorrentPier
*/
class Sessions
{
/**
* Check if session cache ignored
*
* @return bool
*/
private static function ignore_cached_userdata(): bool
{
return defined('IN_PM');
}
/**
* Get userdata from cache
*
* @param string $id
*
* @return bool|array
*/
public static function cache_get_userdata(string $id): bool|array
{
if (self::ignore_cached_userdata()) {
return false;
}
return CACHE('session_cache')->get($id);
}
/**
* Set userdata to cache
*
* @param array|null $userdata
* @param bool $force
*
* @return bool
*/
public static function cache_set_userdata(?array $userdata, bool $force = false): bool
{
if (!$userdata || (self::ignore_cached_userdata() && !$force)) {
return false;
}
$id = ($userdata['user_id'] == GUEST_UID) ? $userdata['session_ip'] : $userdata['session_id'];
return CACHE('session_cache')->set($id, $userdata, config()->get('session_update_intrv'));
}
/**
* Delete userdata from cache
*
* @param array $userdata
*
* @return bool
*/
public static function cache_rm_userdata(array $userdata): bool
{
if (!$userdata) {
return false;
}
$id = ($userdata['user_id'] == GUEST_UID) ? $userdata['session_ip'] : $userdata['session_id'];
return CACHE('session_cache')->rm($id);
}
/**
* Delete user sessions from cache
*
* @param int|string $user_id
*/
public static function cache_rm_user_sessions(int|string $user_id): void
{
$user_id = get_id_csv(explode(',', (string)$user_id));
$rowset = DB()->fetch_rowset("SELECT session_id FROM " . BB_SESSIONS . " WHERE session_user_id IN($user_id)");
foreach ($rowset as $row) {
CACHE('session_cache')->rm($row['session_id']);
}
}
/**
* Update userdata in cache
*
* @param array|null $userdata
*
* @return bool
*/
public static function cache_update_userdata(?array $userdata): bool
{
return self::cache_set_userdata($userdata, true);
}
/**
* Update userdata in database
*
* @param array $userdata
* @param array $sql_ary
* @param bool $data_already_escaped
*
* @return bool
*/
public static function db_update_userdata(array $userdata, array $sql_ary, bool $data_already_escaped = true): bool
{
if (!$userdata) {
return false;
}
$sql_args = DB()->build_array('UPDATE', $sql_ary, $data_already_escaped);
DB()->query("UPDATE " . BB_USERS . " SET $sql_args WHERE user_id = {$userdata['user_id']}");
if (DB()->affected_rows()) {
self::cache_rm_userdata($userdata);
}
return true;
}
/**
* Delete user sessions from cache and database
*
* @param int|string $user_id
*/
public static function delete_user_sessions(int|string $user_id): void
{
self::cache_rm_user_sessions($user_id);
$user_id = get_id_csv(explode(',', (string)$user_id));
DB()->query("DELETE FROM " . BB_SESSIONS . " WHERE session_user_id IN($user_id)");
}
}

View file

@ -1,393 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Whoops;
use Whoops\Handler\Handler;
use Whoops\Handler\HandlerInterface;
/**
* Database Error Handler for Whoops
*
* Enhances error reporting by adding database query information,
* recent SQL activity, and database error details to the error output.
*/
class DatabaseErrorHandler extends Handler implements HandlerInterface
{
private bool $addToOutput = true;
private bool $includeQueryHistory = true;
private int $maxQueryHistory = 5;
/**
* Handle the exception and add database information
*/
public function handle(): int
{
if (!$this->addToOutput) {
return Handler::DONE;
}
$inspector = $this->getInspector();
if (!$inspector) {
return Handler::DONE;
}
$exception = $inspector->getException();
// Add database information to the exception frames
$this->addDatabaseContextToFrames($inspector);
// Add global database state information
$this->addGlobalDatabaseInfo($exception);
return Handler::DONE;
}
/**
* Set whether to add database info to output
*/
public function setAddToOutput(bool $add): self
{
$this->addToOutput = $add;
return $this;
}
/**
* Set whether to include query history
*/
public function setIncludeQueryHistory(bool $include): self
{
$this->includeQueryHistory = $include;
return $this;
}
/**
* Set maximum number of queries to show in history
*/
public function setMaxQueryHistory(int $max): self
{
$this->maxQueryHistory = max(1, $max);
return $this;
}
/**
* Add database context information to exception frames
*/
private function addDatabaseContextToFrames($inspector): void
{
if (!$inspector) {
return;
}
$frames = $inspector->getFrames();
if (!$frames || empty($frames)) {
return;
}
foreach ($frames as $frame) {
$frameData = [];
// Check if this frame involves database operations
$fileName = $frame->getFile();
$className = $frame->getClass();
$functionName = $frame->getFunction();
// Detect database-related frames
$isDatabaseFrame = $this->isDatabaseRelatedFrame($fileName, $className, $functionName);
if ($isDatabaseFrame) {
$frameData['database_context'] = $this->getCurrentDatabaseContext();
// Add frame-specific database info
$frame->addComment('Database Context', 'This frame involves database operations');
foreach ($frameData['database_context'] as $key => $value) {
if (is_string($value) || is_numeric($value)) {
$frame->addComment("DB: $key", $value);
} elseif (is_array($value) && !empty($value)) {
$frame->addComment("DB: $key", json_encode($value, JSON_PRETTY_PRINT));
}
}
}
}
}
/**
* Add global database information to the exception
*/
private function addGlobalDatabaseInfo($exception): void
{
try {
$databaseInfo = $this->collectDatabaseInformation();
// Use Whoops' built-in method if available - this is the proper way
if (method_exists($exception, 'setAdditionalInfo')) {
$exception->setAdditionalInfo('Database Information', $databaseInfo);
}
// For PHP 8.2+ compatibility: Avoid dynamic properties completely
// Instead, we'll add the info as frame comments on the first database-related frame
else {
$this->addDatabaseInfoAsFrameComments($databaseInfo);
}
} catch (\Exception $e) {
// Don't let database info collection break error handling
if (method_exists($exception, 'setAdditionalInfo')) {
$exception->setAdditionalInfo('Database Info Error', $e->getMessage());
}
}
}
/**
* Add database info as frame comments when setAdditionalInfo is not available
*/
private function addDatabaseInfoAsFrameComments(array $databaseInfo): void
{
$inspector = $this->getInspector();
if (!$inspector) {
return;
}
$frames = $inspector->getFrames();
// Find the first frame and add database info as comments
if (!empty($frames) && is_array($frames) && isset($frames[0])) {
$firstFrame = $frames[0];
$firstFrame->addComment('=== Database Information ===', '');
foreach ($databaseInfo as $key => $value) {
if (is_string($value) || is_numeric($value)) {
$firstFrame->addComment("DB Info - $key", $value);
} elseif (is_array($value) && !empty($value)) {
$firstFrame->addComment("DB Info - $key", json_encode($value, JSON_PRETTY_PRINT));
}
}
}
}
/**
* Check if a frame is related to database operations
*/
private function isDatabaseRelatedFrame(?string $fileName, ?string $className, ?string $functionName): bool
{
if (!$fileName) {
return false;
}
// Check file paths
$databaseFiles = [
'/Database/',
'/database/',
'Database.php',
'DatabaseDebugger.php',
'DebugSelection.php',
];
foreach ($databaseFiles as $dbFile) {
if (str_contains($fileName, $dbFile)) {
return true;
}
}
// Check class names
$databaseClasses = [
'Database',
'DatabaseDebugger',
'DebugSelection',
'DB',
'Nette\Database',
];
if ($className) {
foreach ($databaseClasses as $dbClass) {
if (str_contains($className, $dbClass)) {
return true;
}
}
}
// Check function names
$databaseFunctions = [
'sql_query',
'fetch_row',
'fetch_rowset',
'sql_fetchrow',
'query',
'execute',
];
if ($functionName) {
foreach ($databaseFunctions as $dbFunc) {
if (str_contains($functionName, $dbFunc)) {
return true;
}
}
}
return false;
}
/**
* Get current database context
*/
private function getCurrentDatabaseContext(): array
{
$context = [];
try {
// Get main database instance
if (function_exists('DB')) {
$db = DB();
$context['current_query'] = $db->cur_query ?? 'None';
$context['database_server'] = $db->db_server ?? 'Unknown';
$context['selected_database'] = $db->selected_db ?? 'Unknown';
// Connection status
$context['connection_status'] = $db->connection ? 'Active' : 'No connection';
// Query stats
$context['total_queries'] = $db->num_queries ?? 0;
$context['total_time'] = isset($db->sql_timetotal) ? sprintf('%.3f sec', $db->sql_timetotal) : 'Unknown';
// Recent error information
$sqlError = $db->sql_error();
if (!empty($sqlError['message'])) {
$context['last_error'] = $sqlError;
}
}
} catch (\Exception $e) {
$context['error'] = 'Could not retrieve database context: ' . $e->getMessage();
}
return $context;
}
/**
* Collect comprehensive database information
*/
private function collectDatabaseInformation(): array
{
$info = [
'timestamp' => date('Y-m-d H:i:s'),
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'CLI',
'user_ip' => $_SERVER['REMOTE_ADDR'] ?? 'Unknown',
];
try {
// Get information from all database servers
if (class_exists('\TorrentPier\Database\DatabaseFactory')) {
$serverNames = \TorrentPier\Database\DatabaseFactory::getServerNames();
foreach ($serverNames as $serverName) {
try {
$db = \TorrentPier\Database\DatabaseFactory::getInstance($serverName);
$serverInfo = [
'server_name' => $serverName,
'engine' => $db->engine ?? 'Unknown',
'host' => $db->db_server ?? 'Unknown',
'database' => $db->selected_db ?? 'Unknown',
'connection_status' => $db->connection ? 'Connected' : 'Disconnected',
'total_queries' => $db->num_queries ?? 0,
'total_time' => isset($db->sql_timetotal) ? sprintf('%.3f sec', $db->sql_timetotal) : 'Unknown',
];
// Current query
if (!empty($db->cur_query)) {
$serverInfo['current_query'] = $this->formatQueryForDisplay($db->cur_query);
}
// Last error
$sqlError = $db->sql_error();
if (!empty($sqlError['message'])) {
$serverInfo['last_error'] = $sqlError;
}
// Recent query history (if available and enabled)
if ($this->includeQueryHistory && !empty($db->dbg)) {
$recentQueries = array_slice($db->dbg, -$this->maxQueryHistory);
$serverInfo['recent_queries'] = [];
foreach ($recentQueries as $query) {
$serverInfo['recent_queries'][] = [
'sql' => $this->formatQueryForDisplay($query['sql'] ?? 'Unknown'),
'time' => isset($query['time']) ? sprintf('%.3f sec', $query['time']) : 'Unknown',
'source' => $query['src'] ?? 'Unknown',
];
}
}
$info['databases'][$serverName] = $serverInfo;
} catch (\Exception $e) {
$info['databases'][$serverName] = [
'error' => 'Could not retrieve info: ' . $e->getMessage()
];
}
}
}
// Legacy single database support
if (function_exists('DB') && empty($info['databases'])) {
$db = DB();
$info['legacy_database'] = [
'engine' => $db->engine ?? 'Unknown',
'host' => $db->db_server ?? 'Unknown',
'database' => $db->selected_db ?? 'Unknown',
'connection_status' => $db->connection ? 'Connected' : 'Disconnected',
'total_queries' => $db->num_queries ?? 0,
'total_time' => isset($db->sql_timetotal) ? sprintf('%.3f sec', $db->sql_timetotal) : 'Unknown',
];
if (!empty($db->cur_query)) {
$info['legacy_database']['current_query'] = $this->formatQueryForDisplay($db->cur_query);
}
$sqlError = $db->sql_error();
if (!empty($sqlError['message'])) {
$info['legacy_database']['last_error'] = $sqlError;
}
}
} catch (\Exception $e) {
$info['collection_error'] = $e->getMessage();
}
return $info;
}
/**
* Format SQL query for readable display
*/
private function formatQueryForDisplay(string $query, int $maxLength = 500): string
{
// Remove comments at the start (debug info)
$query = preg_replace('#^/\*.*?\*/#', '', $query);
$query = trim($query);
// Truncate if too long
if (strlen($query) > $maxLength) {
$query = substr($query, 0, $maxLength) . '... [truncated]';
}
return $query;
}
/**
* Get priority - run after the main PrettyPageHandler
*/
public function contentType(): ?string
{
return 'text/html';
}
}

View file

@ -1,269 +0,0 @@
<?php
/**
* TorrentPier Bull-powered BitTorrent tracker engine
*
* @copyright Copyright (c) 2005-2025 TorrentPier (https://torrentpier.com)
* @link https://github.com/torrentpier/torrentpier for the canonical source repository
* @license https://github.com/torrentpier/torrentpier/blob/master/LICENSE MIT License
*/
namespace TorrentPier\Whoops;
use Whoops\Handler\PrettyPageHandler;
/**
* Enhanced PrettyPageHandler for TorrentPier
*
* Extends Whoops' default handler to include database query information
* and other TorrentPier-specific debugging details in the error output.
*/
class EnhancedPrettyPageHandler extends PrettyPageHandler
{
public function __construct()
{
parent::__construct();
// Add TorrentPier-specific database information
// Note: We add these during handle() to ensure they're fresh and available
}
/**
* Get comprehensive database information
*/
private function getDatabaseInformation(): array
{
$info = [];
try {
// Get main database instance information
if (function_exists('DB')) {
$db = DB();
$info['Connection Status'] = $db->connection ? 'Connected' : 'Disconnected';
$info['Database Server'] = $db->db_server ?? 'Unknown';
$info['Selected Database'] = $db->selected_db ?? 'Unknown';
$info['Database Engine'] = $db->engine ?? 'Unknown';
$info['Total Queries'] = $db->num_queries ?? 0;
if (isset($db->sql_timetotal)) {
$info['Total Query Time'] = sprintf('%.3f seconds', $db->sql_timetotal);
}
// Current/Last executed query
if (!empty($db->cur_query)) {
$info['Current Query'] = $this->formatSqlQuery($db->cur_query);
}
// Database error information
$sqlError = $db->sql_error();
if (!empty($sqlError['message'])) {
$info['Last Database Error'] = [
'Code' => $sqlError['code'] ?? 'Unknown',
'Message' => $sqlError['message'],
];
}
// Connection details if available
if ($db->connection) {
try {
$pdo = $db->connection->getPdo();
if ($pdo) {
$info['PDO Driver'] = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) ?? 'Unknown';
$info['Server Version'] = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION) ?? 'Unknown';
// Current PDO error state
$errorCode = $pdo->errorCode();
if ($errorCode && $errorCode !== '00000') {
$errorInfo = $pdo->errorInfo();
$info['PDO Error State'] = [
'Code' => $errorCode,
'Info' => $errorInfo[2] ?? 'Unknown'
];
}
}
} catch (\Exception $e) {
$info['PDO Error'] = $e->getMessage();
}
}
}
// Get information from all database servers (new system)
if (class_exists('\TorrentPier\Database\DatabaseFactory')) {
try {
$serverNames = \TorrentPier\Database\DatabaseFactory::getServerNames();
if (count($serverNames) > 1) {
foreach ($serverNames as $serverName) {
try {
$db = \TorrentPier\Database\DatabaseFactory::getInstance($serverName);
$info["Server: $serverName"] = [
'Host' => $db->db_server ?? 'Unknown',
'Database' => $db->selected_db ?? 'Unknown',
'Queries' => $db->num_queries ?? 0,
'Connected' => $db->connection ? 'Yes' : 'No',
];
} catch (\Exception $e) {
$info["Server: $serverName"] = ['Error' => $e->getMessage()];
}
}
}
} catch (\Exception $e) {
$info['Multi-Server Error'] = $e->getMessage();
}
}
} catch (\Exception $e) {
$info['Collection Error'] = $e->getMessage();
}
return $info;
}
/**
* Get recent SQL queries from debug log
*/
private function getRecentSqlQueries(): array
{
$queries = [];
try {
if (function_exists('DB')) {
$db = DB();
// Check if debug information is available
if (!empty($db->dbg) && is_array($db->dbg)) {
// Get last 5 queries
$recentQueries = array_slice($db->dbg, -5);
foreach ($recentQueries as $index => $queryInfo) {
$queryNum = $index + 1;
$queries["Query #$queryNum"] = [
'SQL' => $this->formatSqlQuery($queryInfo['sql'] ?? 'Unknown'),
'Time' => isset($queryInfo['time']) ? sprintf('%.3f sec', $queryInfo['time']) : 'Unknown',
'Source' => $queryInfo['src'] ?? 'Unknown',
'Info' => $queryInfo['info'] ?? '',
];
// Add memory info if available
if (isset($queryInfo['mem_before'], $queryInfo['mem_after'])) {
$memUsed = $queryInfo['mem_after'] - $queryInfo['mem_before'];
$queries["Query #$queryNum"]['Memory'] = sprintf('%+d bytes', $memUsed);
}
}
}
if (empty($queries)) {
$queries['Info'] = 'No query debug information available. Enable debug mode to see recent queries.';
}
}
} catch (\Exception $e) {
$queries['Error'] = $e->getMessage();
}
return $queries;
}
/**
* Get TorrentPier environment information
*/
private function getTorrentPierEnvironment(): array
{
$env = [];
try {
// Basic environment
$env['Application Environment'] = defined('APP_ENV') ? APP_ENV : 'Unknown';
$env['Debug Mode'] = defined('DBG_USER') && DBG_USER ? 'Enabled' : 'Disabled';
$env['SQL Debug'] = defined('SQL_DEBUG') && SQL_DEBUG ? 'Enabled' : 'Disabled';
// Configuration status
if (function_exists('config')) {
$config = config();
$env['Config Loaded'] = 'Yes';
$env['TorrentPier Version'] = $config->get('tp_version', 'Unknown');
$env['Board Title'] = $config->get('sitename', 'Unknown');
} else {
$env['Config Loaded'] = 'No';
}
// Cache system
if (function_exists('CACHE')) {
$env['Cache System'] = 'Available';
}
// Language system
if (function_exists('lang')) {
$env['Language System'] = 'Available';
if (isset(lang()->getCurrentLanguage)) {
$env['Current Language'] = lang()->getCurrentLanguage;
}
}
// Memory and timing
if (defined('TIMESTART')) {
$env['Execution Time'] = sprintf('%.3f sec', microtime(true) - TIMESTART);
}
if (function_exists('sys')) {
// Use plain text formatting for memory values (no HTML entities)
$env['Peak Memory'] = str_replace('&nbsp;', ' ', humn_size(sys('mem_peak')));
$env['Current Memory'] = str_replace('&nbsp;', ' ', humn_size(sys('mem')));
}
// Request information
$env['Request Method'] = $_SERVER['REQUEST_METHOD'] ?? 'Unknown';
$env['Request URI'] = $_SERVER['REQUEST_URI'] ?? 'CLI';
$env['User Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
$env['Remote IP'] = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
} catch (\Exception $e) {
$env['Error'] = $e->getMessage();
}
return $env;
}
/**
* Format SQL query for display
*/
private function formatSqlQuery(string $query): string
{
// Remove debug comments
$query = preg_replace('#^/\*.*?\*/#', '', $query);
$query = trim($query);
// Truncate very long queries but keep them readable
if (strlen($query) > 1000) {
return substr($query, 0, 1000) . "\n... [Query truncated - " . (strlen($query) - 1000) . " more characters]";
}
return $query;
}
/**
* Override parent method to add database info and custom styling
*/
public function handle()
{
// Add TorrentPier-specific database information dynamically
try {
$this->addDataTable('Database Information', $this->getDatabaseInformation());
} catch (\Exception $e) {
$this->addDataTable('Database Information', ['Error' => $e->getMessage()]);
}
try {
$this->addDataTable('Recent SQL Queries', $this->getRecentSqlQueries());
} catch (\Exception $e) {
$this->addDataTable('Recent SQL Queries', ['Error' => $e->getMessage()]);
}
try {
$this->addDataTable('TorrentPier Environment', $this->getTorrentPierEnvironment());
} catch (\Exception $e) {
$this->addDataTable('TorrentPier Environment', ['Error' => $e->getMessage()]);
}
return parent::handle();
}
}

View file

@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use TorrentPier\Infrastructure\DependencyInjection\Bootstrap;
use TorrentPier\Infrastructure\DependencyInjection\Container;
if (!function_exists('container')) {
/**
* Get the dependency injection container instance
*
* @return Container|null
*/
function container(): ?Container
{
return Bootstrap::getContainer();
}
}
if (!function_exists('app')) {
/**
* Get a service from the container or the container itself
*
* @param string|null $id Service identifier
* @return mixed
* @throws RuntimeException If container is not initialized or service not found
*/
function app(?string $id = null): mixed
{
$container = container();
if ($container === null) {
throw new RuntimeException('Container has not been initialized. Call Bootstrap::init() first.');
}
if ($id === null) {
return $container;
}
try {
return $container->get($id);
} catch (NotFoundExceptionInterface $e) {
throw new RuntimeException("Service '$id' not found in container: " . $e->getMessage(), 0, $e);
} catch (ContainerExceptionInterface $e) {
throw new RuntimeException("Container error while resolving '$id': " . $e->getMessage(), 0, $e);
}
}
}

View file

@ -7,7 +7,7 @@ export default function AppLogo() {
<AppLogoIcon className="size-5 fill-current text-white dark:text-black" />
</div>
<div className="ml-1 grid flex-1 text-left text-sm">
<span className="mb-0.5 truncate leading-tight font-semibold">Laravel Starter Kit</span>
<span className="mb-0.5 truncate leading-tight font-semibold">TorrentPier</span>
</div>
</>
);