diff --git a/app/Bonus.php b/app/Bonus.php index 9b0682cc6..a48f5bfbe 100644 --- a/app/Bonus.php +++ b/app/Bonus.php @@ -2,6 +2,8 @@ namespace Gazelle; +use \Gazelle\Exception\BonusException; + class Bonus extends Base { const CACHE_ITEM = 'bonus_item'; const CACHE_OPEN_POOL = 'bonus_pool'; @@ -63,11 +65,16 @@ class Bonus extends Base { return $this->items(); } - public function getListForUser(int $userId) { - return array_map(function ($item) use ($userId) { - $item['Price'] = $this->getEffectivePrice($item['Label'], $userId); - return $item; - }, $this->items()); + public function getListForUser(User $user) { + $items = []; + foreach ($this->items as $item) { + if ($item['Label'] === 'seedbox' && $user->hasAttr('feature-seedbox')) { + continue; + } + $item['Price'] = $this->getEffectivePrice($item['Label'], $user->id()); + $items[] = $item; + } + return $items; } public function getItem($label) { @@ -222,7 +229,7 @@ class Bonus extends Base { $item = $this->items()['invite']; $price = $item['Price']; if (!\Users::canPurchaseInvite($userId, $item['MinClass'])) { - throw new \Exception('Bonus:invite:minclass'); + throw new BonusException('invite:minclass'); } $this->db->prepared_query(' UPDATE user_bonus ub @@ -234,7 +241,7 @@ class Bonus extends Base { ', $price, $price, $userId ); if ($this->db->affected_rows() != 2) { - throw new \Exception('Bonus:invite:nofunds'); + throw new BonusException('invite:nofunds'); } $this->addPurchaseHistory($item['ID'], $userId, $price); $this->flushUserCache($userId); @@ -245,18 +252,18 @@ class Bonus extends Base { $item = $this->items()[$label]; $title = $label === 'title-bb-y' ? \Text::full_format($title) : \Text::strip_bbcode($title); if (mb_strlen($title) > 1024) { - throw new \Exception('Bonus:title:too-long'); + throw new BonusException('title:too-long'); } $price = $this->getEffectivePrice($label, $userId); /* if the price is 0, nothing changes so avoid hitting the db */ if ($price > 0) { if (!$this->removePoints($userId, $price)) { - throw new \Exception('Bonus:title:nofunds'); + throw new BonusException('title:nofunds'); } } if (!\Users::setCustomTitle($userId, $title)) { - throw new \Exception('Bonus:title:set'); + throw new BonusException('title:set'); return false; } $this->addPurchaseHistory($item['ID'], $userId, $price); @@ -267,10 +274,6 @@ class Bonus extends Base { public function purchaseCollage($userId, $label) { $item = $this->items()[$label]; $price = $this->getEffectivePrice($label, $userId); - - if (!\Users::canPurchaseInvite($userId, $item['MinClass'])) { - throw new \Exception('Bonus:invite:minclass'); - } $this->db->prepared_query(' UPDATE user_bonus ub INNER JOIN users_info ui ON (ui.UserID = ub.user_id) SET @@ -281,17 +284,50 @@ class Bonus extends Base { ', $price, $price, $userId ); if ($this->db->affected_rows() != 2) { - throw new \Exception('Bonus:collage:nofunds'); + throw new BonusException('collage:nofunds'); } $this->addPurchaseHistory($item['ID'], $userId, $price); $this->flushUserCache($userId); return true; } + public function unlockSeedbox(int $userId) { + $item = $this->items['seedbox']; + $price = $this->getEffectivePrice('seedbox', $userId); + $this->db->begin_transaction(); + $this->db->prepared_query(' + UPDATE user_bonus ub SET + ub.points = ub.points - ? + WHERE ub.points >= ? + AND ub.user_id = ? + ', $price, $price, $userId + ); + if ($this->db->affected_rows() != 1) { + $this->db->rollback(); + throw new BonusException('seedbox:nofunds'); + } + try { + $this->db->prepared_query(" + INSERT INTO user_has_attr + (UserID, UserAttrID) + VALUES (?, (SELECT ID FROM user_attr WHERE Name = ?)) + ", $userId, 'feature-seedbox' + ); + } catch (\DB_MYSQL_DuplicateKeyException $e) { + // no point in buying a second time + $this->db->rollback(); + throw new BonusException('seedbox:already-purchased'); + } + $this->db->commit(); + $this->addPurchaseHistory($item['ID'], $userId, $price); + $this->flushUserCache($userId); + return true; + } + public function purchaseToken($userId, $label) { $item = $this->items()[$label]; if (!$item) { - throw new \Exception('Bonus:selfToken:badlabel'); + throw new BonusException('selfToken:badlabel'); } $amount = $item['Amount']; $price = $item['Price']; @@ -305,7 +341,7 @@ class Bonus extends Base { ', $price, $amount, $userId, $price ); if ($this->db->affected_rows() != 2) { - throw new \Exception('Bonus:selfToken:funds'); + throw new BonusException('selfToken:funds'); } $this->addPurchaseHistory($item['ID'], $userId, $price); $this->flushUserCache($userId); @@ -314,11 +350,11 @@ class Bonus extends Base { public function purchaseTokenOther($fromID, $toID, $label) { if ($fromID === $toID) { - throw new \Exception('Bonus:otherToken:self'); + throw new BonusException('otherToken:self'); } $item = $this->items()[$label]; if (!$item) { - throw new \Exception('Bonus:otherToken:badlabel'); + throw new BonusException('otherToken:badlabel'); } $amount = $item['Amount']; $price = $item['Price']; @@ -346,7 +382,7 @@ class Bonus extends Base { ", $price, $amount, $toID, $fromID, $price ); if ($this->db->affected_rows() != 2) { - throw new \Exception('Bonus:otherToken:no-gift-funds'); + throw new BonusException('otherToken:no-gift-funds'); } $this->addPurchaseHistory($item['ID'], $fromID, $price, $toID); $this->cache->deleteMulti([ diff --git a/app/Exception/BonusException.php b/app/Exception/BonusException.php new file mode 100644 index 000000000..fc94b58a3 --- /dev/null +++ b/app/Exception/BonusException.php @@ -0,0 +1,5 @@ +setGroupId(1234)->setTorrentId(1666); + // if only the torrentId is set, it will discover the groupId + $torMan->setTorrentId(1666); + // the artist name (A, A & B, Various Artists, Various Composers under Various Conductors etc echo $torMan->artistHtml(); @@ -123,10 +126,13 @@ class Torrent extends \Gazelle\Base { return $nameCache[$this->torrentId]; } + if (!$this->groupId) { + $this->groupId = $this->idToGroupId($this->torrentId); + } if (!$this->torrentId && !$this->groupId) { return $nameCache[$this->torrentId] = ''; } - $key = 'seedbox_groups_artists_' . $this->groupId; + $key = 'short_groups_artists_' . $this->groupId; $roleList = $this->cache->get_value($key); if ($roleList === false) { $this->db->prepared_query(" diff --git a/app/Manager/TorrentLabel.php b/app/Manager/TorrentLabel.php index 59e66dd01..688271704 100644 --- a/app/Manager/TorrentLabel.php +++ b/app/Manager/TorrentLabel.php @@ -118,7 +118,7 @@ class TorrentLabel { $label[] = $this->info['Encoding']; } if (isset($this->info['Media']) && $this->info['Media'] === 'CD') { - if (isset($this->info['HasLog'])) { + if (isset($this->info['HasLog']) && $this->info['HasLog']) { $label[] = 'Log' . (($this->info['HasLogDB'] ?? false) ? " ({$this->info['LogScore']}%)" : ''); } if (isset($this->info['HasCue']) && $this->info['HasCue']) { diff --git a/app/Manager/User.php b/app/Manager/User.php index da1a2aa39..80b3f7928 100644 --- a/app/Manager/User.php +++ b/app/Manager/User.php @@ -3,6 +3,22 @@ namespace Gazelle\Manager; class User extends \Gazelle\Base { + /** + * Get a User object based on their ID + * + * @param int userId + * @return \Gazelle\User object or null if not found + */ + public function findById(int $userId): ?\Gazelle\User { + $userId = (int)$this->db->scalar(" + SELECT ID + FROM users_main + WHERE ID = ? + ", $userId + ); + return $userId ? new \Gazelle\User($userId) : null; + } + /** * Get a User object based on their username * diff --git a/app/Seedbox.php b/app/Seedbox.php new file mode 100644 index 000000000..b2c354ff4 --- /dev/null +++ b/app/Seedbox.php @@ -0,0 +1,290 @@ +userId = $userId; + $this->hashid = new \Hashids\Hashids(SEEDBOX_SALT); + $this->build(); + } + + public function setUnion(bool $isUnion) { + $this->isUnion = $isUnion; + return $this; + } + + public function setSource(string $source) { + $this->source = $this->hashid->decode($source)[0]; + return $this; + } + + public function setTarget(string $target) { + $this->target = $this->hashid->decode($target)[0]; + return $this; + } + + public function hostList() { return $this->host; } + public function freeList() { return $this->free; } + + public function name(string $id): ?string { + return $this->db->scalar(" + SELECT name + FROM user_seedbox + WHERE user_id = ? + AND user_seedbox_id = ? + ", $this->userId, $this->hashid->decode($id)[0] + ); + } + + /** + * Generate a signature of the useragent and IP address. + * + * The useragent and ip address are posted to a client-side form + * When reading the response to match useragent and IP address to a name, + * we need to ensure the information was not altered. + * + * @param string ipv4addr + * @param string useragent + * @return string base64 SHA2556 digest + */ + public function signature(string $ipv4addr, string $useragent): string { + return base64_encode(hash('sha256', implode('/', [$ipv4addr, $useragent, SEEDBOX_SALT]), true)); + } + + /** + * Get a page of torrents. Source and target must be set. + * + * @param Gazelle\Util\Paginator to help figure out where we are + * @param Gazelle\Manager\Torrent to display to release details + * @param Gazelle\Manager\TorrentLAbel to decorate the release details + * @return array list of torrent IDs + */ + public function torrentList(Util\Paginator $paginator, Manager\Torrent $torMan, Manager\TorrentLabel $labelMan): array { + $key = implode('_', [ + 'seedbox', + $this->userId, + $this->isUnion ? 'I' : 'E', + $this->source, + $this->target + ]); + if (($info = $this->cache->get_value($key)) === false) { + if ($this->isUnion) { + $has = 'IN'; + } else { + $has = 'NOT IN'; + } + $from = " + FROM user_seedbox sx + INNER JOIN xbt_files_users xfu ON (xfu.ip = inet_ntoa(sx.ipaddr) AND xfu.useragent = sx.useragent AND xfu.uid = ?) + INNER JOIN torrents t ON (t.ID = xfu.fid) + INNER JOIN torrents_group tg ON (tg.ID = t.GroupID) + WHERE sx.user_seedbox_id = ? + AND xfu.fid $has ( + SELECT xfu2.fid + FROM user_seedbox sx2 + INNER JOIN xbt_files_users xfu2 ON (xfu2.ip = inet_ntoa(sx2.ipaddr) AND xfu2.useragent = sx2.useragent AND xfu2.uid = ?) + WHERE sx2.user_seedbox_id = ? + ) + "; + $paginator->setTotal( + $x = $this->db->scalar(" + SELECT count(*) $from + ", $this->userId, $this->source, $this->userId, $this->target + ) + ); + $this->db->prepared_query(" + SELECT xfu.fid, tg.Name, tg.Year, tg.RecordLabel, + t.GroupID, tg.Name, t.RemasterTitle, t.RemasterRecordLabel, + t.Format, t.Encoding, t.Media, t.HasLog, t.HasLogDB, t.LogScore, t.HasCue, t.Scene + $from + ORDER BY tg.Name + LIMIT ? OFFSET ? + ", $this->userId, $this->source, $this->userId, $this->target, + $paginator->limit(), $paginator->offset() + ); + $info = $this->db->to_array('fid', MYSQLI_ASSOC, false); + $this->cache->cache_value($key, $info, 3); + } + + $labelMan->showFlags(false)->showEdition(true); + $list = []; + foreach ($info as $tid => $details) { + $labelMan->load($details); + $list[] = [ + 'id' => $tid, + 'artist' => $torMan + ->setGroupId($details['GroupID']) + ->setTorrentId($tid) + ->artistHtml(), + 'name' => sprintf('%s (%s) [%s]', + $details['GroupID'], + $tid, + $details['Name'], + $labelMan->edition(), + $labelMan->label() + ), + 'sortname' => $details['Name'], + ]; + } + usort($list, function ($x, $y) { + return $x['sortname'] == $y['sortname'] + ? $x['id'] <=> $y['id'] + : $x['sortname'] > $y['sortname']; + }); + return $list; + } + + /** + * Update the names of the various seeding locations + * + * @param array $update Associate array of 'ipv4' => 'name' + * @param int Number of rows changed. + */ + public function updateNames(array $update) { + $n = 0; + $hostlist = $this->hostList(); + foreach ($update as $seedbox) { + $name = $seedbox['name']; + if ($name == '') { + $this->db->scalar(" + DELETE FROM user_seedbox + WHERE user_id = ? + AND user_seedbox_id = ? + ", $this->userId, $this->hashid->decode($seedbox['id'])[0] + ); + $n += $this->db->affected_rows(); + } else { + try { + $this->db->prepared_query($sql = " + UPDATE user_seedbox SET + name = ? + WHERE user_id = ? + AND user_seedbox_id = ? + ", $name, $this->userId, $this->hashid->decode($seedbox['id'])[0] + ); + } catch (\DB_MYSQL_DuplicateKeyException $e) { + // do nothing + } finally { + $n += $this->db->affected_rows(); + } + } + } + if ($n) { + $this->flushCache()->build(); + } + return $n; + } + + /** + * Remove the names pointed to by a list of ids owned by the user + * + * @param array $remove Associate array of 'ipv4' => 'name' + * @return int Number of rows removed + */ + public function removeNames(array $remove): int { + if (empty ($remove)) { + return 0; + } + $h = $this->hashid; + $this->db->prepared_query(" + DELETE FROM user_seedbox + WHERE user_id = ? + AND user_seedbox_id in (" . placeholders($remove) . ") + ", $this->userId, ...array_map(function ($id) use ($h) {return $h->decode($id);}, $remove) + ); + $n = $this->db->affected_rows(); + $this->flushCache()->build(); + return $n; + } + + protected function flushCache() { + $this->cache->delete_value(self::SUMMARY_KEY . $this->userId); + return $this; + } + + protected function build() { + $key = self::SUMMARY_KEY . $this->userId; + // get the seeding locations and their totals + if (($client = $this->cache->get_value($key)) === false) { + $this->db->prepared_query(" + SELECT concat(IP, '/', useragent) as client, + useragent, + IP as ipv4addr, + count(*) as total + FROM xbt_files_users + WHERE uid = ? + GROUP BY IP, useragent + ", $this->userId + ); + $client = $this->db->to_array('client', MYSQLI_ASSOC); + $this->cache->cache_value($key, $client, 3600); + } + // get the names the user has saved (no need to cache) + $this->db->prepared_query(" + SELECT user_seedbox_id as id, + concat(inet_ntoa(ipaddr), '/', useragent) AS client, + inet_ntoa(ipaddr) AS ipv4addr, + useragent, + name + FROM user_seedbox + WHERE user_id = ? + ", $this->userId + ); + $h = $this->hashid; + $nameList = array_map(function ($a) use ($h) { + $b = $a; $b['id'] = $h->encode($b['id']); return $b; + }, $this->db->to_array('client', MYSQLI_ASSOC)); + + // go through all the peers and use a name if we have one, + // otherwise fallback to ip/useragent as a name. + $this->host = []; + foreach ($client as $clientId => $seedbox) { + $seedbox['sig'] = $this->signature($seedbox['ipv4addr'], $seedbox['useragent']); + if (!isset($nameList[$clientId])) { + $seedbox['name'] = $seedbox['ipv4addr'] . '::' . $seedbox['useragent']; + $this->db->prepared_query(" + INSERT INTO user_seedbox + (user_id, name, useragent, ipaddr) + VALUES (?, ?, ?, inet_aton(?)) + ", $this->userId, $seedbox['name'], $seedbox['useragent'], $seedbox['ipv4addr'] + ); + $seedbox['id'] = $this->hashid->encode($this->db->inserted_id()); + } else { + $seedbox['name'] = $nameList[$clientId]['name']; + $seedbox['id'] = $nameList[$clientId]['id']; + unset($nameList[$clientId]); // name in use + } + $this->host[$clientId] = $seedbox; + } + // TODO: sort $this->host by name + + // any names that didn't match a peer id may be ready for deletion (or merely off-line) + $this->free = $nameList; + } +} diff --git a/app/User.php b/app/User.php index 7d46b8d25..9bdd1b645 100644 --- a/app/User.php +++ b/app/User.php @@ -24,6 +24,9 @@ class User extends BaseObject { /** @var array contents of \Users::user_info */ protected $light; + /** @var array user attributes set for user */ + protected $attr; + const DISCOGS_API_URL = 'https://api.discogs.com/artists/%d'; public function tableName(): string { @@ -103,6 +106,25 @@ class User extends BaseObject { return $this->info; } + public function attr(): array { + if (is_null($this->attr)) { + $this->db->prepared_query(" + SELECT ua.Name, ua.ID + FROM user_attr ua + INNER JOIN user_has_attr uha ON (uha.UserAttrID = ua.ID) + WHERE uha.UserID = ? + ", $this->id + ); + $this->attr = $this->db->to_pair('Name', 'ID'); + } + return $this->attr; + } + + public function hasAttr(string $name): ?int { + $attr = $this->attr(); + return isset($attr[$name]) ? $attr[$name] : null; + } + protected function light() { if (!$this->light) { $this->light = \Users::user_info($this->id); diff --git a/app/Util/Paginator.php b/app/Util/Paginator.php index 58f5e9a65..e72b59f23 100644 --- a/app/Util/Paginator.php +++ b/app/Util/Paginator.php @@ -5,6 +5,9 @@ namespace Gazelle\Util; class Paginator { protected $perPage = 25; protected $page = 1; + protected $total = 0; + protected $linkbox = null; + protected $linkCount = 10; /** * Calculat offset and limit for SQL pagination, @@ -29,4 +32,91 @@ class Paginator { public function offset(): int { return $this->perPage * ($this->page - 1); } + + public function total(): int { + return $this->total; + } + + public function setTotal(int $total) { + $this->total = $total; + return $this; + } + + public function linkbox(): string { + if (!is_null($this->linkbox)) { + return $this->linkbox; + } + $pageCount = 0; + $this->linkbox = ''; + + $uri = str_replace('&', '&', preg_replace('/([?&])page=\d+/', '$1', $_SERVER['REQUEST_URI'])); + if (strpos($uri, '?') === false) { + $uri .= '?'; + } + + if ($this->total > 0) { + $this->page = min($this->page, (int)ceil($this->total / $this->perPage)); + $pageCount = (int)ceil($this->total / $this->perPage); + + if ($pageCount <= $this->linkCount) { + $firstPage = 1; + $lastPage = $pageCount; + } else { + $firstPage = $this->page - (int)round($this->linkCount / 2); + if ($firstPage <= 0) { + $firstPage = 1; + } else { + if ($firstPage >= $pageCount - $this->linkCount) { + $firstPage = max(1, $pageCount - $this->linkCount); + } + } + $lastPage = $this->linkCount + $firstPage; + } + if ($firstPage === $lastPage) { + $this->linkbox = ''; + return $this->linkbox; + } + + if ($this->page > 1) { + $this->linkbox = "« First " + . "page - 1) . '" class="pager_prev">‹ Prev | '; + } + + for ($i = $firstPage; $i <= $lastPage; $i++) { + if ($i != $this->page) { + $this->linkbox .= ""; + } + $this->linkbox .= ''; + $firstEntry = (($i - 1) * $this->perPage) + 1; + if ($i * $this->perPage > $this->total) { + if ($firstEntry == $this->total) { + $this->linkbox .= $this->total; + } else { + $this->linkbox .= "$firstEntry-" . $this->total; + } + } else { + $this->linkbox .= "$firstEntry-" . ($i * $this->perPage); + } + $this->linkbox .= ''; + + if ($i != $this->page) { + $this->linkbox .= ''; + } + if ($i < $lastPage) { + $this->linkbox .= ' | '; + } + $this->linkCount--; + } + + if ($this->page && $this->page < $pageCount) { + $this->linkbox .= " | page + 1) + . '" class="pager_next">Next ›' + . " Last »"; + } + } + if (strlen($this->linkbox)) { + $this->linkbox = '
= $e->getMessage() ?>
= str_replace(SERVER_ROOT .'/', '', $e->getTraceAsString()) ?>table('user_seedbox', ['id' => false, 'primary_key' => 'user_seedbox_id']) + ->addColumn('user_seedbox_id', 'integer', ['limit' => 10, 'signed' => false, 'identity' => true]) + ->addColumn('user_id', 'integer', ['limit' => 10, 'signed' => false]) + ->addColumn('name', 'string', ['limit' => 20, 'encoding' => 'utf8mb4']) + ->addColumn('ipaddr', 'integer', ['limit' => 10, 'signed' => false]) + ->addIndex(['user_id', 'name'], ['unique' => true]) + ->addIndex(['user_id', 'ipaddr'], ['unique' => true]) + ->addForeignKey('user_id', 'users_main', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + } +} diff --git a/db/migrations/20200918065547_artist_name_sort.php b/db/migrations/20200918065547_artist_name_sort.php new file mode 100644 index 000000000..0ded60c64 --- /dev/null +++ b/db/migrations/20200918065547_artist_name_sort.php @@ -0,0 +1,20 @@ +execute(' + ALTER TABLE torrents_artists + DROP PRIMARY KEY, + ADD PRIMARY KEY (GroupID, Importance, ArtistID) + '); + } + public function down() { + $this->execute(' + ALTER TABLE torrents_artists + DROP PRIMARY KEY, + ADD PRIMARY KEY (GroupID, ArtistID, Importance) + '); + } +} diff --git a/db/migrations/20200918213049_user_seedbox_extend.php b/db/migrations/20200918213049_user_seedbox_extend.php new file mode 100644 index 000000000..678a13fe1 --- /dev/null +++ b/db/migrations/20200918213049_user_seedbox_extend.php @@ -0,0 +1,23 @@ +table('user_seedbox') + ->changeColumn('name', 'string', ['limit' => 40]) + ->addColumn('peer_id', 'binary', ['limit' => 20]) + ->addColumn('useragent', 'string', ['limit' => 51]) + ->addIndex(['peer_id'], ['unique' => true]) + ->update(); + } + + public function down() { + $this->table('user_seedbox') + ->changeColumn('name', 'string', ['limit' => 20]) + ->removeColumn('peer') + ->removeColumn('useragent') + ->removeIndex(['user_id', 'ipaddr']) + ->update(); + } +} diff --git a/db/migrations/20201017220826_user_seedbox_drop_peer_id.php b/db/migrations/20201017220826_user_seedbox_drop_peer_id.php new file mode 100644 index 000000000..39f6d25de --- /dev/null +++ b/db/migrations/20201017220826_user_seedbox_drop_peer_id.php @@ -0,0 +1,17 @@ +table('user_seedbox') + ->removeColumn('peer_id') + ->update(); + } + + public function down() { + $this->table('user_seedbox') + ->addColumn('peer_id', 'binary', ['limit' => 20]) + ->update(); + } +} diff --git a/db/migrations/20201028141502_user_attr_seedbox.php b/db/migrations/20201028141502_user_attr_seedbox.php new file mode 100644 index 000000000..54f44e0fd --- /dev/null +++ b/db/migrations/20201028141502_user_attr_seedbox.php @@ -0,0 +1,40 @@ +table('user_attr') + ->insert([ + [ + 'Name' => 'feature-seedbox', + 'Description' => 'This user has purchased the seedbox feature' + ], + ]) + ->save(); + $this->table('bonus_item') + ->insert([ + [ + 'Price' => 8000, + 'Amount' => 1, + 'MinClass' => 150, + 'FreeClass' => 999999, + 'Label' => 'seedbox', + 'Title' => 'Unlock the Seedbox viewer', + 'sequence' => 14, + ], + ]) + ->save(); + } + + public function down() { + $this->getQueryBuilder() + ->delete('user_attr') + ->where(['Name' => 'feature-seedbox']) + ->execute(); + $this->getQueryBuilder() + ->delete('bonus_item') + ->where(['Label' => 'seedbox']) + ->execute(); + } +} diff --git a/sections/bonus/collage.php b/sections/bonus/collage.php deleted file mode 100644 index 1e5a1815d..000000000 --- a/sections/bonus/collage.php +++ /dev/null @@ -1,9 +0,0 @@ -purchaseCollage($LoggedUser['ID'], $Label)) { - header("Location: bonus.php?complete=$Label"); -} -else { - error(403); -} diff --git a/sections/bonus/purchase.php b/sections/bonus/purchase.php new file mode 100644 index 000000000..a569bb8a5 --- /dev/null +++ b/sections/bonus/purchase.php @@ -0,0 +1,18 @@ +purchaseCollage($LoggedUser['ID'], $label)) { + header("Location: bonus.php?complete=$label"); + exit; + } + break; + case 'seedbox': + if ($Bonus->unlockSeedbox($LoggedUser['ID'])) { + header("Location: bonus.php?complete=$label"); + exit; + } +} +error(403); diff --git a/sections/bonus/store.php b/sections/bonus/store.php index 832c738a4..42a5f89ad 100644 --- a/sections/bonus/store.php +++ b/sections/bonus/store.php @@ -44,23 +44,26 @@ if (isset($_GET['action']) && $_GET['action'] == 'donate') { } } +$user = new Gazelle\User($LoggedUser['ID']); +$points = (int)$LoggedUser['BonusPoints']; +$auth = $LoggedUser['AuthKey']; $pool = $Bonus->getOpenPool(); if ($pool) { echo G::$Twig->render('bonus/bonus-pool.twig', [ - 'auth' => $LoggedUser['AuthKey'], - 'points' => (int)$LoggedUser['BonusPoints'], + 'auth' => $auth, + 'points' => $points, 'pool' => $pool, - 'user_id' => $LoggedUser['ID'], + 'user_id' => $user->id(), ]); } echo G::$Twig->render('bonus/store.twig', [ - 'auth' => $LoggedUser['AuthKey'], - 'class' => $LoggedUser['EffectiveClass'], + 'auth' => $auth, + 'class' => $user->primaryClass(), 'discount' => $Bonus->discount(), - 'list' => $Bonus->getListForUser($LoggedUser['ID']), - 'points' => (int)$LoggedUser['BonusPoints'], - 'user_id' => $LoggedUser['ID'], + 'list' => $Bonus->getListForUser($user), + 'points' => $points, + 'user_id' => $user->id(), ]); View::show_footer(); diff --git a/sections/bonus/title.php b/sections/bonus/title.php index 9a3e22cef..d10b8f2d7 100644 --- a/sections/bonus/title.php +++ b/sections/bonus/title.php @@ -32,10 +32,9 @@ if (isset($_POST['confirm'])) { try { $Bonus->purchaseTitle($ID, $Label, $_POST['title']); header('Location: bonus.php?complete=' . urlencode($Label)); - } - catch (\Exception $e) { + } catch (\Gazelle\BonusException $e) { switch ($e->getMessage()) { - case 'Bonus:title:too-long': + case 'title:too-long': error('This title is too long, you must reduce the length.'); break; default: diff --git a/sections/bonus/tokens.php b/sections/bonus/tokens.php index ca2da3c82..11e98086f 100644 --- a/sections/bonus/tokens.php +++ b/sections/bonus/tokens.php @@ -10,8 +10,7 @@ if (!preg_match('/^(token|other)-[1-4]$/', $Label, $match)) { if ($match[1] === 'token') { try { $Bonus->purchaseToken($LoggedUser['ID'], $Label); - } - catch (\Exception $e) { + } catch (\Gazelle\BonusException $e) { $message = $e->getMessage(); error("Purchase not concluded ($message)."); } @@ -23,15 +22,13 @@ else { $ID = Users::ID_from_username(urldecode($_GET['user'])); if (is_null($ID)) { error('Nobody with that name found at ' . SITE_NAME . '. Are you certain the spelling is right?'); - } - elseif ($ID == $LoggedUser['ID']) { + } elseif ($ID == $LoggedUser['ID']) { error('You cannot gift yourself tokens, they are cheaper to buy directly.'); } try { $Bonus->purchaseTokenOther($LoggedUser['ID'], $ID, $Label); - } - catch (Exception $e) { - if ($e->getMessage() == 'Bonus:otherToken:no-gift-funds') { + } catch (\Gazelle\BonusException $e) { + if ($e->getMessage() == 'otherToken:no-gift-funds') { error('Purchase for other not concluded. Either you lacked funds or they have chosen to decline FL tokens.'); } else { error(0); diff --git a/sections/user/index.php b/sections/user/index.php index 020dcae86..0a3e8637d 100644 --- a/sections/user/index.php +++ b/sections/user/index.php @@ -197,6 +197,12 @@ switch ($_REQUEST['action'] ?? '') { case 'moderate': require_once('takemoderate.php'); break; + case 'seedbox': + require_once('seedbox_edit.php'); + break; + case 'seedbox-view': + require_once('seedbox_view.php'); + break; case 'token': require_once(__DIR__ . '/token.php'); break; diff --git a/sections/user/seedbox_edit.php b/sections/user/seedbox_edit.php new file mode 100644 index 000000000..5f6f179bf --- /dev/null +++ b/sections/user/seedbox_edit.php @@ -0,0 +1,94 @@ +findById($userId); +if (!$user) { + error(404); +} +$seedbox = new Gazelle\Seedbox($user->id()); +View::show_header($user->username() . ' › Seedboxes'); + +if (isset($_POST['mode'])) { + switch ($_POST['mode']) { + case 'update': + $id = array_filter($_POST, function ($x) { return preg_match('/^id-\d+$/', $x);}, ARRAY_FILTER_USE_KEY); + $ip = array_filter($_POST, function ($x) { return preg_match('/^ip-\d+$/', $x);}, ARRAY_FILTER_USE_KEY); + $name = array_filter($_POST, function ($x) { return preg_match('/^name-\d+$/', $x);}, ARRAY_FILTER_USE_KEY); + $sig = array_filter($_POST, function ($x) { return preg_match('/^sig-\d+$/', $x);}, ARRAY_FILTER_USE_KEY); + $ua = array_filter($_POST, function ($x) { return preg_match('/^ua-\d+$/', $x);}, ARRAY_FILTER_USE_KEY); + if (count($id) != count($ip)) { + error("id/ip mismatch"); + } elseif (count($id) != count($name)) { + error("id/name mismatch"); + } elseif (count($id) != count($sig)) { + error("id/sig mismatch"); + } elseif (count($id) != count($ua)) { + error("id/ua mismatch"); + } + $update = []; + for ($i = 1, $end = count($name); $i <= $end; ++$i) { + if ($sig["sig-$i"] != $seedbox->signature($ip["ip-$i"], $ua["ua-$i"])) { + error("ip/ua signature failed"); + } + $update[] = [ + 'id' => $id["id-$i"], + 'name' => $name["name-$i"], + 'ipv4' => $ip["ip-$i"], + 'ua' => $ua["ua-$i"], + ]; + } + $seedbox->updateNames($update); + break; + case 'remove': + $rm = array_map( + function($x) {return explode('-', $x)[1];}, + array_keys( + array_filter($_POST, function ($x) {return preg_match('/^rm-\d+$/', $x);}, ARRAY_FILTER_USE_KEY) + ) + ); + $rmid = array_filter($_POST, function ($x) {return preg_match('/^rmid-\d+$/', $x);}, ARRAY_FILTER_USE_KEY); + $remove = []; + foreach ($rm as $id) { + if (isset($rmid["rmid-$id"])) { + $remove[] = (int)$rmid["rmid-$id"]; + } + } + $seedbox->removeNames($remove); + break; + default: + error(403); + } +} +?> +
On this page you can create familiar names for the IP addresses of the + places you seed from (which is not necessarily a seedbox). E.g. "home", + "feral", "seedhost.eu", "basement-pc", "mercury". Using names can help when + seeding from several locations, because who remembers IP addresses?
+Use the View page to see if seeds are active in two locations. + This helps in migrating from one location to another, and identifying where + seeds can be removed to free up space. Another use is migrating from one + computer to another, or even a different BitTorrent client on the same + computer. Compare to make sure you have moved everything over.
+Note that the BitTorrent protocol is under-specified, and there is + no guaranteed way to distinguish two instances of the same client version + running behind a VPN or NAT. If you have, for instance, two Deluge clients + on two machines in your basement, they will be reported as one.
+You have defined these names but have not associated them with a seed location. + If you no longer require a name, click the checkbox and remove it. If you did not + expect to see a name here because it is in uese, check your client to make sure + it is not frozen or has crashed.
+ {% for f in free %} + {% if loop.first %} + + {% endif %} + {% endfor %} +Select one seedbox and choose another to see which seeds appear in both locations, + or do not appear in the second location.
+If you are seeding from only one location, this page will be of no use. You + should look at your seeding torrents.
+ +{% if host is empty %} + No seed locations are reporting. Are your clients seeding and connected? +{% else %} + +{% endif %} +