From b6082d6eec4b135551050ddeedac7c9fa588f031 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 27 Aug 2023 12:07:08 +0300 Subject: [PATCH] initial commit --- .gitignore | 7 + README.md | 104 +- composer.json | 21 + database/yggtracker.mwb | Bin 0 -> 26236 bytes example/environment /crontab | 4 + example/environment /sphinx.conf | 52 + src/config/app.php.example | 102 ++ src/library/database.php | 1032 +++++++++++++++++ src/library/sphinx.php | 49 + src/library/time.php | 45 + src/public/action.php | 442 +++++++ .../assets/theme/default/css/common.css | 71 ++ .../assets/theme/default/css/framework.css | 234 ++++ src/public/edit.php | 560 +++++++++ src/public/index.php | 336 ++++++ 15 files changed, 3058 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 database/yggtracker.mwb create mode 100644 example/environment /crontab create mode 100644 example/environment /sphinx.conf create mode 100644 src/config/app.php.example create mode 100644 src/library/database.php create mode 100644 src/library/sphinx.php create mode 100644 src/library/time.php create mode 100644 src/public/action.php create mode 100644 src/public/assets/theme/default/css/common.css create mode 100644 src/public/assets/theme/default/css/framework.css create mode 100644 src/public/edit.php create mode 100644 src/public/index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81f8b2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.vscode/ +/vendor/ + +/database/yggtracker.mwb.bak +/src/config/app.php + +/composer.lock \ No newline at end of file diff --git a/README.md b/README.md index 2e96018..6411e6a 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# YGGtracker \ No newline at end of file +# YGGtracker + +BitTorrent Catalog for Yggdrasil ecosystem + +YGGtracker uses [Yggdrasil](https://github.com/yggdrasil-network/yggdrasil-go) IPv6 addresses to identify users without registration. + +#### Online instances + + * [http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker](http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/) + * [http://94.140.114.241/yggtracker](http://94.140.114.241/yggtracker/) + +#### Trackers + + * `http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/announce` [stats](http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/stats) + * `http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/announce` [stats](http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/stats) + +#### Requirements + +``` +php8^ +php-pdo +php-mysql +sphinxsearch +``` +#### Installation + +* `git clone https://github.com/YGGverse/YGGtracker.git` +* `cd YGGtracker` +* `composer update` + +#### Setup +* Server configuration `/example/environment` +* The web root dir is `/src/public` +* Deploy the database using [MySQL Workbench](https://www.mysql.com/products/workbench) project presented in the `/database` folder +* Install [Sphinx Search Server](https://sphinxsearch.com) +* Configuration examples presented at `/config` folder +* Set up the `/src/crontab` by following [example](https://github.com/YGGverse/YGGtracker/blob/main/%20example/environment%20/crontab) + +#### Contribute + +Please make new branch for each PR + +``` +git checkout main +git checkout -b my-pr-branch-name +``` + +#### Roadmap + +* [ ] Magnet + + [x] Options + + [x] Public + + [x] Sensitive + + [x] Comments + + [ ] Features + + [x] Stars + + [x] Downloads + + [ ] Comments + + [ ] Views + + [ ] Info page + +* [ ] User + + [ ] Magnets + + [ ] Downloads + + [ ] Stars + + [ ] Following + + [ ] Followers + + [ ] Profile menu + + [ ] Magnets + + [ ] Downloads + + [ ] Stars + + [ ] Comments + +* [x] Other + + [x] RSS + + [x] Moderation + +#### Donate to contributors + +* @d47081: [BTC](https://www.blockchain.com/explorer/addresses/btc/bc1qngdf2kwty6djjqpk0ynkpq9wmlrmtm7e0c534y) | [LTC](https://live.blockcypher.com/ltc/address/LUSiqzKsfB1vBLvpu515DZktG9ioKqLyj7) | [XMR](835gSR1Uvka19gnWPkU2pyRozZugRZSPHDuFL6YajaAqjEtMwSPr4jafM8idRuBWo7AWD3pwFQSYRMRW9XezqrK4BEXBgXE) | [ZEPH](ZEPHsADHXqnhfWhXrRcXnyBQMucE3NM7Ng5ZVB99XwA38PTnbjLKpCwcQVgoie8EJuWozKgBiTmDFW4iY7fNEgSEWyAy4dotqtX) | [DOGE](https://dogechain.info/address/D5Sez493ibLqTpyB3xwQUspZvJ1cxEdRNQ) | Support our server by order [Linux VPS](https://www.yourserver.se/portal/aff.php?aff=610) + +#### License + +* Engine sources [MIT License](https://github.com/YGGverse/YGGtracker/blob/main/LICENSE) + +#### Components + +[Icons](https://icons.getbootstrap.com) + +#### Feedback + +Feel free to [share](https://github.com/YGGverse/YGGtracker/issues) your ideas and bug reports! + +#### Community + +* [Mastodon](https://mastodon.social/@YGGverse) +* [[matrix]](https://matrix.to/#/#YGGtracker:matrix.org) + +#### See also + +* [YGGo - YGGo! Distributed Web Search Engine ](https://github.com/YGGverse/YGGo) +* [YGGwave ~ The Radio Catalog](https://github.com/YGGverse/YGGwave) +* [YGGstate - Yggdrasil Network Explorer](https://github.com/YGGverse/YGGstate) \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1deb105 --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "yggverse/yggtracker", + "description": "Public BitTorrent tracker for Yggdrasil network", + "type": "library", + "require": { + "php": "^8.1", + "yggverse/parser": ">=0.2.0" + }, + "license": "MIT", + "autoload": { + "psr-4": { + "Yggverse\\Yggtracker\\": "src/" + } + }, + "authors": [ + { + "name": "YGGverse" + } + ], + "minimum-stability": "alpha" +} diff --git a/database/yggtracker.mwb b/database/yggtracker.mwb new file mode 100644 index 0000000000000000000000000000000000000000..f06968637294d6fad58eb860c3c3a7de26d0c672 GIT binary patch literal 26236 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 VG<6fC@Upi4>S}6kZ|VB>{{XnAwR!*m literal 0 HcmV?d00001 diff --git a/example/environment /crontab b/example/environment /crontab new file mode 100644 index 0000000..0ec8c1d --- /dev/null +++ b/example/environment /crontab @@ -0,0 +1,4 @@ +@reboot searchd +@reboot indexer --all --rotate + +* * * * * indexer magnet --rotate \ No newline at end of file diff --git a/example/environment /sphinx.conf b/example/environment /sphinx.conf new file mode 100644 index 0000000..c8e5089 --- /dev/null +++ b/example/environment /sphinx.conf @@ -0,0 +1,52 @@ +source yggtracker +{ + type = mysql + + sql_port = 3306 + sql_host = localhost + sql_user = + sql_pass = + sql_db = +} + +source magnet : yggtracker +{ + sql_query = \ + SELECT `magnet`.`timeAdded`, \ + `magnet`.`timeUpdated`, \ + `magnet`.`magnetId`, \ + `magnet`.`metaTitle`, \ + `magnet`.`metaDescription`, \ + `magnet`.`dn`, \ + (SELECT GROUP_CONCAT(DISTINCT `keywordTopic`.`value`) \ + FROM `keywordTopic` \ + JOIN `magnetToKeywordTopic` ON (`magnetToKeywordTopic`.`magnetId` = `magnet`.`magnetId`) \ + WHERE `keywordTopic`.`keywordTopicId` = `magnetToKeywordTopic`.`keywordTopicId`) AS `keywords`, \ + (SELECT GROUP_CONCAT(DISTINCT `magnetComment`.`value`) \ + FROM `magnetComment` \ + WHERE `magnetComment`.`magnetId` = `magnet`.`magnetId`) AS `comments` \ + FROM `magnet`\ + + sql_attr_uint = magnetId +} + +index magnet +{ + source = magnet + path = /var/lib/sphinxsearch/data/magnet + + morphology = stem_cz, stem_ar, lemmatize_de_all, lemmatize_ru_all, lemmatize_en_all # stem_enru + + min_word_len = 2 + min_prefix_len = 2 + + html_strip = 1 + + index_exact_words = 1 +} + +indexer +{ + mem_limit = 256M + lemmatizer_cache = 256M +} \ No newline at end of file diff --git a/src/config/app.php.example b/src/config/app.php.example new file mode 100644 index 0000000..7761939 --- /dev/null +++ b/src/config/app.php.example @@ -0,0 +1,102 @@ + (object) + [ + 'announce' => 'http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/announce', + 'stats' => 'http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/stats', + ], + 'Tracker 2' => (object) + [ + 'announce' => 'http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/announce', + 'stats' => 'http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/stats', + ], + // ... + ] +); + +// Yggdrasil +define('YGGDRASIL_URL_REGEX', '/^0{0,1}[2-3][a-f0-9]{0,2}:/'); // thanks to @ygguser (https://github.com/YGGverse/YGGo/issues/1#issuecomment-1498182228 ) diff --git a/src/library/database.php b/src/library/database.php new file mode 100644 index 0000000..4c8dccc --- /dev/null +++ b/src/library/database.php @@ -0,0 +1,1032 @@ +_db = new PDO('mysql:dbname=' . $database . ';host=' . $host . ';port=' . $port . ';charset=utf8', $username, $password, [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']); + $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->_db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + $this->_db->setAttribute(PDO::ATTR_TIMEOUT, 600); + + $this->_debug = (object) + [ + 'query' => (object) + [ + 'select' => (object) + [ + 'total' => 0 + ], + 'insert' => (object) + [ + 'total' => 0 + ], + 'update' => (object) + [ + 'total' => 0 + ], + 'delete' => (object) + [ + 'total' => 0 + ], + ] + ]; + } + + // Tools + public function beginTransaction() { + + $this->_db->beginTransaction(); + } + + public function commit() { + + $this->_db->commit(); + } + + public function rollBack() { + + $this->_db->rollBack(); + } + + public function getDebug() { + + return $this->_debug; + } + + // Scheme + public function addScheme(string $value) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `scheme` SET `value` = ?'); + + $query->execute([$value]); + + return $this->_db->lastInsertId(); + } + + public function getScheme(int $schemeId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `scheme` WHERE `schemeId` = ?'); + + $query->execute([$schemeId]); + + return $query->fetch(); + } + + public function findScheme(string $value) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `scheme` WHERE `value` = ?'); + + $query->execute([$value]); + + return $query->fetch(); + } + + public function initSchemeId(string $value) : int { + + if ($result = $this->findScheme($value)) { + + return $result->schemeId; + } + + return $this->addScheme($value); + } + + // Host + public function addHost(string $value) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `host` SET `value` = ?'); + + $query->execute([$value]); + + return $this->_db->lastInsertId(); + } + + public function getHost(int $hostId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `host` WHERE `hostId` = ?'); + + $query->execute([$hostId]); + + return $query->fetch(); + } + + public function findHost(string $value) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `host` WHERE `value` = ?'); + + $query->execute([$value]); + + return $query->fetch(); + } + + public function initHostId(string $value) : int { + + if ($result = $this->findHost($value)) { + + return $result->hostId; + } + + return $this->addHost($value); + } + + // Port + public function addPort(mixed $value) : int { + + $this->_debug->query->insert->total++; + + if ($value) { + + $query = $this->_db->prepare('INSERT INTO `port` SET `value` = ?'); + + $query->execute([$value]); + + } else { + + $query = $this->_db->prepare('INSERT INTO `port` SET `value` = NULL'); + + $query->execute(); + } + + return $this->_db->lastInsertId(); + } + + public function getPort(int $portId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `port` WHERE `portId` = ?'); + + $query->execute([$portId]); + + return $query->fetch(); + } + + public function findPort(mixed $value) { + + $this->_debug->query->select->total++; + + if ($value) { + + $query = $this->_db->prepare('SELECT * FROM `port` WHERE `value` = ?'); + + $query->execute([$value]); + + } else { + + $query = $this->_db->prepare('SELECT * FROM `port` WHERE `value` IS NULL'); + + $query->execute(); + } + + return $query->fetch(); + } + + public function initPortId(mixed $value) : int { + + if ($result = $this->findPort($value)) { + + return $result->portId; + } + + return $this->addPort($value); + } + + // URI + public function addUri(string $value) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `uri` SET `value` = ?'); + + $query->execute([$value]); + + return $this->_db->lastInsertId(); + } + + public function getUri(int $uriId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `uri` WHERE `uriId` = ?'); + + $query->execute([$uriId]); + + return $query->fetch(); + } + + public function findUri(string $value) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `uri` WHERE `value` = ?'); + + $query->execute([$value]); + + return $query->fetch(); + } + + public function initUriId(string $value) : int { + + if ($result = $this->findUri($value)) { + + return $result->uriId; + } + + return $this->addUri($value); + } + + // Address Tracker + public function addAddressTracker(int $schemeId, int $hostId, int $portId, int $uriId) : int { + + $this->_debug->query->insert->total++; + + if ($portId) { + + $query = $this->_db->prepare('INSERT INTO `addressTracker` SET `schemeId` = ?, `hostId` = ?, `portId` = ?, `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $portId, $uriId]); + + } else { + + $query = $this->_db->prepare('INSERT INTO `addressTracker` SET `schemeId` = ?, `hostId` = ?, `portId` = NULL, `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $uriId]); + } + + return $this->_db->lastInsertId(); + } + + public function getAddressTracker(int $addressTrackerId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `addressTracker` WHERE `addressTrackerId` = ?'); + + $query->execute([$addressTrackerId]); + + return $query->fetch(); + } + + public function findAddressTracker(int $schemeId, int $hostId, mixed $portId, int $uriId) { + + $this->_debug->query->select->total++; + + if ($portId) { + + $query = $this->_db->prepare('SELECT * FROM `addressTracker` WHERE `schemeId` = ? AND `hostId` = ? AND `portId` = ? AND `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $portId, $uriId]); + + } else { + + $query = $this->_db->prepare('SELECT * FROM `addressTracker` WHERE `schemeId` = ? AND `hostId` = ? AND `portId` IS NULL AND `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $uriId]); + } + + return $query->fetch(); + } + + public function initAddressTrackerId(int $schemeId, int $hostId, mixed $portId, int $uriId) : int { + + if ($result = $this->findAddressTracker($schemeId, $hostId, $portId, $uriId)) { + + return $result->addressTrackerId; + } + + return $this->addAddressTracker($schemeId, $hostId, $portId, $uriId); + } + + // Acceptable Source + public function addAcceptableSource(int $schemeId, int $hostId, int $portId, int $uriId) : int { + + $this->_debug->query->insert->total++; + + if ($portId) { + + $query = $this->_db->prepare('INSERT INTO `acceptableSource` SET `schemeId` = ?, `hostId` = ?, `portId` = ?, `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $portId, $uriId]); + + } else { + + $query = $this->_db->prepare('INSERT INTO `acceptableSource` SET `schemeId` = ?, `hostId` = ?, `portId` = NULL, `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $uriId]); + } + + return $this->_db->lastInsertId(); + } + + public function getAcceptableSource(int $acceptableSourceId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `acceptableSource` WHERE `acceptableSourceId` = ?'); + + $query->execute([$acceptableSourceId]); + + return $query->fetch(); + } + + public function findAcceptableSource(int $schemeId, int $hostId, mixed $portId, int $uriId) { + + $this->_debug->query->select->total++; + + if ($portId) { + + $query = $this->_db->prepare('SELECT * FROM `acceptableSource` WHERE `schemeId` = ? AND `hostId` = ? AND `portId` = ? AND `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $portId, $uriId]); + + } else { + + $query = $this->_db->prepare('SELECT * FROM `acceptableSource` WHERE `schemeId` = ? AND `hostId` = ? AND `portId` IS NULL AND `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $uriId]); + } + + return $query->fetch(); + } + + public function initAcceptableSourceId(int $schemeId, int $hostId, mixed $portId, int $uriId) : int { + + if ($result = $this->findAcceptableSource($schemeId, $hostId, $portId, $uriId)) { + + return $result->acceptableSourceId; + } + + return $this->addAcceptableSource($schemeId, $hostId, $portId, $uriId); + } + + // eXact Source + public function addExactSource(int $schemeId, int $hostId, int $portId, int $uriId) : int { + + $this->_debug->query->insert->total++; + + if ($portId) { + + $query = $this->_db->prepare('INSERT INTO `eXactSource` SET `schemeId` = ?, `hostId` = ?, `portId` = ?, `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $portId, $uriId]); + + } else { + + $query = $this->_db->prepare('INSERT INTO `eXactSource` SET `schemeId` = ?, `hostId` = ?, `portId` = NULL, `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $uriId]); + } + + return $this->_db->lastInsertId(); + } + + public function getExactSource(int $eXactSourceId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `eXactSource` WHERE `eXactSourceId` = ?'); + + $query->execute([$eXactSourceId]); + + return $query->fetch(); + } + + public function findExactSource(int $schemeId, int $hostId, mixed $portId, int $uriId) { + + $this->_debug->query->select->total++; + + if ($portId) { + + $query = $this->_db->prepare('SELECT * FROM `eXactSource` WHERE `schemeId` = ? AND `hostId` = ? AND `portId` = ? AND `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $portId, $uriId]); + + } else { + + $query = $this->_db->prepare('SELECT * FROM `eXactSource` WHERE `schemeId` = ? AND `hostId` = ? AND `portId` IS NULL AND `uriId` = ?'); + + $query->execute([$schemeId, $hostId, $uriId]); + } + + return $query->fetch(); + } + + public function initExactSourceId(int $schemeId, int $hostId, mixed $portId, int $uriId) : int { + + if ($result = $this->findExactSource($schemeId, $hostId, $portId, $uriId)) { + + return $result->eXactSourceId; + } + + return $this->addExactSource($schemeId, $hostId, $portId, $uriId); + } + + // Keyword Topic + public function addKeywordTopic(string $value) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `keywordTopic` SET `value` = ?'); + + $query->execute([$value]); + + return $this->_db->lastInsertId(); + } + + public function findKeywordTopic(string $value) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `keywordTopic` WHERE `value` = ?'); + + $query->execute([$value]); + + return $query->fetch(); + } + + public function getKeywordTopic(int $keywordTopicId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `keywordTopic` WHERE `keywordTopicId` = ?'); + + $query->execute([$keywordTopicId]); + + return $query->fetch(); + } + + public function initKeywordTopicId(string $value) : int { + + if ($result = $this->findKeywordTopic($value)) { + + return $result->keywordTopicId; + } + + return $this->addKeywordTopic($value); + } + + // User + public function addUser(string $address, bool $approved, $timeAdded) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `user` SET `address` = ?, `approved` = ?, `timeAdded` = ?'); + + $query->execute([$address, (int) $approved, $timeAdded]); + + return $this->_db->lastInsertId(); + } + + public function getUser(int $userId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `user` WHERE `userId` = ?'); + + $query->execute([$userId]); + + return $query->fetch(); + } + + public function findUserByAddress(string $address) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `user` WHERE `address` = ?'); + + $query->execute([$address]); + + return $query->fetch(); + } + + public function initUserId(string $address, bool $approved, int $timeAdded) : int { + + if ($result = $this->findUserByAddress($address)) { + + return $result->userId; + } + + return $this->addUser($address, $approved, $timeAdded); + } + + // Magnet + public function addMagnet(int $userId, + string $xt, + int $xl, + string $dn, + string $linkSource, + bool $public, + bool $comments, + bool $sensitive, + bool $approved, + int $timeAdded) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnet` SET `userId` = ?, + `xt` = ?, + `xl` = ?, + `dn` = ?, + `linkSource` = ?, + `public` = ?, + `comments` = ?, + `sensitive` = ?, + `approved` = ?, + `timeAdded` = ?'); + + $query->execute( + [ + $userId, + $xt, + $xl, + $dn, + $linkSource, + $public ? 1 : 0, + $comments ? 1 : 0, + $sensitive ? 1 : 0, + $approved ? 1 : 0, + $timeAdded + ] + ); + + return $this->_db->lastInsertId(); + } + + public function getMagnet(int $magnetId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnet` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetch(); + } + + public function findMagnet(int $userId, string $xt) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnet` WHERE `userId` = ? AND `xt` = ?'); + + $query->execute([$userId, $xt]); + + return $query->fetch(); + } + + public function initMagnetId( int $userId, + string $xt, + int $xl, + string $dn, + string $linkSource, + bool $public, + bool $comments, + bool $sensitive, + bool $approved, + int $timeAdded) : int { + + if ($result = $this->findMagnet($userId, $xt)) { + + return $result->magnetId; + } + + return $this->addMagnet($userId, + $xt, + $xl, + $dn, + $linkSource, + $public, + $comments, + $sensitive, + $approved, + $timeAdded); + } + + public function updateMagnetDn(int $magnetId, string $dn, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `dn` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([$dn, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + public function updateMagnetMetaTitle(int $magnetId, string $metaTitle, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `metaTitle` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([$metaTitle, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + public function updateMagnetMetaDescription(int $magnetId, string $metaDescription, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `metaDescription` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([$metaDescription, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + public function updateMagnetPublic(int $magnetId, bool $public, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `public` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([(int) $public, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + public function updateMagnetComments(int $magnetId, bool $comments, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `comments` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([(int) $comments, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + public function updateMagnetSensitive(int $magnetId, bool $sensitive, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `sensitive` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([(int) $sensitive, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + public function updateMagnetApproved(int $magnetId, bool $approved, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnet` SET `approved` = ?, `timeUpdated` = ? WHERE `magnetId` = ?'); + + $query->execute([(int) $approved, $timeUpdated, $magnetId]); + + return $query->rowCount(); + } + + // Magnet to AddressTracker + public function addMagnetToAddressTracker(int $magnetId, int $addressTrackerId) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetToAddressTracker` SET `magnetId` = ?, `addressTrackerId` = ?'); + + $query->execute([$magnetId, $addressTrackerId]); + + return $this->_db->lastInsertId(); + } + + public function deleteMagnetToAddressTrackerByMagnetId(int $magnetId) : int { + + $this->_debug->query->delete->total++; + + $query = $this->_db->prepare('DELETE FROM `magnetToAddressTracker` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->rowCount(); + } + + public function findMagnetToAddressTracker(int $magnetId, int $addressTrackerId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToAddressTracker` WHERE `magnetId` = ? AND `addressTrackerId` = ?'); + + $query->execute([$magnetId, $addressTrackerId]); + + return $query->fetch(); + } + + public function findAddressTrackerByMagnetId(int $magnetId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToAddressTracker` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetchAll(); + } + + public function initMagnetToAddressTrackerId(int $magnetId, int $addressTrackerId) : int { + + if ($result = $this->findMagnetToAddressTracker($magnetId, $addressTrackerId)) { + + return $result->magnetToAddressTrackerId; + } + + return $this->addMagnetToAddressTracker($magnetId, $addressTrackerId); + } + + // Magnet to AcceptableSource + public function addMagnetToAcceptableSource(int $magnetId, int $acceptableSourceId) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetToAcceptableSource` SET `magnetId` = ?, `acceptableSourceId` = ?'); + + $query->execute([$magnetId, $acceptableSourceId]); + + return $this->_db->lastInsertId(); + } + + public function deleteMagnetToAcceptableSourceByMagnetId(int $magnetId) : int { + + $this->_debug->query->delete->total++; + + $query = $this->_db->prepare('DELETE FROM `magnetToAcceptableSource` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->rowCount(); + } + + public function findMagnetToAcceptableSource(int $magnetId, int $acceptableSourceId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToAcceptableSource` WHERE `magnetId` = ? AND `acceptableSourceId` = ?'); + + $query->execute([$magnetId, $acceptableSourceId]); + + return $query->fetch(); + } + + public function findAcceptableSourceByMagnetId(int $magnetId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToAcceptableSource` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetchAll(); + } + + public function initMagnetToAcceptableSourceId(int $magnetId, int $acceptableSourceId) : int { + + if ($result = $this->findMagnetToAcceptableSource($magnetId, $acceptableSourceId)) { + + return $result->magnetToAcceptableSourceId; + } + + return $this->addMagnetToAcceptableSource($magnetId, $acceptableSourceId); + } + + // Magnet to eXactSource + public function addMagnetToExactSource(int $magnetId, int $eXactSourceId) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetToExactSource` SET `magnetId` = ?, `eXactSourceId` = ?'); + + $query->execute([$magnetId, $eXactSourceId]); + + return $this->_db->lastInsertId(); + } + + + public function deleteMagnetToExactSourceByMagnetId(int $magnetId) : int { + + $this->_debug->query->delete->total++; + + $query = $this->_db->prepare('DELETE FROM `magnetToExactSource` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $this->_db->lastInsertId(); + } + + public function findMagnetToExactSource(int $magnetId, int $eXactSourceId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToExactSource` WHERE `magnetId` = ? AND `eXactSourceId` = ?'); + + $query->execute([$magnetId, $eXactSourceId]); + + return $query->fetch(); + } + + public function findExactSourceByMagnetId(int $magnetId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToExactSource` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetchAll(); + } + + public function initMagnetToExactSourceId(int $magnetId, int $eXactSourceId) : int { + + if ($result = $this->findMagnetToEXactSource($magnetId, $eXactSourceId)) { + + return $result->magnetToExactSourceId; + } + + return $this->addMagnetToEXactSource($magnetId, $eXactSourceId); + } + + // Magnet to KeywordTopic + public function addMagnetToKeywordTopic(int $magnetId, int $keywordTopicId) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetToKeywordTopic` SET `magnetId` = ?, `keywordTopicId` = ?'); + + $query->execute([$magnetId, $keywordTopicId]); + + return $this->_db->lastInsertId(); + } + + public function deleteMagnetToKeywordTopicByMagnetId(int $magnetId) : int { + + $this->_debug->query->delete->total++; + + $query = $this->_db->prepare('DELETE FROM `magnetToKeywordTopic` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->rowCount(); + } + + public function findMagnetToKeywordTopic(int $magnetId, int $keywordTopicId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToKeywordTopic` WHERE `magnetId` = ? AND `keywordTopicId` = ?'); + + $query->execute([$magnetId, $keywordTopicId]); + + return $query->fetch(); + } + + public function findKeywordTopicByMagnetId(int $magnetId) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToKeywordTopic` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetchAll(); + } + + public function initMagnetToKeywordTopicId(int $magnetId, int $keywordTopicId) : int { + + if ($result = $this->findMagnetToKeywordTopic($magnetId, $keywordTopicId)) { + + return $result->magnetToKeywordTopicId; + } + + return $this->addMagnetToKeywordTopic($magnetId, $keywordTopicId); + } + + // Magnet comment + public function getMagnetCommentsTotal(int $magnetId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetComment` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetch()->result; + } + + public function findMagnetCommentsTotalByUserId(int $magnetId, int $userId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetComment` WHERE `magnetId` = ? AND `userId` = ?'); + + $query->execute([$magnetId, $userId]); + + return $query->fetch()->result; + } + + // Magnet star + public function addMagnetStar(int $magnetId, int $userId, int $timeAdded) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetStar` SET `magnetId` = ?, `userId` = ?, `timeAdded` = ?'); + + $query->execute([$magnetId, $userId, $timeAdded]); + + return $this->_db->lastInsertId(); + } + + public function deleteMagnetStarByUserId(int $magnetId, int $userId) : int { + + $this->_debug->query->delete->total++; + + $query = $this->_db->prepare('DELETE FROM `magnetStar` WHERE `magnetId` = ? AND `userId` = ?'); + + $query->execute([$magnetId, $userId]); + + return $query->rowCount(); + } + + public function getMagnetStarsTotal(int $magnetId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetStar` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetch()->result; + } + + public function findMagnetStarsTotalByUserId(int $magnetId, int $userId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetStar` WHERE `magnetId` = ? AND `userId` = ?'); + + $query->execute([$magnetId, $userId]); + + return $query->fetch()->result; + } + + // Magnet download + public function addMagnetDownload(int $magnetId, int $userId, int $timeAdded) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetDownload` SET `magnetId` = ?, `userId` = ?, `timeAdded` = ?'); + + $query->execute([$magnetId, $userId, $timeAdded]); + + return $this->_db->lastInsertId(); + } + + public function getMagnetDownloadsTotal(int $magnetId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetDownload` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetch()->result; + } + + public function deleteMagnetDownloadByUserId(int $magnetId, int $userId) : int { + + $this->_debug->query->delete->total++; + + $query = $this->_db->prepare('DELETE FROM `magnetDownload` WHERE `magnetId` = ? AND `userId` = ?'); + + $query->execute([$magnetId, $userId]); + + return $query->rowCount(); + } + + public function findMagnetDownloadsTotalByUserId(int $magnetId, int $userId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetDownload` WHERE `magnetId` = ? AND `userId` = ?'); + + $query->execute([$magnetId, $userId]); + + return $query->fetch()->result; + } +} \ No newline at end of file diff --git a/src/library/sphinx.php b/src/library/sphinx.php new file mode 100644 index 0000000..2eff9aa --- /dev/null +++ b/src/library/sphinx.php @@ -0,0 +1,49 @@ +_sphinx = new PDO('mysql:host=' . $host . ';port=' . $port . ';charset=utf8', false, false, [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']); + $this->_sphinx->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->_sphinx->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + } + + public function searchMagnetsTotal(string $keyword) { + + $query = $this->_sphinx->prepare('SELECT COUNT(*) AS `total` FROM `magnet` WHERE MATCH(?)'); + + $query->execute( + [ + $keyword + ] + ); + + return $query->fetch()->total; + } + + public function searchMagnets(string $keyword, int $start, int $limit, int $maxMatches) { + + $query = $this->_sphinx->prepare("SELECT * + + FROM `magnet` + + WHERE MATCH(?) + + ORDER BY `magnetId` DESC, WEIGHT() DESC + + LIMIT " . (int) ($start >= $maxMatches ? ($maxMatches > 0 ? $maxMatches - 1 : 0) : $start) . "," . (int) $limit . " + + OPTION `max_matches`=" . (int) ($maxMatches >= 1 ? $maxMatches : 1)); + + $query->execute( + [ + $keyword + ] + ); + + return $query->fetchAll(); + } +} diff --git a/src/library/time.php b/src/library/time.php new file mode 100644 index 0000000..9199d3c --- /dev/null +++ b/src/library/time.php @@ -0,0 +1,45 @@ + _('year'), + 30 * 24 * 60 * 60 => _('month'), + 24 * 60 * 60 => _('day'), + 60 * 60 => _('hour'), + 60 => _('minute'), + 1 => _('second') + ]; + + $plural = [ + _('year') => _('years'), + _('month') => _('months'), + _('day') => _('days'), + _('hour') => _('hours'), + _('minute') => _('minutes'), + _('second') => _('seconds') + ]; + + foreach ($values as $key => $value) + { + $result = $diff / $key; + + if ($result >= 1) + { + $round = round($result); + + return sprintf('%s %s ago', $round, $round > 1 ? $plural[$value] : $value); + } + } + } +} diff --git a/src/public/action.php b/src/public/action.php new file mode 100644 index 0000000..58a9763 --- /dev/null +++ b/src/public/action.php @@ -0,0 +1,442 @@ + true, + 'message' => _('Internal server error') +]; + +// Begin action request +switch (isset($_GET['target']) ? urldecode($_GET['target']) : false) +{ + case 'comment': + + break; + + case 'star': + + // Yggdrasil connections only + if (!preg_match(YGGDRASIL_URL_REGEX, $_SERVER['REMOTE_ADDR'])) + { + $response->success = false; + $response->message = _('Yggdrasil connection required for this action'); + } + + // Init session + else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time())) + { + $response->success = false; + $response->message = _('Could not init user session'); + } + + // Magnet exists + else if (!$magnet = $db->getMagnet(isset($_GET['magnetId']) && $_GET['magnetId'] > 0 ? (int) $_GET['magnetId'] : 0)) + { + $response->success = false; + $response->message = _('Requested magnet not found'); + } + + // Access allowed + else if (!($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved))) { + + $response->success = false; + $response->message = _('Magnet not available for this action'); + } + + // Validate callback + else if (empty($_GET['callback'])) + { + $response->success = false; + $response->message = _('Callback required'); + } + + // Validate base64 + else if (!$callback = (string) @base64_decode($_GET['callback'])) + { + $response->success = false; + $response->message = _('Invalid callback encoding'); + } + + // Request valid + else + { + // Star exists, trigger delete + if ($db->findMagnetStarsTotalByUserId($magnet->magnetId, $userId)) + { + $db->deleteMagnetStarByUserId($magnet->magnetId, $userId); + } + else + { + // Star not exists, trigger add + $db->addMagnetStar($magnet->magnetId, $userId, time()); + } + + // Redirect to edit page + header( + sprintf('Location: %s', $callback) + ); + } + + break; + + case 'download': + + // Yggdrasil connections only + if (!preg_match(YGGDRASIL_URL_REGEX, $_SERVER['REMOTE_ADDR'])) + { + $response->success = false; + $response->message = _('Yggdrasil connection required for this action'); + } + + // Init session + else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time())) + { + $response->success = false; + $response->message = _('Could not init user session'); + } + + // Magnet exists + else if (!$magnet = $db->getMagnet(isset($_GET['magnetId']) && $_GET['magnetId'] > 0 ? (int) $_GET['magnetId'] : 0)) + { + $response->success = false; + $response->message = _('Requested magnet not found'); + } + + // Access allowed + else if (!($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved))) { + + $response->success = false; + $response->message = _('Magnet not available for this action'); + } + + // Request valid + else + { + // Download exists, trigger delete + if (!$db->findMagnetDownloadsTotalByUserId($magnet->magnetId, $userId)) + { + // Download not exists, add new record + $db->addMagnetDownload($magnet->magnetId, $userId, time()); + } + + // Build magnet link + $link = []; + + /// Exact Topic + $link[] = sprintf('magnet:?xt=%s', $magnet->xt); + + /// Display Name + $link[] = sprintf('dn=%s', urlencode($magnet->dn)); + + // Keyword Topic + $kt = []; + + foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result) + { + $kt[] = urlencode($db->getKeywordTopic($result->keywordTopicId)->value); + } + + $link[] = sprintf('kt=%s', implode('+', $kt)); + + /// Address Tracker + foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $result) + { + $addressTracker = $db->getAddressTracker($result->addressTrackerId); + + $scheme = $db->getScheme($addressTracker->schemeId); + $host = $db->getHost($addressTracker->hostId); + $port = $db->getPort($addressTracker->portId); + $uri = $db->getUri($addressTracker->uriId); + + $link[] = sprintf('tr=%s', urlencode($port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value))); + } + + /// Acceptable Source + foreach ($db->findAcceptableSourceByMagnetId($magnet->magnetId) as $result) + { + $acceptableSource = $db->getAcceptableSource($result->acceptableSourceId); + + $scheme = $db->getScheme($acceptableSource->schemeId); + $host = $db->getHost($acceptableSource->hostId); + $port = $db->getPort($acceptableSource->portId); + $uri = $db->getUri($acceptableSource->uriId); + + $link[] = sprintf('as=%s', urlencode($port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value))); + } + + /// Exact Source + foreach ($db->findExactSourceByMagnetId($magnet->magnetId) as $result) + { + $eXactSource = $db->getExactSource($result->eXactSourceId); + + $scheme = $db->getScheme($eXactSource->schemeId); + $host = $db->getHost($eXactSource->hostId); + $port = $db->getPort($eXactSource->portId); + $uri = $db->getUri($eXactSource->uriId); + + $link[] = sprintf('xs=%s', urlencode($port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value))); + } + + // Redirect to edit page + header( + sprintf('Location: %s', implode('&', $link)) + ); + } + + break; + + case 'new': + + // Yggdrasil connections only + if (!preg_match(YGGDRASIL_URL_REGEX, $_SERVER['REMOTE_ADDR'])) + { + $response->success = false; + $response->message = _('Yggdrasil connection required for this action'); + } + + // Init session + else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time())) + { + $response->success = false; + $response->message = _('Could not init user session'); + } + + // Validate link + if (empty($_GET['magnet'])) + { + $response->success = false; + $response->message = _('Link required'); + } + + // Validate base64 + else if (!$link = (string) @base64_decode($_GET['magnet'])) + { + $response->success = false; + $response->message = _('Invalid link encoding'); + } + + // Validate magnet + else if (!$magnet = Yggverse\Parser\Magnet::parse($link)) + { + $response->success = false; + $response->message = _('Invalid magnet link'); + } + + // Request valid + else + { + // Begin magnet registration + try + { + $db->beginTransaction(); + + // Init magnet + if (Yggverse\Parser\Urn::parse($magnet->xt)) + { + if ($magnetId = $db->initMagnetId($userId, + strip_tags($magnet->xt), + strip_tags($magnet->xl), + strip_tags($magnet->dn), + $link, + MAGNET_DEFAULT_PUBLIC, + MAGNET_DEFAULT_COMMENTS, + MAGNET_DEFAULT_SENSITIVE, + MAGNET_DEFAULT_APPROVED, + time())) + { + foreach ($magnet as $key => $value) + { + switch ($key) + { + case 'tr': + foreach ($value as $tr) + { + if ($url = Yggverse\Parser\Url::parse($tr)) + { + $db->initMagnetToAddressTrackerId( + $magnetId, + $db->initAddressTrackerId( + $db->initSchemeId($url->host->scheme), + $db->initHostId($url->host->name), + $db->initPortId($url->host->port), + $db->initUriId($url->page->uri) + ) + ); + } + } + break; + case 'ws': + foreach ($value as $ws) + { + // @TODO + } + break; + case 'as': + foreach ($value as $as) + { + if ($url = Yggverse\Parser\Url::parse($as)) + { + $db->initMagnetToAcceptableSourceId( + $magnetId, + $db->initAcceptableSourceId( + $db->initSchemeId($url->host->scheme), + $db->initHostId($url->host->name), + $db->initPortId($url->host->port), + $db->initUriId($url->page->uri) + ) + ); + } + } + break; + case 'xs': + foreach ($value as $xs) + { + if ($url = Yggverse\Parser\Url::parse($xs)) + { + $db->initMagnetToExactSourceId( + $magnetId, + $db->initExactSourceId( + $db->initSchemeId($url->host->scheme), + $db->initHostId($url->host->name), + $db->initPortId($url->host->port), + $db->initUriId($url->page->uri) + ) + ); + } + } + break; + case 'mt': + foreach ($value as $mt) + { + // @TODO + } + break; + case 'x.pe': + foreach ($value as $xPe) + { + // @TODO + } + break; + case 'kt': + foreach ($value as $kt) + { + $db->initMagnetToKeywordTopicId( + $magnetId, + $db->initKeywordTopicId(trim(mb_strtolower(strip_tags(html_entity_decode($kt))))) + ); + } + break; + } + } + + $db->commit(); + + // Redirect to edit page + header(sprintf('Location: %s/edit.php?magnetId=%s', trim(WEBSITE_URL, '/'), $magnetId)); + } + } + + } catch (Exception $e) { + + var_dump($e); + + $db->rollBack(); + } + } + + break; + default: + header( + sprintf('Location: %s', WEBSITE_URL) + ); +} + +?> + + + + + + + + <?php echo sprintf(_('Oops - %s'), WEBSITE_NAME) ?> + + + + + + +
+
+ +
+
+
+
+
+
+
+
message ?>
+
+
+
+
+
+
+
+
+
+ + | + $value) { ?> + + / + + | + + +
+
+
+
+ + \ No newline at end of file diff --git a/src/public/assets/theme/default/css/common.css b/src/public/assets/theme/default/css/common.css new file mode 100644 index 0000000..c0e8cc4 --- /dev/null +++ b/src/public/assets/theme/default/css/common.css @@ -0,0 +1,71 @@ +* { + border: 0; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: #282b3c; + color: #ccc; + font-family: Sans-serif; + font-size: 13px; +} + +h1, h2, h3, h4, h5 { + display: inline-block; + font-weight: normal; +} + +h2 { + font-size: 16px; +} + +a, +a:visited, +a:active { + color: #96d9a1; + text-decoration: none; + opacity: .9; +} + +a:hover { + opacity: 1; + transition: opacity .5s ease-in-out; +} + +textarea:focus, +input:focus { + outline: none; + color: #fff; +} + +textarea::placeholder, +input::placeholder { + color: #9698a5; + opacity: 1; +} + +input, +textarea { + background: #5d627d; + color: #ccc; + border: 0; + border-radius: 3px; + padding: 6px 8px; + font-size: 13px; +} + +input[type="submit"] { + cursor: pointer; +} + + +header a.logo { + color: #ccc; + font-size: 22px; +} + +header a.logo > span { + color: #96d9a1; +} \ No newline at end of file diff --git a/src/public/assets/theme/default/css/framework.css b/src/public/assets/theme/default/css/framework.css new file mode 100644 index 0000000..bd7f949 --- /dev/null +++ b/src/public/assets/theme/default/css/framework.css @@ -0,0 +1,234 @@ +.container { + position: relative; + overflow: hidden; + max-width: 748px; + margin: 0 auto; +} + +.row { + position: relative; + overflow: hidden; + padding: 8px; +} + +.column { + position: relative; + float: left; +} + +.float-right { + float: right; +} + +.text-center { + text-align: center; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-color-green { + color: #96d9a1; +} + +.text-color-red { + color: #d77575; +} + +.text-color-pink { + color: #a44399; +} + +.text-color-blue { + color: #5785b7; +} + +.label { + padding: 4px 8px; + border-radius: 3px; +} + +.label-green { + color: #277b1b; + border: 1px #92bc8c solid; +} + +.line-height-26 { + line-height: 26px; +} + +.border-radius-3 { + border-radius: 3px; +} + +.border-pink-light { + border: 1px #9b6895 solid; +} + +.border-pink { + border: 1px #a44399 solid; +} + +.border-bottom-pink { + border-bottom: 1px #a44399 solid; +} + +.border-bottom-default { + border-bottom: 1px #5d627d solid; +} + +.background-color-night { + background-color: #34384f; +} + +.background-color-red { + background-color: #9b4a4a; +} + +.cursor-default { + cursor: default; +} + +.cursor-help { + cursor: help; +} + +.font-width-normal { + font-weight: normal; +} + +.font-size-12 { + font-size: 12px; +} + +.font-size-22 { + font-size: 22px; +} + +.padding-0 { + padding: 0; +} + +.padding-x-0 { + padding-left: 0; + padding-right: 0; +} + +.padding-4 { + padding: 4px; +} + +.padding-x-4 { + padding-left: 4px; + padding-right: 4px; +} + +.padding-8 { + padding: 8px; +} + +.padding-y-8 { + padding-top: 8px; + padding-bottom: 8px; +} + +.padding-x-16 { + padding-left: 16px; + padding-right: 16px; +} + +.padding-16 { + padding: 16px; +} + +.margin-l-8 { + margin-left: 8px; +} + +.margin-r-8 { + margin-right: 8px; +} + +.margin-l-12 { + margin-left: 12px; +} + +.margin-y-8 { + margin-top: 8px; + margin-bottom: 8px; +} + +.margin-t-8 { + margin-top: 8px; +} + +.margin-b-8 { + margin-bottom: 8px; +} + +.margin-b-16 { + margin-bottom: 16px; +} + +.display-block { + display: block; +} + +.opacity-0 { + opacity: 0; +} + +.opacity-06 { + opacity: .6; +} + +.opacity-hover-1:hover { + opacity: 1; + transition: opacity .2s ease-in-out; +} + +*:hover > .parent-hover-opacity-09 { + opacity: .9; + transition: opacity .2s ease-in-out; +} + +.bloor-2 { + filter: blur(2px); +} + +.bloor-hover-0:hover { + filter: blur(0); +} + +/* responsive rules */ + +.width-100 { + width: 100%; +} + +.width-50 { + width: 50%; +} + +.width-13px { + width: 13px; +} + +@media (max-width: 1220px) { + + .width-tablet-100 { + width: 100%; + } +} + +@media (max-width: 512px) { + + .width-mobile-100 { + width: 100%; + } +} diff --git a/src/public/edit.php b/src/public/edit.php new file mode 100644 index 0000000..d25dcf5 --- /dev/null +++ b/src/public/edit.php @@ -0,0 +1,560 @@ + true, + 'message' => false, + 'form' => (object) + [ + 'metaTitle' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'metaDescription' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'dn' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'kt' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'tr' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'as' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'xs' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'public' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'comments' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + 'sensitive' => (object) + [ + 'value' => false, + 'valid' => (object) + [ + 'success' => true, + 'message' => false, + ] + ], + ] +]; + +// Yggdrasil connections only +if (!preg_match(YGGDRASIL_URL_REGEX, $_SERVER['REMOTE_ADDR'])) +{ + $response->success = false; + $response->message = _('Yggdrasil connection required to enable resource features'); +} + +// Init session +else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time())) +{ + $response->success = false; + $response->message = _('Could not init user session'); +} + +// Init magnet +else if (!$magnet = $db->getMagnet(isset($_GET['magnetId']) ? (int) $_GET['magnetId'] : 0)) { + + $response->success = false; + $response->message = _('Magnet not found!'); +} + +// Validate access +else if (!($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST))) { + + $response->success = false; + $response->message = _('You have no permissions to edit this magnet!'); +} + +// Process form +else { + + // Update form + if (!empty($_POST)) { + + // Approve by moderation request + if (isset($_POST['approved']) && in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST)) + { + $db->updateMagnetApproved($magnet->magnetId, true, time()); + } + + // Set default approve status + else + { + $db->updateMagnetApproved($magnet->magnetId, MAGNET_DEFAULT_APPROVED, time()); + } + + // Meta + if (MAGNET_META_TITLE_MIN_LENGTH <= mb_strlen($_POST['metaTitle'])) + { + $db->updateMagnetMetaTitle($magnet->magnetId, trim(strip_tags(html_entity_decode($_POST['metaTitle']))), time()); + + $response->form->metaTitle->valid->success = true; + $response->form->metaTitle->valid->message = false; + } + else + { + $response->form->metaTitle->valid->success = false; + $response->form->metaTitle->valid->message = sprintf(_('* required, minimum %s chars'), MAGNET_META_TITLE_MIN_LENGTH); + } + + if (MAGNET_META_DESCRIPTION_MIN_LENGTH <= mb_strlen($_POST['metaDescription'])) + { + $db->updateMagnetMetaDescription($magnet->magnetId, trim(strip_tags(html_entity_decode($_POST['metaDescription']))), time()); + } + else + { + $response->form->metaDescription->valid->success = false; + $response->form->metaDescription->valid->message = sprintf(_('* required, minimum %s chars'), MAGNET_META_DESCRIPTION_MIN_LENGTH); + } + + // Social + $db->updateMagnetPublic($magnet->magnetId, isset($_POST['public']) ? true : false, time()); + $db->updateMagnetComments($magnet->magnetId, isset($_POST['comments']) ? true : false, time()); + $db->updateMagnetSensitive($magnet->magnetId, isset($_POST['sensitive']) ? true : false, time()); + + // Display Name + if (isset($_POST['dn'])) + { + $db->updateMagnetDn($magnet->magnetId, trim(strip_tags(html_entity_decode($_POST['dn']))), time()); + } + + // Keyword Topic + $db->deleteMagnetToKeywordTopicByMagnetId($magnet->magnetId); + + if (!empty($_POST['kt'])) + { + foreach (explode(PHP_EOL, str_replace(['#', ',', ' '], PHP_EOL, $_POST['kt'])) as $kt) + { + if (!empty(trim($kt))) + { + $db->initMagnetToKeywordTopicId( + $magnet->magnetId, + $db->initKeywordTopicId(trim(mb_strtolower(strip_tags(html_entity_decode($kt))))) + ); + } + } + } + + // Address Tracker + $db->deleteMagnetToAddressTrackerByMagnetId($magnet->magnetId); + + if (!empty($_POST['tr'])) + { + $response->form->tr->valid->success = false; + $response->form->tr->valid->message = _('* please, provide at least one Yggdrasil address'); + + foreach (explode(PHP_EOL, str_replace(['#', ',', ' '], PHP_EOL, $_POST['tr'])) as $tr) + { + $tr = trim($tr); + + if (!empty($tr)) + { + if ($url = Yggverse\Parser\Url::parse($tr)) + { + $db->initMagnetToAddressTrackerId( + $magnet->magnetId, + $db->initAddressTrackerId( + $db->initSchemeId($url->host->scheme), + $db->initHostId($url->host->name), + $db->initPortId($url->host->port), + $db->initUriId($url->page->uri) + ) + ); + + if (preg_match(YGGDRASIL_URL_REGEX, $url->host->name)) + { + $response->form->tr->valid->success = true; + $response->form->tr->valid->message = false; + } + } + } + } + } + + // Acceptable Source + $db->deleteMagnetToAcceptableSourceByMagnetId($magnet->magnetId); + + if (!empty($_POST['as'])) + { + $response->form->as->valid->success = false; + $response->form->as->valid->message = _('* please, provide at least one Yggdrasil address'); + + foreach (explode(PHP_EOL, str_replace(['#', ',', ' '], PHP_EOL, $_POST['as'])) as $as) + { + $as = trim($as); + + if (!empty($as)) + { + if ($url = Yggverse\Parser\Url::parse($as)) + { + $db->initMagnetToAcceptableSourceId( + $magnet->magnetId, + $db->initAcceptableSourceId( + $db->initSchemeId($url->host->scheme), + $db->initHostId($url->host->name), + $db->initPortId($url->host->port), + $db->initUriId($url->page->uri) + ) + ); + + if (preg_match(YGGDRASIL_URL_REGEX, $url->host->name)) + { + $response->form->as->valid->success = true; + $response->form->as->valid->message = false; + } + } + } + } + } + + // Exact Source + $db->deleteMagnetToExactSourceByMagnetId($magnet->magnetId); + + if (!empty($_POST['xs'])) + { + $response->form->xs->valid->success = false; + $response->form->xs->valid->message = _('* please, provide at least one Yggdrasil address'); + + foreach (explode(PHP_EOL, str_replace(['#', ',', ' '], PHP_EOL, $_POST['xs'])) as $xs) + { + $xs = trim($xs); + + if (!empty($xs)) + { + if ($url = Yggverse\Parser\Url::parse($xs)) + { + $db->initMagnetToExactSourceId( + $magnet->magnetId, + $db->initExactSourceId( + $db->initSchemeId($url->host->scheme), + $db->initHostId($url->host->name), + $db->initPortId($url->host->port), + $db->initUriId($url->page->uri) + ) + ); + + if (preg_match(YGGDRASIL_URL_REGEX, $url->host->name)) + { + $response->form->xs->valid->success = true; + $response->form->xs->valid->message = false; + } + } + } + } + } + + // Refresh magnet data + $magnet = $db->getMagnet($magnet->magnetId); + } + + // Meta Title, auto-replace with Display Name on empty value + $response->form->metaTitle->value = $magnet->metaTitle ? $magnet->metaTitle : $magnet->dn; + + // Meta Description + $response->form->metaDescription->value = $magnet->metaDescription; + + // Magnet settings + $response->form->public->value = (bool) $magnet->public; + $response->form->comments->value = (bool) $magnet->comments; + $response->form->sensitive->value = (bool) $magnet->sensitive; + + // Display Name + $response->form->dn->value = $magnet->dn; + + // Keyword Topic + $kt = []; + foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result) + { + $kt[] = $db->getKeywordTopic($result->keywordTopicId)->value; + } + + $response->form->kt->value = implode(', ', $kt); + + // Address Tracker + $tr = []; + foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $result) + { + $addressTracker = $db->getAddressTracker($result->addressTrackerId); + + $scheme = $db->getScheme($addressTracker->schemeId); + $host = $db->getHost($addressTracker->hostId); + $port = $db->getPort($addressTracker->portId); + $uri = $db->getUri($addressTracker->uriId); + + $tr[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value); + } + + $response->form->tr->value = implode(PHP_EOL, $tr); + + // Acceptable Source + $as = []; + foreach ($db->findAcceptableSourceByMagnetId($magnet->magnetId) as $result) + { + $acceptableSource = $db->getAcceptableSource($result->acceptableSourceId); + + $scheme = $db->getScheme($acceptableSource->schemeId); + $host = $db->getHost($acceptableSource->hostId); + $port = $db->getPort($acceptableSource->portId); + $uri = $db->getUri($acceptableSource->uriId); + + $as[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value); + } + + $response->form->as->value = implode(PHP_EOL, $as); + + // Exact Source + $xs = []; + foreach ($db->findExactSourceByMagnetId($magnet->magnetId) as $result) + { + $eXactSource = $db->getExactSource($result->eXactSourceId); + + $scheme = $db->getScheme($eXactSource->schemeId); + $host = $db->getHost($eXactSource->hostId); + $port = $db->getPort($eXactSource->portId); + $uri = $db->getUri($eXactSource->uriId); + + $xs[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value); + } + + $response->form->xs->value = implode(PHP_EOL, $xs); +} + +?> + + + + + + + + <?php echo sprintf(_('Edit - %s'), WEBSITE_NAME) ?> + + + + + + + +
+
+ +
+
+
+
+
+
+ success) { ?> +
+ + +
+
+

+
+
+ + + form->metaTitle->valid->message) { ?> +
form->metaTitle->valid->message ?>
+ + + + form->metaDescription->valid->message) { ?> +
form->metaDescription->valid->message ?>
+ + +
+
+ + + + + + + form->tr->valid->message) { ?> +
form->tr->valid->message ?>
+ + + + form->as->valid->message) { ?> +
form->as->valid->message ?>
+ + + + form->xs->valid->message) { ?> +
form->xs->valid->message ?>
+ + +
+
+ +
+ form->public->value) { ?> + + + + + +
+
+ form->comments->value) { ?> + + + + + +
+
+ form->sensitive->value) { ?> + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+
+
+ +
+
message ?>
+
+ +
+
+
+
+
+
+
+
+ + | + $value) { ?> + + / + + | + + +
+
+
+
+ + \ No newline at end of file diff --git a/src/public/index.php b/src/public/index.php new file mode 100644 index 0000000..9aca5f1 --- /dev/null +++ b/src/public/index.php @@ -0,0 +1,336 @@ + false, + 'page' => 1, +]; + +// Prepare request +$request->query = isset($_GET['query']) ? urldecode((string) $_GET['query']) : ''; +$request->page = isset($_GET['page']) && $_GET['page'] > 0 ? (int) $_GET['page'] : 1; + +// Define response +$response = (object) +[ + 'success' => true, + 'message' => false, + 'magnets' => [], +]; + +// Yggdrasil connections only +if (!preg_match(YGGDRASIL_URL_REGEX, $_SERVER['REMOTE_ADDR'])) +{ + $response->success = false; + $response->message = _('Yggdrasil connection required to enable resource features'); +} + +// Init session +else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time())) +{ + $response->success = false; + $response->message = _('Could not init user session'); +} + +// Request valid +else +{ + // Query is magnet link + if ($magnet = Yggverse\Parser\Magnet::is($request->query)) + { + header( + sprintf('Location: %s/action.php?target=new&magnet=%s', WEBSITE_URL, base64_encode($request->query)) + ); + } + + // Get index + $total = $sphinx->searchMagnetsTotal($request->query); + $results = $sphinx->searchMagnets( + $request->query, + $request->page * WEBSITE_PAGINATION_LIMIT - WEBSITE_PAGINATION_LIMIT, + WEBSITE_PAGINATION_LIMIT, + $total + ); + + foreach ($results as $result) + { + if ($magnet = $db->getMagnet($result->magnetid)) + { + $keywords = []; + + foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $keyword) + { + $keywords[] = $db->getKeywordTopic($keyword->keywordTopicId)->value; + } + + $response->magnets[] = (object) + [ + 'magnetId' => $magnet->magnetId, + 'metaTitle' => $magnet->metaTitle ? htmlentities($magnet->metaTitle) : ($magnet->dn ? htmlentities($magnet->dn): false), + 'metaDescription' => $magnet->metaDescription ? nl2br( + htmlentities( + substr($magnet->metaDescription, 0, WEBSITE_MAGNET_SHORT_META_DESCRIPTION_LENGTH) + ) + ) : false, + 'approved' => (bool) $magnet->approved, + 'public' => (bool) $magnet->public, + 'sensitive' => (bool) $magnet->sensitive, + 'comments' => (bool) $magnet->comments, + 'timeAdded' => Time::ago($magnet->timeAdded), + 'timeUpdated' => Time::ago($magnet->timeUpdated), + 'keywords' => $keywords, + 'comment' => (object) + [ + 'total' => $db->getMagnetCommentsTotal($magnet->magnetId), + 'status' => $db->findMagnetCommentsTotalByUserId($magnet->magnetId, $userId), + ], + 'download' => (object) + [ + 'total' => $db->getMagnetDownloadsTotal($magnet->magnetId), + 'status' => $db->findMagnetDownloadsTotalByUserId($magnet->magnetId, $userId), + ], + 'star' => (object) + [ + 'total' => $db->getMagnetStarsTotal($magnet->magnetId), + 'status' => $db->findMagnetStarsTotalByUserId($magnet->magnetId, $userId), + ], + 'access' => (object) + [ + 'read' => ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved)), + 'edit' => ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST)), + ], + ]; + } + } +} + +if (isset($_GET['rss']) && $response->success) { ?>' . PHP_EOL ?> + + + + <?php echo WEBSITE_NAME ?> + + /index.phpquery ? sprintf('?query=%s', urlencode($request->query)) : false ?> + magnets as $magnet) { ?> + access->read) { ?> + + <?php echo htmlspecialchars($magnet->metaTitle, ENT_QUOTES, 'UTF-8') ?> + #magnet-magnetId ?> + metaDescription), ENT_QUOTES, 'UTF-8') ?> + #magnet-magnetId ?> + + + + + + + + + + + + + <?php echo sprintf(_('%s - BitTorrent Catalog for Yggdrasil'), WEBSITE_NAME) ?> + + + + + + + +
+
+ +
+
+
+
+
+
+ success) { ?> + magnets) { ?> + magnets as $magnet) { ?> + access->read) { ?> +
+
+ +

metaTitle ?>

+
+ access->edit) { ?> + + + + + + + + + +
+ metaDescription) { ?> +
metaDescription ?>
+ + keywords) { ?> +
+ keywords as $keyword) { ?> + + # + + +
+ +
+
+ public) { ?> + + + + + + + + approved) { ?> + + + + + + + timeUpdated ? sprintf('Updated %s', $magnet->timeUpdated) : sprintf('Added %s', $magnet->timeAdded) ?> + + + star->status) { ?> + + + + + + + + + + star->total ?> + + + + + download->status) { ?> + + + + + + + + + + download->total ?> + +
+
+ +
+
+
+ + + +
+

+
+
+ + +
+
message ?>
+
+ +
+
+
+
+
+
+
+
+ + | + $value) { ?> + + / + + | + + +
+
+
+
+ + + \ No newline at end of file