Browse Source

init symfony framework #14

main
ghost 1 year ago
parent
commit
380377b27c
  1. 50
      .env
  2. 6
      .env.test
  3. 42
      .gitignore
  4. 17
      bin/console
  5. 19
      bin/phpunit
  6. 106
      composer.json
  7. 9748
      composer.lock
  8. 14
      config/bundles.php
  9. 19
      config/packages/cache.yaml
  10. 5
      config/packages/debug.yaml
  11. 48
      config/packages/doctrine.yaml
  12. 6
      config/packages/doctrine_migrations.yaml
  13. 25
      config/packages/framework.yaml
  14. 3
      config/packages/mailer.yaml
  15. 24
      config/packages/messenger.yaml
  16. 61
      config/packages/monolog.yaml
  17. 13
      config/packages/notifier.yaml
  18. 12
      config/packages/routing.yaml
  19. 39
      config/packages/security.yaml
  20. 15
      config/packages/translation.yaml
  21. 9
      config/packages/twig.yaml
  22. 13
      config/packages/validator.yaml
  23. 17
      config/packages/web_profiler.yaml
  24. 5
      config/preload.php
  25. 5
      config/routes.yaml
  26. 4
      config/routes/framework.yaml
  27. 8
      config/routes/web_profiler.yaml
  28. 26
      config/services.yaml
  29. BIN
      database/yggtracker.mwb
  30. 14
      docker-compose.override.yml
  31. 21
      docker-compose.yml
  32. 10
      example/environment/crontab
  33. 29
      example/environment/nginx
  34. 71
      example/environment/sphinx.conf
  35. 0
      migrations/.gitignore
  36. 38
      phpunit.xml.dist
  37. 0
      public/asset/default/css/common.css
  38. 74
      public/asset/default/css/framework.css
  39. 9
      public/index.php
  40. 0
      src/Controller/.gitignore
  41. 20
      src/Controller/HomeController.php
  42. 67
      src/Controller/PageController.php
  43. 76
      src/Controller/ProfileController.php
  44. 31
      src/Controller/SearchController.php
  45. 0
      src/Entity/.gitignore
  46. 27
      src/Entity/Page.php
  47. 147
      src/Entity/User.php
  48. 11
      src/Kernel.php
  49. 0
      src/Repository/.gitignore
  50. 48
      src/Repository/PageRepository.php
  51. 48
      src/Repository/UserRepository.php
  52. 12
      src/Service/User.php
  53. 151
      src/app/controller/index.php
  54. 13
      src/app/controller/module/footer.php
  55. 54
      src/app/controller/module/head.php
  56. 19
      src/app/controller/module/header.php
  57. 9
      src/app/controller/module/page.php
  58. 43
      src/app/controller/module/pagination.php
  59. 54
      src/app/controller/module/profile.php
  60. 24
      src/app/controller/module/search.php
  61. 428
      src/app/controller/page.php
  62. 65
      src/app/controller/response.php
  63. 117
      src/app/controller/user.php
  64. 1866
      src/app/model/database.php
  65. 37
      src/app/model/locale.php
  66. 104
      src/app/model/request.php
  67. 111
      src/app/model/sphinx.php
  68. 2216
      src/app/model/validator.php
  69. 46
      src/app/model/website.php
  70. 32
      src/app/view/theme/default/index.phtml
  71. 27
      src/app/view/theme/default/module/footer.phtml
  72. 7
      src/app/view/theme/default/module/head.phtml
  73. 10
      src/app/view/theme/default/module/header.phtml
  74. 126
      src/app/view/theme/default/module/page.phtml
  75. 15
      src/app/view/theme/default/module/pagination.phtml
  76. 20
      src/app/view/theme/default/module/search.phtml
  77. 140
      src/app/view/theme/default/page/form/submit.phtml
  78. 24
      src/app/view/theme/default/response.phtml
  79. 160
      src/config/bootstrap.php
  80. 7
      src/config/database.json
  81. 277
      src/config/locales.json
  82. 6
      src/config/memcached.json
  83. 3
      src/config/moderators.json
  84. 12
      src/config/nodes.json
  85. 7
      src/config/peers.json
  86. 4
      src/config/sphinx.json
  87. 3
      src/config/themes.json
  88. 23
      src/config/trackers.json
  89. 74
      src/config/validator.json
  90. 23
      src/config/website.json
  91. 558
      src/crontab/export/feed.php
  92. 506
      src/crontab/export/push.php
  93. 1193
      src/crontab/import/feed.php
  94. 158
      src/crontab/scrape.php
  95. 86
      src/crontab/sitemap.php
  96. 97
      src/library/curl.php
  97. 27
      src/library/environment.php
  98. 48
      src/library/filter.php
  99. 692
      src/library/scrapeer.php
  100. 45
      src/library/time.php
  101. Some files were not shown because too many files have changed in this diff Show More

50
.env

@ -0,0 +1,50 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=EDITME
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> symfony/mailer ###
# MAILER_DSN=null://null
###< symfony/mailer ###
###> symfony/crowdin-translation-provider ###
# CROWDIN_DSN=crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default
###< symfony/crowdin-translation-provider ###
# YGGtracker
APP_VERSION='2.0.0'
APP_NAME=YGGtracker

6
.env.test

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

42
.gitignore vendored

@ -1,30 +1,22 @@
/.vscode/
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/ /vendor/
###< symfony/framework-bundle ###
/database/* ###> phpunit/phpunit ###
!/database/yggtracker.mwb /phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
/src/public/api/*.json ###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
/src/config/* .vscode
!/src/config/bootstrap.php
!/src/config/website.json
!/src/config/locales.json
!/src/config/themes.json
!/src/config/sphinx.json
!/src/config/memcached.json
!/src/config/database.json
!/src/config/validator.json
!/src/config/moderators.json
!/src/config/nodes.json
!/src/config/trackers.json
!/src/config/peers.json
/src/public/sitemap.xml
/src/storage/log/*.log
/composer.lock
*test*

17
bin/console

@ -0,0 +1,17 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

19
bin/phpunit

@ -0,0 +1,19 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

106
composer.json

@ -1,24 +1,108 @@
{ {
"name": "yggverse/yggtracker", "name": "yggverse/yggtracker",
"description": "Public BitTorrent tracker for Yggdrasil network", "description": "BitTorrent tracker for Yggdrasil network",
"type": "project", "type": "project",
"license": "MIT",
"minimum-stability": "stable",
"prefer-stable": true,
"require": { "require": {
"php": "^8.1", "php": ">=8.1",
"yggverse/cache": ">=0.3.0", "ext-ctype": "*",
"yggverse/parser": ">=0.4.0", "ext-iconv": "*",
"christeredvartsen/php-bittorrent": "^2.0",
"doctrine/annotations": "^2.0",
"doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.16",
"jdenticon/jdenticon": "^1.0", "jdenticon/jdenticon": "^1.0",
"christeredvartsen/php-bittorrent": "^2.0" "phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.24",
"symfony/asset": "6.3.*",
"symfony/console": "6.3.*",
"symfony/crowdin-translation-provider": "6.3.*",
"symfony/doctrine-messenger": "6.3.*",
"symfony/dotenv": "6.3.*",
"symfony/expression-language": "6.3.*",
"symfony/flex": "^2",
"symfony/form": "6.3.*",
"symfony/framework-bundle": "6.3.*",
"symfony/http-client": "6.3.*",
"symfony/intl": "6.3.*",
"symfony/mailer": "6.3.*",
"symfony/mime": "6.3.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.3.*",
"symfony/process": "6.3.*",
"symfony/property-access": "6.3.*",
"symfony/property-info": "6.3.*",
"symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*",
"symfony/string": "6.3.*",
"symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*",
"symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*",
"symfony/yaml": "6.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
}, },
"license": "MIT",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Yggverse\\Yggtracker\\": "src/" "App\\": "src/"
} }
}, },
"authors": [ "autoload-dev": {
{ "psr-4": {
"name": "YGGverse" "App\\Tests\\": "tests/"
} }
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
], ],
"minimum-stability": "alpha" "post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.3.*"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*",
"symfony/debug-bundle": "6.3.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^6.3",
"symfony/stopwatch": "6.3.*",
"symfony/web-profiler-bundle": "6.3.*"
}
} }

9748
composer.lock generated

File diff suppressed because it is too large Load Diff

14
config/bundles.php

@ -0,0 +1,14 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
];

19
config/packages/cache.yaml

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

5
config/packages/debug.yaml

@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

48
config/packages/doctrine.yaml

@ -0,0 +1,48 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '15'
profiling_collect_backtrace: '%kernel.debug%'
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

6
config/packages/doctrine_migrations.yaml

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

25
config/packages/framework.yaml

@ -0,0 +1,25 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
http_method_override: false
handle_all_throwables: true
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
#esi: true
#fragments: true
php_errors:
log: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

3
config/packages/mailer.yaml

@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

24
config/packages/messenger.yaml

@ -0,0 +1,24 @@
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
# 'App\Message\YourMessage': async

61
config/packages/monolog.yaml

@ -0,0 +1,61 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr

13
config/packages/notifier.yaml

@ -0,0 +1,13 @@
framework:
notifier:
chatter_transports:
texter_transports:
crowdin: '%env(CROWDIN_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

12
config/packages/routing.yaml

@ -0,0 +1,12 @@
framework:
router:
utf8: true
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

39
config/packages/security.yaml

@ -0,0 +1,39 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

15
config/packages/translation.yaml

@ -0,0 +1,15 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
# providers:
# crowdin:
# dsn: '%env(CROWDIN_DSN)%'
# loco:
# dsn: '%env(LOCO_DSN)%'
# lokalise:
# dsn: '%env(LOKALISE_DSN)%'
# phrase:
# dsn: '%env(PHRASE_DSN)%'

9
config/packages/twig.yaml

@ -0,0 +1,9 @@
twig:
default_path: '%kernel.project_dir%/templates'
globals:
version: '%app.version%'
name: '%app.name%'
when@test:
twig:
strict_variables: true

13
config/packages/validator.yaml

@ -0,0 +1,13 @@
framework:
validation:
email_validation_mode: html5
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

17
config/packages/web_profiler.yaml

@ -0,0 +1,17 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
only_exceptions: false
collect_serializer_data: true
when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

5
config/preload.php

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

5
config/routes.yaml

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

4
config/routes/framework.yaml

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

8
config/routes/web_profiler.yaml

@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

26
config/services.yaml

@ -0,0 +1,26 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
app.version: '%env(APP_VERSION)%'
app.name: '%env(APP_NAME)%'
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

BIN
database/yggtracker.mwb

Binary file not shown.

14
docker-compose.override.yml

@ -0,0 +1,14 @@
version: '3'
services:
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: schickling/mailcatcher
ports: ["1025", "1080"]
###< symfony/mailer ###

21
docker-compose.yml

@ -0,0 +1,21 @@
version: '3'
services:
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-15}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

10
example/environment/crontab

@ -1,10 +0,0 @@
@reboot searchd
@reboot indexer --all --rotate
* * * * * indexer magnet --rotate > /dev/null 2>&1
* * * * * /usr/bin/php /YGGtracker/src/crontab/scrape.php > /dev/null 2>&1
* * * * * /usr/bin/php /YGGtracker/src/crontab/export/push.php > /dev/null 2>&1
0 5 * * * /usr/bin/php /YGGtracker/src/crontab/import/feed.php > /dev/null 2>&1
0 0 * * * /usr/bin/php /YGGtracker/src/crontab/export/feed.php > /dev/null 2>&1
0 0 * * * /usr/bin/php /YGGtracker/src/crontab/sitemap.php > /dev/null 2>&1

29
example/environment/nginx

@ -1,29 +0,0 @@
server {
listen [::]:80 default;
allow 0200::/7;
deny all;
root /var/www/html;
index index.html index.htm index.nginx-debian.html index.php;
server_name _;
location / {
try_files $uri $uri/ =404 @yggtracker;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
location ~ /\. {
deny all;
}
location @yggtracker {
rewrite ^/(.*)$ /index.php?$1 last;
}
}

71
example/environment/sphinx.conf

@ -1,71 +0,0 @@
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`.`title`, \
`magnet`.`preview`, \
`magnet`.`description`, \
`magnet`.`dn`, \
(SELECT GROUP_CONCAT(DISTINCT `infoHash`.`value`) \
FROM `infoHash` \
JOIN `magnetToInfoHash` ON (`magnetToInfoHash`.`magnetId` = `magnet`.`magnetId`) \
WHERE `infoHash`.`infoHashId` = `magnetToInfoHash`.`infoHashId`) AS `infoHash`, \
(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, stem_enru
min_word_len = 2
min_prefix_len = 2
html_strip = 1
index_exact_words = 1
}
indexer
{
mem_limit = 256M
}
searchd
{
listen = 127.0.0.1:9306:mysql41
log = /var/log/sphinxsearch/searchd.log
query_log = /var/log/sphinxsearch/query.log
pid_file = /run/sphinxsearch/searchd.pid
binlog_path = /var/lib/sphinxsearch/data
read_timeout = 5
max_children = 30
seamless_rotate = 1
preopen_indexes = 1
unlink_old = 1
workers = threads # for RT to work
}

0
src/storage/log/index.html → migrations/.gitignore vendored

38
phpunit.xml.dist

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertDeprecationsToExceptions="false"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
</extensions>
</phpunit>

0
src/public/assets/theme/default/css/common.css → public/asset/default/css/common.css

74
src/public/assets/theme/default/css/framework.css → public/asset/default/css/framework.css

@ -122,19 +122,19 @@ a.label-green:hover {
vertical-align: middle; vertical-align: middle;
} }
.top--2 { .top--2-px {
top: -2px; top: -2px;
} }
.top-2 { .top-2-px {
top: 2px; top: 2px;
} }
.line-height-26 { .line-height-26-px {
line-height: 26px; line-height: 26px;
} }
.border-radius-3 { .border-radius-3-px {
border-radius: 3px; border-radius: 3px;
} }
@ -227,138 +227,138 @@ a:visited.background-color-hover-night-light:hover {
padding-right: 0; padding-right: 0;
} }
.padding-4 { .padding-4-px {
padding: 4px; padding: 4px;
} }
.padding-t-4 { .padding-t-4-px {
padding-top: 4px; padding-top: 4px;
} }
.padding-y-4 { .padding-y-4-px {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
} }
.padding-x-4 { .padding-x-4-px {
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;
} }
.padding-x-8 { .padding-x-8-px {
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
} }
.padding-y-6 { .padding-y-6-px {
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
} }
.padding-8 { .padding-8-px {
padding: 8px; padding: 8px;
} }
.padding-t-8 { .padding-t-8-px {
padding-top: 8px; padding-top: 8px;
} }
.padding-b-8 { .padding-b-8-px {
padding-bottom: 8px; padding-bottom: 8px;
} }
.padding-y-8 { .padding-y-8-px {
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
} }
.padding-y-12 { .padding-y-12-px {
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
} }
.padding-b-16 { .padding-b-16-px {
padding-bottom: 16px; padding-bottom: 16px;
} }
.padding-t-16 { .padding-t-16-px {
padding-top: 16px; padding-top: 16px;
} }
.padding-y-16 { .padding-y-16-px {
padding-top: 16px; padding-top: 16px;
padding-bottom: 16px; padding-bottom: 16px;
} }
.padding-x-16 { .padding-x-16-px {
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
} }
.padding-16 { .padding-16-px {
padding: 16px; padding: 16px;
} }
.margin-l-4 { .margin-l-4-px {
margin-left: 4px; margin-left: 4px;
} }
.margin-l-8 { .margin-l-8-px {
margin-left: 8px; margin-left: 8px;
} }
.margin-l-16 { .margin-l-16-px {
margin-left: 16px; margin-left: 16px;
} }
.margin-x-4 { .margin-x-4-px {
margin-left: 4px; margin-left: 4px;
margin-right: 4px; margin-right: 4px;
} }
.margin-r-4 { .margin-r-4-px {
margin-right: 4px; margin-right: 4px;
} }
.margin-r-8 { .margin-r-8-px {
margin-right: 8px; margin-right: 8px;
} }
.margin-l-12 { .margin-l-12-px {
margin-left: 12px; margin-left: 12px;
} }
.margin-l--196 { .margin-l--196-px {
margin-left: -196px; margin-left: -196px;
} }
.margin-y-8 { .margin-y-8-px {
margin-top: 8px; margin-top: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.margin-t-8 { .margin-t-8-px {
margin-top: 8px; margin-top: 8px;
} }
.margin-b-8 { .margin-b-8-px {
margin-bottom: 8px; margin-bottom: 8px;
} }
.margin-y-16 { .margin-y-16-px {
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.margin-t-16 { .margin-t-16-px {
margin-top: 16px; margin-top: 16px;
} }
.margin-b-16 { .margin-b-16-px {
margin-bottom: 16px; margin-bottom: 16px;
} }
.margin-b-24 { .margin-b-24-px {
margin-bottom: 24px; margin-bottom: 24px;
} }
@ -410,10 +410,6 @@ a:visited.background-color-hover-night-light:hover {
width: 80%; width: 80%;
} }
.width-13px {
width: 13px;
}
.width-180-px { .width-180-px {
width: 180px; width: 180px;
} }

9
public/index.php

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

0
src/Controller/.gitignore vendored

20
src/Controller/HomeController.php

@ -0,0 +1,20 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class HomeController extends AbstractController
{
#[Route(
'/{_locale}/',
name: 'home_index'
)]
public function index(Request $request): Response
{
return $this->render('default/home/index.html.twig');
}
}

67
src/Controller/PageController.php

@ -0,0 +1,67 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class PageController extends AbstractController
{
#[Route(
'/{_locale}/page/submit',
name: 'page_submit'
)]
public function submit(): Response
{
return $this->render('default/page/submit.html.twig', [
// @TODO
]);
}
#[Route(
'/{_locale}/page/stars',
name: 'page_stars'
)]
public function stars(): Response
{
// @TODO
}
#[Route(
'/{_locale}/page/views',
name: 'page_views'
)]
public function views(): Response
{
// @TODO
}
#[Route(
'/{_locale}/page/downloads',
name: 'page_downloads'
)]
public function downloads(): Response
{
// @TODO
}
#[Route(
'/{_locale}/page/comments',
name: 'page_comments'
)]
public function comments(): Response
{
// @TODO
}
#[Route(
'/{_locale}/page/editions',
name: 'page_editions'
)]
public function editions(): Response
{
// @TODO
}
}

76
src/Controller/ProfileController.php

@ -0,0 +1,76 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Service\User;
class ProfileController extends AbstractController
{
#[Route(
'/{_locale}/profile',
name: 'profile_index'
)]
public function index(Request $request, User $user): Response
{
return $this->render(
'default/profile/index.html.twig',
[
'user' => $user->init($request->getClientIp())
]
);
}
#[Route(
'/{_locale}/profile/setting',
name: 'profile_setting'
)]
public function setting(): Response
{
// @TODO
return $this->render(
'default/profile/setting.html.twig'
);
}
public function module(string $route = ''): Response
{
return $this->render(
'default/profile/module.html.twig',
[
'route' => $route,
'stars' => 0,
'views' => 0,
'comments' => 0,
'downloads' => 0,
'editions' => 0,
'identicon' => $this->_getIdenticon(
'@TODO',
17,
[
'backgroundColor' => 'rgba(255, 255, 255, 0)',
]
)
]
);
}
private function _getIdenticon(
mixed $id,
int $size,
array $style,
string $format = 'webp') : string
{
$identicon = new \Jdenticon\Identicon();
$identicon->setValue($id);
$identicon->setSize($size);
$identicon->setStyle($style);
return $identicon->getImageDataUri($format);
}
}

31
src/Controller/SearchController.php

@ -0,0 +1,31 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class SearchController extends AbstractController
{
#[Route(
'/{_locale}/search',
name: 'search_index'
)]
public function index(Request $request): Response
{
$query = $request->query->get('query');
return $this->render('default/search/index.html.twig', [
'query' => $query
]);
}
public function module(string $query = ''): Response
{
return $this->render('default/search/module.html.twig', [
'query' => $query,
]);
}
}

0
src/Entity/.gitignore vendored

27
src/Entity/Page.php

@ -0,0 +1,27 @@
<?php
namespace App\Entity;
use App\Repository\PageRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PageRepository::class)]
class Page
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
}

147
src/Entity/User.php

@ -0,0 +1,147 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $address = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column]
private ?int $updated = null;
#[ORM\Column]
private ?int $visited = null;
#[ORM\Column]
private ?bool $public = null;
#[ORM\Column]
private ?bool $moderator = null;
#[ORM\Column]
private ?bool $approved = null;
#[ORM\Column]
private ?bool $status = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getAddress(): ?string
{
return $this->address;
}
public function setAddress(string $address): static
{
$this->address = $address;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function getUpdated(): ?int
{
return $this->updated;
}
public function setUpdated(int $updated): static
{
$this->updated = $updated;
return $this;
}
public function getVisited(): ?int
{
return $this->visited;
}
public function setVisited(int $visited): static
{
$this->visited = $visited;
return $this;
}
public function isPublic(): ?bool
{
return $this->public;
}
public function setPublic(bool $public): static
{
$this->public = $public;
return $this;
}
public function isModerator(): ?bool
{
return $this->moderator;
}
public function setModerator(bool $moderator): static
{
$this->moderator = $moderator;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
public function isStatus(): ?bool
{
return $this->status;
}
public function setStatus(bool $status): static
{
$this->status = $status;
return $this;
}
}

11
src/Kernel.php

@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

0
src/Repository/.gitignore vendored

48
src/Repository/PageRepository.php

@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\Page;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Page>
*
* @method Page|null find($id, $lockMode = null, $lockVersion = null)
* @method Page|null findOneBy(array $criteria, array $orderBy = null)
* @method Page[] findAll()
* @method Page[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PageRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Page::class);
}
// /**
// * @return Page[] Returns an array of Page objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Page
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

48
src/Repository/UserRepository.php

@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
// /**
// * @return User[] Returns an array of User objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('u.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?User
// {
// return $this->createQueryBuilder('u')
// ->andWhere('u.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

12
src/Service/User.php

@ -0,0 +1,12 @@
<?php
namespace App\Service;
class User
{
public function init(string $address): string
{
// @TODO
return $address;
}
}

151
src/app/controller/index.php

@ -1,151 +0,0 @@
<?php
class AppControllerIndex
{
private $_database;
private $_validator;
private $_website;
private $_session;
public function __construct(
AppModelDatabase $database,
AppModelValidator $validator,
AppModelWebsite $website,
AppModelSession $session
)
{
$this->_database = $database;
$this->_validator = $validator;
$this->_website = $website;
$this->_session = $session;
}
private function _initUser(string $address)
{
$error = [];
if (!$this->_validator->host($address, $error))
{
$this->_response(
sprintf(
_('Error - %s'),
$this->_website->getName()
),
_('406'),
$error,
406
);
}
try
{
$this->_database->beginTransaction();
$user = $this->_database->getUser(
$this->_database->initUserId(
$address,
$this->_website->getDefaultUserStatus(),
$this->_website->getDefaultUserApproved(),
time()
)
);
$this->_database->commit();
}
catch (Exception $error)
{
$this->_database->rollback();
$this->_response(
sprintf(
_('Error - %s'),
$this->_website->getName()
),
_('500'),
$error,
500
);
}
// Access denied
if (!$user->status)
{
$this->_response(
sprintf(
_('Error - %s'),
$this->_website->getName()
),
_('403'),
_('Access denied'),
403
);
}
}
public function render()
{
$user = $this->_initUser(
$this->_session->getAddress()
);
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
$pages = [];
require_once __DIR__ . '/module/pagination.php';
$appControllerModulePagination = new appControllerModulePagination();
require_once __DIR__ . '/module/head.php';
$appControllerModuleHead = new AppControllerModuleHead(
$this->_website->getUrl(),
$page > 1 ?
sprintf(
_('Page %s - BitTorrent Registry for Yggdrasil - %s'),
$page,
$this->_website->getName()
) :
sprintf(
_('%s - BitTorrent Registry for Yggdrasil'),
$this->_website->getName()
),
[
[
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => sprintf(
'assets/theme/default/css/common.css?%s',
CSS_VERSION
),
],
[
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => sprintf(
'assets/theme/default/css/framework.css?%s',
CSS_VERSION
),
],
]
);
require_once __DIR__ . '/module/profile.php';
$appControllerModuleProfile = new AppControllerModuleProfile(
$this->_database,
$this->_website,
$this->_session
);
require_once __DIR__ . '/module/header.php';
$appControllerModuleHeader = new AppControllerModuleHeader();
require_once __DIR__ . '/module/footer.php';
$appControllerModuleFooter = new AppControllerModuleFooter();
include __DIR__ . '/../view/theme/default/index.phtml';
}
}

13
src/app/controller/module/footer.php

@ -1,13 +0,0 @@
<?php
class AppControllerModuleFooter
{
public function render()
{
$trackers = Environment::config('trackers');
$api = Environment::config('website')->api->export;
include __DIR__ . '../../../view/theme/default/module/footer.phtml';
}
}

54
src/app/controller/module/head.php

@ -1,54 +0,0 @@
<?php
class AppControllerModuleHead
{
private $_title;
private $_base;
private $_links = [];
public function __construct(string $base, string $title, array $links = [])
{
$this->setBase($base);
$this->setTitle($title);
foreach ($links as $link)
{
$this->addLink(
$link['rel'],
$link['type'],
$link['href'],
);
}
}
public function setBase(string $base) : void
{
$this->_base = $base;
}
public function setTitle(string $title) : void
{
$this->_title = $title;
}
public function addLink(string $rel, string $type, string $href) : void
{
$this->_links[] = (object)
[
'rel' => $rel,
'type' => $type,
'href' => $href,
];
}
public function render()
{
$base = $this->_base;
$links = $this->_links;
$title = htmlentities($this->_title);
include __DIR__ . '../../../view/theme/default/module/head.phtml';
}
}

19
src/app/controller/module/header.php

@ -1,19 +0,0 @@
<?php
class AppControllerModuleHeader
{
public function render()
{
$name = str_replace(
'YGG',
'<span>YGG</span>',
Environment::config('website')->name
);
require_once __DIR__ . '/search.php';
$appControllerModuleSearch = new AppControllerModuleSearch();
include __DIR__ . '../../../view/theme/default/module/header.phtml';
}
}

9
src/app/controller/module/page.php

@ -1,9 +0,0 @@
<?php
class AppControllerCommonPage
{
public function render(int $pageId)
{
include __DIR__ . '../../../view/theme/default/common/page.phtml';
}
}

43
src/app/controller/module/pagination.php

@ -1,43 +0,0 @@
<?php
class AppControllerModulePagination
{
public function render(string $url, int $total, int $limit)
{
if ($total > $limit)
{
parse_str($url, $query);
$pagination->page = isset($query['total']) ? (int) $query['total'] : 1;
$pagination->pages = ceil($total / $limit);
// Previous
if ($page > 1)
{
$query['page'] = $page - 1;
$pagination->back = sprintf('%s', WEBSITE_URL, http_build_query($query));
}
else
{
$pagination->back = false;
}
// Next
if ($page < ceil($total / $limit))
{
$query['page'] = $page + 1;
$pagination->next = sprintf('%s', WEBSITE_URL, http_build_query($query));
}
else
{
$pagination->next = false;
}
// Render
}
}
}

54
src/app/controller/module/profile.php

@ -1,54 +0,0 @@
<?php
class AppControllerModuleProfile
{
private $_database;
private $_website;
private $_session;
public function __construct(
AppModelDatabase $database,
AppModelWebsite $website,
AppModelSession $session)
{
$this->_database = $database;
$this->_website = $website;
$this->_session = $session;
}
public function render()
{
$route = isset($_GET['_route_']) ? (string) $_GET['_route_'] : '';
$user = $this->_database->getUser(
$this->_database->initUserId(
$this->_session->getAddress(),
$this->_website->getDefaultUserStatus(),
$this->_website->getDefaultUserApproved(),
time()
)
);
$stars = $this->_database->findUserPageStarsDistinctTotalByValue($user->userId, true);
$views = $this->_database->findUserPageViewsDistinctTotal($user->userId);
$downloads = 0; // @TODO $this->_database->findUserPageDownloadsDistinctTotal($user->userId);
$comments = $this->_database->findUserPageCommentsDistinctTotal($user->userId);
$editions = 0; // @TODO $this->_database->findUserPageEditionsDistinctTotal($user->userId);
$address = $user->address;
$icon = new Jdenticon\Identicon();
$icon->setValue($user->address);
$icon->setSize(16);
$icon->setStyle(
[
'backgroundColor' => 'rgba(255, 255, 255, 0)',
]
);
$identicon = $icon->getImageDataUri('webp');
include __DIR__ . '../../../view/theme/default/module/profile.phtml';
}
}

24
src/app/controller/module/search.php

@ -1,24 +0,0 @@
<?php
class AppControllerModuleSearch
{
public function render()
{
$query = empty($_GET['query']) ? false : urldecode($_GET['query']);
$locale = empty($_GET['locale']) ? 'all' : urldecode($_GET['locale']);
$locales = [];
foreach (Environment::config('locales') as $key => $value)
{
$locales[$key] = (object)
[
'key' => $key,
'value' => $value[0],
'active' => $key === $locale // false !== stripos($_SERVER['HTTP_ACCEPT_LANGUAGE'], $key) ? true : false,
];
}
include __DIR__ . '../../../view/theme/default/module/search.phtml';
}
}

428
src/app/controller/page.php

@ -1,428 +0,0 @@
<?php
class AppControllerPage
{
private $_database;
private $_validator;
private $_locale;
private $_website;
private $_session;
private $_request;
public function __construct(
AppModelDatabase $database,
AppModelValidator $validator,
AppModelLocale $locale,
AppModelWebsite $website,
AppModelSession $session,
AppModelRequest $request
)
{
$this->_database = $database;
$this->_validator = $validator;
$this->_locale = $locale;
$this->_website = $website;
$this->_session = $session;
$this->_request = $request;
}
private function _response(string $title, string $h1, mixed $data, int $code = 200)
{
require_once __DIR__ . '/response.php';
if (is_array($data))
{
$data = implode('<br />', $data);
}
$appControllerResponse = new AppControllerResponse(
$title,
$h1,
$data,
$code
);
$appControllerResponse->render();
exit;
}
private function _initUser(string $address)
{
$error = [];
if (!$this->_validator->host($address, $error))
{
$this->_response(
sprintf(
_('Error - %s'),
$this->_website->getName()
),
_('406'),
$error,
406
);
}
try
{
$this->_database->beginTransaction();
$user = $this->_database->getUser(
$this->_database->initUserId(
$address,
$this->_website->getDefaultUserStatus(),
$this->_website->getDefaultUserApproved(),
time()
)
);
$this->_database->commit();
}
catch (Exception $error)
{
$this->_database->rollback();
$this->_response(
sprintf(
_('Error - %s'),
$this->_website->getName()
),
_('500'),
$error,
500
);
}
// Access denied
if (!$user->status)
{
$this->_response(
sprintf(
_('Error - %s'),
$this->_website->getName()
),
_('403'),
_('Access denied'),
403
);
}
}
private function _initLocale(string $value)
{
if (!$locale = $this->_database->findLocale($value))
{
$locale = $this->_database->getLocale(
$this->_database->addLocale(
$value
)
);
}
return $locale;
}
private function _initPage(int $pageId = 0)
{
if (!$page = $this->_database->getPage($pageId))
{
$page = $this->_database->getPage(
$this->_database->addPage(
time()
)
);
}
return $page;
}
private function _initText(string $value, string $mime = 'text/plain')
{
if (!$text = $this->_database->findText($mime, md5($value)))
{
$text = $this->_database->getText(
$this->_database->addText(
$mime,
md5($value),
$value,
time()
)
);
}
return $text;
}
private function _commitPageTitle(int $pageId, int $userId, int $localeId, string $text, string $mime = 'text/plain')
{
$textId = $this->_initText(
$text,
$mime
)->textId;
if (!$this->_database->findPageTitleLatest($pageId,
$userId,
$localeId,
$textId))
{
$this->_database->addPageTitle(
$pageId,
$userId,
$localeId,
$textId,
time()
);
}
}
public function renderFormSubmit()
{
// Init user
$user = $this->_initUser(
$this->_session->getAddress()
);
// Init page
if ($this->_request->get('pageId'))
{
$page = $this->_initPage(
(int) $this->_request->get('pageId')
);
}
else if ($this->_request->post('pageId'))
{
$page = $this->_initPage(
(int) $this->_request->post('pageId')
);
}
else
{
$page = $this->_initPage();
}
// Init locale
if ($this->_locale->codeExists($this->_request->get('locale')))
{
$localeCode = (int) $this->_request->get('locale');
}
else if ($this->_locale->codeExists($this->_request->post('locale')))
{
$localeCode = (int) $this->_request->post('locale');
}
else
{
$localeCode = $this->_website->getDefaultLocale();
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) // @TODO environment
{
foreach ($this->_locale->getList() as $value)
{
if (false !== stripos($_SERVER['HTTP_ACCEPT_LANGUAGE'], $value->code))
{
$localeCode = $value->code;
break;
}
}
}
}
$locale = $this->_initLocale($localeCode);
// Init form
$form = (object)
[
'pageId' => (object)
[
'error' => [],
'type' => 'hidden',
'attribute' => (object)
[
'value' => $page->pageId,
]
],
'locale' => (object)
[
'error' => [],
'type' => 'select',
'options' => $this->_locale->getList(),
'value' => $locale->value,
'placeholder' => _('Page content language'),
],
'title' => (object)
[
'error' => [],
'type' => 'text',
'attribute' => (object)
[
'value' => null,
'required' => $this->_validator->getPageTitleRequired(),
'minlength' => $this->_validator->getPageTitleLengthMin(),
'maxlength' => $this->_validator->getPageTitleLengthMax(),
'placeholder' => sprintf(
_('Page subject (%s-%s chars)'),
number_format($this->_validator->getPageTitleLengthMin()),
number_format($this->_validator->getPageTitleLengthMax())
),
]
],
'description' => (object)
[
'error' => [],
'type' => 'textarea',
'attribute' => (object)
[
'value' => null,
'required' => $this->_validator->getPageDescriptionRequired(),
'minlength' => $this->_validator->getPageDescriptionLengthMin(),
'maxlength' => $this->_validator->getPageDescriptionLengthMax(),
'placeholder' => sprintf(
_('Page description text (%s-%s chars)'),
number_format($this->_validator->getPageDescriptionLengthMin()),
number_format($this->_validator->getPageDescriptionLengthMax())
),
]
],
'keywords' => (object)
[
'error' => [],
'type' => 'textarea',
'attribute' => (object)
[
'value' => null,
'required' => $this->_validator->getPageKeywordsRequired(),
'placeholder' => sprintf(
_('Page keywords (%s-%s total / %s-%s chars per item)'),
number_format($this->_validator->getPageKeywordsQuantityMin()),
number_format($this->_validator->getPageKeywordsQuantityMax()),
number_format($this->_validator->getPageKeywordLengthMin()),
number_format($this->_validator->getPageKeywordLengthMax())
),
]
],
'sensitive' => (object)
[
'error' => [],
'type' => 'checkbox',
'attribute' => (object)
[
'value' => null,
'placeholder' => _('Apply NSFW filters for this publication'),
]
]
];
// Submit request
if ($this->_request->hasPost())
{
/// Title
if ($title = $this->_request->post('title'))
{
$error = [];
if (!$this->_validator->pageTitle($title, $error))
{
$form->title->error[] = $error;
}
else
{
$this->_commitPageTitle(
$page->pageId,
$user->userId,
$locale->localeId,
$title
);
}
$form->title->attribute->value = htmlentities($title);
}
if (isset($_POST['description']))
{
$error = [];
if (!$this->_validator->pageDescription($_POST['description'], $error))
{
$form->description->error[] = $error;
}
$form->description->attribute->value = htmlentities($_POST['description']);
}
if (isset($_POST['keywords']))
{
$error = [];
if (!$this->_validator->pageKeywords($_POST['keywords'], $error))
{
$form->keywords->error[] = $error;
}
$form->keywords->attribute->value = htmlentities($_POST['keywords']);
}
if (isset($_POST['sensitive']))
{
$form->sensitive->attribute->value = (bool) $_POST['sensitive'];
}
// Request valid
if (empty($error))
{
// @TODO redirect
}
}
// Render template
require_once __DIR__ . '/module/head.php';
$appControllerModuleHead = new AppControllerModuleHead(
$this->_website->getUrl(),
sprintf(
_('Submit - %s'),
$this->_website->getName()
),
[
[
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => sprintf(
'assets/theme/default/css/common.css?%s',
CSS_VERSION
),
],
[
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => sprintf(
'assets/theme/default/css/framework.css?%s',
CSS_VERSION
),
],
]
);
require_once __DIR__ . '/module/profile.php';
$appControllerModuleProfile = new AppControllerModuleProfile(
$this->_database,
$this->_website,
$this->_session
);
require_once __DIR__ . '/module/header.php';
$appControllerModuleHeader = new AppControllerModuleHeader();
require_once __DIR__ . '/module/footer.php';
$appControllerModuleFooter = new AppControllerModuleFooter();
include __DIR__ . '../../view/theme/default/page/form/submit.phtml';
}
}

65
src/app/controller/response.php

@ -1,65 +0,0 @@
<?php
class AppControllerResponse
{
private $_title;
private $_h1;
private $_text;
private $_code;
public function __construct(string $title, string $h1, string $text, int $code = 200)
{
$this->_title = $title;
$this->_h1 = $h1;
$this->_text = $text;
$this->_code = $code;
}
public function render()
{
header(
sprintf(
'HTTP/1.0 %s Not Found',
$this->_code
)
);
$h1 = $this->_h1;
$text = $this->_text;
require_once __DIR__ . '/module/head.php';
$appControllerModuleHead = new AppControllerModuleHead(
Environment::config('website')->url,
$this->_title,
[
[
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => sprintf(
'assets/theme/default/css/common.css?%s',
CSS_VERSION
),
],
[
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => sprintf(
'assets/theme/default/css/framework.css?%s',
CSS_VERSION
),
],
]
);
require_once __DIR__ . '/module/header.php';
$appControllerModuleHeader = new AppControllerModuleHeader();
require_once __DIR__ . '/module/footer.php';
$appControllerModuleFooter = new AppControllerModuleFooter();
include __DIR__ . '../../view/theme/default/response.phtml';
}
}

117
src/app/controller/user.php

@ -1,117 +0,0 @@
<?php
class AppControllerUser
{
private $_database;
private $_validator;
private $_website;
private $_user;
public function __construct(
AppModelDatabase $database,
AppModelValidator $validator,
AppModelWebsite $website
)
{
$this->_database = $database;
$this->_validator = $validator;
$this->_website = $website;
}
private function _response(string $title, string $h1, mixed $data, int $code = 200)
{
require_once __DIR__ . '/response.php';
if (is_array($data))
{
$data = implode('<br />', $data);
}
$appControllerResponse = new AppControllerResponse(
$title,
$h1,
$data,
$code
);
$appControllerResponse->render();
exit;
}
public function getIdenticon(int $size)
{
$icon = new Jdenticon\Identicon();
$icon->setValue($this->_user->public ? $this->_user->address : $this->_user->userId);
$icon->setSize($size);
$icon->setStyle(
[
'backgroundColor' => 'rgba(255, 255, 255, 0)',
]
);
return $icon->getImageDataUri('webp');
}
public function getUser()
{
return $this->_user;
}
public function getPublic()
{
return $this->_user->public;
}
public function getAddress()
{
return $this->_user->address;
}
public function findUserPageStarsDistinctTotalByValue(bool $value) : int
{
return $this->_database->findUserPageStarsDistinctTotal(
$this->_user->userId,
$value
);
}
public function findUserPageViewsDistinctTotal() : int
{
return $this->_database->findUserPageViewsDistinctTotal(
$this->_user->userId
);
}
public function findUserPageDownloadsDistinctTotal() : int
{
return $this->_database->findUserPageDownloadsDistinctTotal(
$this->_user->userId
);
}
public function findUserPageCommentsDistinctTotal() : int
{
return $this->_database->findUserPageCommentsDistinctTotal(
$this->_user->userId
);
}
public function findUserPageEditionsDistinctTotal() : int
{
return $this->_database->findUserPageEditionsDistinctTotal(
$this->_user->userId
);
}
public function updateUserPublic(bool $public, int $time) : int
{
return $this->_database->updateUserPublic(
$this->_user->userId,
$public,
$time
);
}
}

1866
src/app/model/database.php

File diff suppressed because it is too large Load Diff

37
src/app/model/locale.php

@ -1,37 +0,0 @@
<?php
class AppModelLocale {
private $_locales = [];
public function __construct(object $locales)
{
foreach ($locales as $code => $value)
{
$this->_locales[] = (object)
[
'code' => $code,
'value' => $value[0],
'active' => false,
];
}
}
public function getList() : object
{
return (object) $this->_locales;
}
public function codeExists(string $code) : bool
{
foreach ($this->_locales as $locale)
{
if ($locale->code === $code)
{
return true;
}
}
return false;
}
}

104
src/app/model/request.php

@ -1,104 +0,0 @@
<?php
class AppModelRequest {
private array $_get;
private array $_post;
private array $_files;
private array $_server;
public function __construct(array $get, array $post, array $files, array $server)
{
$this->_get = $get;
$this->_post = $post;
$this->_files = $files;
$this->_server = $server;
}
public function get(string $key, mixed $value = null) : mixed
{
if ($value)
{
$this->_get[$key] = $value;
}
if (isset($this->_get[$key]))
{
return $this->_get[$key];
}
else
{
return false;
}
}
public function post(string $key, mixed $value = null) : mixed
{
if ($value)
{
$this->_get[$key] = $value;
}
if (isset($this->_post[$key]))
{
return $this->_post[$key];
}
else
{
return false;
}
}
public function files(string $key, mixed $value = null) : mixed
{
if ($value)
{
$this->_get[$key] = $value;
}
if (isset($this->_files[$key]))
{
return $this->_files[$key];
}
else
{
return false;
}
}
public function server(string $key, mixed $value = null) : mixed
{
if ($value)
{
$this->_get[$key] = $value;
}
if (isset($this->_get[$key]))
{
return $this->_get[$key];
}
else
{
return false;
}
}
public function hasPost() : bool
{
return !empty($this->_post);
}
public function hasGet() : bool
{
return !empty($this->_get);
}
public function hasFiles() : bool
{
return !empty($this->_files);
}
}

111
src/app/model/sphinx.php

@ -1,111 +0,0 @@
<?php
class AppModelSphinx {
private $_sphinx;
public function __construct(string $host, int $port)
{
$this->_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, string $mode = 'default', array $stopWords = []) : int
{
$query = $this->_sphinx->prepare('SELECT COUNT(*) AS `total` FROM `magnet` WHERE MATCH(?)');
$query->execute(
[
self::_match($keyword, $mode, $stopWords)
]
);
return $query->fetch()->total;
}
public function searchMagnets(string $keyword, int $start, int $limit, int $maxMatches, string $mode = 'default', array $stopWords = [])
{
$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(
[
self::_match($keyword, $mode, $stopWords)
]
);
return $query->fetchAll();
}
private static function _match(string $keyword, string $mode = 'default', array $stopWords = []) : string
{
$keyword = trim($keyword);
if (empty($keyword))
{
return $keyword;
}
$keyword = str_replace(['"'], ' ', $keyword);
$keyword = preg_replace('/[\W]/ui', ' ', $keyword);
$keyword = preg_replace('/[\s]+/ui', ' ', $keyword);
$keyword = trim($keyword);
switch ($mode)
{
case 'similar':
$result = [];
$keyword = preg_replace('/[\d]/ui', ' ', $keyword);
$keyword = preg_replace('/[\s]+/ui', ' ', $keyword);
$keyword = trim($keyword);
foreach ((array) explode(' ', $keyword) as $value)
{
if (mb_strlen($value) > 5)
{
if (!in_array(mb_strtolower($value), array_map('strtolower', $stopWords)))
{
$result[] = sprintf('@title "%s" | @dn "%s"', $value, $value);
}
}
}
if (empty($result))
{
return '*';
}
else
{
return implode(' | ', $result);
}
break;
default:
$result = [];
foreach ((array) explode(' ', $keyword) as $value)
{
if (!in_array(mb_strtolower($value), $stopWords))
{
$result[] = sprintf('@"*%s*"', $value);
}
}
return implode(' | ', $result);
}
}
}

2216
src/app/model/validator.php

File diff suppressed because it is too large Load Diff

46
src/app/model/website.php

@ -1,46 +0,0 @@
<?php
class AppModelWebsite
{
private $_config;
public function __construct(object $config)
{
$this->_config = $config;
}
public function getConfig() : object
{
return $this->_config->name;
}
public function getName() : string
{
return $this->_config->name;
}
public function getUrl() : string
{
return $this->_config->url;
}
public function getDefaultLocale() : string
{
return $this->_config->default->locale;
}
public function getDefaultUserStatus() : bool
{
return $this->_config->default->user->status;
}
public function getDefaultUserApproved() : bool
{
return $this->_config->default->user->approved;
}
public function getApiExportEnabled() : bool
{
return $this->_config->api->export->enabled;
}
}

32
src/app/view/theme/default/index.phtml

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<?php $appControllerModuleHead->render() ?>
<body>
<?php $appControllerModuleHeader->render() ?>
<main>
<div class="container">
<div class="row">
<div class="column width-100">
<?php $appControllerModuleProfile->render() ?>
<?php if ($pages) { ?>
<?php foreach ($pages as $page) { ?>
<?php $appControllerModulePage->render($page->pageId) ?>
<?php } ?>
<?php $appControllerModulePagination->render() ?>
<?php } else { ?>
<div class="padding-16 margin-y-8 border-radius-3 background-color-night text-center">
<h1 class="margin-b-8">
<?php echo _('Nothing found') ?>
</h1>
<div class="text-color-night">
<?php echo _('* share your magnet links to change it') ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
</main>
<?php $appControllerModuleFooter->render() ?>
</body>
</html>

27
src/app/view/theme/default/module/footer.phtml

@ -1,27 +0,0 @@
<footer>
<div class="container">
<div class="row">
<div class="column width-100 text-center margin-y-8">
<?php foreach ($trackers as $i => $tracker) { ?>
<a href="<?php echo $tracker->announce ?>"><?php echo sprintf('Tracker %s', $i + 1) ?></a>
/
<a href="<?php echo $tracker->stats ?>"><?php echo _('Stats') ?></a>
|
<?php } ?>
<a href="faq"><?php echo _('F.A.Q') ?></a>
|
<a href="node"><?php echo _('Node') ?></a>
|
<a rel="nofollow" href="rss"><?php echo _('RSS') ?></a>
<?php if ($api) { ?>
|
<a rel="nofollow" href="api/manifest.json"><?php echo _('API') ?></a>
<?php } ?>
|
<a href="https://github.com/YGGverse/YGGtracker"><?php echo _('GitHub') ?></a>
</div>
</div>
</div>
</footer>
</body>
</html>

7
src/app/view/theme/default/module/head.phtml

@ -1,7 +0,0 @@
<head>
<base href="<?php echo $base ?>" />
<title><?php echo $title ?></title>
<?php foreach ($links as $link) { ?>
<link rel="<?php echo $link->rel ?>" type="<?php echo $link->type ?>" href="<?php echo $link->href ?>" />
<?php } ?>
</head>

10
src/app/view/theme/default/module/header.phtml

@ -1,10 +0,0 @@
<header>
<div class="container">
<div class="row margin-t-8 text-center">
<a class="logo" href="">
<?php echo $name ?>
</a>
<?php $appControllerModuleSearch->render() ?>
</div>
</div>
</header>

126
src/app/view/theme/default/module/page.phtml

@ -1,126 +0,0 @@
<a name="magnet-<?php echo $pageId ?>"></a>
<div class="margin-y-8 border-radius-3 background-color-night <?php echo !$approved ? 'opacity-06 opacity-hover-1' : false ?>">
<div class="padding-16 <?php echo $sensitive ? 'blur-2 blur-hover-0' : false ?>">
<a href="<?php echo sprintf('%s/magnet.php?magnetId=%s', WEBSITE_URL, $pageId) ?>">
<h2 class="margin-b-8"><?php echo $title ?></h2>
<?php if ($leechers && !$seeders) { ?>
<span class="label label-green margin-x-4 font-size-10 position-relative top--2 cursor-default"
title="<?php echo _('Active leechers waiting for seeds') ?>">
<?php echo _('wanted') ?>
</span>
<?php } ?>
</a>
<div class="float-right opacity-0 parent-hover-opacity-09">
<?php if (!$approved) { ?>
<span class="margin-l-8" title="<?php echo _('Waiting for approve') ?>">
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hourglass-split" viewBox="0 0 16 16">
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
</svg>
</span>
<?php } ?>
<a class="text-color-green margin-l-12" href="<?php echo WEBSITE_URL ?>/edit.php?magnetId=<?php echo $magnet->magnetId ?>" title="<?php echo _('Edit') ?>">
<svg class="text-color-green" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16">
<path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
<path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z"/>
</svg>
</a>
</div>
<?php if ($magnet->preview) { ?>
<div class="margin-y-8"><?php echo $magnet->preview ?></div>
<?php } ?>
<?php if ($magnet->keywords) { ?>
<div class="margin-y-8">
<?php foreach ($magnet->keywords as $keyword) { ?>
<small>
<a href="<?php echo WEBSITE_URL ?>/search.php?query=<?php echo urlencode($keyword) ?>">#<?php echo htmlentities($keyword) ?></a>
</small>
<?php } ?>
</div>
<?php } ?>
<div class="width-100 padding-y-4"></div>
<span class="margin-t-8 margin-r-8 cursor-default">
<sup>
<?php echo $magnet->timeUpdated ? _('Updated') : _('Added') ?>
<?php echo $magnet->timeUpdated ? $magnet->timeUpdated : $magnet->timeAdded ?>
</sup>
</span>
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Seeds') ?>">
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/>
</svg>
<sup><?php echo $magnet->seeders ?></sup>
</span>
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Peers') ?>">
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/>
</svg>
<sup><?php echo $magnet->completed ?></sup>
</span>
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Leechers') ?>">
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cup-hot" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M.5 6a.5.5 0 0 0-.488.608l1.652 7.434A2.5 2.5 0 0 0 4.104 16h5.792a2.5 2.5 0 0 0 2.44-1.958l.131-.59a3 3 0 0 0 1.3-5.854l.221-.99A.5.5 0 0 0 13.5 6H.5ZM13 12.5a2.01 2.01 0 0 1-.316-.025l.867-3.898A2.001 2.001 0 0 1 13 12.5ZM2.64 13.825 1.123 7h11.754l-1.517 6.825A1.5 1.5 0 0 1 9.896 15H4.104a1.5 1.5 0 0 1-1.464-1.175Z"/>
<path d="m4.4.8-.003.004-.014.019a4.167 4.167 0 0 0-.204.31 2.327 2.327 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.31 3.31 0 0 1-.202.388 5.444 5.444 0 0 1-.253.382l-.018.025-.005.008-.002.002A.5.5 0 0 1 3.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 3.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 3 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 4.4.8Zm3 0-.003.004-.014.019a4.167 4.167 0 0 0-.204.31 2.327 2.327 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.31 3.31 0 0 1-.202.388 5.444 5.444 0 0 1-.253.382l-.018.025-.005.008-.002.002A.5.5 0 0 1 6.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 6.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 6 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 7.4.8Zm3 0-.003.004-.014.019a4.077 4.077 0 0 0-.204.31 2.337 2.337 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.198 3.198 0 0 1-.202.388 5.385 5.385 0 0 1-.252.382l-.019.025-.005.008-.002.002A.5.5 0 0 1 9.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 9.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 9 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 10.4.8Z"/>
</svg>
<sup><?php echo $magnet->leechers ?></sup>
</span>
<?php if ($magnet->directs) { ?>
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Direct') ?>">
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database" viewBox="0 0 16 16">
<path d="M4.318 2.687C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4c0-.374.356-.875 1.318-1.313ZM13 5.698V7c0 .374-.356.875-1.318 1.313C10.766 8.729 9.464 9 8 9s-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777A4.92 4.92 0 0 0 13 5.698ZM14 4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13V4Zm-1 4.698V10c0 .374-.356.875-1.318 1.313C10.766 11.729 9.464 12 8 12s-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777A4.92 4.92 0 0 0 13 8.698Zm0 3V13c0 .374-.356.875-1.318 1.313C10.766 14.729 9.464 15 8 15s-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13s3.022-.289 4.096-.777c.324-.147.633-.323.904-.525Z"/>
</svg>
<sup><?php echo $magnet->directs ?></sup>
</span>
<?php } ?>
<span class="float-right margin-l-12">
<a rel="nofollow" href="<?php echo sprintf('%s/action.php?target=magnet&toggle=star&magnetId=%s&callback=%s',
WEBSITE_URL,
$magnet->magnetId,
base64_encode(sprintf('%s/search.php?%s#magnet-%s',
WEBSITE_URL,
($request->query ? sprintf('&query=%s', urlencode($request->query)) : false).
($request->page ? sprintf('&page=%s', urlencode($request->page)) : false),
$magnet->magnetId))) ?>" title="<?php echo _('Star') ?>">
<?php if ($magnet->star->status) { ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
</svg>
<?php } else { ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star" viewBox="0 0 16 16">
<path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
</svg>
<?php } ?>
</a>
<sup><?php echo $magnet->star->total ?></sup>
</span>
<?php if ($magnet->comments) { ?>
<span class="float-right margin-l-12">
<a rel="nofollow" href="<?php echo WEBSITE_URL ?>/magnet.php?magnetId=<?php echo $magnet->magnetId ?>#comment" title="<?php echo _('Comment') ?>">
<?php if ($magnet->comment->status) { ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat-fill" viewBox="0 0 16 16">
<path d="M8 15c4.418 0 8-3.134 8-7s-3.582-7-8-7-8 3.134-8 7c0 1.76.743 3.37 1.97 4.6-.097 1.016-.417 2.13-.771 2.966-.079.186.074.394.273.362 2.256-.37 3.597-.938 4.18-1.234A9.06 9.06 0 0 0 8 15z"/>
</svg>
<?php } else { ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16">
<path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/>
</svg>
<?php } ?>
</a>
<sup><?php echo $magnet->comment->total ?></sup>
</span>
<?php } ?>
<span class="float-right margin-l-12">
<a rel="nofollow" href="<?php echo WEBSITE_URL ?>/download.php?magnetId=<?php echo $magnet->magnetId ?>" title="<?php echo _('Download') ?>">
<?php if ($magnet->download->status) { ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V4.5z"/>
</svg>
<?php } else { ?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-circle" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V4.5z"/>
</svg>
<?php } ?>
</a>
<sup><?php echo $magnet->download->total ?></sup>
</span>
</div>
</div>

15
src/app/view/theme/default/module/pagination.phtml

@ -1,15 +0,0 @@
<div class="row">
<div class="column width-100 text-right">
<?php echo sprintf(_('page %s / %s'), $pagination->page, $pagination->total) ?>
<?php if ($pagination->back) { ?>
<a class="button margin-l-8" rel="nofollow" href="<?php echo $pagination->back ?>">
<?php echo _('back') ?>
</a>
<?php } ?>
<?php if ($pagination->next) { ?>
<a class="button margin-l-4" rel="nofollow" href="<?php echo $pagination->next ?>">
<?php echo _('next') ?>
</a>
<?php } ?>
</div>
</div>

20
src/app/view/theme/default/module/search.phtml

@ -1,20 +0,0 @@
<form class="margin-t-8" name="search" method="get" action="search">
<input class="min-width-200-px" type="text" name="query" value="<?php echo $query ?>" placeholder="<?php echo _('Keyword, file, extension, hash...') ?>" />
<select class="min-width-120-px" type="text" name="locale">
<option value="">
<?php echo _('All languages') ?>
</option>
<?php foreach ($locales as $locale) { ?>
<?php if ($locale->active) { ?>
<option value="<?php echo $locale->key ?>" selected="selected">
<?php echo $locale->value ?>
</option>
<?php } else { ?>
<option value="<?php echo $locale->key ?>">
<?php echo $locale->value ?>
</option>
<?php } ?>
<?php } ?>
</select>
<input type="submit" value="<?php echo _('Search') ?>" />
</form>

140
src/app/view/theme/default/page/form/submit.phtml

@ -1,140 +0,0 @@
<!DOCTYPE html>
<html>
<?php $appControllerModuleHead->render() ?>
<body>
<?php $appControllerModuleHeader->render() ?>
<main>
<div class="container">
<div class="row">
<div class="column width-100">
<?php $appControllerModuleProfile->render() ?>
<div class="padding-16 margin-y-8 border-radius-3 background-color-night">
<div class="margin-b-24 padding-b-16 border-bottom-default">
<h1><?php echo _('Submit') ?></h1>
</div>
<form class="margin-t-8" name="submit" method="post" enctype="multipart/form-data" action="submit">
<input type="hidden" name="pageId" value="<?php echo $form->pageId->attribute->value ?>" />
<div class="margin-b-16">
<label for="locale">
<?php echo _('Content language') ?>
</label>
<sub class="opacity-0 parent-hover-opacity-09"
title="<?php echo $form->locale->placeholder ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
</sub>
<select class="width-100 margin-t-8" type="text" name="locale" id="locale">
<?php foreach ($form->locale->options as $locale) { ?>
<?php if ($locale->active) { ?>
<option value="<?php echo $locale->code ?>" selected="selected">
<?php echo $locale->value ?>
</option>
<?php } else { ?>
<option value="<?php echo $locale->code ?>">
<?php echo $locale->value ?>
</option>
<?php } ?>
<?php } ?>
</select>
</div>
<div class="margin-b-16">
<label for="title">
<?php echo _('Title') ?>
</label>
<sub class="opacity-0 parent-hover-opacity-09"
title="<?php echo $form->title->attribute->placeholder ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
</sub>
<?php foreach ($form->title->error as $errors) { ?>
<?php foreach ($errors as $error) { ?>
<div class="text-color-red margin-y-8">
<?php echo $error ?>
</div>
<?php } ?>
<?php } ?>
<input class="width-100 margin-t-8"
type="text"
name="title"
id="title"
<?php echo $form->title->attribute->required ? 'required="required"' : false ?>
value="<?php echo $form->title->attribute->value ?>"
placeholder="<?php echo $form->title->attribute->placeholder ?>"
minlength="<?php echo $form->title->attribute->minlength ?>"
maxlength="<?php echo $form->title->attribute->maxlength ?>" />
</div>
<div class="margin-y-8 padding-t-4">
<label for="description">
<?php echo _('Description') ?>
</label>
<sub class="opacity-0 parent-hover-opacity-09"
title="<?php echo $form->description->attribute->placeholder ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
</sub>
<?php foreach ($form->description->error as $errors) { ?>
<?php foreach ($errors as $error) { ?>
<div class="text-color-red margin-y-8">
<?php echo $error ?>
</div>
<?php } ?>
<?php } ?>
<textarea class="width-100 margin-t-8"
name="description"
id="description"
<?php echo $form->description->attribute->required ? 'required="required"' : false ?>
placeholder="<?php echo $form->description->attribute->placeholder ?>"
minlength="<?php echo $form->description->attribute->minlength ?>"
maxlength="<?php echo $form->description->attribute->maxlength ?>"><?php echo $form->description->attribute->value ?></textarea>
</div>
<div class="margin-y-8 padding-t-4">
<label for="keywords">
<?php echo _('Keywords') ?>
</label>
<sub class="opacity-0 parent-hover-opacity-09"
title="<?php echo $form->keywords->attribute->placeholder ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
</sub>
<?php foreach ($form->keywords->error as $errors) { ?>
<?php foreach ($errors as $error) { ?>
<div class="text-color-red margin-y-8">
<?php echo $error ?>
</div>
<?php } ?>
<?php } ?>
<textarea class="width-100 margin-t-8"
name="keywords"
<?php echo $form->keywords->attribute->required ? 'required="required"' : false ?>
placeholder="<?php echo $form->keywords->attribute->placeholder ?>"
minlength="<?php echo $form->keywords->attribute->minlength ?>"
maxlength="<?php echo $form->keywords->attribute->maxlength ?>"><?php echo $form->keywords->attribute->value ?></textarea>
</div>
<div class="margin-y-16">
<input type="checkbox" name="sensitive" id="sensitive" value="true" <?php echo $form->sensitive->attribute->value ? 'checked="checked"' : false ?> />
<label for="sensitive">
<?php echo _('Sensitive') ?>
</label>
<sub class="opacity-0 parent-hover-opacity-09"
title="<?php echo $form->sensitive->attribute->placeholder ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
</sub>
</div>
<div class="text-right">
<input class="button-green" type="submit" value="<?php echo _('Submit') ?>" />
</div>
</form>
</div>
</div>
</div>
</div>
</main>
<?php $appControllerModuleFooter->render() ?>
</body>
</html>

24
src/app/view/theme/default/response.phtml

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<?php $appControllerModuleHead->render() ?>
<body>
<?php $appControllerModuleHeader->render() ?>
<main>
<div class="container">
<div class="row">
<div class="column width-100">
<div class="padding-16 margin-y-8 border-radius-3 background-color-night text-center">
<h1 class="margin-b-8">
<?php echo $h1 ?>
</h1>
<div>
<?php echo $text ?>
</div>
</div>
</div>
</div>
</div>
</main>
<?php $appControllerModuleFooter->render() ?>
</body>
</html>

160
src/config/bootstrap.php

@ -1,160 +0,0 @@
<?php
// PHP
declare(strict_types=1);
// Debug
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
// Application
define('APP_VERSION', '2.0.0');
define('API_VERSION', APP_VERSION);
define('CSS_VERSION', APP_VERSION);
// Environment
require_once __DIR__ . '/../library/environment.php';
// Autoload
require_once __DIR__ . '/../../vendor/autoload.php';
// Route
parse_str($_SERVER['QUERY_STRING'], $request);
if (isset($request['_route_']))
{
switch ($request['_route_'])
{
case 'stars':
require_once __DIR__ . '/../app/controller/stars.php';
$appControllerStars = new AppControllerStars();
$appControllerStars->render();
break;
case 'views':
require_once __DIR__ . '/../app/controller/views.php';
$appControllerViews = new AppControllerViews();
$appControllerViews->render();
break;
case 'downloads':
require_once __DIR__ . '/../app/controller/downloads.php';
$appControllerDownloads = new AppControllerDownloads();
$appControllerDownloads->render();
break;
case 'comments':
require_once __DIR__ . '/../app/controller/comments.php';
$appControllerComments = new AppControllerComments();
$appControllerComments->render();
break;
case 'editions':
require_once __DIR__ . '/../app/controller/editions.php';
$appControllerEditions = new AppControllerEditions();
$appControllerEditions->render();
break;
case 'submit':
require_once __DIR__ . '/../app/model/database.php';
require_once __DIR__ . '/../app/model/validator.php';
require_once __DIR__ . '/../app/model/locale.php';
require_once __DIR__ . '/../app/model/website.php';
require_once __DIR__ . '/../app/model/session.php';
require_once __DIR__ . '/../app/model/request.php';
require_once __DIR__ . '/../app/controller/page.php';
$appControllerPage = new AppControllerPage(
new AppModelDatabase(
Environment::config('database')
),
new AppModelValidator(
Environment::config('validator')
),
new AppModelLocale(
Environment::config('locales')
),
new AppModelWebsite(
Environment::config('website')
),
new AppModelSession(
$_SERVER['REMOTE_ADDR']
),
new AppModelRequest(
$_GET,
$_POST,
$_FILES
)
);
$appControllerPage->renderFormSubmit();
break;
default:
require_once __DIR__ . '/../app/controller/response.php';
$appControllerResponse = new AppControllerResponse(
sprintf(
_('404 - Not found - %s'),
Environment::config('website')->name
),
_('404'),
_('Page not found'),
404
);
$appControllerResponse->render();
}
}
else
{
require_once __DIR__ . '/../app/model/database.php';
require_once __DIR__ . '/../app/model/validator.php';
require_once __DIR__ . '/../app/model/website.php';
require_once __DIR__ . '/../app/model/session.php';
require_once __DIR__ . '/../app/controller/index.php';
$appControllerIndex = new AppControllerIndex(
new AppModelDatabase(
Environment::config('database')
),
new AppModelValidator(
Environment::config('validator')
),
new AppModelWebsite(
Environment::config('website')
),
new AppModelSession(
$_SERVER['REMOTE_ADDR']
)
);
$appControllerIndex->render();
}

7
src/config/database.json

@ -1,7 +0,0 @@
{
"port":3306,
"host":"127.0.0.1",
"name":"",
"user":"",
"password":""
}

277
src/config/locales.json

@ -1,277 +0,0 @@
{
"af-ZA":
[
"Afrikaans",
"Afrikaans"
],
"ar":
[
"العربية",
"Arabic"
],
"bg-BG":
[
"Български",
"Bulgarian"
],
"ca-AD":
[
"Català",
"Catalan"
],
"cs-CZ":
[
"Čeština",
"Czech"
],
"cy-GB":
[
"Cymraeg",
"Welsh"
],
"da-DK":
[
"Dansk",
"Danish"
],
"de-AT":
[
"Deutsch (Österreich)",
"German (Austria)"
],
"de-CH":
[
"Deutsch (Schweiz)",
"German (Switzerland)"
],
"de-DE":
[
"Deutsch (Deutschland)",
"German (Germany)"
],
"el-GR":
[
"Ελληνικά",
"Greek"
],
"en-GB":
[
"English (UK)",
"English (UK)"
],
"en-US":
[
"English (US)",
"English (US)"
],
"es-CL":
[
"Español (Chile)",
"Spanish (Chile)"
],
"es-ES":
[
"Español (España)",
"Spanish (Spain)"
],
"es-MX":
[
"Español (México)",
"Spanish (Mexico)"
],
"et-EE":
[
"Eesti keel",
"Estonian"
],
"eu":
[
"Euskara",
"Basque"
],
"fa-IR":
[
"فارسی",
"Persian"
],
"fi-FI":
[
"Suomi",
"Finnish"
],
"fr-CA":
[
"Français (Canada)",
"French (Canada)"
],
"fr-FR":
[
"Français (France)",
"French (France)"
],
"gl-ES":
[
"Galego (Spain)",
"Galician (Spain)"
],
"he-IL":
[
"עברית",
"Hebrew"
],
"hi-IN":
[
"हि",
"Hindi"
],
"hr-HR":
[
"Hrvatski",
"Croatian"
],
"hu-HU":
[
"Magyar",
"Hungarian"
],
"id-ID":
[
"Bahasa Indonesia",
"Indonesian"
],
"is-IS":
[
"Íslenska",
"Icelandic"
],
"it-IT":
[
"Italiano",
"Italian"
],
"ja-JP":
[
"日本語",
"Japanese"
],
"km-KH":
[
"ភរ",
"Khmer"
],
"ko-KR":
[
"한국어",
"Korean"
],
"la":
[
"Latina",
"Latin"
],
"lt-LT":
[
"Lietuvių kalba",
"Lithuanian"
],
"lv-LV":
[
"Latviešu",
"Latvian"
],
"mn-MN":
[
"Монгол",
"Mongolian"
],
"nb-NO":
[
"Norsk bokmål",
"Norwegian (Bokmål)"
],
"nl-NL":
[
"Nederlands",
"Dutch"
],
"nn-NO":
[
"Norsk nynorsk",
"Norwegian (Nynorsk)"
],
"pl-PL":
[
"Polski",
"Polish"
],
"pt-BR":
[
"Português (Brasil)",
"Portuguese (Brazil)"
],
"pt-PT":
[
"Português (Portugal)",
"Portuguese (Portugal)"
],
"ro-RO":
[
"Română",
"Romanian"
],
"ru-RU":
[
"Русский",
"Russian"
],
"sk-SK":
[
"Slovenčina",
"Slovak"
],
"sl-SI":
[
"Slovenščina",
"Slovenian"
],
"sr-RS":
[
"Српски / Srpski",
"Serbian"
],
"sv-SE":
[
"Svenska",
"Swedish"
],
"th-TH":
[
"ไทย",
"Thai"
],
"tr-TR":
[
"Türkçe",
"Turkish"
],
"uk-UA":
[
"Українська",
"Ukrainian"
],
"vi-VN":
[
"Tiếng Việt",
"Vietnamese"
],
"zh-CN":
[
"中文 (中国大陆)",
"Chinese (PRC)"
],
"zh-TW":
[
"中文 (台灣)",
"Chinese (Taiwan)"
]
}

6
src/config/memcached.json

@ -1,6 +0,0 @@
{
"port": 11211,
"host": "127.0.0.1",
"namespace": "",
"timeout": 3600
}

3
src/config/moderators.json

@ -1,3 +0,0 @@
[
""
]

12
src/config/nodes.json

@ -1,12 +0,0 @@
[
{
"description":"YGGtracker instance #1 running latest stable release",
"url":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker",
"manifest":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/api/manifest.json"
},
{
"description":"YGGtracker instance #2 running latest stable release",
"url":"http://[200:e6fd:bb3c:b354:cd3a:f939:753e:cd72]/yggtracker",
"manifest":"http://[200:e6fd:bb3c:b354:cd3a:f939:753e:cd72]/yggtracker/api/manifest.json"
}
]

7
src/config/peers.json

@ -1,7 +0,0 @@
[
{
"description":"YGGtracker public peer instance without traffic limit",
"url":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggstate",
"address":"tls://94.140.114.241:4708"
}
]

4
src/config/sphinx.json

@ -1,4 +0,0 @@
{
"port":9306,
"host":"127.0.0.1"
}

3
src/config/themes.json

@ -1,3 +0,0 @@
[
"default"
]

23
src/config/trackers.json

@ -1,23 +0,0 @@
[
{
"description":"YGGtracker instance, yggdrasil-only connections",
"url":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker",
"announce":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/announce",
"stats":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/stats",
"scrape":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/scrape"
},
{
"description":"Yggdrasil-only torrent tracker, operated by jeff",
"url":false,
"announce":"http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/announce",
"stats":"http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/stats",
"scrape":"http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/scrape"
},
{
"description":"Yggdrasil torrent tracker, operated by R4SAS",
"url":false,
"announce":"http://[316:c51a:62a3:8b9::5]/announce",
"stats":"http://[316:c51a:62a3:8b9::5]/stats",
"scrape":"http://[316:c51a:62a3:8b9::5]/scrape"
}
]

74
src/config/validator.json

@ -1,74 +0,0 @@
{
"host":
{
"regex": "/^0{0,1}[2-3][a-f0-9]{0,2}:/"
},
"page":
{
"title":
{
"required": true,
"length":
{
"min": 10,
"max": 255
},
"regex": "/.*/ui"
},
"description":
{
"required": false,
"length":
{
"min": 0,
"max": 10000
},
"regex": "/.*/ui"
},
"keyword":
{
"length":
{
"min": 0,
"max": 140
},
"regex": "/[\\w]+/ui"
},
"keywords":
{
"required": false,
"quantity":
{
"min": 0,
"max": 20
}
},
"image":
{
"required": false,
"mime": [
"image/png",
"image/gif",
"image/jpeg",
"image/webp"
],
"quantity":
{
"min": 0,
"max": 20
}
},
"torrent":
{
"required": true,
"mime": [
"application/x-bittorrent"
],
"quantity":
{
"min": 0,
"max": 20
}
}
}
}

23
src/config/website.json

@ -1,23 +0,0 @@
{
"name":"YGGtracker",
"scheme":"",
"host":"",
"port":"",
"path":"",
"default":
{
"locale":"en-US",
"user":
{
"status": true,
"approved": false
}
},
"api":
{
"export":
{
"enabled" : true
}
}
}

558
src/crontab/export/feed.php

@ -1,558 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.export.feed'), 1);
if (false === sem_acquire($semaphore, true))
{
exit (_('yggtracker.crontab.export.feed process locked by another thread.'));
}
// Bootstrap
require_once __DIR__ . '/../../config/bootstrap.php';
// Init Debug
$debug =
[
'dump' => [],
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Define public registry
$public = [
'user' => [],
'magnet' => [],
];
// Begin export
try
{
// Init API folder if not exists
@mkdir(__DIR__ . '/../public/api');
// Delete cached feeds
@unlink(__DIR__ . '/../public/api/manifest.json');
@unlink(__DIR__ . '/../public/api/users.json');
@unlink(__DIR__ . '/../public/api/magnets.json');
@unlink(__DIR__ . '/../public/api/magnetComments.json');
@unlink(__DIR__ . '/../public/api/magnetDownloads.json');
@unlink(__DIR__ . '/../public/api/magnetStars.json');
@unlink(__DIR__ . '/../public/api/magnetViews.json');
if (API_EXPORT_ENABLED)
{
// Manifest
$manifest =
[
'updated' => time(),
'version' => (string) API_VERSION,
'settings' => (object)
[
'YGGDRASIL_HOST_REGEX' => (string) YGGDRASIL_HOST_REGEX,
'NODE_RULE_SUBJECT' => (string) NODE_RULE_SUBJECT,
'NODE_RULE_LANGUAGES' => (string) NODE_RULE_LANGUAGES,
'USER_DEFAULT_APPROVED' => (bool) USER_DEFAULT_APPROVED,
'USER_AUTO_APPROVE_ON_MAGNET_APPROVE' => (bool) USER_AUTO_APPROVE_ON_MAGNET_APPROVE,
'USER_AUTO_APPROVE_ON_COMMENT_APPROVE' => (bool) USER_AUTO_APPROVE_ON_COMMENT_APPROVE,
'USER_DEFAULT_IDENTICON' => (string) USER_DEFAULT_IDENTICON,
'USER_IDENTICON_FIELD' => (string) USER_IDENTICON_FIELD,
'MAGNET_DEFAULT_APPROVED' => (bool) MAGNET_DEFAULT_APPROVED,
'MAGNET_DEFAULT_PUBLIC' => (bool) MAGNET_DEFAULT_PUBLIC,
'MAGNET_DEFAULT_COMMENTS' => (bool) MAGNET_DEFAULT_COMMENTS,
'MAGNET_DEFAULT_SENSITIVE' => (bool) MAGNET_DEFAULT_SENSITIVE,
'MAGNET_EDITOR_LOCK_TIMEOUT' => (int) MAGNET_EDITOR_LOCK_TIMEOUT,
'MAGNET_TITLE_MIN_LENGTH' => (int) MAGNET_TITLE_MIN_LENGTH,
'MAGNET_TITLE_MAX_LENGTH' => (int) MAGNET_TITLE_MAX_LENGTH,
'MAGNET_TITLE_REGEX' => (string) MAGNET_TITLE_REGEX,
'MAGNET_PREVIEW_MIN_LENGTH' => (int) MAGNET_PREVIEW_MIN_LENGTH,
'MAGNET_PREVIEW_MAX_LENGTH' => (int) MAGNET_PREVIEW_MAX_LENGTH,
'MAGNET_PREVIEW_REGEX' => (string) MAGNET_PREVIEW_REGEX,
'MAGNET_DESCRIPTION_MIN_LENGTH' => (int) MAGNET_DESCRIPTION_MIN_LENGTH,
'MAGNET_DESCRIPTION_MAX_LENGTH' => (int) MAGNET_DESCRIPTION_MAX_LENGTH,
'MAGNET_DESCRIPTION_REGEX' => (string) MAGNET_DESCRIPTION_REGEX,
'MAGNET_DN_MIN_LENGTH' => (int) MAGNET_DN_MIN_LENGTH,
'MAGNET_DN_MAX_LENGTH' => (int) MAGNET_DN_MAX_LENGTH,
'MAGNET_DN_REGEX' => (string) MAGNET_DN_REGEX,
'MAGNET_KT_MIN_LENGTH' => (int) MAGNET_KT_MIN_LENGTH,
'MAGNET_KT_MAX_LENGTH' => (int) MAGNET_KT_MAX_LENGTH,
'MAGNET_KT_MIN_QUANTITY' => (int) MAGNET_KT_MIN_QUANTITY,
'MAGNET_KT_MAX_QUANTITY' => (int) MAGNET_KT_MAX_QUANTITY,
'MAGNET_KT_REGEX' => (string) MAGNET_KT_REGEX,
'MAGNET_TR_MIN_QUANTITY' => (int) MAGNET_TR_MIN_QUANTITY,
'MAGNET_TR_MAX_QUANTITY' => (int) MAGNET_TR_MAX_QUANTITY,
'MAGNET_AS_MIN_QUANTITY' => (int) MAGNET_AS_MIN_QUANTITY,
'MAGNET_AS_MAX_QUANTITY' => (int) MAGNET_AS_MAX_QUANTITY,
'MAGNET_WS_MIN_QUANTITY' => (int) MAGNET_WS_MIN_QUANTITY,
'MAGNET_WS_MAX_QUANTITY' => (int) MAGNET_WS_MAX_QUANTITY,
'MAGNET_COMMENT_DEFAULT_APPROVED' => (bool) MAGNET_COMMENT_DEFAULT_APPROVED,
'MAGNET_COMMENT_DEFAULT_PUBLIC' => (bool) MAGNET_COMMENT_DEFAULT_PUBLIC,
'MAGNET_COMMENT_DEFAULT_PUBLIC' => (bool) MAGNET_COMMENT_DEFAULT_PUBLIC,
'MAGNET_COMMENT_MIN_LENGTH' => (int) MAGNET_COMMENT_MIN_LENGTH,
'MAGNET_COMMENT_MAX_LENGTH' => (int) MAGNET_COMMENT_MAX_LENGTH,
'MAGNET_STOP_WORDS_SIMILAR' => (object) MAGNET_STOP_WORDS_SIMILAR,
'API_USER_AGENT' => (string) API_USER_AGENT,
'API_EXPORT_ENABLED' => (bool) API_EXPORT_ENABLED,
'API_EXPORT_PUSH_ENABLED' => (bool) API_EXPORT_PUSH_ENABLED,
'API_EXPORT_USERS_ENABLED' => (bool) API_EXPORT_USERS_ENABLED,
'API_EXPORT_MAGNETS_ENABLED' => (bool) API_EXPORT_MAGNETS_ENABLED,
'API_EXPORT_MAGNET_DOWNLOADS_ENABLED' => (bool) API_EXPORT_MAGNET_DOWNLOADS_ENABLED,
'API_EXPORT_MAGNET_COMMENTS_ENABLED' => (bool) API_EXPORT_MAGNET_COMMENTS_ENABLED,
'API_EXPORT_MAGNET_STARS_ENABLED' => (bool) API_EXPORT_MAGNET_STARS_ENABLED,
'API_EXPORT_MAGNET_STARS_ENABLED' => (bool) API_EXPORT_MAGNET_STARS_ENABLED,
'API_EXPORT_MAGNET_VIEWS_ENABLED' => (bool) API_EXPORT_MAGNET_VIEWS_ENABLED,
'API_IMPORT_ENABLED' => (bool) API_IMPORT_ENABLED,
'API_IMPORT_PUSH_ENABLED' => (bool) API_IMPORT_PUSH_ENABLED,
'API_IMPORT_USERS_ENABLED' => (bool) API_IMPORT_USERS_ENABLED,
'API_IMPORT_USERS_APPROVED_ONLY' => (bool) API_IMPORT_USERS_APPROVED_ONLY,
'API_IMPORT_MAGNETS_ENABLED' => (bool) API_IMPORT_MAGNETS_ENABLED,
'API_IMPORT_MAGNETS_APPROVED_ONLY' => (bool) API_IMPORT_MAGNETS_APPROVED_ONLY,
'API_IMPORT_MAGNET_DOWNLOADS_ENABLED' => (bool) API_IMPORT_MAGNET_DOWNLOADS_ENABLED,
'API_IMPORT_MAGNET_COMMENTS_ENABLED' => (bool) API_IMPORT_MAGNET_COMMENTS_ENABLED,
'API_IMPORT_MAGNET_COMMENTS_APPROVED_ONLY' => (bool) API_IMPORT_MAGNET_COMMENTS_APPROVED_ONLY,
'API_IMPORT_MAGNET_STARS_ENABLED' => (bool) API_IMPORT_MAGNET_STARS_ENABLED,
'API_IMPORT_MAGNET_VIEWS_ENABLED' => (bool) API_IMPORT_MAGNET_VIEWS_ENABLED,
],
'totals' => (object)
[
'magnets' => (object)
[
'total' => $db->getMagnetsTotal(),
'distributed' => $db->getMagnetsTotalByUsersPublic(true),
'local' => $db->getMagnetsTotalByUsersPublic(false),
],
'downloads' => (object)
[
'total' => $db->getMagnetDownloadsTotal(),
'distributed' => $db->findMagnetDownloadsTotalByUsersPublic(true),
'local' => $db->findMagnetDownloadsTotalByUsersPublic(false),
],
'comments' => (object)
[
'total' => $db->getMagnetCommentsTotal(),
'distributed' => $db->findMagnetCommentsTotalByUsersPublic(true),
'local' => $db->findMagnetCommentsTotalByUsersPublic(false),
],
'stars' => (object)
[
'total' => $db->getMagnetStarsTotal(),
'distributed' => $db->findMagnetStarsTotalByUsersPublic(true),
'local' => $db->findMagnetStarsTotalByUsersPublic(false),
],
'views' => (object)
[
'total' => $db->getMagnetViewsTotal(),
'distributed' => $db->findMagnetViewsTotalByUsersPublic(true),
'local' => $db->findMagnetViewsTotalByUsersPublic(false),
],
],
'import' => (object)
[
'push' => API_IMPORT_PUSH_ENABLED ? sprintf('%s/api/push.php', WEBSITE_URL) : false,
],
'export' => (object)
[
'users' => API_EXPORT_USERS_ENABLED ? sprintf('%s/api/users.json', WEBSITE_URL) : false,
'magnets' => API_EXPORT_MAGNETS_ENABLED ? sprintf('%s/api/magnets.json', WEBSITE_URL) : false,
'magnetDownloads' => API_EXPORT_MAGNET_DOWNLOADS_ENABLED ? sprintf('%s/api/magnetDownloads.json', WEBSITE_URL) : false,
'magnetComments' => API_EXPORT_MAGNET_COMMENTS_ENABLED ? sprintf('%s/api/magnetComments.json', WEBSITE_URL) : false,
'magnetStars' => API_EXPORT_MAGNET_STARS_ENABLED ? sprintf('%s/api/magnetStars.json', WEBSITE_URL) : false,
'magnetViews' => API_EXPORT_MAGNET_VIEWS_ENABLED ? sprintf('%s/api/magnetViews.json', WEBSITE_URL) : false,
],
'trackers' => (object) json_decode(file_get_contents(__DIR__ . '/../../config/trackers.json')),
'nodes' => (object) json_decode(file_get_contents(__DIR__ . '/../../config/nodes.json')),
'peers' => (object) json_decode(file_get_contents(__DIR__ . '/../../config/peers.json')),
];
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/manifest.json', 'w+'))
{
fwrite($handle, json_encode($manifest));
fclose($handle);
chmod(__DIR__ . '/../../public/api/manifest.json', 0774);
}
// Users
if (API_EXPORT_USERS_ENABLED)
{
$users = [];
foreach ($db->getUsers() as $user)
{
// Dump public data only
if ($user->public)
{
$users[] = (object)
[
'userId' => (int) $user->userId,
'address' => (string) $user->address,
'timeAdded' => (int) $user->timeAdded,
'timeUpdated' => (int) $user->timeUpdated,
'approved' => (bool) $user->approved,
'magnets' => (int) $db->findMagnetsTotalByUserId($user->userId),
'downloads' => (int) $db->findMagnetDownloadsTotalByUserId($user->userId),
'comments' => (int) $db->findMagnetCommentsTotalByUserId($user->userId),
'stars' => (int) $db->findMagnetStarsTotalByUserId($user->userId),
'views' => (int) $db->findMagnetViewsTotalByUserId($user->userId),
];
}
// Cache public status
$public['user'][$user->userId] = (bool) $user->public;
}
/// Dump users feed
if ($handle = fopen(__DIR__ . '/../../public/api/users.json', 'w+'))
{
fwrite($handle, json_encode($users));
fclose($handle);
chmod(__DIR__ . '/../../public/api/users.json', 0774);
}
}
// Magnets
if (API_EXPORT_MAGNETS_ENABLED)
{
$magnets = [];
foreach ($db->getMagnets($user->userId) as $magnet)
{
// Dump public data only
if ($magnet->public &&
$public['user'][$magnet->userId]) // After upgrade, some users have not updated their public status.
// Remote node have warning on import, because user info still hidden to init new profile there.
// Stop magnets export without public profile available, even magnet is public.
{
// Info Hash
$xt = [];
foreach ($db->findMagnetToInfoHashByMagnetId($magnet->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
$xt[] = (object) [
'version' => (float) $infoHash->version,
'value' => (string) $infoHash->value,
];
}
}
// Keyword Topic
$kt = [];
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result)
{
$kt[] = $db->getKeywordTopic($result->keywordTopicId)->value;
}
// 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);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$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);
}
// 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);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$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);
}
// 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);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$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);
}
$magnets[] = (object)
[
'magnetId' => (int) $magnet->magnetId,
'userId' => (int) $magnet->userId,
'title' => (string) $magnet->title,
'preview' => (string) $magnet->preview,
'description' => (string) $magnet->description,
'comments' => (bool) $magnet->comments,
'sensitive' => (bool) $magnet->sensitive,
'approved' => (bool) $magnet->approved,
'timeAdded' => (int) $magnet->timeAdded,
'timeUpdated' => (int) $magnet->timeUpdated,
'dn' => (string) $magnet->dn,
'xl' => (float) $magnet->xl,
'xt' => (object) $xt,
'kt' => (object) $kt,
'tr' => (object) $tr,
'as' => (object) $as,
'xs' => (object) $xs,
];
}
// Cache public status
if (!empty($public['user'][$magnet->userId]))
{
$public['magnet'][$magnet->magnetId] = (bool) $magnet->public;
} else {
$public['magnet'][$magnet->magnetId] = false;
}
}
/// Dump magnets feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnets.json', 'w+'))
{
fwrite($handle, json_encode($magnets));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnets.json', 0774);
}
}
// Magnet downloads
if (API_EXPORT_MAGNET_DOWNLOADS_ENABLED)
{
$magnetDownloads = [];
foreach ($db->getMagnetDownloads() as $magnetDownload)
{
// Dump public data only
if (!empty($public['magnet'][$magnetDownload->magnetId]) &&
!empty($public['user'][$magnetDownload->userId]))
{
$magnetDownloads[] = (object)
[
'magnetDownloadId' => (int) $magnetDownload->magnetDownloadId,
'userId' => (int) $magnetDownload->userId,
'magnetId' => (int) $magnetDownload->magnetId,
'timeAdded' => (int) $magnetDownload->timeAdded,
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetDownloads.json', 'w+'))
{
fwrite($handle, json_encode($magnetDownloads));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetDownloads.json', 0774);
}
}
// Magnet comments
if (API_EXPORT_MAGNET_COMMENTS_ENABLED)
{
$magnetComments = [];
foreach ($db->getMagnetComments() as $magnetComment)
{
// Dump public data only
if (!empty($public['magnet'][$magnetComment->magnetId]) &&
!empty($public['user'][$magnetComment->userId]))
{
$magnetComments[] = (object)
[
'magnetCommentId' => (int) $magnetComment->magnetCommentId,
'magnetCommentIdParent' => $magnetComment->magnetCommentIdParent,
'userId' => (int) $magnetComment->userId,
'magnetId' => (int) $magnetComment->magnetId,
'timeAdded' => (int) $magnetComment->timeAdded,
'approved' => (bool) $magnetComment->approved,
'value' => (string) $magnetComment->value
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetComments.json', 'w+'))
{
fwrite($handle, json_encode($magnetComments));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetComments.json', 0774);
}
}
// Magnet stars
if (API_EXPORT_MAGNET_STARS_ENABLED)
{
$magnetStars = [];
foreach ($db->getMagnetStars() as $magnetStar)
{
// Dump public data only
if (!empty($public['magnet'][$magnetStar->magnetId]) &&
!empty($public['user'][$magnetStar->userId]))
{
$magnetStars[] = (object)
[
'magnetStarId' => (int) $magnetStar->magnetStarId,
'userId' => (int) $magnetStar->userId,
'magnetId' => (int) $magnetStar->magnetId,
'value' => (bool) $magnetStar->value,
'timeAdded' => (int) $magnetStar->timeAdded,
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetStars.json', 'w+'))
{
fwrite($handle, json_encode($magnetStars));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetStars.json', 0774);
}
}
// Magnet views
if (API_EXPORT_MAGNET_VIEWS_ENABLED)
{
$magnetViews = [];
foreach ($db->getMagnetViews() as $magnetView)
{
// Dump public data only
if (!empty($public['magnet'][$magnetView->magnetId]) &&
!empty($public['user'][$magnetView->userId]))
{
$magnetViews[] = (object)
[
'magnetViewId' => (int) $magnetView->magnetViewId,
'userId' => (int) $magnetView->userId,
'magnetId' => (int) $magnetView->magnetId,
'timeAdded' => (int) $magnetView->timeAdded,
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetViews.json', 'w+'))
{
fwrite($handle, json_encode($magnetViews));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetViews.json', 0774);
}
}
}
} catch (EXception $e) {
var_dump($e);
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_EXPORT_FEED_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0774, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_FEED_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_FEED_FILENAME, 0774);
}
}

506
src/crontab/export/push.php

@ -1,506 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.export.push'), 1);
if (false === sem_acquire($semaphore, true))
{
exit (_('yggtracker.crontab.export.push process locked by another thread.'));
}
// Bootstrap
require_once __DIR__ . '/../../config/bootstrap.php';
// Init Debug
$debug =
[
'dump' => [],
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Define public registry
$public = [
'user' => [],
'magnet' => [],
];
// Push export enabled
if (API_EXPORT_PUSH_ENABLED)
{
// Get push queue from memory pool
foreach((array) $memoryApiExportPush = $memory->get('api.export.push') as $id => $push)
{
// Init request
$request = [];
// User request
if (!empty($push->userId) && API_EXPORT_USERS_ENABLED)
{
// Get user info
if ($user = $db->getUser($push->userId))
{
// Dump public data only
if ($user->public)
{
$request['user'] = (object)
[
'userId' => (int) $user->userId,
'address' => (string) $user->address,
'timeAdded' => (int) $user->timeAdded,
'timeUpdated' => (int) $user->timeUpdated,
'approved' => (bool) $user->approved,
];
// Cache public status
$public['user'][$user->userId] = (bool) $user->public;
}
}
}
// Magnet request
if (!empty($push->magnetId) && API_EXPORT_MAGNETS_ENABLED)
{
// Get magnet info
if ($magnet = $db->getMagnet($push->magnetId))
{
if ($magnet->public &&
$public['user'][$magnet->userId]) // After upgrade, some users have not updated their public status.
// Remote node have warning on import, because user info still hidden to init new profile there.
// Stop magnets export without public profile available, even magnet is public.
{
// Info Hash
$xt = [];
foreach ($db->findMagnetToInfoHashByMagnetId($magnet->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
$xt[] = (object) [
'version' => (float) $infoHash->version,
'value' => (string) $infoHash->value,
];
}
}
// Keyword Topic
$kt = [];
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result)
{
$kt[] = $db->getKeywordTopic($result->keywordTopicId)->value;
}
// 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);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$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);
}
// 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);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$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);
}
// 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);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$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);
}
$request['magnet'] = (object)
[
'magnetId' => (int) $magnet->magnetId,
'userId' => (int) $magnet->userId,
'title' => (string) $magnet->title,
'preview' => (string) $magnet->preview,
'description' => (string) $magnet->description,
'comments' => (bool) $magnet->comments,
'sensitive' => (bool) $magnet->sensitive,
'approved' => (bool) $magnet->approved,
'timeAdded' => (int) $magnet->timeAdded,
'timeUpdated' => (int) $magnet->timeUpdated,
'dn' => (string) $magnet->dn,
'xl' => (float) $magnet->xl,
'xt' => (object) $xt,
'kt' => (object) $kt,
'tr' => (object) $tr,
'as' => (object) $as,
'xs' => (object) $xs,
];
}
// Cache public status
if (!empty($public['user'][$magnet->userId]))
{
$public['magnet'][$magnet->magnetId] = (bool) $magnet->public;
} else {
$public['magnet'][$magnet->magnetId] = false;
}
}
}
// Magnet download request
if (!empty($push->magnetDownloadId) && API_EXPORT_MAGNET_DOWNLOADS_ENABLED)
{
// Get magnet download info
if ($magnetDownload = $db->getMagnetDownload($push->magnetDownloadId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetDownload->magnetId]) &&
!empty($public['user'][$magnetDownload->userId]))
{
$request['magnetDownload'] = (object)
[
'magnetDownloadId' => (int) $magnetDownload->magnetDownloadId,
'userId' => (int) $magnetDownload->userId,
'magnetId' => (int) $magnetDownload->magnetId,
'timeAdded' => (int) $magnetDownload->timeAdded,
];
}
}
}
// Magnet comment request
if (!empty($push->magnetCommentId) && API_EXPORT_MAGNET_COMMENTS_ENABLED)
{
// Get magnet comment info
if ($magnetComment = $db->getMagnetComment($push->magnetCommentId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetComment->magnetId]) &&
!empty($public['user'][$magnetComment->userId]))
{
$request['magnetComment'] = (object)
[
'magnetCommentId' => (int) $magnetComment->magnetCommentId,
'magnetCommentIdParent' => (int) $magnetComment->magnetCommentIdParent,
'userId' => (int) $magnetComment->userId,
'magnetId' => (int) $magnetComment->magnetId,
'timeAdded' => (int) $magnetComment->timeAdded,
'approved' => (bool) $magnetComment->approved,
'value' => (string) $magnetComment->value
];
}
}
}
// Magnet star request
if (!empty($push->magnetStarId) && API_EXPORT_MAGNET_STARS_ENABLED)
{
// Get magnet star info
if ($magnetStar = $db->getMagnetStar($push->magnetStarId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetStar->magnetId]) &&
!empty($public['user'][$magnetStar->userId]))
{
$request['magnetStar'] = (object)
[
'magnetStarId' => (int) $magnetStar->magnetStarId,
'userId' => (int) $magnetStar->userId,
'magnetId' => (int) $magnetStar->magnetId,
'value' => (bool) $magnetStar->value,
'timeAdded' => (int) $magnetStar->timeAdded,
];
}
}
}
// Magnet view request
if (!empty($push->magnetViewId) && API_EXPORT_MAGNET_VIEWS_ENABLED)
{
// Get magnet view info
if ($magnetView = $db->getMagnetView($push->magnetViewId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetView->magnetId]) &&
!empty($public['user'][$magnetView->userId]))
{
$request['magnetView'] = (object)
[
'magnetViewId' => (int) $magnetView->magnetViewId,
'userId' => (int) $magnetView->userId,
'magnetId' => (int) $magnetView->magnetId,
'timeAdded' => (int) $magnetView->timeAdded,
];
}
}
}
// Check request
if (empty($request))
{
// Amy request data match conditions, skip
continue;
}
// Send push data
foreach (json_decode(
file_get_contents(__DIR__ . '/../../config/nodes.json')
) as $node)
{
// Manifest exists
if (empty($node->manifest))
{
$debug['dump']['warning'][] = sprintf(
_('Manifest URL not provided: %s'),
$node
);
continue;
}
// Skip non-condition addresses
$error = [];
if (!Valid::url($node->manifest, $error))
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest URL invalid: %s'),
print_r(
$error,
true
)
);
continue;
}
// Skip current host
$thisUrl = Yggverse\Parser\Url::parse(WEBSITE_URL);
$manifestUrl = Yggverse\Parser\Url::parse($node->manifest);
if (empty($thisUrl->host->name) ||
empty($manifestUrl->host->name) ||
$manifestUrl->host->name == $thisUrl->host->name) // @TODO some mirrors could be available, improve condition
{
// No debug warnings in this case, continue next item
continue;
}
// Get node manifest
// @TODO cache to prevent extra-queries as usually this script running every minute
$curl = new Curl($node->manifest, API_USER_AGENT);
$debug['http']['total']++;
if (200 != $code = $curl->getCode())
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest unreachable with code: "%s"'),
$code
);
continue;
}
if (!$manifest = $curl->getResponse())
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest URL "%s" has broken response'),
$node->manifest
);
continue;
}
// API channel not exists
if (empty($manifest->import))
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest import URL not provided: %s'),
$node
);
continue;
}
// Push API channel not exists
if (empty($manifest->import->push))
{
$debug['dump'][$manifest->import->push]['warning'][] = sprintf(
_('Manifest import push URL not provided: %s'),
$node
);
continue;
}
// Skip sending to non-condition addresses
$error = [];
if (!Valid::url($manifest->import->push, $error))
{
$debug['dump'][$manifest->import->push]['warning'][] = sprintf(
_('Manifest import push URL invalid: %s'),
print_r(
$error,
true
)
);
continue;
}
// Skip current host
$thisUrl = Yggverse\Parser\Url::parse(WEBSITE_URL);
$pushUrl = Yggverse\Parser\Url::parse($manifest->import->push);
if (empty($thisUrl->host->name) ||
empty($pushUrl->host->name) ||
$pushUrl->host->name == $thisUrl->host->name) // @TODO some mirrors could be available, improve condition
{
// No debug warnings in this case, continue next item
continue;
}
// @TODO add recipient manifest conditions check to not disturb it API without needs
// Send push request
$debug['dump'][$manifest->import->push]['request'][] = $request;
$curl = new Curl(
$manifest->import->push,
API_USER_AGENT,
[
'data' => json_encode($request)
]
);
$debug['http']['total']++;
if (200 != $code = $curl->getCode())
{
$debug['dump'][$manifest->import->push]['warning'][] = sprintf(
_('Server returned code "%s"'),
$code
);
continue;
}
if (!$response = $curl->getResponse())
{
$debug['dump'][$manifest->import->push]['warning'][] = _('Could not receive server response');
continue;
}
$debug['dump'][$manifest->import->push]['response'][] = $response;
}
// Drop processed item from queue
unset($memoryApiExportPush[$id]);
}
// Update memory pool
$memory->set('api.export.push', $memoryApiExportPush, 3600);
}
// Export push disabled, free api.export.push pool
else
{
$memory->delete('api.export.push');
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_EXPORT_PUSH_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0770, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_PUSH_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_PUSH_FILENAME, 0770);
}
}

1193
src/crontab/import/feed.php

File diff suppressed because it is too large Load Diff

158
src/crontab/scrape.php

@ -1,158 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.scrape'), 1);
if (false === sem_acquire($semaphore, true)) {
exit (PHP_EOL . 'yggtracker.crontab.scrape process locked by another thread.' . PHP_EOL);
}
// Bootstrap
require_once __DIR__ . '/../config/bootstrap.php';
// Init Debug
$debug = [
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Init Scraper
try {
$scraper = new Scrapeer\Scraper();
} catch(Exception $e) {
var_dump($e);
exit;
}
// Begin
try {
$db->beginTransaction();
// Reset time offline by timeout
$db->resetMagnetToAddressTrackerTimeOfflineByTimeout(
CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT
);
foreach ($db->getMagnetToAddressTrackerScrapeQueue(CRAWLER_SCRAPE_QUEUE_LIMIT) as $queue)
{
$hashes = [];
foreach ($db->findMagnetToInfoHashByMagnetId($queue->magnetId) as $result)
{
$hashes[] = $db->getInfoHash($result->infoHashId)->value;
}
if ($addressTracker = $db->getAddressTracker($queue->addressTrackerId))
{
// Build url
$scheme = $db->getScheme($addressTracker->schemeId);
$host = $db->getHost($addressTracker->hostId);
$port = $db->getPort($addressTracker->portId);
$uri = $db->getUri($addressTracker->uriId);
$url = $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);
foreach ($hashes as $hash)
{
if ($scrape = $scraper->scrape([$hash], [$url], null, 1))
{
$db->updateMagnetToAddressTrackerTimeOffline(
$queue->magnetToAddressTrackerId,
null
);
if (isset($scrape[$hash]['seeders']))
{
$db->updateMagnetToAddressTrackerSeeders(
$queue->magnetToAddressTrackerId,
(int) $scrape[$hash]['seeders'],
time()
);
}
if (isset($scrape[$hash]['completed']))
{
$db->updateMagnetToAddressTrackerCompleted(
$queue->magnetToAddressTrackerId,
(int) $scrape[$hash]['completed'],
time()
);
}
if (isset($scrape[$hash]['leechers']))
{
$db->updateMagnetToAddressTrackerLeechers(
$queue->magnetToAddressTrackerId,
(int) $scrape[$hash]['leechers'],
time()
);
}
}
else
{
$db->updateMagnetToAddressTrackerTimeOffline(
$queue->magnetToAddressTrackerId,
time()
);
}
}
}
}
$db->commit();
} catch (EXception $e) {
$db->rollback();
var_dump($e);
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_SCRAPE_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0774, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_SCRAPE_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_SCRAPE_FILENAME, 0774);
}
}

86
src/crontab/sitemap.php

@ -1,86 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.sitemap'), 1);
if (false === sem_acquire($semaphore, true)) {
exit (PHP_EOL . 'yggtracker.crontab.sitemap process locked by another thread.' . PHP_EOL);
}
// Bootstrap
require_once __DIR__ . '/../config/bootstrap.php';
// Init Debug
$debug = [
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Begin
try {
// Delete cache
@unlink(__DIR__ . '/../public/sitemap.xml');
if ($handle = fopen(__DIR__ . '/../public/sitemap.xml', 'w+'))
{
fwrite($handle, '<?xml version="1.0" encoding="UTF-8"?>');
fwrite($handle, '<urlset>');
foreach ($db->getMagnets() as $magnet)
{
if ($magnet->public && $magnet->approved)
{
fwrite($handle, sprintf('<url><loc>%s/magnet.php?magnetId=%s</loc></url>', WEBSITE_URL, $magnet->magnetId));
}
}
fwrite($handle, '</urlset>');
fclose($handle);
}
} catch (EXception $e) {
var_dump($e);
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_SITEMAP_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0774, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_SITEMAP_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_SITEMAP_FILENAME, 0774);
}
}

97
src/library/curl.php

@ -1,97 +0,0 @@
<?php
class Curl
{
private $_connection;
private $_response;
public function __construct(string $url,
string $userAgent = 'YGGtracker',
array $post = [],
int $connectTimeout = 10,
bool $header = false,
bool $followLocation = false,
int $maxRedirects = 10,
bool $sslVerifyHost = false,
bool $sslVerifyPeer = false)
{
$this->_connection = curl_init($url);
if ($userAgent)
{
curl_setopt($this->_connection, CURLOPT_USERAGENT, $userAgent);
}
if (!empty($post))
{
curl_setopt($this->_connection, CURLOPT_POST, true);
curl_setopt($this->_connection, CURLOPT_POSTFIELDS, http_build_query($post));
}
if ($header) {
curl_setopt($this->_connection, CURLOPT_HEADER, true);
}
if ($followLocation) {
curl_setopt($this->_connection, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($this->_connection, CURLOPT_MAXREDIRS, $maxRedirects);
}
curl_setopt($this->_connection, CURLOPT_FRESH_CONNECT, true);
curl_setopt($this->_connection, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->_connection, CURLOPT_CONNECTTIMEOUT, $connectTimeout);
curl_setopt($this->_connection, CURLOPT_TIMEOUT, $connectTimeout);
curl_setopt($this->_connection, CURLOPT_SSL_VERIFYHOST, $sslVerifyHost);
curl_setopt($this->_connection, CURLOPT_SSL_VERIFYPEER, $sslVerifyPeer);
$this->_response = curl_exec($this->_connection);
}
public function __destruct()
{
curl_close($this->_connection);
}
public function getError()
{
if (curl_errno($this->_connection))
{
return curl_errno($this->_connection);
}
else
{
return false;
}
}
public function getCode()
{
return curl_getinfo($this->_connection, CURLINFO_HTTP_CODE);
}
public function getContentType()
{
return curl_getinfo($this->_connection, CURLINFO_CONTENT_TYPE);
}
public function getSizeDownload()
{
return curl_getinfo($this->_connection, CURLINFO_SIZE_DOWNLOAD);
}
public function getSizeRequest()
{
return curl_getinfo($this->_connection, CURLINFO_REQUEST_SIZE);
}
public function getTotalTime()
{
return curl_getinfo($this->_connection, CURLINFO_TOTAL_TIME_T);
}
public function getResponse(bool $json = true)
{
return $json ? json_decode($this->_response) : $this->_response;
}
}

27
src/library/environment.php

@ -1,27 +0,0 @@
<?php
class Environment
{
public static function config(string $name) : object
{
$config = __DIR__ . '/../config/' . $name . '.json';
if (file_exists(__DIR__ . '/../config/.env'))
{
$environment = file_get_contents(__DIR__ . '/../config/.env');
$filename = __DIR__ . '/../config/' . $environment . '/' . $name . '.json';
if (file_exists($filename))
{
$config = $filename;
}
}
return (object) json_decode(
file_get_contents(
$config
)
);
}
}

48
src/library/filter.php

@ -1,48 +0,0 @@
<?php
class Filter
{
public static function magnetTitle(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
public static function magnetPreview(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
public static function magnetDescription(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
public static function magnetDn(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
}

692
src/library/scrapeer.php

@ -1,692 +0,0 @@
<?php
/**
* Scrapeer, a tiny PHP library that lets you scrape
* HTTP(S) and UDP trackers for torrent information.
*
* This file is extensively based on Johannes Zinnau's
* work, which can be found at https://goo.gl/7hyjde
*
* Licensed under a Creative Commons
* Attribution-ShareAlike 3.0 Unported License
* http://creativecommons.org/licenses/by-sa/3.0
*
* @package Scrapeer
*/
namespace Scrapeer;
/**
* The one and only class you'll ever need.
*/
class Scraper {
/**
* Current version of Scrapeer
*
* @var string
*/
const VERSION = '0.5.4';
/**
* Array of errors
*
* @var array
*/
private $errors = array();
/**
* Array of infohashes to scrape
*
* @var array
*/
private $infohashes = array();
/**
* Timeout for a single tracker
*
* @var int
*/
private $timeout;
/**
* Initiates the scraper
*
* @throws \RangeException In case of invalid amount of info-hashes.
*
* @param array|string $hashes List (>1) or string of infohash(es).
* @param array|string $trackers List (>1) or string of tracker(s).
* @param int|null $max_trackers Optional. Maximum number of trackers to be scraped, Default all.
* @param int $timeout Optional. Maximum time for each tracker scrape in seconds, Default 2.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
public function scrape( $hashes, $trackers, $max_trackers = null, $timeout = 2, $announce = false ) {
$final_result = array();
if ( empty( $trackers ) ) {
$this->errors[] = 'No tracker specified, aborting.';
return $final_result;
} else if ( ! is_array( $trackers ) ) {
$trackers = array( $trackers );
}
if ( is_int( $timeout ) ) {
$this->timeout = $timeout;
} else {
$this->timeout = 2;
$this->errors[] = 'Timeout must be an integer. Using default value.';
}
try {
$this->infohashes = $this->normalize_infohashes( $hashes );
} catch ( \RangeException $e ) {
$this->errors[] = $e->getMessage();
return $final_result;
}
$max_iterations = is_int( $max_trackers ) ? $max_trackers : count( $trackers );
foreach ( $trackers as $index => $tracker ) {
if ( ! empty( $this->infohashes ) && $index < $max_iterations ) {
$info = parse_url( $tracker );
$protocol = $info['scheme'];
$host = $info['host'];
if ( empty( $protocol ) || empty( $host ) ) {
$this->errors[] = 'Skipping invalid tracker (' . $tracker . ').';
continue;
}
$port = isset( $info['port'] ) ? $info['port'] : null;
$path = isset( $info['path'] ) ? $info['path'] : null;
$passkey = $this->get_passkey( $path );
$result = $this->try_scrape( $protocol, $host, $port, $passkey, $announce );
$final_result = array_merge( $final_result, $result );
continue;
}
break;
}
return $final_result;
}
/**
* Normalizes the given hashes
*
* @throws \RangeException If amount of valid infohashes > 64 or < 1.
*
* @param array $infohashes List of infohash(es).
* @return array Normalized infohash(es).
*/
private function normalize_infohashes( $infohashes ) {
if ( ! is_array( $infohashes ) ) {
$infohashes = array( $infohashes );
}
foreach ( $infohashes as $index => $infohash ) {
if ( ! preg_match( '/^[a-f0-9]{40}$/i', $infohash ) ) {
$this->errors[] = 'Invalid infohash skipped (' . $infohash . ').';
unset( $infohashes[ $index ] );
}
}
$total_infohashes = count( $infohashes );
if ( $total_infohashes > 64 || $total_infohashes < 1 ) {
throw new \RangeException( 'Invalid amount of valid infohashes (' . $total_infohashes . ').' );
}
$infohashes = array_values( $infohashes );
return $infohashes;
}
/**
* Returns the passkey found in the scrape request.
*
* @param string $path Path from the scrape request.
* @return string Passkey or empty string.
*/
private function get_passkey( $path ) {
if ( ! is_null( $path ) && preg_match( '/[a-z0-9]{32}/i', $path, $matches ) ) {
return '/' . $matches[0];
}
return '';
}
/**
* Tries to scrape with a single tracker.
*
* @throws \Exception In case of unsupported protocol.
*
* @param string $protocol Protocol of the tracker.
* @param string $host Domain or address of the tracker.
* @param int $port Optional. Port number of the tracker.
* @param string $passkey Optional. Passkey provided in the scrape request.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
private function try_scrape( $protocol, $host, $port, $passkey, $announce ) {
$infohashes = $this->infohashes;
$this->infohashes = array();
$results = array();
try {
switch ( $protocol ) {
case 'udp':
$port = isset( $port ) ? $port : 80;
$results = $this->scrape_udp( $infohashes, $host, $port, $announce );
break;
case 'http':
$port = isset( $port ) ? $port : 80;
$results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce );
break;
case 'https':
$port = isset( $port ) ? $port : 443;
$results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce );
break;
default:
throw new \Exception( 'Unsupported protocol (' . $protocol . '://' . $host . ').' );
}
} catch ( \Exception $e ) {
$this->infohashes = $infohashes;
$this->errors[] = $e->getMessage();
}
return $results;
}
/**
* Initiates the HTTP(S) scraping
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $protocol Protocol to use for the scraping.
* @param string $host Domain or IP address of the tracker.
* @param int $port Optional. Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @param string $passkey Optional. Passkey provided in the scrape request.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
private function scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce ) {
if ( true === $announce ) {
$response = $this->http_announce( $infohashes, $protocol, $host, $port, $passkey );
} else {
$query = $this->http_query( $infohashes, $protocol, $host, $port, $passkey );
$response = $this->http_request( $query, $host, $port );
}
$results = $this->http_data( $response, $infohashes, $host );
return $results;
}
/**
* Builds the HTTP(S) query
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $protocol Protocol to use for the scraping.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @param string $passkey Optional. Passkey provided in the scrape request.
* @return string Request query.
*/
private function http_query( $infohashes, $protocol, $host, $port, $passkey ) {
$tracker_url = $protocol . '://' . $host . ':' . $port . $passkey;
$scrape_query = '';
foreach ( $infohashes as $index => $infohash ) {
if ( $index > 0 ) {
$scrape_query .= '&info_hash=' . urlencode( pack( 'H*', $infohash ) );
} else {
$scrape_query .= '/scrape?info_hash=' . urlencode( pack( 'H*', $infohash ) );
}
}
$request_query = $tracker_url . $scrape_query;
return $request_query;
}
/**
* Executes the query and returns the result
*
* @throws \Exception If the connection can't be established.
* @throws \Exception If the response isn't valid.
*
* @param string $query The query that will be executed.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @return string Request response.
*/
private function http_request( $query, $host, $port ) {
$context = stream_context_create( array(
'http' => array(
'timeout' => $this->timeout,
),
));
if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) {
throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' );
}
if ( substr( $response, 0, 12 ) !== 'd5:filesd20:' ) {
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
}
return $response;
}
/**
* Builds the query, sends the announce request and returns the data
*
* @throws \Exception If the connection can't be established.
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $protocol Protocol to use for the scraping.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @param string $passkey Optional. Passkey provided in the scrape request.
* @return string Request response.
*/
private function http_announce( $infohashes, $protocol, $host, $port, $passkey ) {
$tracker_url = $protocol . '://' . $host . ':' . $port . $passkey;
$context = stream_context_create( array(
'http' => array(
'timeout' => $this->timeout,
),
));
$response_data = '';
foreach ( $infohashes as $infohash ) {
$query = $tracker_url . '/announce?info_hash=' . urlencode( pack( 'H*', $infohash ) );
if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) {
throw new \Exception( 'Invalid announce connection (' . $host . ':' . $port . ').' );
}
if ( substr( $response, 0, 12 ) !== 'd8:completei' ||
substr( $response, 0, 46 ) === 'd8:completei0e10:downloadedi0e10:incompletei1e' ) {
continue;
}
$ben_hash = '20:' . pack( 'H*', $infohash ) . 'd';
$response_data .= $ben_hash . $response;
}
return $response_data;
}
/**
* Parses the response and returns the data
*
* @param string $response The response that will be parsed.
* @param array $infohashes List of infohash(es).
* @param string $host Domain or IP address of the tracker.
* @return array Parsed data.
*/
private function http_data( $response, $infohashes, $host ) {
$torrents_data = array();
foreach ( $infohashes as $infohash ) {
$ben_hash = '20:' . pack( 'H*', $infohash ) . 'd';
$start_pos = strpos( $response, $ben_hash );
if ( false !== $start_pos ) {
$start = $start_pos + 24;
$head = substr( $response, $start );
$end = strpos( $head, 'ee' ) + 1;
$data = substr( $response, $start, $end );
$seeders = '8:completei';
$torrent_info['seeders'] = $this->get_information( $data, $seeders, 'e' );
$completed = '10:downloadedi';
$torrent_info['completed'] = $this->get_information( $data, $completed, 'e' );
$leechers = '10:incompletei';
$torrent_info['leechers'] = $this->get_information( $data, $leechers, 'e' );
$torrents_data[ $infohash ] = $torrent_info;
} else {
$this->collect_infohash( $infohash );
$this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.';
}
}
return $torrents_data;
}
/**
* Parses a string and returns the data between $start and $end.
*
* @param string $data The data that will be parsed.
* @param string $start Beginning part of the data.
* @param string $end Ending part of the data.
* @return int Parsed information or 0.
*/
private function get_information( $data, $start, $end ) {
$start_pos = strpos( $data, $start );
if ( false !== $start_pos ) {
$start = $start_pos + strlen( $start );
$head = substr( $data, $start );
$end = strpos( $head, $end );
$information = substr( $data, $start, $end );
return (int) $information;
}
return 0;
}
/**
* Initiates the UDP scraping
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $host Domain or IP address of the tracker.
* @param int $port Optional. Port number of the tracker, Default 80.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
private function scrape_udp( $infohashes, $host, $port, $announce ) {
list( $socket, $transaction_id, $connection_id ) = $this->prepare_udp( $host, $port );
if ( true === $announce ) {
$response = $this->udp_announce( $socket, $infohashes, $connection_id );
$keys = 'Nleechers/Nseeders';
$start = 12;
$end = 16;
$offset = 20;
} else {
$response = $this->udp_scrape( $socket, $infohashes, $connection_id, $transaction_id, $host, $port );
$keys = 'Nseeders/Ncompleted/Nleechers';
$start = 8;
$end = $offset = 12;
}
$results = $this->udp_scrape_data( $response, $infohashes, $host, $keys, $start, $end, $offset );
return $results;
}
/**
* Prepares the UDP connection
*
* @param string $host Domain or IP address of the tracker.
* @param int $port Optional. Port number of the tracker, Default 80.
* @return array Created socket, transaction ID and connection ID.
*/
private function prepare_udp( $host, $port ) {
$socket = $this->udp_create_connection( $host, $port );
$transaction_id = $this->udp_connection_request( $socket );
$connection_id = $this->udp_connection_response( $socket, $transaction_id, $host, $port );
return array( $socket, $transaction_id, $connection_id );
}
/**
* Creates the UDP socket and establishes the connection
*
* @throws \Exception If the socket couldn't be created or connected to.
*
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80.
* @return resource $socket Created and connected socket.
*/
private function udp_create_connection( $host, $port ) {
if ( false === ( $socket = @socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ) ) ) {
throw new \Exception( "Couldn't create socket." );
}
$timeout = $this->timeout;
socket_set_option( $socket, SOL_SOCKET, SO_RCVTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) );
socket_set_option( $socket, SOL_SOCKET, SO_SNDTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) );
if ( false === @socket_connect( $socket, $host, $port ) ) {
throw new \Exception( "Couldn't connect to socket." );
}
return $socket;
}
/**
* Writes to the connected socket and returns the transaction ID
*
* @throws \Exception If the socket couldn't be written to.
*
* @param resource $socket The socket resource.
* @return int The transaction ID.
*/
private function udp_connection_request( $socket ) {
$connection_id = "\x00\x00\x04\x17\x27\x10\x19\x80";
$action = pack( 'N', 0 );
$transaction_id = mt_rand( 0, 2147483647 );
$buffer = $connection_id . $action . pack( 'N', $transaction_id );
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
socket_close( $socket );
throw new \Exception( "Couldn't write to socket." );
}
return $transaction_id;
}
/**
* Reads the connection response and returns the connection ID
*
* @throws \Exception If anything fails with the scraping.
*
* @param resource $socket The socket resource.
* @param int $transaction_id The transaction ID.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80.
* @return string The connection ID.
*/
private function udp_connection_response( $socket, $transaction_id, $host, $port ) {
if ( false === ( $response = @socket_read( $socket, 16 ) ) ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape connection! (' . $host . ':' . $port . ').' );
}
if ( strlen( $response ) < 16 ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
}
$result = unpack( 'Naction/Ntransaction_id', $response );
if ( 0 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' );
}
$connection_id = substr( $response, 8, 8 );
return $connection_id;
}
/**
* Reads the socket response and returns the torrent data
*
* @throws \Exception If anything fails while reading the response.
*
* @param resource $socket The socket resource.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $connection_id The connection ID.
* @param int $transaction_id The transaction ID.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80.
* @return string Response data.
*/
private function udp_scrape( $socket, $hashes, $connection_id, $transaction_id, $host, $port ) {
$this->udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id );
$read_length = 8 + ( 12 * count( $hashes ) );
if ( false === ( $response = @socket_read( $socket, $read_length ) ) ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' );
}
socket_close( $socket );
if ( strlen( $response ) < $read_length ) {
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
}
$result = unpack( 'Naction/Ntransaction_id', $response );
if ( 2 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' );
}
return $response;
}
/**
* Writes to the connected socket
*
* @throws \Exception If the socket couldn't be written to.
*
* @param resource $socket The socket resource.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $connection_id The connection ID.
* @param int $transaction_id The transaction ID.
*/
private function udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id ) {
$action = pack( 'N', 2 );
$infohashes = '';
foreach ( $hashes as $infohash ) {
$infohashes .= pack( 'H*', $infohash );
}
$buffer = $connection_id . $action . pack( 'N', $transaction_id ) . $infohashes;
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
socket_close( $socket );
throw new \Exception( "Couldn't write to socket." );
}
}
/**
* Writes the announce to the connected socket
*
* @throws \Exception If the socket couldn't be written to.
*
* @param resource $socket The socket resource.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $connection_id The connection ID.
* @return string Torrent(s) data.
*/
private function udp_announce( $socket, $hashes, $connection_id ) {
$action = pack( 'N', 1 );
$downloaded = $left = $uploaded = "\x30\x30\x30\x30\x30\x30\x30\x30";
$peer_id = $this->random_peer_id();
$event = pack( 'N', 3 );
$ip_addr = pack( 'N', 0 );
$key = pack( 'N', mt_rand( 0, 2147483647 ) );
$num_want = -1;
$ann_port = pack( 'N', mt_rand( 0, 255 ) );
$response_data = '';
foreach ( $hashes as $infohash ) {
$transaction_id = mt_rand( 0, 2147483647 );
$buffer = $connection_id . $action . pack( 'N', $transaction_id ) . pack( 'H*', $infohash ) .
$peer_id . $downloaded . $left . $uploaded . $event . $ip_addr . $key . $num_want . $ann_port;
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
socket_close( $socket );
throw new \Exception( "Couldn't write announce to socket." );
}
$response = $this->udp_verify_announce( $socket, $transaction_id );
if ( false === $response ) {
continue;
}
$response_data .= $response;
}
socket_close( $socket );
return $response_data;
}
/**
* Generates a random peer ID
*
* @return string Generated peer ID.
*/
private function random_peer_id() {
$identifier = '-SP0054-';
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$peer_id = $identifier . substr( str_shuffle( $chars ), 0, 12 );
return $peer_id;
}
/**
* Verifies the correctness of the announce response
*
* @param resource $socket The socket resource.
* @param int $transaction_id The transaction ID.
* @return string Response data.
*/
private function udp_verify_announce( $socket, $transaction_id ) {
if ( false === ( $response = @socket_read( $socket, 20 ) ) ) {
return false;
}
if ( strlen( $response ) < 20 ) {
return false;
}
$result = unpack( 'Naction/Ntransaction_id', $response );
if ( 1 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
return false;
}
return $response;
}
/**
* Reads the socket response and returns the torrent data
*
* @param string $response Data from the request response.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $host Domain or IP address of the tracker.
* @param string $keys Keys for the unpacked information.
* @param int $start Start of the content we want to unpack.
* @param int $end End of the content we want to unpack.
* @param int $offset Offset to the next content part.
* @return array Scraped torrent data.
*/
private function udp_scrape_data( $response, $hashes, $host, $keys, $start, $end, $offset ) {
$torrents_data = array();
foreach ( $hashes as $infohash ) {
$byte_string = substr( $response, $start, $end );
$data = unpack( 'N', $byte_string );
$content = $data[1];
if ( ! empty( $content ) ) {
$results = unpack( $keys, $byte_string );
$torrents_data[ $infohash ] = $results;
} else {
$this->collect_infohash( $infohash );
$this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.';
}
$start += $offset;
}
return $torrents_data;
}
/**
* Collects info-hashes that couldn't be scraped.
*
* @param string $infohash Infohash that wasn't scraped.
*/
private function collect_infohash( $infohash ) {
$this->infohashes[] = $infohash;
}
/**
* Checks if there are any errors
*
* @return bool True or false, depending if errors are present or not.
*/
public function has_errors() {
return ! empty( $this->errors );
}
/**
* Returns all the errors that were logged
*
* @return array All the logged errors.
*/
public function get_errors() {
return $this->errors;
}
}

45
src/library/time.php

@ -1,45 +0,0 @@
<?php
class Time
{
public static function ago(int $time)
{
$diff = time() - $time;
if ($diff < 1)
{
return _('now');
}
$values =
[
365 * 24 * 60 * 60 => _('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);
}
}
}
}

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

Loading…
Cancel
Save