Import existing stuff.

This commit is contained in:
flash 2022-07-03 23:44:11 +00:00
commit c593bfee53
22 changed files with 2964 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.debug
/config.php
/errors.log

18
public/_footer.php Normal file
View File

@ -0,0 +1,18 @@
<?php
if($_SERVER['SCRIPT_FILENAME'] === __FILE__)
die('no');
?>
</div>
<footer class="footer">
<div class="copyright">
<span>Powered by <a href="//flashii.org/~flashii/seria">Seria</a> &copy; <a href="//flash.moe">Flashwave</a> 2021-<?=date('Y');?></span>
<span>/ <a href="//flashii.org/~flashii/seria/tree/00000000">00000000</a> # <a href="//flashii.org/~flashii/seria/commit/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">xxxxxxx</a></span>
<?php if($sUserInfo->isFlash()): ?>
<span>/ SQL Queries: <?=number_format((int)$pdo->query('SHOW SESSION STATUS LIKE "Questions"')->fetchColumn(1));?></span>
<?php endif; ?>
</div>
<div class="disclaimer">None of the files shown here are actually hosted on this server. The administrator of this site (<?=$_SERVER['HTTP_HOST'];?>) holds NO RESPONSIBILITY if these files are misused in any way and cannot be held responsible for what its users post, or any other actions of it.</div>
</footer>
</div>
</body>
</html>

52
public/_header.php Normal file
View File

@ -0,0 +1,52 @@
<?php
if($_SERVER['SCRIPT_FILENAME'] === __FILE__)
die('no');
if(empty($tPageTitle))
$tPageTitle = SERIA_FLASHII . ' Tracker';
else
$tPageTitle = $tPageTitle . ' :: ' . SERIA_FLASHII . ' Tracker';
$isLoggedIn = $sUserInfo->isLoggedIn();
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title><?=$tPageTitle;?></title>
<link href="/seria.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<div class="wrapper">
<nav class="header">
<?php
if($isLoggedIn):
$hTransferCount = $sUserInfo->getActiveTransferCounts();
$hTransferRatio = $sUserInfo->calculateRatio();
?>
<div class="header-stats">
<a href="/profile.php?name=<?=$sUserInfo->getName();?>"><?=$sUserInfo->getName();?></a>
&nbsp;&#183;&nbsp; <a href="/history.php">R:&nbsp; <span style="color: <?=seria_ratio_colour($hTransferRatio);?>;"><?=number_format($hTransferRatio, 3);?></span></a>
&nbsp;&#183;&nbsp; <a href="/history.php?filter=uploaded"><span style="color: green;">U:</span>&nbsp; <?=byte_symbol($sUserInfo->getBytesUploaded());?></a>
&nbsp;&#183;&nbsp; <a href="/history.php?filter=downloaded"><span style="color: red;">D:</span>&nbsp; <?=byte_symbol($sUserInfo->getBytesDownloaded());?></a>
&nbsp;&#183;&nbsp; <a href="/history.php?filter=uploading"><span style="color: green;">&#8593;</span>&nbsp; <?=number_format($hTransferCount->user_uploading);?></a>
&nbsp;&#183;&nbsp; <a href="/history.php?filter=downloading"><span style="color: red;">&#8595;</span>&nbsp; <?=number_format($hTransferCount->user_downloading);?></a>
</div>
<?php endif; ?>
<div class="header-logo"><a href="/"><?=SERIA_FLASHII;?> Tracker</a></div>
<div class="header-menu">
<?php if(!$isLoggedIn): ?>
<a href="<?=SERIA_CAUTH_LOGIN;?>">Log in</a>
<?php else: ?>
<a href="/settings.php">Settings</a>
<?php endif; ?>
<a href="/available.php">Available Downloads</a>
<?php if($sUserInfo->canCreateTorrents()): ?>
<a href="/create.php">Create Torrent</a>
<?php endif; ?>
<?php if($sUserInfo->canApproveTorrents()): ?>
<a href="/pending.php">Pending Torrents</a>
<?php endif; ?>
</div>
</nav>
<div class="wrapper-body">

144
public/announce.php Normal file
View File

@ -0,0 +1,144 @@
<?php
require_once __DIR__ . '/../seria.php';
function announce_fail(string $reason): never {
die(bencode(['failure reason' => $reason]));
}
if(empty($_GET))
die('There is nothing here.');
header('Content-Type: text/plain; charset=us-ascii');
header('X-Tracker-Version: Seria/' . SERIA_VERSION);
if(!$sIsIPv4)
announce_fail('Tracker is only supported over IPv4, please reset your DNS cache.');
//file_put_contents(SERIA_ERRORS, $_SERVER['REQUEST_URI'] . PHP_EOL, FILE_APPEND);
$urlParts = explode('/', trim($_SERVER['PATH_INFO'], '/'));
$cInfoHash = (string)filter_input(INPUT_GET, 'info_hash');
if(strlen($cInfoHash) !== 20)
announce_fail('Invalid info hash.');
$cPeerId = (string)filter_input(INPUT_GET, 'peer_id');
if(strlen($cPeerId) !== 20)
announce_fail('Invalid peer id format.');
$cPeerAddress = $_SERVER['REMOTE_ADDR'];
$cPeerPort = (int)filter_input(INPUT_GET, 'port', FILTER_SANITIZE_NUMBER_INT);
if($cPeerPort < 1 || $cPeerPort > 0xFFFF)
announce_fail('Invalid port number.');
$cPeerKey = (string)filter_input(INPUT_GET, 'key');
if(strlen($cPeerKey) > 128)
announce_fail('Key is ridiculous.');
$cPeerEvent = (string)filter_input(INPUT_GET, 'event');
if(strlen($cPeerEvent) > 128)
announce_fail('Event is fucked up.');
$cPeerAgent = (string)filter_input(INPUT_SERVER, 'HTTP_USER_AGENT');
if(strlen($cPeerAgent) > 255)
announce_fail('Agent is stupid.');
$cCompactPeers = !empty($_GET['compact']);
$cNoPeerId = !empty($_GET['no_peer_id']);
$cShortAnnounce = !empty($_GET['short']);
$cBytesUploaded = (int)filter_input(INPUT_GET, 'uploaded', FILTER_SANITIZE_NUMBER_INT);
$cBytesDownloaded = (int)filter_input(INPUT_GET, 'downloaded', FILTER_SANITIZE_NUMBER_INT);
$cBytesRemaining = (int)filter_input(INPUT_GET, 'left', FILTER_SANITIZE_NUMBER_INT);
$cPeerWant = (int)filter_input(INPUT_GET, 'numwant', FILTER_SANITIZE_NUMBER_INT);
$cPassKey = $urlParts[0] ?? '';
if(!empty($cPassKey)) {
try {
$cUserInfo = SeriaUser::byPassKey($pdo, $cPassKey);
} catch(SeriaUserNotFoundException $ex) {
sleep(3);
announce_fail('Authentication failed.');
}
} else $cUserInfo = SeriaUser::anonymous();
$interval = SERIA_ANNOUNCE_INTERVAL;
$minInterval = SERIA_ANNOUNCE_INTERVAL_MIN;
if($cShortAnnounce && SERIA_ANNOUNCE_SHORT) {
$interval = SERIA_ANNOUNCE_SHORT_INTERVAL;
$minInterval = SERIA_ANNOUNCE_SHORT_INTERVAL_MIN;
}
try {
$cTorrentInfo = SeriaTorrent::byHash($pdo, $cInfoHash);
} catch(SeriaTorrentNotFoundException $ex) {
announce_fail('Info hash not found.');
}
$cCanDownload = $cTorrentInfo->canDownload($cUserInfo);
if($cCanDownload !== '')
switch($cCanDownload) {
case 'inactive':
announce_fail('This download is inactive.');
case 'private':
announce_fail('You must be logged in for this download.');
case 'pending':
announce_fail('This download is pending approval.');
default:
announce_fail($cCanDownload);
}
$cPeerInfo = SeriaTorrentPeer::byPeerId($pdo, $cTorrentInfo, $cPeerId);
if(empty($cPeerInfo)) {
// could probably skip this is the event is 'stopped'
$cPeerInfo = SeriaTorrentPeer::create(
$pdo, $cTorrentInfo, $cUserInfo, $cPeerId, $cPeerAddress, $cPeerPort, $interval,
$cPeerAgent, $cPeerKey, $cBytesUploaded, $cBytesDownloaded, $cBytesRemaining
);
} else {
if(!$cPeerInfo->verifyKey($cPeerKey)) {
sleep(3);
announce_fail('Peer verification failed.');
}
if(!$cPeerInfo->verifyUser($cUserInfo)) {
sleep(3);
announce_fail('User verification failed.');
}
$cUserInfo->incrementTransferCounts(
$cBytesDownloaded - $cPeerInfo->getBytesDownloaded(),
$cBytesUploaded - $cPeerInfo->getBytesUploaded()
);
$cPeerInfo->update(
$cUserInfo, $cPeerAddress, $cPeerPort, $interval, $cPeerAgent,
$cBytesUploaded, $cBytesDownloaded, $cBytesRemaining
);
}
if($cPeerEvent === 'stopped') {
$cPeerInfo->delete();
die(bencode(new SeriaAnnounceResponse));
}
SeriaTorrentPeer::deleteExpired($pdo);
$response = new SeriaAnnounceResponse(
$interval + mt_rand(0, 10),
$minInterval,
$cTorrentInfo,
$cNoPeerId && SERIA_ANNOUNCE_NO_PEER_ID,
$cCompactPeers
);
if(!$cPeerInfo->isSeed() || !SERIA_ANNOUNCE_NO_SEED_P2P) {
$peers = $cTorrentInfo->getSeeds($cPeerInfo);
foreach($peers as $peer)
$response->addPeer($peer);
}
echo bencode($response);

62
public/available.php Normal file
View File

@ -0,0 +1,62 @@
<?php
require_once __DIR__ . '/../seria.php';
if(isset($_GET['name'])) {
$aUserName = (string)filter_input(INPUT_GET, 'name');
try {
$aUserInfo = SeriaUser::byName($pdo, $aUserName);
} catch(SeriaUserNotFoundException $ex) {
http_response_code(404);
die('User not found.');
}
} else $aUserInfo = null;
if($aUserInfo !== null)
$aPageTitle = 'Downloads submitted by ' . $aUserInfo->getName();
else
$aPageTitle = 'Available Downloads';
$tPageTitle = $aPageTitle;
$aPageUrl = '/available.php?';
if($aUserInfo !== null)
$aPageUrl .= 'name=' . $aUserInfo->getName() . '&amp;';
$aStartAt = -1;
$aTake = 20;
if(isset($_GET['start']))
$aStartAt = (int)filter_input(INPUT_GET, 'start', FILTER_SANITIZE_NUMBER_INT);
$aTorrents = SeriaTorrent::all($pdo, !$sUserInfo->isLoggedIn(), true, $aUserInfo, $aStartAt, $aTake);
require_once __DIR__ . '/_header.php';
echo '<div class="downloads">';
echo '<div class="downloads-header">' . $aPageTitle . '</div>';
echo '<div class="downloads-listing">';
$aShowMore = false;
$aLastId = 0;
if(empty($aTorrents)) {
echo '<div class="downloads-nothing">Sorry, nothing.</div>';
} else {
foreach($aTorrents as $torrent) {
echo $torrent->toHTML($sUserInfo, 'downloads-item');
$aLastId = $torrent->getId();
}
$aShowMore = count($aTorrents) === $aTake;
}
echo '</div>';
if($aShowMore)
echo '<div class="downloads-more"><a href="' . $aPageUrl . 'start=' . $aLastId . '">View more</a></div>';
echo '</div>';
require_once __DIR__ . '/_footer.php';

88
public/create.php Normal file
View File

@ -0,0 +1,88 @@
<?php
require_once __DIR__ . '/../seria.php';
if(!$sUserInfo->isLoggedIn()) {
http_response_code(403);
die('You must be logged in to view this page.');
}
if(!$sUserInfo->canCreateTorrents()) {
http_response_code(403);
die('You are not allowed to view this page.');
}
if(!empty($_FILES['torrent'])) {
if(empty($_POST['boob']) || !hash_equals($sVerification, (string)filter_input(INPUT_POST, 'boob'))) {
$cError = 'Request verification failed.';
} else {
if(!isset($_FILES['torrent']['error']) || is_array($_FILES['torrent']['error'])) {
$cError = 'Invalid parameters.';
} else {
if($_FILES['torrent']['error'] !== UPLOAD_ERR_OK) {
$cError = [
UPLOAD_ERR_NO_FILE => 'No file was sent.',
UPLOAD_ERR_INI_SIZE => 'File size limit exceeded.',
UPLOAD_ERR_FORM_SIZE => 'File size limit exceeded.',
][$_FILES['torrent']['error']] ?? 'An unexpected error occurred.';
} else {
if(!is_uploaded_file($_FILES['torrent']['tmp_name'])) {
$cError = 'File was not an uploaded file(?).';
} else {
$torrentFile = fopen($_FILES['torrent']['tmp_name'], 'rb');
try {
$cTorrentBuilder = SeriaTorrentBuilder::decode($torrentFile);
$cTorrentBuilder->setUser($sUserInfo);
$cTorrentInfo = $cTorrentBuilder->create($pdo);
header('Location: /info.php?id=' . $cTorrentInfo->getId());
exit;
} catch(Exception $ex) {
$cError = $ex->getMessage();
if(empty($cError))
$cError = (string)$ex;
} finally {
fclose($torrentFile);
}
}
}
}
}
}
$tPageTitle = 'Create Torrent';
$cTrackerUrl = 'https://' . $_SERVER['HTTP_HOST'] . '/announce.php/' . $sUserInfo->getPassKey();
require_once __DIR__ . '/_header.php';
echo '<div class="create">';
echo '<div class="create-header">';
echo '<h2>Creating a torrent</h2>';
echo '<p>Here you can submit a torrent for tracking.</p>';
echo '<p>You can use any client for submission. This page does not contain any of its own fields, the creator in your client should be sufficient.</p>';
echo '<p>Use <input type="text" readonly value="' . $cTrackerUrl . '" onfocus="this.select()" /> as the tracker url so you\'re immediately seeding the torrent, the tracker will error but that\'s temporary since the torrent has not been submitted yet. This URL contains your private pass key, <b>do not share it</b>.</p>';
echo '<p>After your torrent has been submitted it will be queued for approval and won\'t immediately be available.</p>';
echo '</div>';
if(!empty($cError)) {
echo '<div class="create-error">';
echo '<div class="create-error-icon"><img src="//static.flash.moe/images/silk/error.png" alt="Error" /></div>';
echo '<div class="create-error-text">';
echo htmlspecialchars($cError);
echo '</div>';
echo '</div>';
}
echo '<div class="create-form">';
echo '<form action="/create.php" method="post" enctype="multipart/form-data">';
echo '<input type="hidden" name="boob" value="' . $sVerification . '" />';
echo '<label class="create-form-file"><input type="file" name="torrent" /></label>';
echo '<input class="create-form-submit" type="submit" value="Submit!" />';
echo '</form>';
echo '</div>';
echo '</div>';
require_once __DIR__ . '/_footer.php';

37
public/download.php Normal file
View File

@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/../seria.php';
$dTorrentId = (string)filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
header('Content-Type: text/plain; charset=us-ascii');
try {
$dTorrentInfo = SeriaTorrent::byId($pdo, $dTorrentId);
} catch(SeriaTorrentNotFoundException $ex) {
http_response_code(404);
die('Download not found.');
}
$dCanDownload = $dTorrentInfo->canDownload($sUserInfo);
if($dCanDownload !== '') {
http_response_code(403);
switch($dCanDownload) {
case 'inactive':
die('This download is inactive.');
case 'private':
die('You must be logged in for this download.');
case 'pending':
die('This download is pending approval.');
default:
die($dCanDownload);
}
}
$trackerUrl = SERIA_ANNOUNCE_URL_ANON;
if($sUserInfo->isLoggedIn())
$trackerUrl = sprintf(SERIA_ANNOUNCE_URL, $sUserInfo->getPassKey($pdo));
header('Content-Type: application/x-bittorrent');
header('Content-Disposition: inline; filename="' . htmlspecialchars($dTorrentInfo->getName()) . '.torrent"');
echo $dTorrentInfo->encode($trackerUrl);

27
public/history.php Normal file
View File

@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/../seria.php';
if(!$sUserInfo->isLoggedIn()) {
http_response_code(404);
die('You must be logged in to view this page.');
}
if(isset($_GET['name'])) {
$aUserName = (string)filter_input(INPUT_GET, 'name');
try {
$aUserInfo = SeriaUser::byName($pdo, $aUserName);
} catch(SeriaUserNotFoundException $ex) {
http_response_code(404);
die('User not found.');
}
} else $aUserInfo = $sUserInfo;
$tPageTitle = 'Transfer History';
require_once __DIR__ . '/_header.php';
echo 'Transfer history should be listed here<br/>';
var_dump($aUserInfo->getName());
require_once __DIR__ . '/_footer.php';

13
public/index.php Normal file
View File

@ -0,0 +1,13 @@
<?php
require_once __DIR__ . '/../seria.php';
require_once __DIR__ . '/_header.php';
echo '<div class="index">';
echo '<h2>Welcome to the ' . SERIA_FLASHII . ' Tracker!</h2>';
echo '<p>This tracker is provided as a central download repository for files relevant to the community.</p>';
echo '<p>Among intended uses are archives and modifications. Some downloads are only available to authenticated users. Stats only function as Internet Points and won\'t incur repercussions if bad. Certain users are able to submit downloads for approval.</p>';
echo '<p>Please enjoy responsibly!</p>';
echo '</div>';
require_once __DIR__ . '/_footer.php';

154
public/info.php Normal file
View File

@ -0,0 +1,154 @@
<?php
require_once __DIR__ . '/../seria.php';
$dTorrentId = (string)filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
try {
$dTorrentInfo = SeriaTorrent::byId($pdo, $dTorrentId);
} catch(SeriaTorrentNotFoundException $ex) {
http_response_code(404);
die('Download not found.');
}
$dCanDownload = $dTorrentInfo->canDownload($sUserInfo);
if($dCanDownload !== '') {
http_response_code(403);
switch($dCanDownload) {
case 'inactive':
die('This download is inactive.');
case 'private':
die('You must be logged in for this download.');
case 'pending':
die('This download is pending approval.');
default:
die($dCanDownload);
}
}
if(isset($_GET['action'])) {
if(empty($_GET['boob']) || !hash_equals($sVerification, (string)filter_input(INPUT_GET, 'boob'))) {
header('Location: /info.php?id=' . $dTorrentInfo->getId());
exit;
}
switch(filter_input(INPUT_GET, 'action')) {
case 'recalculate-info-hash':
if($sUserInfo->canRecalculateInfoHash()) {
$builder = SeriaTorrentBuilder::import($dTorrentInfo);
$dTorrentInfo->setHash($builder->calculateInfoHash());
$dTorrentInfo->update();
}
header('Location: /info.php?id=' . $dTorrentInfo->getId());
exit;
case 'approve':
if($sUserInfo->canApproveTorrents()) {
$dTorrentInfo->approve();
$dTorrentInfo->update();
}
header('Location: /info.php?id=' . $dTorrentInfo->getId());
exit;
case 'deny':
if($sUserInfo->canApproveTorrents())
$dTorrentInfo->nuke();
header('Location: /pending.php');
exit;
}
}
$tPageTitle = $dTorrentInfo->getName();
require_once __DIR__ . '/_header.php';
echo '<div class="info">';
echo '<div class="info-header">';
echo '<h2>' . htmlspecialchars($dTorrentInfo->getName()) . '</h2>';
echo '<ul>';
echo '<li><a href="/download.php?id=' . $dTorrentInfo->getId() . '"><img src="//static.flash.moe/images/silk/link.png" alt="" /> Download</a></li>';
if($sUserInfo->canRecalculateInfoHash())
echo '<li><a href="/info.php?id=' . $dTorrentInfo->getId() . '&amp;action=recalculate-info-hash&amp;boob=' . $sVerification . '"><img src="//static.flash.moe/images/silk/calculator.png" alt="" /> Recalculate Info Hash</a></li>';
echo '</ul>';
echo '</div>';
echo '<div class="info-stats">';
echo '<div class="info-stats-item info-stats-submitted">';
echo '<div class="info-stats-item-title">Submitted on</div>';
echo '<div class="info-stats-item-value"><time datetime="' . date('c', $dTorrentInfo->getCreatedTime()) . '">' . date('Y-m-d H:i:s e', $dTorrentInfo->getCreatedTime()) . '</time></div>';
echo '</div>';
if($sUserInfo->canApproveTorrents() && $dTorrentInfo->isApproved()) {
echo '<div class="info-stats-item info-stats-approved">';
echo '<div class="info-stats-item-title">Approved on</div>';
echo '<div class="info-stats-item-value"><time datetime="' . date('c', $dTorrentInfo->getApprovedTime()) . '">' . date('Y-m-d H:i:s e', $dTorrentInfo->getApprovedTime()) . '</time></div>';
echo '</div>';
}
echo '<div class="info-stats-item info-stats-size" style="border-color: ' . seria_size_colour($dTorrentInfo->getSize()) . '">';
echo '<div class="info-stats-item-title">Total size</div>';
echo '<div class="info-stats-item-value">' . byte_symbol($dTorrentInfo->getSize()) . ' (' . number_format($dTorrentInfo->getSize()) . ' bytes)</div>';
echo '</div>';
echo '<div class="info-stats-item info-stats-uploading">';
echo '<div class="info-stats-item-title">Uploading</div>';
echo '<div class="info-stats-item-value">' . number_format($dTorrentInfo->getCompletePeers()) . '</div>';
echo '</div>';
echo '<div class="info-stats-item info-stats-downloading">';
echo '<div class="info-stats-item-title">Downloading</div>';
echo '<div class="info-stats-item-value">' . number_format($dTorrentInfo->getIncompletePeers()) . '</div>';
echo '</div>';
if($dTorrentInfo->isPrivate()) {
echo '<div class="info-stats-item info-stats-visibility">';
echo '<div class="info-stats-item-title">Visibility</div>';
echo '<div class="info-stats-item-value">Private</div>';
echo '</div>';
}
echo '</div>';
if($dTorrentInfo->hasUser()) {
try {
$dUserInfo = SeriaUser::byId($pdo, $dTorrentInfo->getUserId());
echo '<div class="info-user" style="--user-colour:' . (string)$dUserInfo->getColour() . '">';
echo '<div class="info-user-uploaded">Submitted by</div>';
echo '<div class="avatar info-user-avatar"><a href="/profile.php?name=' . $dUserInfo->getName() . '"><img src="' . sprintf(SERIA_AVATAR_FORMAT_RES, $dUserInfo->getId(), 40) . '" alt="" width="20" height="20" /></a></div>';
echo '<div class="info-user-name"><a href="/profile.php?name=' . $dUserInfo->getName() . '">' . $dUserInfo->getName() . '</a></div>';
echo '</div>';
} catch(SeriaUserNotFoundException $ex) {}
}
if(!$dTorrentInfo->isApproved() && $sUserInfo->canApproveTorrents()) {
echo '<div class="info-pending">';
echo '<div class="info-pending-text">This torrent is pending approval.</div>';
echo '<a class="info-pending-approve" href="/info.php?id=' . $dTorrentInfo->getId() . '&amp;action=approve&amp;boob=' . $sVerification . '">APPROVE</a>';
echo '<a class="info-pending-deny" href="/info.php?id=' . $dTorrentInfo->getId() . '&amp;action=deny&amp;boob=' . $sVerification . '">DENY</a>';
echo '</div>';
}
$dComment = $dTorrentInfo->getComment();
if(!empty($dComment)) {
echo '<div class="info-comment">';
echo '<div class="info-comment-header">Description</div>';
echo '<div class="info-comment-content"><pre>';
echo htmlspecialchars($dComment);
echo '</pre></div>';
echo '</div>';
}
echo '</div>';
require_once __DIR__ . '/_footer.php';

55
public/pending.php Normal file
View File

@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/../seria.php';
if(!$sUserInfo->isLoggedIn()) {
http_response_code(403);
die('You must be logged in to view this page.');
}
if(!$sUserInfo->canApproveTorrents()) {
http_response_code(403);
die('You are not allowed to view this page.');
}
$tPageTitle = 'Pending Torrents';
$pPageUrl = '/pending.php?';
$pStartAt = -1;
$pTake = 20;
if(isset($_GET['start']))
$pStartAt = (int)filter_input(INPUT_GET, 'start', FILTER_SANITIZE_NUMBER_INT);
$pTorrents = SeriaTorrent::all($pdo, false, false, null, $pStartAt, $pTake);
require_once __DIR__ . '/_header.php';
echo '<div class="downloads">';
echo '<div class="downloads-header">Pending Torrents</div>';
echo '<div class="downloads-listing">';
$pShowMore = false;
$pLastId = 0;
if(empty($pTorrents)) {
echo '<div class="downloads-nothing">There are no pending torrents!</div>';
} else {
foreach($pTorrents as $torrent) {
echo $torrent->toHTML($sUserInfo, 'downloads-item', true, $sVerification);
$pLastId = $torrent->getId();
}
$pShowMore = count($pTorrents) === $pTake;
}
echo '</div>';
if($pShowMore)
echo '<div class="downloads-more"><a href="' . $pPageUrl . 'start=' . $pLastId . '">View more</a></div>';
echo '</div>';
require_once __DIR__ . '/_footer.php';

71
public/profile.php Normal file
View File

@ -0,0 +1,71 @@
<?php
require_once __DIR__ . '/../seria.php';
if(!$sUserInfo->isLoggedIn()) {
http_response_code(403);
die('You must be logged in to view this page.');
}
$pUserName = (string)filter_input(INPUT_GET, 'name');
try {
$pUserInfo = SeriaUser::byName($pdo, $pUserName);
} catch(SeriaUserNotFoundException $ex) {
http_response_code(404);
die('User not found.');
}
$pTransferCount = $pUserInfo->getActiveTransferCounts();
$pTransferRatio = $pUserInfo->calculateRatio();
$tPageTitle = $pUserInfo->getName();
require_once __DIR__ . '/_header.php';
echo '<div class="profile" style="--user-colour:' . (string)$pUserInfo->getColour() . '" id="p' . $pUserInfo->getId() . '">';
echo '<div class="profile-header" id="p' . $pUserInfo->getId() . 'h">';
echo '<div class="avatar profile-header-avatar"><img src="' . sprintf(SERIA_AVATAR_FORMAT_RES, $pUserInfo->getId(), 200) . '" alt="" width="100" height="100" /></div>';
echo '<div class="profile-header-info">';
echo '<div class="profile-header-info-name"><span>' . $pUserInfo->getName() . '</span></div>';
echo '<div class="profile-header-info-flashii"><a href="' . sprintf(SERIA_PROFILE_FORMAT, $pUserInfo->getId()) . '">View full profile on ' . SERIA_FLASHIINET . '</a></div>';
echo '</div>';
echo '</div>';
echo '<div class="profile-transfer" id="p' . $pUserInfo->getId() . 'r">';
echo '<a href="/history.php?name=' . $pUserInfo->getName() . '">Ratio <span style="color: ' . seria_ratio_colour($pTransferRatio) . ';">' . number_format($pTransferRatio, 3) . '</span></a>';
echo '<a href="/history.php?name=' . $pUserInfo->getName() . '&amp;filter=uploaded"><span style="color: green;">Uploaded</span> ' . byte_symbol($pUserInfo->getBytesUploaded()) . '</a>';
echo '<a href="/history.php?name=' . $pUserInfo->getName() . '&amp;filter=downloaded"><span style="color: red;">Downloaded</span> ' . byte_symbol($pUserInfo->getBytesDownloaded()) . '</a>';
echo '<a href="/history.php?name=' . $pUserInfo->getName() . '&amp;filter=uploading"><span style="color: green;">Seeding</span> ' . number_format($pTransferCount->user_uploading) . '</a>';
echo '<a href="/history.php?name=' . $pUserInfo->getName() . '&amp;filter=downloading"><span style="color: red;">Leeching</span> ' . number_format($pTransferCount->user_downloading) . '</a>';
echo '</div>';
$pSubmissions = $pUserInfo->getProfileSubmissions();
if(!empty($pSubmissions)) {
echo '<div class="profile-submissions" id="p' . $pUserInfo->getId() . 's">';
echo '<div class="profile-submissions-header">Latest Submissions</div>';
foreach($pSubmissions as $submission)
echo $submission->toHTML($sUserInfo, 'profile-submission', false);
echo '<div class="profile-submissions-full"><a href="/available.php?name=' . $pUserInfo->getName() . '">View all submissions</a></div>';
echo '</div>';
}
echo '<div class="profile-history" id="p' . $pUserInfo->getId() . 't">';
echo '<div class="profile-history-header">Latest Transfers</div>';
echo 'todo: keep track of this';
echo '<div class="profile-history-full"><a href="/history.php?name=' . $pUserInfo->getName() . '">View full transfer history</a></div>';
echo '</div>';
echo '</div>';
require_once __DIR__ . '/_footer.php';

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

726
public/seria.css Normal file
View File

@ -0,0 +1,726 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
outline-style: none;
}
html,
body,
.wrapper {
width: 100%;
height: 100%;
}
.wrapper {
font: 12px/20px Verdana, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
background: #111;
color: #fff;
padding: 0 5px;
display: flex;
flex-direction: column;
}
.wrapper-body {
width: 100%;
max-width: 1200px;
margin: 0 auto;
flex: 1 0 auto;
}
.avatar {
flex-shrink: 0;
background-color: #111;
display: block;
border: 0;
border-radius: 5%;
box-sizing: content-box;
vertical-align: middle;
max-width: 100%;
max-height: 100%;
overflow: hidden;
}
.avatar a {
display: block;
width: 100%;
height: 100%;
}
.avatar img {
vertical-align: top;
}
.header {
background: url('//flashii.net/images/clouds.png') fixed #8559a5;
background-blend-mode: multiply;
overflow: hidden;
margin: 5px auto;
width: 100%;
max-width: 1200px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
flex: 0 0 auto;
}
.header-logo {
font-size: 2.5em;
line-height: 1.1em;
padding: 15px 20px;
}
.header-logo a {
color: #fff;
text-decoration: none;
}
.header-menu {
display: flex;
}
.header-menu a {
display: block;
color: #fff;
text-decoration: none;
flex: 0 0 auto;
padding: 2px 10px;
margin: 5px 0;
margin-left: 5px;
border-radius: 2px;
text-align: center;
transition: background .2s;
}
.header-menu a:hover,
.header-menu a:focus {
background: rgba(255, 255, 255, .2);
}
.header-menu a:active {
background: rgba(255, 255, 255, .1);
}
.header-stats {
/*border: 1px solid #000;
background: linear-gradient(180deg, #545454 0%, #1a1a1a 50%, #000 50%) #545454;*/
background-color: rgba(64, 64, 64, .5);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border-radius: 5px;
margin: 5px;
margin-bottom: 0;
user-select: none;
overflow: hidden;
}
.header-stats a {
color: #fff;
text-decoration: none;
padding: 2px 10px;
display: inline-block;
transition: background-color .1s;
}
.header-stats a:hover,
.header-stats a:focus {
background-color: rgba(255, 255, 255, .1);
}
.header-stats a:active {
background-color: rgba(0, 0, 0, .5);
}
.footer {
background: #161616;
overflow: hidden;
margin: 5px auto;
width: 100%;
max-width: 1200px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
padding: 5px;
flex: 0 0 auto;
}
.footer .copyright {
text-align: center;
font-size: .9em;
line-height: 1.4em;
}
.footer .disclaimer {
font-size: .8em;
line-height: 1.3em;
padding-top: 3px;
}
.footer a {
color: inherit;
text-decoration: none;
}
.footer a:hover,
.footer a:focus {
text-decoration: underline;
}
.info {
/**/
}
.info-header {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
padding: 5px;
}
.info-header h2 {
margin: 5px;
}
.info-header ul {
list-style: none;
display: flex;
}
.info-header a {
color: #fff;
text-decoration: none;
text-transform: lowercase;
font-variant: small-caps;
display: inline-block;
padding: 2px 5px;
}
.info-header a:hover,
.info-header a:focus {
text-decoration: underline;
}
.info-header img {
vertical-align: middle;
}
.info-stats {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
padding: 0 5px;
display: flex;
align-items: center;
margin-top: 2px;
flex-wrap: wrap;
}
.info-stats-item {
display: flex;
flex-direction: column;
flex: 0 0 auto;
margin-right: 10px;
border-bottom: 2px solid #fff;
}
.info-stats-item-title {
font-variant: small-caps;
padding: 5px;
padding-bottom: 0;
}
.info-stats-item-value {
font-size: 1.2em;
line-height: 1.4em;
padding: 5px;
padding-top: 0;
}
.info-stats-submitted {
border-color: #09f;
}
.info-stats-approved {
border-color: #0a0;
}
.info-stats-uploading {
border-color: green;
}
.info-stats-downloading {
border-color: red;
}
.info-stats-visibility {
border-color: orange;
}
.info-user {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
padding: 0 5px;
display: flex;
align-items: center;
margin-top: 2px;
}
.info-user-uploaded {
margin-right: 4px;
}
.info-user-avatar {
margin-right: 4px;
}
.info-user-name {}
.info-user-name a {
color: #fff;
text-decoration: none;
border-bottom: 2px solid var(--user-colour, #fff);
border-top: 2px solid transparent;
font-weight: 700;
display: inline-block;
}
.info-pending {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
display: flex;
margin-top: 2px;
height: 50px;
}
.info-pending-text {
flex: 1 1 auto;
padding: 0 20px;
font-size: 1.2em;
line-height: 48px;
}
.info-pending-approve {
flex: 0 0 auto;
display: block;
color: #fff;
text-decoration: none;
background: #2a2;
width: 100px;
text-align: center;
line-height: 49px;
transition: background .2s;
}
.info-pending-approve:hover,
.info-pending-approve:focus {
background: #2c2;
}
.info-pending-approve:active {
background: #282;
}
.info-pending-deny {
flex: 0 0 auto;
display: block;
color: #fff;
text-decoration: none;
background: #a22;
width: 100px;
text-align: center;
line-height: 49px;
transition: background .2s;
}
.info-pending-deny:hover,
.info-pending-deny:focus {
background: #c22;
}
.info-pending-deny:active {
background: #822;
}
.info-comment {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
padding: 2px;
margin-top: 2px;
}
.info-comment-header {
font-size: 1.4em;
padding: 5px 10px;
}
.info-comment-content {
padding: 5px 10px;
overflow: auto;
}
.info-comment-content pre {
line-height: 14px;
}
.profile {}
.profile-header {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
display: flex;
align-items: center;
padding: 2px;
}
.profile-header-info {
display: flex;
flex-direction: column;
padding: 0 10px;
}
.profile-header-info-name {
font-weight: 700;
font-size: 2em;
line-height: 1.5em;
}
.profile-header-info-name span {
border-bottom: 2px solid var(--user-colour, #fff);
}
.profile-header-info-flashii {
font-variant: small-caps;
}
.profile-header-info-flashii a {
color: #fff;
text-decoration: none;
}
.profile-header-info-flashii a:hover,
.profile-header-info-flashii a:focus {
text-decoration: underline;
}
.profile-transfer {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
display: flex;
align-items: center;
padding: 2px;
margin-top: 2px;
font-size: 1.4em;
}
.profile-transfer a {
display: inline-block;
color: #fff;
text-decoration: none;
padding: 5px 10px;
}
.profile-submissions {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
padding: 2px;
margin-top: 2px;
}
.profile-submissions-header {
font-size: 1.4em;
padding: 5px 10px;
}
.profile-submissions-full {
width: 100%;
}
.profile-submissions-full a {
display: block;
width: 100%;
padding: 5px 10px;
text-align: center;
color: #fff;
text-decoration: none;
transition: background .2s;
}
.profile-submissions-full a:hover,
.profile-submissions-full a:focus {
background: rgba(255, 255, 255, .2);
}
.profile-submissions-full a:active {
background: rgba(255, 255, 255, .1);
}
.profile-submission {
margin: 0 2px;
margin-bottom: 2px;
}
.profile-history {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
padding: 2px;
margin-top: 2px;
}
.profile-history-header {
font-size: 1.4em;
padding: 5px 10px;
}
.profile-history-full {
width: 100%;
}
.profile-history-full a {
display: block;
width: 100%;
padding: 5px 10px;
text-align: center;
color: #fff;
text-decoration: none;
transition: background .2s;
}
.profile-history-full a:hover,
.profile-history-full a:focus {
background: rgba(255, 255, 255, .2);
}
.profile-history-full a:active {
background: rgba(255, 255, 255, .1);
}
.tdl {
display: flex;
border-radius: 2px;
background-color: rgba(17, 17, 17, .6);
transition: background-color .2s, box-shadow .2s;
}
.tdl:nth-child(even) {
background-color: rgba(25, 25, 25, .6);
}
.tdl:hover,
.tdl:focus {
background-color: rgba(34, 34, 34, .6);
box-shadow: 0 1px 4px #222;
}
.tdl-details {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
padding: 2px 4px;
margin: 0 2px;
}
.tdl-details-name {
font-size: 1.4em;
line-height: 1.5em;
}
.tdl-details-name a {
color: #fff;
text-decoration: none;
padding: 2px;
display: inline-block;
}
.tdl-details-name a:hover,
.tdl-details-name a:active,
.tdl-details-name a:focus {
text-decoration: underline;
}
.tdl-user {
display: flex;
align-items: center;
margin: 0 2px;
}
.tdl-user-avatar {
flex: 0 0 auto;
margin-right: 4px;
}
.tdl-user-name {}
.tdl-user-name a {
color: #fff;
text-decoration: none;
border-bottom: 2px solid var(--user-colour, #fff);
border-top: 2px solid transparent;
font-weight: 700;
display: inline-block;
}
.tdl-stats {
flex: 0 0 auto;
display: flex;
font-size: 1.5em;
align-items: center;
}
.tdl-stats > div {
display: flex;
padding-left: 8px;
padding-bottom: 1px;
}
.tdl-stats .arrow {
margin-right: 2px;
}
.tdl-stats .arrow,
.tdl-stats .number {
flex: 0 0 auto;
}
.tdl-stats-uploading .arrow {
color: green;
}
.tdl-stats-downloading .arrow {
color: red;
}
.tdl-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1px;
margin-left: 10px;
max-width: 200px;
width: 100%;
}
.tdl-actions img {
vertical-align: top;
}
.tdl-actions a {
display: flex;
justify-content: center;
align-items: center;
margin: 1px;
padding: 8px;
border-radius: 2px;
color: #fff;
text-decoration: none;
background: rgba(255, 255, 255, .1);
transition: background .2s;
}
.tdl-actions a:hover,
.tdl-actions a:focus {
background: rgba(255, 255, 255, .3);
}
.tdl-actions a:active {
background: rgba(255, 255, 255, .2);
}
.downloads {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
padding: 2px;
}
.downloads-header {
font-size: 1.4em;
padding: 5px 10px;
}
.downloads-more {
width: 100%;
}
.downloads-more a {
display: block;
width: 100%;
padding: 5px 10px;
text-align: center;
color: #fff;
text-decoration: none;
transition: background .2s;
}
.downloads-more a:hover,
.downloads-more a:focus {
background: rgba(255, 255, 255, .2);
}
.downloads-more a:active {
background: rgba(255, 255, 255, .1);
}
.downloads-item {
margin: 0 2px;
margin-bottom: 2px;
}
.downloads-nothing {
text-align: center;
font-size: 1.2em;
padding: 10px;
}
.index {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
padding: 2px;
}
.index h2 {
font-size: 1.4em;
font-weight: 400;
padding: 5px 10px;
}
.index p {
margin: .2em 10px;
}
.create {}
.create-header {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
padding: 2px;
}
.create-header h2 {
font-size: 1.4em;
font-weight: 400;
padding: 5px 10px;
}
.create-header p {
margin: .2em 10px;
}
.create-header code {
display: inline-block;
}
.create-form {
background: #161616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
margin-top: 2px;
}
.create-form form {
display: flex;
}
.create-form-file {
flex: 1 1 auto;
display: flex;
align-items: center;
padding: 0 14px;
}
.create-form-submit {
flex: 0 0 auto;
background: #333;
color: #fff;
border: 0;
font-family: Verdana, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 32px;
padding: 10px;
transition: background .2s;
}
.create-form-submit:hover,
.create-form-submit:focus {
background: #3A3A3A;
}
.create-form-submit:active {
background: #303030;
}
.create-error {
background: #361616;
overflow: hidden;
width: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
text-shadow: 0 1px 4px #000;
align-items: center;
margin-top: 2px;
display: flex;
}
.create-error-icon {
flex: 0 0 auto;
background: #561616;
width: 40px;
height: 40px;
text-align: center;
line-height: 38px;
}
.create-error-icon img {
vertical-align: middle;
}
.create-error-text {
flex: 1 1 auto;
font-size: 1.2em;
padding: 4px 10px;
}

15
public/settings.php Normal file
View File

@ -0,0 +1,15 @@
<?php
require_once __DIR__ . '/../seria.php';
if(!$sUserInfo->isLoggedIn()) {
http_response_code(404);
die('You must be logged in to view this page.');
}
$tPageTitle = 'Settings';
require_once __DIR__ . '/_header.php';
echo 'Provide option to reset pass key and shit here, maybe also a nuke tracker profile option but probably not.';
require_once __DIR__ . '/_footer.php';

41
public/test.php Normal file
View File

@ -0,0 +1,41 @@
<!doctype html>
<?php
ini_set('display_errors', 'on');
error_reporting(-1);
function seria_weighted_number(float $num1, float $num2, float $weight): int {
$weight = min(1, max(0, $weight));
return round(($num1 * $weight) + ($num2 * (1 - $weight)));
}
function seria_weighted_colour(int $colour1, int $colour2, float $weight): int {
$colour1 = [ ($colour1 >> 16) & 0xFF, ($colour1 >> 8) & 0xFF, $colour1 & 0xFF ];
$colour2 = [ ($colour2 >> 16) & 0xFF, ($colour2 >> 8) & 0xFF, $colour2 & 0xFF ];
return (seria_weighted_number($colour1[0], $colour2[0], $weight) << 16)
| (seria_weighted_number($colour1[1], $colour2[1], $weight) << 8)
| seria_weighted_number($colour1[2], $colour2[2], $weight);
}
function seria_weighted_colour_hex(int $colour1, int $colour2, float $weight): string {
return sprintf('#%06x', seria_weighted_colour($colour1, $colour2, $weight));
}
function seria_easeInQuad(float $n): float {
return $n * $n;
}
function seria_easeOutQuad(float $n): float {
return 1 - (1 - $n) * (1 - $n);
}
function seria_ratio_colour(float $ratio): string {
$ratio *= 2;
if($ratio > 1)
return seria_weighted_colour_hex(0x008000, 0xFFAA00, seria_easeInQuad($ratio - 1));
return seria_weighted_colour_hex(0xFFAA00, 0xFF0000, seria_easeOutQuad($ratio));
}
for($i = 0; $i <= 100; ++$i) {
$if = $i / 100;
printf('<span style="color: %2$s;">%1$01.2f %2$s</span><br/>', $if, seria_ratio_colour($if));
}

164
seria.php Normal file
View File

@ -0,0 +1,164 @@
<?php
define('SERIA_ROOT', __DIR__);
define('SERIA_STARTUP', microtime(true));
define('SERIA_DEBUG', is_file(SERIA_ROOT . '/.debug'));
define('SERIA_SRC', SERIA_ROOT . '/src');
define('SERIA_ERRORS', SERIA_ROOT . '/errors.log');
define('SERIA_VERSION', '1.0');
ini_set('display_errors', 'on');
if(SERIA_DEBUG) {
set_error_handler(function($errno, $errstr, $errfile, $errline) {
if(!is_file(SERIA_ERRORS))
touch(SERIA_ERRORS);
file_put_contents(SERIA_ERRORS, sprintf("(%s) [%s:%s] %s\r\n", date('Y-m-d H:i:s'), $errfile, $errline, $errstr), FILE_APPEND);
}, -1);
} else error_reporting(0);
require_once SERIA_ROOT . '/config.php';
try {
$pdo = new PDO(SERIA_PDO_DSN, SERIA_PDO_USER, SERIA_PDO_PASS, [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET SESSION time_zone = \'+00:00\''
. ', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\'',
]);
} catch(PDOException $ex) {
die($ex->getMessage());
}
require_once SERIA_SRC . '/user.php';
require_once SERIA_SRC . '/benben.php';
require_once SERIA_SRC . '/torrent.php';
require_once SERIA_SRC . '/announce.php';
header('X-Powered-By: Seria/' . SERIA_VERSION);
$sInAnnounce = $_SERVER['SCRIPT_NAME'] === '/announce.php';
$sIsIPv4 = strlen(inet_pton($_SERVER['REMOTE_ADDR'])) === 4;
if(!$sInAnnounce && !$sIsIPv4)
die('The tracker is only supported over IPv4, please reset your DNS cache.');
$sUserInfo = SeriaUser::anonymous();
$sVerification = '';
if(!$sInAnnounce) {
// replace this with id.flashii.net shit
$mszAuth = (string)filter_input(INPUT_COOKIE, 'msz_auth');
if(!empty($mszAuth)) {
$mszAuthDecoded = str_pad(base64_decode(str_pad(strtr($mszAuth, '-_', '+/'), strlen($mszAuth) % 4, '=', STR_PAD_RIGHT)), 37, "\0");
$mszAuthUnpacked = unpack('Cversion/Nuser/H*token', $mszAuthDecoded);
if(isset($mszAuthUnpacked['version'])
&& $mszAuthUnpacked['version'] >= 1
&& isset($mszAuthUnpacked['user'])
&& $mszAuthUnpacked['user'] > 0) {
$loginRequest = [
'user_id' => $mszAuthUnpacked['user'],
'token' => 'SESS:' . $mszAuth,
'ip' => $_SERVER['REMOTE_ADDR'],
];
$loginSignature = hash_hmac('sha256', implode('#', $loginRequest), SERIA_MSZ_SECRET);
$login = curl_init(SERIA_CAUTH_ENDPOINT);
curl_setopt_array($login, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($loginRequest),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Seria/' . SERIA_VERSION,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-SharpChat-Signature: ' . $loginSignature,
],
]);
$loginResponse = json_decode(curl_exec($login));
curl_close($login);
if(!empty($loginResponse->success))
$sUserInfo = SeriaUser::fromMisuzu($pdo, $loginResponse);
unset($mszAuth, $mszAuthDecoded, $mszAuthUnpacked, $loginRequest, $loginSignature, $login, $loginResponse);
}
}
if(empty($_COOKIE['seria_random'])) {
$sVerification = SeriaUser::generatePassKey(32);
setcookie('seria_random', $sVerification, strtotime('1 day'), '/', $_SERVER['HTTP_HOST']);
} else
$sVerification = (string)filter_input(INPUT_COOKIE, 'seria_random');
$sVerification = hash('sha256', $sVerification);
}
function byte_symbol(int $bytes, bool $decimal = true, array $symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']): string {
if($bytes < 1)
return '0 B';
$divider = $decimal ? 1000 : 1024;
$exp = floor(log($bytes) / log($divider));
$bytes = $bytes / pow($divider, $exp);
$symbol = $symbols[$exp];
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
}
function seria_weighted_number(float $num1, float $num2, float $weight): int {
$weight = min(1, max(0, $weight));
return round(($num1 * $weight) + ($num2 * (1 - $weight)));
}
function seria_weighted_colour(int $colour1, int $colour2, float $weight): int {
$colour1 = [ ($colour1 >> 16) & 0xFF, ($colour1 >> 8) & 0xFF, $colour1 & 0xFF ];
$colour2 = [ ($colour2 >> 16) & 0xFF, ($colour2 >> 8) & 0xFF, $colour2 & 0xFF ];
return (seria_weighted_number($colour1[0], $colour2[0], $weight) << 16)
| (seria_weighted_number($colour1[1], $colour2[1], $weight) << 8)
| seria_weighted_number($colour1[2], $colour2[2], $weight);
}
function seria_weighted_colour_hex(int $colour1, int $colour2, float $weight): string {
return sprintf('#%06x', seria_weighted_colour($colour1, $colour2, $weight));
}
function seria_easeInQuad(float $n): float {
return $n * $n;
}
function seria_easeOutQuad(float $n): float {
return 1 - (1 - $n) * (1 - $n);
}
function seria_ratio_colour(float $ratio): string {
$ratio *= 2;
if($ratio > 1)
return seria_weighted_colour_hex(0x008000, 0xFFAA00, seria_easeInQuad($ratio - 1));
return seria_weighted_colour_hex(0xFFAA00, 0xFF0000, seria_easeOutQuad($ratio));
}
function seria_size_colour(int $bytes): string {
if($bytes >= 53687090000)
return '#ec32a4';
if($bytes >= 21474840000)
return '#db5ff1';
if($bytes >= 10737420000)
return '#cca1f4';
if($bytes >= 5368709000)
return '#cdd';
if($bytes >= 1073742000)
return '#bae9c7';
return '#a0f5b8';
}

51
src/announce.php Normal file
View File

@ -0,0 +1,51 @@
<?php
class SeriaAnnounceResponse implements BEncodeSerializable {
private int $interval;
private int $minInterval;
private ?SeriaTorrent $torrent;
private bool $includePeerId;
private bool $compactPeers;
private array $peers = [];
public function __construct(
int $interval = 0,
int $minInterval = 0,
SeriaTorrent $torrent = null,
bool $includePeerId = false,
bool $compactPeers = false
) {
$this->interval = $interval;
$this->minInterval = $minInterval;
$this->torrent = $torrent;
$this->includePeerId = $includePeerId;
$this->compactPeers = $compactPeers;
}
public function addPeer(SeriaTorrentPeer $peer): void {
if(!in_array($peer, $this->peers))
$this->peers[] = $peer;
}
public function bencodeSerialize(): mixed {
$data = [
'interval' => $this->interval,
'min interval' => $this->minInterval,
'complete' => $this->torrent?->getCompletePeers() ?? 0,
'incomplete' => $this->torrent?->getIncompletePeers() ?? 0,
];
if($this->compactPeers) {
$peers = '';
foreach($this->peers as $peer)
$peers .= $peer->getAddressRaw() . pack('n', $peer->getPort());
$data['peers'] = $peers;
} else {
$peers = [];
foreach($this->peers as $peer)
$peers[] = $peer->encodeInfo($this->includePeerId);
$data['peers'] = $peers;
}
return $data;
}
}

146
src/benben.php Normal file
View File

@ -0,0 +1,146 @@
<?php
interface BEncodeSerializable {
function bencodeSerialize(): mixed;
}
// todo: if $depth < 1 return unencoded, or just get mad i dunno yet
function bdecode(mixed $input, bool $dictAsObject = false, int $depth = 512): mixed {
if(is_string($input)) {
$file = fopen('php://temp', 'rb+');
fwrite($file, $input);
fseek($file, 0, SEEK_SET);
$output = bdecode($file, $dictAsObject, $depth);
fclose($file);
return $output;
} elseif(!is_resource($input))
return null;
$char = fgetc($input);
if($char === false)
die('bdecode: unexpected eof');
switch($char) {
case 'i':
$number = '';
for(;;) {
$char = fgetc($input);
if($char === false)
die('bdecode: integer unexpected eof');
if($char === 'e')
break;
if($char === '-' && $number !== '')
die('bdecode: integer unexpected minus');
if($char === '0' && $number === '-')
die('bdecode: integer negative zero');
if($char === '0' && $number === '0')
die('bdecode: integer double zero');
if(!ctype_digit($char))
die('bdecode: integer unexpected char');
$number .= $char;
}
return intval($number);
case 'l':
$list = [];
for(;;) {
$char = fgetc($input);
if($char === false)
die('bdecode: list unexpected eof');
if($char === 'e')
break;
fseek($input, -1, SEEK_CUR);
$list[] = bdecode($input, $dictAsObject, $depth - 1);
}
return $list;
case 'd':
$dict = [];
for(;;) {
$char = fgetc($input);
if($char === false)
die('bdecode: dict unexpected eof');
if($char === 'e')
break;
if(!ctype_digit($char))
die('bdecode: dict expecting string');
fseek($input, -1, SEEK_CUR);
$dict[bdecode($input)] = bdecode($input, $dictAsObject, $depth - 1);
}
if($dictAsObject)
$dict = (object)$dict;
return $dict;
default:
if(!ctype_digit($char))
die('bdecode: unexpected char');
$length = $char;
for(;;) {
$char = fgetc($input);
if($char === false)
die('bdecode: string unexpected eof');
if($char === ':')
break;
if($char === '0' && $length === '0')
die('bdecode: string double zero');
if(!ctype_digit($char))
die('bdecode: string unexpected char');
$length .= $char;
}
return fread($input, intval($length));
}
return null;
}
function bencode(mixed $input): string {
switch(gettype($input)) {
case 'string':
return sprintf('%d:%s', strlen($input), $input);
case 'integer':
return sprintf('i%de', $input);
case 'array':
if(array_is_list($input)) {
$output = 'l';
foreach($input as $item)
$output .= bencode($item);
} else {
$output = 'd';
foreach($input as $key => $value) {
$output .= bencode(strval($key));
$output .= bencode($value);
}
}
return $output . 'e';
case 'object':
if($input instanceof BEncodeSerializable)
return bencode($input->bencodeSerialize());
$input = get_object_vars($input);
$output = 'd';
foreach($input as $key => $value) {
$output .= bencode(strval($key));
$output .= bencode($value);
}
return $output . 'e';
default:
return '';
}
}

877
src/torrent.php Normal file
View File

@ -0,0 +1,877 @@
<?php
class SeriaTorrentNotFoundException extends RuntimeException {}
class SeriaTorrentFileNotFoundException extends RuntimeException {}
class SeriaTorrentPieceNotFoundException extends RuntimeException {}
class SeriaTorrentDuplicateFileException extends RuntimeException {}
class SeriaTorrentCreateFailedException extends RuntimeException {}
class SeriaTorrentUpdateFailedException extends RuntimeException {}
class SeriaTorrentNukeFailedException extends RuntimeException {}
class SeriaTorrentPeerUpdateFailedException extends RuntimeException {}
class SeriaTorrentPeerDeleteFailedException extends RuntimeException {}
class SeriaTorrentPeerFetchFailedException extends RuntimeException {}
class SeriaTorrentPeerCreateFailedException extends RuntimeException {}
class SeriaTorrentBuilder {
private ?string $userId = null;
private string $name = '';
private int $created;
private int $pieceLength = 0;
private bool $isPrivate = false;
private string $comment = '';
private array $files = [];
private array $pieces = [];
public function __construct() {
$this->created = time();
}
public function setUser(SeriaUser $user): self {
return $this->setUserId($user->isLoggedIn() ? $user->getId() : null);
}
public function setUserId(?string $userId): self {
$this->userId = $userId;
return $this;
}
public function setName(string $name): self {
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
$this->name = $name;
return $this;
}
public function setCreatedTime(int $created): self {
if($created < 0 && $created > 0x7FFFFFFF)
throw new InvalidArgumentException('$created is not a valid timestamp.');
$this->created = $created;
return $this;
}
public function setPieceLength(int $pieceLength): self {
if($pieceLength < 1)
throw new InvalidArgumentException('$pieceLength is not a valid piece length.');
$this->pieceLength = $pieceLength;
return $this;
}
public function setPrivate(bool $private): self {
$this->isPrivate = $private;
return $this;
}
public function setComment(string $comment): self {
$this->comment = $comment;
return $this;
}
public function addFile(string|array $path, int $length): self {
if(is_array($path))
$path = implode('/', $path);
$path = trim($path, '/');
if(array_key_exists($path, $this->files))
throw new SeriaTorrentDuplicateFileException('Duplicate file.');
$this->files[$path] = [
'length' => $length,
'path' => explode('/', $path),
];
return $this;
}
public function addPiece(string $hash): self {
if(strlen($hash) !== 20)
throw new InvalidArgumentException('$hash is not a valid piece hash.');
$this->pieces[] = $hash;
return $this;
}
public function calculateInfoHash(): string {
$info = [
'files' => array_values($this->files),
'name' => $this->name,
'piece length' => $this->pieceLength,
'pieces' => implode($this->pieces),
];
if(!empty($this->isPrivate))
$info['private'] = 1;
return hash('sha1', bencode($info), true);
}
public function create(PDO $pdo): SeriaTorrent {
$pdo->beginTransaction();
try {
$infoHash = $this->calculateInfoHash();
$insertTorrent = $pdo->prepare('INSERT INTO `ser_torrents` (`user_id`, `torrent_hash`, `torrent_name`, `torrent_created`, `torrent_piece_length`, `torrent_private`, `torrent_comment`) VALUES (:user, :info_hash, :name, FROM_UNIXTIME(:created), :piece_length, :private, :comment)');
$insertTorrentFile = $pdo->prepare('INSERT INTO `ser_torrents_files` (`torrent_id`, `file_length`, `file_path`) VALUES (:torrent, :length, :path)');
$insertTorrentPiece = $pdo->prepare('INSERT INTO `ser_torrents_pieces` (`torrent_id`, `piece_hash`) VALUES (:torrent, :hash)');
$insertTorrent->bindValue('user', $this->userId);
$insertTorrent->bindValue('info_hash', $infoHash);
$insertTorrent->bindValue('name', $this->name);
$insertTorrent->bindValue('created', $this->created);
$insertTorrent->bindValue('piece_length', $this->pieceLength);
$insertTorrent->bindValue('private', $this->isPrivate ? 1 : 0);
$insertTorrent->bindValue('comment', $this->comment);
if(!$insertTorrent->execute())
throw new SeriaTorrentCreateFailedException('Torrent insert query execution failed (duplicate?).');
$torrentId = $pdo->lastInsertId();
if($torrentId === false)
throw new SeriaTorrentCreateFailedException('Failed to grab torrent id.');
$insertTorrentFile->bindValue('torrent', $torrentId);
$insertTorrentPiece->bindValue('torrent', $torrentId);
foreach($this->files as $file) {
$insertTorrentFile->bindValue('length', $file['length']);
$insertTorrentFile->bindValue('path', implode('/', $file['path']));
if(!$insertTorrentFile->execute())
throw new SeriaTorrentCreateFailedException('Failed to insert torrent file.');
}
foreach($this->pieces as $piece) {
$insertTorrentPiece->bindValue('hash', $piece);
if(!$insertTorrentPiece->execute())
throw new SeriaTorrentCreateFailedException('Failed to insert torrent piece.');
}
$pdo->commit();
} catch(Exception $ex) {
$pdo->rollBack();
throw $ex;
}
return SeriaTorrent::byId($pdo, $torrentId);
}
public static function import(SeriaTorrent $torrent): self {
$builder = new static;
$builder->setUserId($torrent->getUserId());
$builder->setName($torrent->getName());
$builder->setPieceLength($torrent->getPieceLength());
$builder->setPrivate($torrent->isPrivate());
$builder->setCreatedTime($torrent->getCreatedTime());
$builder->setComment($torrent->getComment());
$pieces = $torrent->getPieces();
foreach($pieces as $piece)
$builder->addPiece($piece->getHash());
$files = $torrent->getFiles();
foreach($files as $file)
$builder->addFile($file->getPath(), $file->getLength());
return $builder;
}
public static function decode(mixed $source): self {
if(is_string($source) || is_resource($source))
$source = bdecode($source);
if(!isset($source['info']) || !is_array($source['info']))
throw new InvalidArgumentException('info key missing.');
if(!isset($source['info']['name']) || !is_string($source['info']['name']))
throw new InvalidArgumentException('info.name key missing.');
if(!isset($source['info']['files']) || !is_array($source['info']['files']))
throw new InvalidArgumentException('info.files key missing.');
if(!isset($source['info']['pieces']) || !is_string($source['info']['pieces']))
throw new InvalidArgumentException('info.pieces key missing.');
if(!isset($source['info']['piece length']) || !is_int($source['info']['piece length']))
throw new InvalidArgumentException('info.piece length key missing.');
$builder = new static;
$builder->setName($source['info']['name']);
$builder->setPieceLength($source['info']['piece length']);
$builder->setPrivate(!empty($source['info']['private']));
if(isset($source['creation date'])
&& is_int($source['creation date']))
$builder->setCreatedTime($source['creation date']);
if(!empty($source['comment']))
$builder->setComment($source['comment']);
foreach($source['info']['files'] as $file) {
if(empty($file)
|| !is_array($file)
|| !isset($file['length'])
|| !is_int($file['length'])
|| !isset($file['path'])
|| !is_array($file['path']))
throw new InvalidArgumentException('Invalid info.files entry.');
foreach($file['path'] as $pathPart)
if(!is_string($pathPart))
throw new InvalidArgumentException('Invalid info.files entry path.');
$builder->addFile($file['path'], $file['length']);
}
$pieces = str_split($source['info']['pieces'], 20);
foreach($pieces as $piece)
$builder->addPiece($piece);
return $builder;
}
}
class SeriaTorrent implements BEncodeSerializable {
private PDO $pdo;
private string $torrent_id;
private ?string $user_id;
private string $torrent_hash;
private int $torrent_active = 0;
private string $torrent_name;
private int $torrent_created;
private ?int $torrent_approved;
private int $torrent_piece_length;
private int $torrent_private;
private string $torrent_comment;
private int $peers_complete;
private int $peers_incomplete;
private int $torrent_size;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getId(): string {
return $this->torrent_id;
}
public function hasUser(): bool {
return $this->user_id !== null;
}
public function getUserId(): ?string {
return $this->user_id;
}
public function getHash(): string {
return $this->torrent_hash;
}
public function setHash(string $hash): self {
$this->torrent_hash = $hash;
return $this;
}
public function isActive(): bool {
return $this->torrent_active !== 0;
}
public function setActive(bool $active): self {
$this->torrent_active = $active ? 1 : 0;
return $this;
}
public function getName(): string {
return $this->torrent_name;
}
public function getCreatedTime(): int {
return $this->torrent_created;
}
public function getApprovedTime(): int {
return $this->torrent_approved ?? -1;
}
public function isApproved(): bool {
return $this->torrent_approved !== null;
}
public function approve(): void {
if(!$this->isApproved())
$this->torrent_approved = time();
}
public function getPieceLength(): int {
return $this->torrent_piece_length;
}
public function isPrivate(): bool {
return $this->torrent_private !== 0;
}
public function getComment(): string {
return $this->torrent_comment;
}
public function getFiles(): array {
return SeriaTorrentFile::byTorrent($this->pdo, $this);
}
public function getPieces(): array {
return SeriaTorrentPiece::byTorrent($this->pdo, $this);
}
public function getCompletePeers(): int {
return $this->peers_complete;
}
public function getIncompletePeers(): int {
return $this->peers_incomplete;
}
public function getSize(): int {
return $this->torrent_size;
}
public function toHTML(SeriaUser $user, string $class, bool $showSubmitter = true, ?string $verification = null): string {
$html = '<div class="tdl ' . $class . '" id="tdl' . $this->getId() . '">';
$html .= '<div class="tdl-details">';
$html .= '<div class="tdl-details-name"><a href="/info.php?id=' . $this->getId() . '">' . htmlspecialchars($this->getName()) . '</a></div>';
if($showSubmitter) {
try {
$submitter = SeriaUser::byId($this->pdo, $this->getUserId());
$html .= '<div class="tdl-user" style="--user-colour:' . (string)$submitter->getColour() . '">';
$html .= '<div class="avatar tdl-user-avatar"><a href="/profile.php?name=' . $submitter->getName() . '"><img src="' . sprintf(SERIA_AVATAR_FORMAT_RES, $submitter->getId(), 40) . '" alt="" width="20" height="20" /></a></div>';
$html .= '<div class="tdl-user-name"><a href="/profile.php?name=' . $submitter->getName() . '">' . $submitter->getName() . '</a></div>';
$html .= '</div>';
} catch(SeriaUserNotFoundException $ex) {}
}
$html .= '</div>';
$html .= '<div class="tdl-stats">';
$html .= '<div class="tdl-stats-uploading" title="Uploading"><div class="arrow">&#8593;</div><div class="number">' . number_format($this->getCompletePeers()) . '</div></div>';
$html .= '<div class="tdl-stats-downloading" title="Downloading"><div class="arrow">&#8595;</div><div class="number">' . number_format($this->getIncompletePeers()) . '</div></div>';
$html .= '</div>';
$html .= '<div class="tdl-actions">';
if(!$this->isApproved() && $user->canApproveTorrents() && $verification !== null) {
$html .= '<a href="/info.php?id=' . $this->getId() . '&amp;action=approve&amp;boob=' . $verification . '" title="Approve"><img src="//static.flash.moe/images/silk/tick.png" alt="Approve" /></a>';
$html .= '<a href="/info.php?id=' . $this->getId() . '&amp;action=deny&amp;boob=' . $verification . '" title="Deny"><img src="//static.flash.moe/images/silk/cross.png" alt="Deny" /></a>';
}
if($this->canDownload($user) === '') {
$html .= '<a href="/download.php?id=' . $this->getId() . '" title="Download"><img src="//static.flash.moe/images/silk/link.png" alt="Download" /></a>';
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
public function getPeers(?SeriaTorrentPeer $exclude = null): array {
return SeriaTorrentPeer::byTorrent($this->pdo, $this, $exclude);
}
public function getSeeds(?SeriaTorrentPeer $exclude = null): array {
return SeriaTorrentPeer::byTorrent($this->pdo, $this, $exclude, true);
}
public function getInfo(): array {
$info = [
'files' => [],
'name' => $this->getName(),
'piece length' => $this->getPieceLength(),
'pieces' => '',
];
if($this->isPrivate())
$info['private'] = 1;
$files = $this->getFiles();
foreach($files as $file)
$info['files'][] = $file;
$pieces = $this->getPieces();
foreach($pieces as $piece)
$info['pieces'] .= $piece->getHash();
return $info;
}
public function canDownload(SeriaUser $user): string {
if(!$this->isActive())
return 'inactive';
if($this->isPrivate() && !$user->isLoggedIn())
return 'private';
if(!$this->isApproved() && (!$user->canApproveTorrents() && $this->getUserId() !== $user->getId()))
return 'pending';
return '';
}
public function bencodeSerialize(): mixed {
return [
'announce' => SERIA_ANNOUNCE_URL_ANON,
'created by' => 'Seria v' . SERIA_VERSION,
'creation date' => $this->getCreatedTime(),
'info' => $this->getInfo(),
];
}
public function encode(string $announceUrl): string {
$data = $this->bencodeSerialize();
$data['announce'] = $announceUrl;
return bencode($data);
}
public function update(): void {
$updateTorrent = $this->pdo->prepare('UPDATE `ser_torrents` SET `torrent_hash` = :hash, `torrent_active` = :active, `torrent_approved` = FROM_UNIXTIME(:approved) WHERE `torrent_id` = :torrent');
$updateTorrent->bindValue('hash', $this->torrent_hash);
$updateTorrent->bindValue('active', $this->torrent_active ? 1 : 0);
$updateTorrent->bindValue('approved', $this->torrent_approved);
$updateTorrent->bindValue('torrent', $this->torrent_id);
if(!$updateTorrent->execute())
throw new SeriaTorrentUpdateFailedException;
}
public function nuke(): void {
$nukeTorrent = $this->pdo->prepare('DELETE FROM `ser_torrents` WHERE `torrent_id` = :torrent');
$nukeTorrent->bindValue('torrent', $this->torrent_id);
if(!$nukeTorrent->execute())
throw new SeriaTorrentNukeFailedException;
}
public static function byHash(PDO $pdo, string $infoHash): SeriaTorrent {
$getTorrent = $pdo->prepare('SELECT `torrent_id`, `user_id`, `torrent_hash`, `torrent_active`, `torrent_name`, UNIX_TIMESTAMP(`torrent_created`) AS `torrent_created`, UNIX_TIMESTAMP(`torrent_approved`) AS `torrent_approved`, `torrent_piece_length`, `torrent_private`, `torrent_comment`, (SELECT COUNT(*) FROM `ser_torrents_peers` AS tp WHERE tp.`torrent_id` = t.`torrent_id` AND `peer_left` = 0) AS `peers_complete`, (SELECT COUNT(*) FROM `ser_torrents_peers` AS tp WHERE tp.`torrent_id` = t.`torrent_id` AND `peer_left` > 0) AS `peers_incomplete`, (SELECT SUM(tf.`file_length`) FROM `ser_torrents_files` AS tf WHERE tf.`torrent_id` = t.`torrent_id`) AS `torrent_size` FROM `ser_torrents` AS t WHERE `torrent_hash` = :info_hash');
$getTorrent->bindValue('info_hash', $infoHash);
if(!$getTorrent->execute())
throw new SeriaTorrentNotFoundException('Failed to execute hash query.');
$obj = $getTorrent->fetchObject(self::class, [$pdo]);
if($obj === false)
throw new SeriaTorrentNotFoundException('Hash not found.');
return $obj;
}
public static function byId(PDO $pdo, string $torrentId): SeriaTorrent {
$getTorrent = $pdo->prepare('SELECT `torrent_id`, `user_id`, `torrent_hash`, `torrent_active`, `torrent_name`, UNIX_TIMESTAMP(`torrent_created`) AS `torrent_created`, UNIX_TIMESTAMP(`torrent_approved`) AS `torrent_approved`, `torrent_piece_length`, `torrent_private`, `torrent_comment`, (SELECT COUNT(*) FROM `ser_torrents_peers` AS tp WHERE tp.`torrent_id` = t.`torrent_id` AND `peer_left` = 0) AS `peers_complete`, (SELECT COUNT(*) FROM `ser_torrents_peers` AS tp WHERE tp.`torrent_id` = t.`torrent_id` AND `peer_left` > 0) AS `peers_incomplete`, (SELECT SUM(tf.`file_length`) FROM `ser_torrents_files` AS tf WHERE tf.`torrent_id` = t.`torrent_id`) AS `torrent_size` FROM `ser_torrents` AS t WHERE `torrent_id` = :torrent');
$getTorrent->bindValue('torrent', $torrentId);
if(!$getTorrent->execute())
throw new SeriaTorrentNotFoundException('Failed to execute id query.');
$obj = $getTorrent->fetchObject(self::class, [$pdo]);
if($obj === false)
throw new SeriaTorrentNotFoundException('Id not found.');
return $obj;
}
public static function byUser(PDO $pdo, SeriaUser $user, int $startAt = -1, int $take = -1): array {
return self::all($pdo, false, true, $user, $startAt, $take);
}
public static function all(
PDO $pdo,
bool $publicOnly = true,
?bool $approved = true,
?SeriaUser $user = null,
int $startAt = -1,
int $take = -1
): array {
$hasUser = $user !== null;
$hasApproved = $approved !== null;
$hasStartAt = $startAt >= 0;
$hasTake = $take > 0;
$query = 'SELECT `torrent_id`, `user_id`, `torrent_hash`, `torrent_active`, `torrent_name`, UNIX_TIMESTAMP(`torrent_created`) AS `torrent_created`, UNIX_TIMESTAMP(`torrent_approved`) AS `torrent_approved`, `torrent_piece_length`, `torrent_private`, `torrent_comment`, (SELECT COUNT(*) FROM `ser_torrents_peers` AS tp WHERE tp.`torrent_id` = t.`torrent_id` AND `peer_left` = 0) AS `peers_complete`, (SELECT COUNT(*) FROM `ser_torrents_peers` AS tp WHERE tp.`torrent_id` = t.`torrent_id` AND `peer_left` > 0) AS `peers_incomplete`, (SELECT SUM(tf.`file_length`) FROM `ser_torrents_files` AS tf WHERE tf.`torrent_id` = t.`torrent_id`) AS `torrent_size` FROM `ser_torrents` AS t WHERE `torrent_active` <> 0';
if($publicOnly)
$query .= ' AND `torrent_private` = 0';
if($hasUser)
$query .= ' AND `user_id` = :user';
if($hasApproved)
$query .= ' AND `torrent_approved` IS' . ($approved ? ' NOT ' : ' ') . 'NULL';
if($hasStartAt)
$query .= ' AND `torrent_id` < :start';
$query .= ' ORDER BY `torrent_id` DESC';
if($hasTake)
$query .= ' LIMIT :take';
$getTorrents = $pdo->prepare($query);
if($hasUser)
$getTorrents->bindValue('user', $user->getId());
if($hasStartAt)
$getTorrents->bindValue('start', $startAt);
if($hasTake)
$getTorrents->bindValue('take', $take);
if(!$getTorrents->execute())
throw new SeriaTorrentNotFoundException('Failed to execute user query.');
$objs = [];
while(($obj = $getTorrents->fetchObject(self::class, [$pdo])) !== false)
$objs[] = $obj;
return $objs;
}
}
class SeriaTorrentFile implements BEncodeSerializable {
private PDO $pdo;
private ?string $file_id = null;
private ?string $torrent_id = null;
private int $file_length;
private string $file_path;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getId(): ?string {
return $this->file_id;
}
public function getTorrentId(): ?string {
return $this->torrent_id;
}
public function getLength(): int {
return $this->file_length;
}
public function getPath(): string {
return $this->file_path;
}
public function bencodeSerialize(): mixed {
return [
'length' => $this->getLength(),
'path' => explode('/', $this->getPath()),
];
}
public static function byTorrent(PDO $pdo, SeriaTorrent $torrent): array {
$getFiles = $pdo->prepare('SELECT `file_id`, `torrent_id`, `file_length`, `file_path` FROM `ser_torrents_files` WHERE `torrent_id` = :torrent ORDER BY `file_id` ASC');
$getFiles->bindValue('torrent', $torrent->getId());
if(!$getFiles->execute())
throw new SeriaTorrentFileNotFoundException('Failed to fetch torrent files.');
$files = [];
while(($obj = $getFiles->fetchObject(self::class, [$pdo])) !== false)
$files[] = $obj;
return $files;
}
}
class SeriaTorrentPiece {
private PDO $pdo;
private ?string $piece_id = null;
private ?string $torrent_id = null;
private string $piece_hash;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getId(): ?string {
return $this->piece_id;
}
public function getTorrentId(): ?string {
return $this->torrent_id;
}
public function getHash(): string {
return $this->piece_hash;
}
public static function byTorrent(PDO $pdo, SeriaTorrent $torrent): array {
$getPieces = $pdo->prepare('SELECT `piece_id`, `torrent_id`, `piece_hash` FROM `ser_torrents_pieces` WHERE `torrent_id` = :torrent ORDER BY `piece_id` ASC');
$getPieces->bindValue('torrent', $torrent->getId());
if(!$getPieces->execute())
throw new SeriaTorrentPieceNotFoundException('Failed to fetch torrent pieces.');
$pieces = [];
while(($obj = $getPieces->fetchObject(self::class, [$pdo])) !== false)
$pieces[] = $obj;
return $pieces;
}
}
class SeriaTorrentPeer implements BEncodeSerializable {
private PDO $pdo;
private string $peer_id;
private string $torrent_id;
private ?string $user_id;
private string $peer_address;
private int $peer_port;
private int $peer_updated;
private int $peer_expires;
private string $peer_agent;
private string $peer_key;
private int $peer_uploaded;
private int $peer_downloaded;
private int $peer_left;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getId(): string {
return $this->peer_id;
}
public function getTorrentId(): string {
return $this->torrent_id;
}
public function getUserId(): ?string {
return $this->user_id;
}
public function hasUserId(): bool {
return !empty($this->user_id);
}
public function getUser(): SeriaUser {
if($this->user_id === null)
return SeriaUser::anonymous();
return SeriaUser::byId($this->pdo, $this->user_id);
}
public function getAddress(): string {
return $this->peer_address;
}
public function verifyUser(SeriaUser $user): bool {
return !$this->hasUserId()
|| ($user->isLoggedIn() && $user->getId() === $this->user_id);
}
public function getAddressRaw(): string {
return inet_pton($this->peer_address);
}
public function getPort(): int {
return $this->peer_port;
}
public function getUpdatedTime(): int {
return $this->peer_updated;
}
public function getExpiresTime(): int {
return $this->peer_expires;
}
public function getAgent(): string {
return $this->peer_agent;
}
public function getKey(): string {
return $this->peer_key;
}
public function getBytesUploaded(): int {
return $this->peer_uploaded;
}
public function getBytesDownloaded(): int {
return $this->peer_downloaded;
}
public function getBytesRemaining(): int {
return $this->peer_left;
}
public function isSeed(): bool {
return $this->peer_left === 0;
}
public function isLeech(): bool {
return $this->peer_left > 0;
}
public function verifyKey(string $key): bool {
return hash_equals($this->getKey(), $key);
}
public function update(
SeriaUser $user,
string $remoteAddr,
int $remotePort,
int $interval,
string $peerAgent,
int $bytesUploaded,
int $bytesDownloaded,
int $bytesRemaining
): void {
$updatePeer = $this->pdo->prepare('UPDATE `ser_torrents_peers` SET `user_id` = :user, `peer_address` = INET6_ATON(:address), `peer_port` = :port, `peer_updated` = NOW(), `peer_expires` = NOW() + INTERVAL :interval SECOND, `peer_agent` = :agent, `peer_uploaded` = :uploaded, `peer_downloaded` = :downloaded, `peer_left` = :remaining WHERE `torrent_id` = :torrent AND `peer_id` = :peer');
$updatePeer->bindValue('torrent', $this->getTorrentId());
$updatePeer->bindValue('user', $user->isLoggedIn() ? $user->getId() : null);
$updatePeer->bindValue('peer', $this->getId());
$updatePeer->bindValue('address', $this->peer_address = $remoteAddr);
$updatePeer->bindValue('port', $this->peer_port = ($remotePort & 0xFFFF));
$updatePeer->bindValue('interval', $interval);
$updatePeer->bindValue('agent', $this->peer_agent = $peerAgent);
$updatePeer->bindValue('uploaded', $this->peer_uploaded = $bytesUploaded);
$updatePeer->bindValue('downloaded', $this->peer_downloaded = $bytesDownloaded);
$updatePeer->bindValue('remaining', $this->peer_left = $bytesRemaining);
if(!$updatePeer->execute())
throw new SeriaTorrentPeerUpdateFailedException;
$this->peer_updated = time();
$this->peer_expires = $this->peer_updated + $interval;
}
public function delete(): void {
$deletePeer = $this->pdo->prepare('DELETE FROM `ser_torrents_peers` WHERE `torrent_id` = :torrent AND `peer_id` = :peer');
$deletePeer->bindValue('torrent', $this->getTorrentId());
$deletePeer->bindValue('peer', $this->getId());
if(!$deletePeer->execute())
throw new SeriaTorrentPeerDeleteFailedException;
}
public function encodeInfo(bool $includeId): mixed {
$info = [
'ip' => $this->getAddress(),
'port' => $this->getPort(),
];
if($includeId)
$info['peer id'] = $this->getId();
return $info;
}
public function encode(bool $includeId): string {
return bencode($this->encodeInfo($includeId));
}
public function bencodeSerialize(): mixed {
return $this->encodeInfo(false);
}
public static function countUserStats(PDO $pdo, SeriaUser $user): stdClass {
$countActive = $pdo->prepare('SELECT :user AS `user`, (SELECT COUNT(*) FROM `ser_torrents_peers` WHERE `user_id` = `user` AND `peer_left` <> 0) AS `user_downloading`, (SELECT COUNT(*) FROM `ser_torrents_peers` WHERE `user_id` = `user` AND `peer_left` = 0) AS `user_uploading`');
$countActive->bindValue('user', $user->getId());
$countActive->execute();
$counts = $countActive->fetchObject();
if($counts === false)
$counts = (object)[
'user_downloading' => 0,
'user_uploading' => 0,
];
else
unset($counts->user);
return $counts;
}
public static function byTorrent(
PDO $pdo,
SeriaTorrent $torrent,
?SeriaTorrentPeer $exclude = null,
?bool $complete = null
): array {
$hasExclude = $exclude !== null;
$hasComplete = $complete !== null;
$query = 'SELECT `peer_id`, `torrent_id`, `user_id`, INET6_NTOA(`peer_address`) AS `peer_address`, `peer_port`, UNIX_TIMESTAMP(`peer_updated`) AS `peer_updated`, UNIX_TIMESTAMP(`peer_expires`) AS `peer_expires`, `peer_agent`, `peer_key`, `peer_uploaded`, `peer_downloaded`, `peer_left` FROM `ser_torrents_peers` WHERE `torrent_id` = :torrent';
if($hasExclude)
$query .= ' AND `peer_id` <> :peer';
if($hasComplete)
$query .= ' AND `peer_left` ' . ($complete ? '=' : '<>') . ' 0';
$getPeers = $pdo->prepare($query);
$getPeers->bindValue('torrent', $torrent->getId());
if($hasExclude)
$getPeers->bindValue('peer', $exclude->getId());
if(!$getPeers->execute())
throw new SeriaTorrentPeerFetchFailedException('Failed to fetch peers by torrent.');
$objs = [];
while(($obj = $getPeers->fetchObject(self::class, [$pdo])) !== false)
$objs[] = $obj;
return $objs;
}
public static function byPeerId(PDO $pdo, SeriaTorrent $torrent, string $peerId): ?SeriaTorrentPeer {
$getPeer = $pdo->prepare('SELECT `peer_id`, `torrent_id`, `user_id`, INET6_NTOA(`peer_address`) AS `peer_address`, `peer_port`, UNIX_TIMESTAMP(`peer_updated`) AS `peer_updated`, UNIX_TIMESTAMP(`peer_expires`) AS `peer_expires`, `peer_agent`, `peer_key`, `peer_uploaded`, `peer_downloaded`, `peer_left` FROM `ser_torrents_peers` WHERE `torrent_id` = :torrent AND `peer_id` = :peer');
$getPeer->bindValue('torrent', $torrent->getId());
$getPeer->bindValue('peer', $peerId);
if(!$getPeer->execute())
throw new SeriaTorrentPeerFetchFailedException('Failed to fetch peers by peer.');
return ($obj = $getPeer->fetchObject(self::class, [$pdo])) ? $obj : null;
}
public static function create(
PDO $pdo,
SeriaTorrent $torrent,
SeriaUser $user,
string $peerId,
string $remoteAddr,
int $remotePort,
int $interval,
string $peerAgent,
string $peerKey,
int $bytesUploaded,
int $bytesDownloaded,
int $bytesRemaining
): SeriaTorrentPeer {
$insertPeer = $pdo->prepare('INSERT INTO `ser_torrents_peers` (`peer_id`, `torrent_id`, `user_id`, `peer_address`, `peer_port`, `peer_updated`, `peer_expires`, `peer_agent`, `peer_key`, `peer_uploaded`, `peer_downloaded`, `peer_left`) VALUES (:id, :torrent, :user, INET6_ATON(:address), :port, NOW(), NOW() + INTERVAL :interval SECOND, :agent, :key, :uploaded, :downloaded, :remaining)');
$insertPeer->bindValue('id', $peerId);
$insertPeer->bindValue('torrent', $torrent->getId());
$insertPeer->bindValue('user', $user->isLoggedIn() ? $user->getId() : null);
$insertPeer->bindValue('address', $remoteAddr);
$insertPeer->bindValue('port', $remotePort & 0xFFFF);
$insertPeer->bindValue('interval', $interval);
$insertPeer->bindValue('agent', $peerAgent);
$insertPeer->bindValue('key', $peerKey);
$insertPeer->bindValue('uploaded', $bytesUploaded);
$insertPeer->bindValue('downloaded', $bytesDownloaded);
$insertPeer->bindValue('remaining', $bytesRemaining);
if(!$insertPeer->execute())
throw new SeriaTorrentPeerCreateFailedException('Query failed.');
$peer = self::byPeerId($pdo, $torrent, $peerId);
if($peer === null)
throw new SeriaTorrentPeerCreateFailedException('Fetch failed.');
return $peer;
}
public static function deleteExpired(PDO $pdo): void {
$pdo->exec('DELETE FROM `ser_torrents_peers` WHERE `peer_expires` < NOW()');
}
}

217
src/user.php Normal file
View File

@ -0,0 +1,217 @@
<?php
class SeriaUserNotFoundException extends RuntimeException {}
class SeriaUserExtremelyNotFoundException extends SeriaUserNotFoundException {}
class SeriaUserColour {
public const INHERIT = 0x40000000;
private int $raw;
public function __construct(int $raw) {
$this->raw = $raw;
}
public function __toString(): string {
if($this->raw & self::INHERIT)
return 'inherit';
return '#' . str_pad(dechex($this->raw & 0xFFFFFF), 6, '0', STR_PAD_LEFT);
}
}
class SeriaUser {
private const KEY_CHARS = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789';
private const KEY_LENGTH = 48;
private ?PDO $pdo;
private string $user_id = '';
private string $user_name = 'Anonymous';
private int $user_colour = SeriaUserColour::INHERIT;
private int $user_rank = 0;
private int $user_permissions = 0;
private ?string $user_pass_key = null;
private int $user_bytes_downloaded = 0;
private int $user_bytes_uploaded = 0;
public function __construct(?PDO $pdo = null) {
$this->pdo = $pdo;
}
public function getId(): string {
return $this->user_id;
}
public function getName(): string {
return $this->user_name;
}
public function getColour(): SeriaUserColour {
return new SeriaUserColour($this->user_colour);
}
public function getColourRaw(): int {
return $this->user_colour;
}
public function getRank(): int {
return $this->user_rank;
}
public function getPermissionsRaw(): int {
return $this->user_permissions;
}
public function getPassKey(): string {
if(!$this->isLoggedIn())
return '';
if(($passKey = $this->user_pass_key) === null)
return $this->resetPassKey();
return $passKey;
}
public function getBytesDownloaded(): int {
return $this->user_bytes_downloaded;
}
public function getBytesUploaded(): int {
return $this->user_bytes_uploaded;
}
public function isLoggedIn() {
return !empty($this->user_id);
}
public function isFlash(): bool {
return $this->user_id === '1';
}
public function canCreateTorrents(): bool {
return $this->isFlash()
|| $this->user_id === '32'
|| $this->user_id === '145';
}
public function canApproveTorrents(): bool {
return $this->isFlash();
}
public function canRecalculateInfoHash(): bool {
return $this->isFlash();
}
public function calculateRatio(): float {
$bd = $this->getBytesDownloaded();
if($bd === 0)
return 0;
return $this->getBytesUploaded() / $bd;
}
public function getActiveTransferCounts(): stdClass {
return SeriaTorrentPeer::countUserStats($this->pdo, $this);
}
public function getProfileSubmissions(): array {
return $this->getSubmissions(-1, 3);
}
public function getSubmissions(int $startAt, int $take): array {
return SeriaTorrent::byUser($this->pdo, $this, $startAt, $take);
}
public static function generatePassKey(int $length): string {
$keyChars = strlen(self::KEY_CHARS) - 1;
$bytes = str_repeat("\0", $length);
for($i = 0; $i < $length; ++$i)
$bytes[$i] = self::KEY_CHARS[random_int(0, $keyChars)];
return $bytes;
}
public function resetPassKey(): string {
if(!$this->isLoggedIn())
return '';
$updatePassKey = $this->pdo->prepare('UPDATE `ser_users` SET `user_pass_key` = :pass WHERE `user_id` = :user');
$updatePassKey->bindValue('user', $this->user_id);
do {
$updatePassKey->bindValue('pass', $passKey = self::generatePassKey(self::KEY_LENGTH));
} while(!$updatePassKey->execute());
return $this->user_pass_key = $passKey;
}
public function incrementTransferCounts(int $bytesDownloaded, int $bytesUploaded): void {
if(!$this->isLoggedIn())
return;
$updateStats = $this->pdo->prepare('UPDATE `ser_users` SET `user_bytes_downloaded` = `user_bytes_downloaded` + :downloaded, `user_bytes_uploaded` = `user_bytes_uploaded` + :uploaded WHERE `user_id` = :user');
$updateStats->bindValue('downloaded', $bytesDownloaded);
$updateStats->bindValue('uploaded', $bytesUploaded);
$updateStats->bindValue('user', $this->user_id);
if($updateStats->execute()) {
$this->user_bytes_downloaded += $bytesDownloaded;
$this->user_bytes_uploaded += $bytesUploaded;
}
}
public static function anonymous(): self {
return new static;
}
public static function fromMisuzu(PDO $pdo, stdClass $info): self {
$update = $pdo->prepare('INSERT INTO `ser_users` (`user_id`, `user_name`, `user_colour`, `user_rank`, `user_permissions`) VALUES (:id, :name_1, :colour_1, :rank_1, :perms_1) ON DUPLICATE KEY UPDATE `user_name` = :name_2, `user_colour` = :colour_2, `user_rank` = :rank_2, `user_permissions` = :perms_2');
$update->bindValue('id', $info->user_id);
$update->bindValue('name_1', $info->username);
$update->bindValue('name_2', $info->username);
$update->bindValue('colour_1', $info->colour_raw);
$update->bindValue('colour_2', $info->colour_raw);
$update->bindValue('rank_1', $info->hierarchy);
$update->bindValue('rank_2', $info->hierarchy);
$update->bindValue('perms_1', $info->perms);
$update->bindValue('perms_2', $info->perms);
$update->execute();
return self::byId($pdo, $info->user_id);
}
public static function byId(PDO $pdo, string $userId): self {
$getUser = $pdo->prepare('SELECT `user_id`, `user_name`, `user_colour`, `user_rank`, `user_permissions`, `user_pass_key`, `user_bytes_downloaded`, `user_bytes_uploaded` FROM `ser_users` WHERE `user_id` = :user');
$getUser->bindValue('user', $userId);
if(!$getUser->execute())
throw new SeriaUserExtremelyNotFoundException;
if(($obj = $getUser->fetchObject(self::class, [$pdo])) === false)
throw new SeriaUserNotFoundException;
return $obj;
}
public static function byName(PDO $pdo, string $userName): self {
$getUser = $pdo->prepare('SELECT `user_id`, `user_name`, `user_colour`, `user_rank`, `user_permissions`, `user_pass_key`, `user_bytes_downloaded`, `user_bytes_uploaded` FROM `ser_users` WHERE `user_name` = :user');
$getUser->bindValue('user', $userName);
if(!$getUser->execute())
throw new SeriaUserExtremelyNotFoundException;
if(($obj = $getUser->fetchObject(self::class, [$pdo])) === false)
throw new SeriaUserNotFoundException;
return $obj;
}
public static function byPassKey(PDO $pdo, string $passKey): self {
$getUser = $pdo->prepare('SELECT `user_id`, `user_name`, `user_colour`, `user_rank`, `user_permissions`, `user_pass_key`, `user_bytes_downloaded`, `user_bytes_uploaded` FROM `ser_users` WHERE `user_pass_key` = :pass');
$getUser->bindValue('pass', $passKey);
if(!$getUser->execute())
throw new SeriaUserExtremelyNotFoundException;
if(($obj = $getUser->fetchObject(self::class, [$pdo])) === false)
throw new SeriaUserNotFoundException;
return $obj;
}
}