Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
108744cec1 WIP stuff from months ago. 2025-02-02 20:07:19 +00:00
42 changed files with 2237 additions and 1205 deletions

5
.gitignore vendored
View file

@ -1,4 +1,9 @@
.DS_Store
[Dd]esktop.ini
/.debug
/.maintenance
/public/ss
/uploads
/config.php
/ytkns.cfg
/vendor

12
composer.json Normal file
View file

@ -0,0 +1,12 @@
{
"autoload": {
"psr-4": {
"YTKNS\\": "src"
}
},
"require": {
"flashwave/index": "^0.2408.182001",
"flashwave/syokuhou": "^1.2",
"flashwave/sasae": "^1.1"
}
}

836
composer.lock generated Normal file
View file

@ -0,0 +1,836 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a0eede9f7e6be2b84763c0ba76404b98",
"packages": [
{
"name": "flashwave/index",
"version": "v0.2408.401738",
"source": {
"type": "git",
"url": "https://patchii.net/flash/index.git",
"reference": "1339de93bf08d773207468227f7d134d41e68688"
},
"require": {
"ext-mbstring": "*",
"php": ">=8.3"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11.2"
},
"suggest": {
"ext-mysqli": "Support for the Index\\Data\\MariaDB namespace (both mysqlnd and libmysql are supported).",
"ext-sqlite3": "Support for the Index\\Data\\SQLite namespace."
},
"type": "library",
"autoload": {
"psr-4": {
"Index\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"bsd-3-clause-clear"
],
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"description": "Composer package for the common library for my projects.",
"homepage": "https://railgun.sh/index",
"time": "2024-09-13T15:38:44+00:00"
},
{
"name": "flashwave/sasae",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://patchii.net/flash/sasae.git",
"reference": "897a28e56926ad465bf0daf587caf3d92a311383"
},
"require": {
"flashwave/index": "^0.2408.40014",
"php": ">=8.3",
"twig/html-extra": "^3.12",
"twig/twig": "^3.12"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Sasae\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"bsd-3-clause-clear"
],
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"description": "A wrapper for Twig with added common functionality.",
"homepage": "https://railgun.sh/sasae",
"time": "2024-09-01T20:38:47+00:00"
},
{
"name": "flashwave/syokuhou",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://patchii.net/flash/syokuhou.git",
"reference": "129a46c0d917382f9bc195cce278be51984eb87d"
},
"require": {
"flashwave/index": "^0.2408.40014",
"php": ">=8.3"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Syokuhou\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"bsd-3-clause-clear"
],
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"description": "Configuration library for PHP.",
"homepage": "https://railgun.sh/syokuhou",
"time": "2024-08-04T01:07:23+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/mime",
"version": "v7.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "ccaa6c2503db867f472a587291e764d6a1e58758"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/ccaa6c2503db867f472a587291e764d6a1e58758",
"reference": "ccaa6c2503db867f472a587291e764d6a1e58758",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/serializer": "^6.4.3|^7.0.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.1.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-08-13T14:28:19+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
"shasum": ""
},
"require": {
"php": ">=7.2",
"symfony/polyfill-intl-normalizer": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Trevor Rowbotham",
"email": "trevor.rowbotham@pm.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "twig/html-extra",
"version": "v3.13.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/html-extra.git",
"reference": "8229e750091171c1f11801a525927811c7ac5a7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/html-extra/zipball/8229e750091171c1f11801a525927811c7ac5a7e",
"reference": "8229e750091171c1f11801a525927811c7ac5a7e",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/mime": "^5.4|^6.4|^7.0",
"twig/twig": "^3.13|^4.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Twig\\Extra\\Html\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Twig extension for HTML",
"homepage": "https://twig.symfony.com",
"keywords": [
"html",
"twig"
],
"support": {
"source": "https://github.com/twigphp/html-extra/tree/v3.13.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2024-09-03T13:08:40+00:00"
},
{
"name": "twig/twig",
"version": "v3.14.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.14.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2024-09-09T17:55:12+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View file

@ -20,30 +20,32 @@ try {
// Prevent running cron script during maintenance
if(!YTKNS_MAINTENANCE) {
// Destroy old sessions
UserSession::purge();
$ctx->getUsers()->getSessions()->purgeExpiredSessions();
// Resynchronise use counts
Upload::resync(EFFECT_UPLOADS);
// Destroy orphaned uploads
Upload::purgeOrphans();
$ctx->getUploads()->purgeOrphans();
// Get task queue
$taskQueue = ZoneTask::queue();
$zones = $ctx->getZones();
$tasks = $zones->getTasks();
$taskQueue = $tasks->queuedTasks();
// Plow through tasks
// TODO: make task functions modular
while($task = array_shift($taskQueue)) {
if(!isset($zoneInfo) || $zoneInfo->getId() !== $task->getZoneId())
$zoneInfo = $task->getZone();
foreach($taskQueue as $task) {
if(!isset($zoneInfo) || $zoneInfo->getIdStr() !== $task->getZoneId())
$zoneInfo = Zone::byId($task->getZoneId());
switch($task->getName()) {
case 'screenshot':
$zoneInfo->takeScreenshot();
$zones->takeScreenshot($zoneInfo);
break;
}
$task->delete();
$tasks->deleteTask($task);
}
}
} finally {

View file

@ -1,7 +1,8 @@
<?php
namespace YTKNS;
use Exception;
use InvalidArgumentException;
use RuntimeException;
require_once __DIR__ . '/../startup.php';
@ -17,11 +18,26 @@ if(!function_exists('base64uri_decode')) {
}
}
$sessionInfo = null;
if(!YTKNS_MAINTENANCE && filter_has_var(INPUT_COOKIE, 'ytkns_login'))
try {
$sessionsData = $ctx->getUsers()->getSessions();
$sessionInfo = $sessionsData->getSessionInfo((string)filter_input(INPUT_COOKIE, 'ytkns_login'));
$sessionsData->updateSession($sessionInfo, (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'));
if($sessionInfo->shouldBumpExpiry())
setcookie('ytkns_login', $sessionInfo->getToken(), $sessionInfo->getExpiresTime(), '/', '.' . $cfg->getString('domain:main'), false, true);
} catch(RuntimeException $ex) {
$sessionInfo = null;
}
function page_url(string $path, array $params = []): string {
global $cfg;
if(isset($params['p']))
unset($params['p']);
$mainDomain = Config::get('domain.main');
$mainDomain = $cfg->getString('domain:main');
$url = '';
if($_SERVER['HTTP_HOST'] !== $mainDomain)
@ -36,6 +52,8 @@ function page_url(string $path, array $params = []): string {
}
function html_header(array $vars = []): string {
global $ctx, $sessionInfo;
$vars['title'] ??= 'You\'re the kid now, squid';
$vars['head'] ??= '';
@ -58,8 +76,8 @@ function html_header(array $vars = []): string {
$userMenu = [];
if(UserSession::hasInstance()) {
$userName = UserSession::instance()->getUser()->getUsername();
if($sessionInfo !== null) {
$userName = $ctx->getUsers()->getUserById($sessionInfo->getUserId())->getName();
$userMenu = [
['text' => "@{$userName}", 'link' => page_url("/@{$userName}")],
@ -67,7 +85,7 @@ function html_header(array $vars = []): string {
['text' => 'Settings', 'link' => page_url('/settings')],
];
$userMenu[] = ['text' => 'Log out', 'link' => page_url('/auth/logout', ['s' => UserSession::instance()->getSmallToken()])];
$userMenu[] = ['text' => 'Log out', 'link' => page_url('/auth/logout', ['s' => $sessionInfo->createSmallToken()])];
} else {
$userMenu = [
['text' => 'Log in', 'link' => page_url('/auth/login')],
@ -127,32 +145,19 @@ function html_pagination(int $pages, int $current, string $urlFormat): string {
return $html . '</div>';
}
if(!YTKNS_MAINTENANCE && !empty($_COOKIE['ytkns_login']) && is_string($_COOKIE['ytkns_login'])) {
try {
$session = UserSession::byToken($_COOKIE['ytkns_login']);
$session->update();
$session->setInstance();
if($session->getBump())
setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
unset($session);
} catch(UserSessionNotFoundException $ex) {}
}
$zoneName = strtolower(substr($_SERVER['HTTP_HOST'], 0, -strlen(Config::get('domain.main')) - 1));
$zoneName = strtolower(substr($_SERVER['HTTP_HOST'], 0, -strlen($cfg->getString('domain:main')) - 1));
if(!empty($zoneName)) {
$redirect = ZoneRedirect::find($zoneName);
$zones = $ctx->getZones();
$redirect = $zones->getRedirects()->getRedirectTarget($zoneName);
if($redirect !== null) {
$redirect->execute();
header(sprintf('Location: %s', $redirect));
return;
}
try {
$zoneInfo = Zone::byName($zoneName);
} catch(ZoneNotFoundException $ex) {
} catch(RuntimeException $ex) {
http_response_code(404);
echo html_header(['title' => 'Zone not found!']);
Template::render('zones/none', [
@ -165,14 +170,14 @@ if(!empty($zoneName)) {
if(!YTKNS_MAINTENANCE) {
if(!empty($_GET['_refresh_screenshot'])) {
header('Location: /');
$zoneInfo->takeScreenshot();
$zones->queueTakeScreenshot($zoneInfo);
return;
}
ZoneView::increment($zoneInfo, $_SERVER['REMOTE_ADDR']);
$zones->registerZoneView($zoneInfo, (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'));
}
echo (string)$zoneInfo->getPageBuilder(true);
echo (string)$zoneInfo->getPageBuilder($ctx, true);
return;
}
@ -194,8 +199,8 @@ if(substr($reqPath, 0, 4) === '/ss/') {
if(preg_match('#^/@([A-Za-z0-9-_]+)$#', $reqPath, $matches)) {
try {
$profile = User::forProfile($matches[1]);
} catch(Exception $ex) {
$profile = $ctx->getUsers()->getUserByName($matches[1]);
} catch(RuntimeException $ex) {
http_response_code(404);
echo html_header(['title' => 'User not found - YTKNS']);
Template::render('profile/notfound');
@ -203,21 +208,22 @@ if(preg_match('#^/@([A-Za-z0-9-_]+)$#', $reqPath, $matches)) {
return;
}
$zones = Zone::byUser($profile, 'zone_views', false);
$zones = $ctx->getZones();
$zoneInfos = Zone::byUser($profile, 'zone_views', false);
if(count($zones) < 1) {
if(count($zoneInfos) < 1) {
$profileZones = Template::renderRaw('profile/zone-none');
} else {
$profileZones = [];
foreach($zones as $zone)
foreach($zoneInfos as $zoneInfo)
$profileZones[] = [
'zone_id' => $zone->getId(),
'zone_name' => $zone->getName(),
'zone_title' => $zone->getTitle(),
'zone_views' => number_format($zone->getViews()),
'zone_url' => $zone->getUrl(),
'zone_screenshot' => $zone->getScreenshotUrl(),
'zone_id' => $zoneInfo->getId(),
'zone_name' => $zoneInfo->getName(),
'zone_title' => $zoneInfo->getTitle(),
'zone_views' => number_format($zoneInfo->getViews()),
'zone_url' => $zones->getZoneRemotePath($zoneInfo),
'zone_screenshot' => $zones->getScreenshotRemotePath($zoneInfo),
];
$profileZones = Template::renderRaw('profile/zone-list', [
@ -225,9 +231,9 @@ if(preg_match('#^/@([A-Za-z0-9-_]+)$#', $reqPath, $matches)) {
]);
}
echo html_header(['title' => $profile->username . ' @ YTKNS']);
echo html_header(['title' => $profile->getName() . ' @ YTKNS']);
Template::render('profile/index', [
'profile_username' => $profile->username,
'profile_username' => $profile->getName(),
'profile_zones' => $profileZones,
]);
echo html_footer();
@ -237,11 +243,12 @@ if(preg_match('#^/@([A-Za-z0-9-_]+)$#', $reqPath, $matches)) {
if($reqPath === '/zones') {
$zoneFilter = filter_input(INPUT_GET, 'f');
if($zoneFilter === 'my' && !UserSession::hasInstance()) {
if($zoneFilter === 'my' && $sessionInfo === null) {
echo html_information('You must be logged in to do this.');
return;
}
$zones = $ctx->getZones();
$zoneTake = 20;
$zonePage = max(filter_input(INPUT_GET, 'page', FILTER_SANITIZE_NUMBER_INT) - 1, 0);
$zoneOffset = $zonePage * $zoneTake;
@ -269,13 +276,13 @@ if($reqPath === '/zones') {
switch($zoneFilter) {
case 'my':
$zones = Zone::byUser(UserSession::instance()->getUser(), $zoneOrderings[$zoneSort], $zoneSortDesc, $zoneTake, $zoneOffset);
$zoneInfos = Zone::byUser($sessionInfo->getUserId(), $zoneOrderings[$zoneSort], $zoneSortDesc, $zoneTake, $zoneOffset);
$zoneListTemplate = 'zones/my-item';
$zoneListTitle = 'My Zones';
break;
default:
$zones = Zone::all($zoneOrderings[$zoneSort], $zoneSortDesc, $zoneTake, $zoneOffset);
$zoneInfos = Zone::all($zoneOrderings[$zoneSort], $zoneSortDesc, $zoneTake, $zoneOffset);
$zoneListTemplate = 'zones/item';
$zoneListTitle = 'Zones';
$zoneFilter = '';
@ -286,19 +293,24 @@ if($reqPath === '/zones') {
$zonePages = ceil($zoneCount / $zoneTake);
$zoneList = [];
foreach($zones as $zone)
$usersCtx = $ctx->getUsers();
foreach($zoneInfos as $zoneInfo) {
$userInfo = $usersCtx->getUserById($zoneInfo->getUserId());
$zoneList[] = [
'zone_id' => $zone->getId(),
'zone_name' => $zone->getName(),
'zone_title' => $zone->getTitle(),
'zone_views' => number_format($zone->getViews()),
'zone_url' => $zone->getUrl(),
'zone_edit_url' => page_url(sprintf('/zones/%d', $zone->getId())),
'zone_delete_url' => page_url(sprintf('/zones/%d/delete', $zone->getId())),
'zone_screenshot' => $zone->getScreenshotUrl(),
'zone_user_url' => page_url('/@' . $zone->getUser()->getUsername()),
'zone_user_name' => $zone->getUser()->getUsername(),
'zone_id' => $zoneInfo->getId(),
'zone_name' => $zoneInfo->getName(),
'zone_title' => $zoneInfo->getTitle(),
'zone_views' => number_format($zoneInfo->getViews()),
'zone_url' => $zones->getZoneRemotePath($zoneInfo),
'zone_edit_url' => page_url(sprintf('/zones/%d', $zoneInfo->getId())),
'zone_delete_url' => page_url(sprintf('/zones/%d/delete', $zoneInfo->getId())),
'zone_screenshot' => $zones->getScreenshotRemotePath($zoneInfo),
'zone_user_url' => page_url('/@' . $userInfo->getName()),
'zone_user_name' => $userInfo->getName(),
];
}
$zoneSortings = '';
@ -329,20 +341,22 @@ if($reqPath === '/zones') {
if($reqPath === '/zones/random') {
$zoneInfo = Zone::byRandom();
header('Location: ' . $zoneInfo->getUrl());
header('Location: ' . $ctx->getZones()->getZoneRemotePath($zoneInfo));
return;
}
if($reqPath === '/zones/create') {
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
}
$createToken = UserSession::instance()->getSmallToken(4);
$createToken = $sessionInfo->createSmallToken(4);
if($reqMethod === 'POST') {
$zones = $ctx->getZones();
$zoneToken = filter_input(INPUT_POST, 'zone_token');
$zoneName = filter_input(INPUT_POST, 'zone_subdomain');
$zoneTitle = filter_input(INPUT_POST, 'zone_title');
@ -355,13 +369,13 @@ if($reqPath === '/zones/create') {
} else {
if(!Zone::validName($zoneName)) {
$createError = 'Name contains invalid characters or is too long.';
} elseif(ZoneRedirect::exists($zoneName) || Zone::exists($zoneName)) {
} elseif($zones->getRedirects()->getRedirectTarget($zoneName) !== null || Zone::exists($zoneName)) {
$createError = 'A Zone with this name already exists.';
} elseif(!ctype_alpha($zoneName)
|| mb_strlen($zoneTitle) > 255) {
$createError = 'Invalid data.';
} else {
$createZone = Zone::create(UserSession::instance()->getUser(), $zoneName, $zoneTitle);
$createZone = Zone::create($sessionInfo->getUserId(), $zoneName, $zoneTitle);
$createZone->addEffect(new \YTKNS\Effects\NewlyCreatedPageEffect);
echo html_information('Zone created!', 'Information - YTKNS', "/zones/{$createZone->getId()}");
@ -383,7 +397,7 @@ if($reqPath === '/zones/create') {
Template::render('zones/create', [
'create_action' => page_url('/zones/create'),
'create_domain' => Config::get('domain.main'),
'create_domain' => $cfg->getString('domain:main'),
'create_token' => $createToken,
'create_error' => $createError ?? '',
'create_subdomain' => $zoneName ?? '',
@ -397,7 +411,7 @@ if($reqPath === '/zones/create') {
if($reqPath === '/zones/_effects') {
header('Content-Type: application/json; charset=utf-8');
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in.',
@ -421,7 +435,7 @@ if($reqPath === '/zones/_effects') {
}
if($reqPath === '/zones/_preview') {
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
@ -435,7 +449,7 @@ if($reqPath === '/zones/_preview') {
$zoneToken = filter_input(INPUT_POST, 'zone_token');
if($zoneToken !== UserSession::instance()->getSmallToken(10)) {
if($zoneToken !== $sessionInfo->createSmallToken(10)) {
http_response_code(403);
echo html_information('Invalid token.');
return;
@ -454,30 +468,22 @@ if($reqPath === '/zones/_preview') {
$effectInfo->setEffectParams($effectValues);
$zoneInfo->addEffect($effectInfo);
}
} catch(ZoneInvalidTitleException $ex) {
} catch(InvalidArgumentException $ex) {
http_response_code(400);
echo html_information('Invalid title.');
echo html_information($ex->getMessage());
return;
} catch(ZoneEffectClassNotFoundException $ex) {
http_response_code(400);
echo html_information(sprintf('Invalid effect name: %s.', $ex->getMessage()));
return;
} catch(PageEffectException $ex) {
http_response_code(400);
echo html_information(sprintf('%s: %s', $effectName ?? '', $ex->getMessage()));
return;
} catch(Exception $ex) {
} catch(RuntimeException $ex) {
http_response_code(500);
echo html_information(sprintf('Failed to generate preview.<br/>%s: %s', get_class($ex), $ex->getMessage()));
echo html_information($ex->getMessage());
return;
}
echo (string)$zoneInfo->getPageBuilder();
echo (string)$zoneInfo->getPageBuilder($ctx);
return;
}
if(preg_match('#^/zones/([0-9]+)/delete$#', $reqPath, $matches)) {
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
@ -485,19 +491,19 @@ if(preg_match('#^/zones/([0-9]+)/delete$#', $reqPath, $matches)) {
try {
$zoneInfo = Zone::byId($matches[1]);
} catch(ZoneNotFoundException $ex) {
} catch(RuntimeException $ex) {
http_response_code(404);
echo html_information('This zone doesn\'t exist.');
return;
}
if($zoneInfo->getUserId() !== UserSession::instance()->getUserId()) {
if($zoneInfo->getUserId() != $sessionInfo->getUserId()) { // TODO: switch to === when zone user ids are strings
http_response_code(403);
echo html_information('You aren\'t allowed to touch this zone.');
return;
}
$deleteToken = UserSession::instance()->getSmallToken(20);
$deleteToken = $sessionInfo->createSmallToken(20);
if($reqMethod === 'POST') {
if(filter_input(INPUT_POST, 'zone_token') !== $deleteToken) {
@ -530,7 +536,7 @@ if(preg_match('#^/zones/([0-9]+)/sbs$#', $reqPath, $matches)) {
}
if(preg_match('#^/zones/([0-9]+)$#', $reqPath, $matches)) {
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
@ -538,14 +544,13 @@ if(preg_match('#^/zones/([0-9]+)$#', $reqPath, $matches)) {
try {
$zoneInfo = Zone::byId($matches[1]);
} catch(ZoneNotFoundException $ex) {
} catch(RuntimeException $ex) {
http_response_code(404);
echo html_information('This zone doesn\'t exist.');
return;
}
if(UserSession::instance()->getUserId() !== $zoneInfo->getUserId()
&& UserSession::instance()->getUserId() !== 1) {
if($zoneInfo->getUserId() != $sessionInfo->getUserId()) { // TODO: switch to === when zone user ids are strings
http_response_code(403);
echo html_information('You aren\'t allowed to touch this zone.');
return;
@ -571,10 +576,10 @@ if(preg_match('#^/zones/([0-9]+)$#', $reqPath, $matches)) {
Template::render('zones/edit', [
'edit_id' => $zoneInfo->getId(),
'edit_token' => UserSession::instance()->getSmallToken(10),
'edit_token' => $sessionInfo->createSmallToken(10),
'edit_css_ver' => substr($cssHash, 0, 16),
'edit_js_ver' => substr($jsHash, 0, 16),
'upload_token' => UserSession::instance()->getSmallToken(6),
'upload_token' => $sessionInfo->createSmallToken(6),
]);
if($isSBS) {
@ -600,7 +605,7 @@ HTML;
if(preg_match('#^/zones/([0-9]+).json$#', $reqPath, $matches)) {
header('Content-Type: application/json; charset=utf-8');
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in to do this.',
@ -608,9 +613,11 @@ if(preg_match('#^/zones/([0-9]+).json$#', $reqPath, $matches)) {
return;
}
$zones = $ctx->getZones();
try {
$zoneInfo = Zone::byId($matches[1]);
} catch(ZoneNotFoundException $ex) {
} catch(RuntimeException $ex) {
http_response_code(404);
echo json_encode([
'err' => 'This zone doesn\'t exist',
@ -618,8 +625,7 @@ if(preg_match('#^/zones/([0-9]+).json$#', $reqPath, $matches)) {
return;
}
if(UserSession::instance()->getUserId() !== $zoneInfo->getUserId()
&& UserSession::instance()->getUserId() !== 1) {
if($zoneInfo->getUserId() != $sessionInfo->getUserId()) { // TODO: switch to === when zone user ids are strings
http_response_code(403);
echo json_encode([
'err' => 'You aren\'t allowed to touch this zone.',
@ -642,7 +648,7 @@ if(preg_match('#^/zones/([0-9]+).json$#', $reqPath, $matches)) {
$zoneToken = filter_input(INPUT_POST, 'zone_token');
if($zoneToken !== UserSession::instance()->getSmallToken(10)) {
if($zoneToken !== $sessionInfo->createSmallToken(10)) {
http_response_code(403);
echo json_encode([
'err' => 'Invalid token.',
@ -670,37 +676,23 @@ if(preg_match('#^/zones/([0-9]+).json$#', $reqPath, $matches)) {
if(is_array($zoneEffects)) {
foreach($zoneEffects as $effectName => $effectValues) {
$effectInfo = ZoneEffect::effectClass($effectName);
$effectInfo->setEffectParams($effectValues);
$effectInfo->setEffectParams($ctx, $effectValues);
$zoneInfo->addEffect($effectInfo);
}
}
$zoneInfo->update(['zone_title']);
// Schedule a screenshot to be taken
$zoneInfo->queueTask('screenshot');
} catch(ZoneInvalidTitleException $ex) {
$zones->queueTakeScreenshot($zoneInfo);
} catch(InvalidArgumentException $ex) {
http_response_code(400);
echo json_encode([
'err' => 'Invalid title.',
'err' => $ex->getMessage(),
]);
return;
} catch(ZoneEffectClassNotFoundException $ex) {
http_response_code(400);
echo json_encode([
'err' => sprintf('Invalid effect name: %s.', $ex->getMessage()),
]);
return;
} catch(PageEffectException $ex) {
http_response_code(400);
echo json_encode([
'err' => sprintf('%s: %s', $effectName ?? '', $ex->getMessage()),
]);
return;
} catch(Exception $ex) {
} catch(RuntimeException $ex) {
http_response_code(500);
echo json_encode([
'err' => sprintf("An unexpected error occurred.\r\n%s: %s", get_class($ex), $ex->getMessage()),
'err' => $ex->getMessage(),
]);
return;
}
@ -722,7 +714,7 @@ if($reqPath === '/uploads') {
return;
}
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in to upload files.',
@ -730,7 +722,7 @@ if($reqPath === '/uploads') {
return;
}
if(filter_input(INPUT_POST, 'upload_token') !== UserSession::instance()->getSmallToken(6)) {
if(filter_input(INPUT_POST, 'upload_token') !== $sessionInfo->createSmallToken(6)) {
http_response_code(403);
echo json_encode([
'err' => 'Invalid upload token.',
@ -746,11 +738,14 @@ if($reqPath === '/uploads') {
return;
}
$hash = hash_file('sha256', $_FILES['upload_file']['tmp_name']);
$existing = Upload::byHash($hash);
$uploadsCtx = $ctx->getUploads();
$uploadsRec = $uploadsCtx->getRecords();
if($existing !== null) {
if($existing->getDMCA() > 0) {
$hash = hash_file('sha256', $_FILES['upload_file']['tmp_name']);
try {
$uploadInfo = $uploadsRec->getUploadInfo($hash, Uploads\UploadsRecords::BY_HASH);
if($uploadInfo->isDmca()) {
http_response_code(451);
echo json_encode([
'err' => 'This file has been removed in response to a DMCA takedown request. It may not be used.',
@ -758,7 +753,7 @@ if($reqPath === '/uploads') {
return;
}
if($existing->getDeleted() > 0) {
if($uploadInfo->isDeleted()) {
http_response_code(404);
echo json_encode([
'err' => 'This file has been flagged for deletion, it cannot be reuploaded at this time.',
@ -769,10 +764,10 @@ if($reqPath === '/uploads') {
http_response_code(200);
echo json_encode([
'err' => 'File has been uploaded already.',
'file' => $existing->getId(),
'file' => $uploadInfo->getId(),
]);
return;
}
} catch(RuntimeException) {}
if($_FILES['upload_file']['size'] > 5242880) {
http_response_code(413);
@ -793,8 +788,14 @@ if($reqPath === '/uploads') {
}
try {
$upload = Upload::create(UserSession::instance()->getUser(), $_FILES['upload_file']['name'], $contentType, $hash);
} catch(UploadCreationFailedException $ex) {
$uploadInfo = $uploadsRec->createUpload(
(string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'),
$sessionInfo->getUserId(),
$_FILES['upload_file']['name'],
$contentType,
$hash
);
} catch(RuntimeException $ex) {
http_response_code(500);
echo json_encode([
'err' => 'Failed to create upload record.',
@ -802,7 +803,7 @@ if($reqPath === '/uploads') {
return;
}
if(!move_uploaded_file($_FILES['upload_file']['tmp_name'], $upload->getPath())) {
if(!move_uploaded_file($_FILES['upload_file']['tmp_name'], $uploadsCtx->getLocalPath($uploadInfo))) {
http_response_code(500);
echo json_encode([
'err' => 'Upload failed.',
@ -812,40 +813,40 @@ if($reqPath === '/uploads') {
http_response_code(201);
echo json_encode([
'file' => $upload->getId(),
'file' => $uploadInfo->getId(),
]);
return;
}
if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16})$#', $reqPath, $matches)) {
try {
$uploadInfo = Upload::byId($matches[1]);
} catch(UploadNotFoundException $ex) {
$uploadInfo = $ctx->getUploads()->getRecords()->getUploadInfo($matches[1], Uploads\UploadsRecords::BY_ID);
} catch(RuntimeException $ex) {
http_response_code(404);
return;
}
if(!empty($_SERVER['HTTP_ORIGIN'])) {
$zoneDomain = sprintf(Config::get('domain.zone'), '');
$zoneDomain = sprintf($cfg->getString('domain:zone'), '');
$originPart = substr($_SERVER['HTTP_ORIGIN'], strlen($_SERVER['HTTP_ORIGIN']) - strlen($zoneDomain));
if($originPart === $zoneDomain) {
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
header(sprintf('Access-Control-Allow-Origin: %s', $_SERVER['HTTP_ORIGIN']));
}
}
if($_SERVER['REQUEST_METHOD'] !== 'HEAD' && $_SERVER['REQUEST_METHOD'] !== 'OPTIONS')
header('X-Accel-Redirect: /raw-uploads/' . $uploadInfo->getId());
header(sprintf('X-Accel-Redirect: /raw-uploads/%s', $uploadInfo->getId()));
header('Content-Type: ' . $uploadInfo->getType());
header('Content-Disposition: inline; filename="' . $uploadInfo->getName() . '"');
header(sprintf('Content-Type: %s', $uploadInfo->getType()));
header(sprintf('Content-Disposition: inline; filename="%s"', $uploadInfo->getName()));
return;
}
if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16}).json$#', $reqPath, $matches)) {
header('Content-Type: application/json; charset=utf-8');
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in to do this.',
@ -854,8 +855,8 @@ if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16}).json$#', $reqPath, $matches)) {
}
try {
$uploadInfo = Upload::byId($matches[1]);
} catch(UploadNotFoundException $ex) {
$uploadInfo = $ctx->getUploads()->getRecords()->getUploadInfo($matches[1], Uploads\UploadsRecords::BY_ID);
} catch(RuntimeException $ex) {
http_response_code(404);
echo json_encode([
'err' => 'Upload not found.',
@ -863,12 +864,12 @@ if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16}).json$#', $reqPath, $matches)) {
return;
}
echo $uploadInfo->toJson(true);
echo json_encode($uploadInfo);
return;
}
if($reqPath === '/settings') {
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(403);
echo html_information('You must be logged in to access this page.');
return;
@ -887,7 +888,7 @@ if($reqPath === '/auth/login') {
return;
}
if(UserSession::hasInstance()) {
if($sessionInfo !== null) {
http_response_code(404);
echo html_information('You are logged in already.');
return;
@ -901,7 +902,7 @@ if($reqPath === '/auth/login') {
return;
}
$signature = hash_hmac('sha256', substr($state, 32), YTKNS_OA2_STATE_SECRET, true);
$signature = hash_hmac('sha256', substr($state, 32), $cfg->getString('oauth2:stateSecret'), true);
if(!hash_equals($signature, substr($state, 0, 32))) {
http_response_code(403);
echo html_information('Request verification failed.');
@ -945,7 +946,7 @@ if($reqPath === '/auth/login') {
rawurlencode((string)filter_input(INPUT_GET, 'code')),
rawurldecode($state['verifier'])
);
$authz = sprintf('Basic %s', base64_encode(sprintf('%s:%s', YTKNS_OA2_CLIENT_ID, YTKNS_OA2_CLIENT_SECRET)));
$authz = sprintf('Basic %s', base64_encode(sprintf('%s:%s', $cfg->getString('oauth2:clientId'), $cfg->getString('oauth2:clientSecret'))));
$tokenInfo = json_decode(file_get_contents('https://api.flashii.net/oauth2/token', false, stream_context_create([
'http' => [
@ -977,23 +978,28 @@ if($reqPath === '/auth/login') {
return;
}
$usersCtx = $ctx->getUsers();
try {
$userInfo = User::byRemoteId($fUserInfo->id);
$userInfo = $usersCtx->getUserByRemoteId($fUserInfo->id);
$loginMessage = 'You are now logged in!';
} catch(UserNotFoundException) {
} catch(RuntimeException) {
$usersData = $usersCtx->getUsers();
try {
$userInfo = User::create($fUserInfo->id, $fUserInfo->name);
} catch(\PDOException) {
$userInfo = User::create($fUserInfo->id, sprintf('%s_%04d', $fUserInfo->name, random_int(0, 9999)));
$userInfo = $usersData->createUser($fUserInfo->id, $fUserInfo->name);
} catch(\Exception) {
$userInfo = $usersData->createUser($fUserInfo->id, sprintf('%s_%04d', $fUserInfo->name, random_int(0, 9999)));
}
$loginMessage = 'Your account been created!';
}
// leaving session bumping off for now, the implementation needs to be better for that
$session = UserSession::create($userInfo, false);
$session->setInstance();
setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
$sessionsData = $ctx->getUsers()->getSessions();
$sessionInfo = $sessionsData->createSession(
(string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'),
$userInfo
);
setcookie('ytkns_login', $sessionInfo->getToken(), $sessionInfo->getExpiresTime(), '/', '.' . $cfg->getString('domain:main'), false, true);
echo html_information($loginMessage, 'Welcome', '/');
return;
}
@ -1007,34 +1013,33 @@ if($reqPath === '/auth/login') {
$verifier = random_bytes(20);
$time = pack('J', time());
$signature = hash_hmac('sha256', $time . $verifier, YTKNS_OA2_STATE_SECRET, true);
$signature = hash_hmac('sha256', $time . $verifier, $cfg->getString('oauth2:stateSecret'), true);
$state = base64uri_encode($signature . $time . $verifier);
header(sprintf(
'Location: https://id.flashii.net/oauth2/authorise?response_type=code&scope=identify&code_challenge_method=S256&client_id=%s&state=%s&code_challenge=%s&redirect_uri=%s',
rawurlencode(YTKNS_OA2_CLIENT_ID),
rawurlencode($cfg->getString('oauth2:clientId')),
rawurlencode($state),
rawurlencode(base64uri_encode(hash('sha256', $verifier, true))),
rawurlencode('https://ytkns.com/auth/login'),
rawurlencode(sprintf('https://%s/auth/login', $cfg->getString('domain:main'))),
));
return;
}
if($reqPath === '/auth/logout') {
if(!UserSession::hasInstance()) {
if($sessionInfo === null) {
http_response_code(404);
echo html_information('You are not logged in.');
return;
}
if(filter_input(INPUT_GET, 's') !== UserSession::instance()->getSmallToken()) {
if(filter_input(INPUT_GET, 's') !== $sessionInfo->createSmallToken()) {
http_response_code(403);
echo html_information('Log out verification failed, please try again.');
return;
}
UserSession::instance()->destroy();
UserSession::unsetInstance();
$ctx->getUsers()->getSessions()->destroySession($sessionInfo);
echo html_information('You have been logged out.', 'Log out', '/');
return;
}

View file

@ -1,56 +0,0 @@
<?php
namespace YTKNS;
final class Config {
public const TYPE_ANY = '';
public const TYPE_STR = 'string';
public const TYPE_INT = 'integer';
public const TYPE_BOOL = 'boolean';
public const TYPE_ARR = 'array';
public const DEFAULTS = [
self::TYPE_ANY => null,
self::TYPE_STR => '',
self::TYPE_INT => 0,
self::TYPE_BOOL => false,
self::TYPE_ARR => [],
];
private static array $config = [];
public static function init(): void {
$raw = DB::prepare('SELECT `config_key`, `config_value` FROM `ytkns_config`');
$raw->execute();
$raw = $raw->fetchAll();
foreach($raw as $entry)
self::$config[$entry['config_key']] = unserialize($entry['config_value']);
}
public static function get(string $key, string $type = self::TYPE_ANY, $default = null) {
$value = self::$config[$key] ?? null;
if($type !== self::TYPE_ANY && gettype($value) !== $type)
$value = null;
return $value ?? $default ?? self::DEFAULTS[$type];
}
public static function set(string $key, $value, bool $soft = false): void {
self::$config[$key] = $value;
if(!YTKNS_MAINTENANCE && !$soft) {
$value = serialize($value);
$save = DB::prepare('REPLACE INTO `ytkns_config` (`config_key`, `config_value`) VALUES (:key, :value)');
$save->bindValue('key', $key);
$save->bindValue('value', $value);
$save->execute();
}
}
public static function setDefault(string $key, $value, bool $soft = false): void {
if($value !== null && self::get($key) === null)
self::set($key, $value, $soft);
}
}

View file

@ -1,13 +1,12 @@
<?php
namespace YTKNS\Effects;
use Exception;
use YTKNS\HtmlTag;
use RuntimeException;
use YTKNS\PageBuilder;
use YTKNS\PageEffectInterface;
use YTKNS\PageEffectException;
use YTKNS\Upload;
use YTKNS\UploadNotFoundException;
use YTKNS\YtknsContext;
use YTKNS\Html\HtmlTag;
class BackgroundAudioEffect implements PageEffectInterface {
private $oggUpload = null;
@ -66,41 +65,33 @@ class BackgroundAudioEffect implements PageEffectInterface {
];
}
public function setEffectParams(array $vars, bool $quiet = false): void {
try {
if(isset($vars['ogg']) && is_string($vars['ogg'])) {
try {
$this->oggUpload = Upload::byId($vars['ogg']);
} catch(Exception $ex) {
if(!$quiet)
throw $ex;
}
if(!$quiet && !in_array($this->oggUpload->getType(), self::OGG_MIME))
throw new PageEffectException('Ogg source upload was of invalid type.');
public function setEffectParams(YtknsContext $context, array $vars, bool $quiet = false): void {
if(isset($vars['ogg']) && is_string($vars['ogg'])) {
try {
$this->oggUpload = $context->getUploads()->getRecords()->getUploadInfo($vars['ogg'], \YTKNS\Uploads\UploadsRecords::BY_ID);
} catch(RuntimeException $ex) {
if(!$quiet)
throw $ex;
}
} catch(UploadNotFoundException $ex) {
$noOGG = true;
if(!$quiet && !in_array($this->oggUpload->getType(), self::OGG_MIME))
throw new RuntimeException('Ogg source upload was of invalid type.');
}
try {
if(isset($vars['mp3']) && is_string($vars['mp3'])) {
try {
$this->mp3Upload = Upload::byId($vars['mp3']);
} catch(Exception $ex) {
if(!$quiet)
throw $ex;
}
if(!$quiet && !in_array($this->mp3Upload->getType(), self::MP3_MIME))
throw new PageEffectException('MP3 source upload was of invalid type.');
if(isset($vars['mp3']) && is_string($vars['mp3'])) {
try {
$this->mp3Upload = $context->getUploads()->getRecords()->getUploadInfo($vars['mp3'], \YTKNS\Uploads\UploadsRecords::BY_ID);
} catch(RuntimeException $ex) {
if(!$quiet)
throw $ex;
}
} catch(UploadNotFoundException $ex) {
$noMP3 = true;
if(!$quiet && !in_array($this->mp3Upload->getType(), self::MP3_MIME))
throw new RuntimeException('MP3 source upload was of invalid type.');
}
if(!empty($noMP3) && !empty($noOGG))
throw new PageEffectException('No audio source uploaded.');
throw new RuntimeException('No audio source uploaded.');
if(isset($vars['autoplay']))
$this->autoPlay = is_bool($vars['autoplay']) ? $vars['autoplay'] : (is_string($vars['autoplay']) ? boolval($vars['autoplay']) : false);
@ -118,7 +109,9 @@ class BackgroundAudioEffect implements PageEffectInterface {
];
}
public function applyEffect(PageBuilder $builder): void {
public function applyEffect(YtknsContext $context, PageBuilder $builder): void {
$domain = $context->getConfig()->getString('domain:main');
$audioTag = new HtmlTag('audio');
$audioTag->setAttribute('id', 'BackgroundAudio');
@ -128,9 +121,9 @@ class BackgroundAudioEffect implements PageEffectInterface {
$audioTag->setAttribute('loop', 'loop');
if(!empty($this->oggUpload))
$audioTag->appendChild(new HtmlTag('source', ['type' => 'audio/ogg', 'src' => $this->oggUpload->getUrl()], true));
$audioTag->appendChild(new HtmlTag('source', ['type' => 'audio/ogg', 'src' => $context->getUploads()->getRemotePath($this->oggUpload)], true));
if(!empty($this->mp3Upload))
$audioTag->appendChild(new HtmlTag('source', ['type' => 'audio/mpeg', 'src' => $this->mp3Upload->getUrl()], true));
$audioTag->appendChild(new HtmlTag('source', ['type' => 'audio/mpeg', 'src' => $context->getUploads()->getRemotePath($this->mp3Upload)], true));
$builder->getBody()->appendChild($audioTag);
}

View file

@ -1,15 +1,14 @@
<?php
namespace YTKNS\Effects;
use Exception;
use RuntimeException;
use YTKNS\Colour;
use YTKNS\Gradient;
use YTKNS\HtmlTag;
use YTKNS\PageBuilder;
use YTKNS\PageEffectInterface;
use YTKNS\PageEffectException;
use YTKNS\Upload;
use YTKNS\UploadNotFoundException;
use YTKNS\YtknsContext;
use YTKNS\Html\HtmlTag;
class BackgroundImageEffect implements PageEffectInterface {
private const IMG_MIME = [
@ -215,21 +214,17 @@ class BackgroundImageEffect implements PageEffectInterface {
];
}
public function setEffectParams(array $vars, bool $quiet = false): void {
try {
if(isset($vars['img']) && is_string($vars['img'])) {
try {
$this->imageUpload = Upload::byId($vars['img']);
} catch(Exception $ex) {
if(!$quiet)
throw $ex;
}
if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME))
throw new PageEffectException('Image upload was of invalid type.');
public function setEffectParams(YtknsContext $context, array $vars, bool $quiet = false): void {
if(isset($vars['img']) && is_string($vars['img'])) {
try {
$this->imageUpload = $context->getUploads()->getRecords()->getUploadInfo($vars['img'], \YTKNS\Uploads\UploadsRecords::BY_ID);
} catch(RuntimeException $ex) {
if(!$quiet)
throw $ex;
}
} catch(UploadNotFoundException $ex) {
$this->imageUpload = null;
if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME))
throw new RuntimeException('Image upload was of invalid type.');
}
if(isset($vars['sld']))
@ -245,13 +240,13 @@ class BackgroundImageEffect implements PageEffectInterface {
$this->slideSpeed = is_int($vars['sldspd']) || is_float($vars['sldspd']) ? $vars['sldspd'] : (is_string($vars['sldspd']) ? floatval($vars['sldspd']) : 1);
if(!$quiet && ($this->slideSpeed < self::SLIDE_MIN || $this->slideSpeed > self::SLIDE_MAX))
throw new PageEffectException(sprintf('Slide speed may not be less than %d or more than %d', self::SLIDE_MIN, self::SLIDE_MAX));
throw new RuntimeException(sprintf('Slide speed may not be less than %d or more than %d', self::SLIDE_MIN, self::SLIDE_MAX));
}
if(isset($vars['spnspd'])) {
$this->spinSpeed = is_int($vars['spnspd']) || is_float($vars['spnspd']) ? $vars['spnspd'] : (is_string($vars['spnspd']) ? floatval($vars['spnspd']) : 1);
if(!$quiet && ($this->spinSpeed < self::SPIN_MIN || $this->spinSpeed > self::SPIN_MAX))
throw new PageEffectException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX));
throw new RuntimeException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX));
}
if(isset($vars['slddir']) && is_string($vars['slddir']) && array_key_exists($vars['slddir'], self::SLIDE_DIRS))
@ -262,13 +257,8 @@ class BackgroundImageEffect implements PageEffectInterface {
if(isset($vars['col']))
$this->colour = Colour::create($vars['col']);
try {
if(isset($vars['grad']))
$this->gradient = Gradient::fromArray($vars['grad']);
} catch(Exception $ex) {
if(!$quiet)
throw $ex;
}
if(isset($vars['grad']))
$this->gradient = Gradient::fromArray($vars['grad']);
if(isset($vars['attach']) && is_string($vars['attach']) && array_key_exists($vars['attach'], self::ATTACH_OPTS))
$this->attachment = $vars['attach'];
@ -300,7 +290,7 @@ class BackgroundImageEffect implements PageEffectInterface {
];
}
public function applyEffect(PageBuilder $builder): void {
public function applyEffect(YtknsContext $context, PageBuilder $builder): void {
$head = $builder->getHead();
$body = $builder->getContainer();
$bgTarget = $body;
@ -316,7 +306,8 @@ class BackgroundImageEffect implements PageEffectInterface {
if(empty($_GET['preview']) && !empty($this->imageUpload)) {
if($this->slide) {
$imageSize = getimagesize($this->imageUpload->getPath());
$localPath = $context->getUploads()->getLocalPath($this->imageUpload);
$imageSize = getimagesize($localPath);
$imageWidth = $imageSize[0];
$imageHeight = $imageSize[1];
@ -350,7 +341,7 @@ class BackgroundImageEffect implements PageEffectInterface {
$syncWithAudio = empty($_GET['preview']) && $this->syncWithAudio;
if(!empty($this->imageUpload) && !$syncWithAudio)
$backgroundImage[] = sprintf('url(\'%s\')', $this->imageUpload->getUrl());
$backgroundImage[] = sprintf('url(\'%s\')', $context->getUploads()->getRemotePath($this->imageUpload));
if($bgTarget !== $body) {
$styleText .= '#container {';
@ -391,7 +382,7 @@ class BackgroundImageEffect implements PageEffectInterface {
if(!empty($this->imageUpload) && $syncWithAudio) {
$scriptText = 'window.addEventListener(\'DOMContentLoaded\', function() {';
$scriptText .= 'synchroniseBackgroundWithAudio(\'' . $this->imageUpload->getUrl() . '\');';
$scriptText .= 'synchroniseBackgroundWithAudio(\'' . $context->getUploads()->getRemotePath($this->imageUpload) . '\');';
$scriptText .= '});';
$scriptTag = new HtmlTag('script', ['type' => 'text/javascript']);
$scriptTag->setTextContent($scriptText);

View file

@ -1,14 +1,13 @@
<?php
namespace YTKNS\Effects;
use Exception;
use YTKNS\HtmlTag;
use YTKNS\HtmlText;
use RuntimeException;
use YTKNS\PageBuilder;
use YTKNS\PageEffectInterface;
use YTKNS\PageEffectException;
use YTKNS\Upload;
use YTKNS\UploadNotFoundException;
use YTKNS\YtknsContext;
use YTKNS\Html\HtmlTag;
use YTKNS\Html\HtmlText;
class ForegroundImageEffect implements PageEffectInterface {
private const IMG_MIME = [
@ -81,21 +80,17 @@ class ForegroundImageEffect implements PageEffectInterface {
];
}
public function setEffectParams(array $vars, bool $quiet = false): void {
try {
if(isset($vars['img']) && is_string($vars['img'])) {
try {
$this->imageUpload = Upload::byId($vars['img']);
} catch(Exception $ex) {
if(!$quiet)
throw $ex;
}
if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME))
throw new PageEffectException('Image upload was of invalid type.');
public function setEffectParams(YtknsContext $context, array $vars, bool $quiet = false): void {
if(isset($vars['img']) && is_string($vars['img'])) {
try {
$this->imageUpload = $context->getUploads()->getRecords()->getUploadInfo($vars['img'], \YTKNS\Uploads\UploadsRecords::BY_ID);
} catch(RuntimeException $ex) {
if(!$quiet)
throw $ex;
}
} catch(UploadNotFoundException $ex) {
$this->imageUpload = null;
if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME))
throw new RuntimeException('Image upload was of invalid type.');
}
if(isset($vars['spin']))
@ -107,7 +102,7 @@ class ForegroundImageEffect implements PageEffectInterface {
$this->spinSpeed = is_int($vars['spnspd']) || is_float($vars['spnspd']) ? $vars['spnspd'] : (is_string($vars['spnspd']) ? floatval($vars['spnspd']) : 1);
if(!$quiet && ($this->spinSpeed < self::SPIN_MIN || $this->spinSpeed > self::SPIN_MAX))
throw new PageEffectException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX));
throw new RuntimeException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX));
}
if(isset($vars['spndir']) && is_string($vars['spndir']) && array_key_exists($vars['spndir'], self::SPIN_DIRS))
@ -124,18 +119,20 @@ class ForegroundImageEffect implements PageEffectInterface {
];
}
public function applyEffect(PageBuilder $builder): void {
public function applyEffect(YtknsContext $context, PageBuilder $builder): void {
$element = $builder->getContainer()->appendChild(new HtmlTag('div', ['class' => 'ForegroundImage']));
$imageTarget = $element->appendChild(new HtmlTag('div', ['class' => 'ForegroundImage_Image', 'id' => 'ForegroundImage']));
if(!empty($this->imageUpload)) {
$imageSize = getimagesize($this->imageUpload->getPath());
$localPath = $context->getUploads()->getLocalPath($this->imageUpload);
$imageSize = getimagesize($localPath);
if($imageSize !== false) {
$remotePath = $context->getUploads()->getRemotePath($this->imageUpload);
$styleText = sprintf('width: %dpx; height: %dpx', $imageSize[0], $imageSize[1]);
if(!empty($_GET['preview']) || !$this->syncWithAudio)
$styleText .= sprintf(';background-image:url(\'%s\')', $this->imageUpload->getUrl());
$styleText .= sprintf(';background-image:url(\'%s\')', $remotePath);
if(empty($_GET['preview']) && $this->spin)
$styleText .= sprintf(';animation: SharedAnimation_Spin360 infinite linear %s %Fs', $this->spinDirection === 'cw' ? 'normal' : 'reverse', $this->spinSpeed);
@ -143,7 +140,7 @@ class ForegroundImageEffect implements PageEffectInterface {
$imageTarget->setAttribute('style', $styleText);
$scriptText = 'window.addEventListener(\'DOMContentLoaded\', function() {';
$scriptText .= 'synchroniseBackgroundWithAudio(\'' . $this->imageUpload->getUrl() . '\', \'ForegroundImage\');';
$scriptText .= 'synchroniseBackgroundWithAudio(\'' . $remotePath . '\', \'ForegroundImage\');';
$scriptText .= '});';
$scriptTag = new HtmlTag('script', ['type' => 'text/javascript']);
$scriptTag->setTextContent($scriptText);

View file

@ -1,10 +1,11 @@
<?php
namespace YTKNS\Effects;
use YTKNS\HtmlTag;
use YTKNS\HtmlText;
use YTKNS\PageBuilder;
use YTKNS\PageEffectInterface;
use YTKNS\YtknsContext;
use YTKNS\Html\HtmlTag;
use YTKNS\Html\HtmlText;
class NewlyCreatedPageEffect implements PageEffectInterface {
private $headerText = null;
@ -18,7 +19,7 @@ class NewlyCreatedPageEffect implements PageEffectInterface {
return [];
}
public function setEffectParams(array $vars, bool $quiet = false): void {
public function setEffectParams(YtknsContext $context, array $vars, bool $quiet = false): void {
if(!empty($vars['h']))
$this->headerText = $vars['h'];
if(!empty($vars['s']))
@ -36,7 +37,7 @@ class NewlyCreatedPageEffect implements PageEffectInterface {
return $vars;
}
public function applyEffect(PageBuilder $builder): void {
public function applyEffect(YtknsContext $context, PageBuilder $builder): void {
$builder->getContainer()->appendChild(new HtmlTag('div', ['class' => 'NewCreatePageEffect_Main'], [
new HtmlTag('h1', [], [new HtmlText($this->headerText ?? 'This page is still empty')]),
new HtmlTag('p', [], [new HtmlText($this->subText ?? 'Please come back later')]),

View file

@ -1,11 +1,12 @@
<?php
namespace YTKNS\Effects;
use YTKNS\HtmlTag;
use YTKNS\HtmlText;
use RuntimeException;
use YTKNS\PageBuilder;
use YTKNS\PageEffectInterface;
use YTKNS\PageEffectException;
use YTKNS\YtknsContext;
use YTKNS\Html\HtmlTag;
use YTKNS\Html\HtmlText;
class ZoomTextEffect implements PageEffectInterface {
private const TEXT_MIN = 1;
@ -31,10 +32,10 @@ class ZoomTextEffect implements PageEffectInterface {
];
}
public function setEffectParams(array $vars, bool $quiet = false): void {
public function setEffectParams(YtknsContext $context, array $vars, bool $quiet = false): void {
if(isset($vars['txt']) && is_string($vars['txt'])) {
if(!$quiet && (mb_strlen($vars['txt']) < self::TEXT_MIN || mb_strlen($vars['txt']) > self::TEXT_MAX))
throw new PageEffectException('Your text is too long or too short.');
throw new RuntimeException('Your text is too long or too short.');
$this->text = $vars['txt'];
}
}
@ -45,12 +46,10 @@ class ZoomTextEffect implements PageEffectInterface {
];
}
public function applyEffect(PageBuilder $builder): void {
public function applyEffect(YtknsContext $context, PageBuilder $builder): void {
$tags = [];
for($i = 1; $i <= 50; $i++) {
for($i = 1; $i <= 50; $i++)
$tags[] = new HtmlTag('div', ['class' => 'ZoomText_Child', 'style' => sprintf('font-size: %1$dpt; top: %1$dpx; left: %2$dpx; color: rgb(%3$d, %3$d, %3$d);', $i * 2, $i, $i === 50 ? 0 : (4 * $i))], [new HtmlText($this->text)]);
}
$builder->getContainer()->appendChild(new HtmlTag('div', ['class' => 'ZoomText'], $tags));
}

View file

@ -1,126 +1,126 @@
<?php
namespace YTKNS;
class HtmlTag implements HtmlTypeInterface {
private string $tagName = 'div';
private array $attributes = [];
private array $children = [];
private bool $selfClosing = false;
public function __construct(string $tagName, array $attributes = [], $children = null) {
$this->tagName = $tagName;
foreach($attributes as $key => $val)
$this->setAttribute($key, $val);
if(is_bool($children))
$this->selfClosing = $children;
elseif(is_array($children)) {
foreach($children as $child)
if($child !== null && $child instanceof HtmlTypeInterface)
$this->appendChild($child);
}
}
public function getTagName(): string {
return $this->tagName;
}
public function getAttribute(string $name): ?string {
return $this->attributes[$name] ?? null;
}
public function setAttribute(string $name, $value): void {
if($value === null) {
$this->removeAttribute($name);
return;
}
$this->attributes[$name] = $value;
}
public function removeAttribute(string $name): void {
unset($this->attributes[$name]);
}
public function appendChild(HtmlTypeInterface $child): HtmlTypeInterface {
return $this->children[] = $child;
}
public function removeChild(HtmlTypeInterface $target): void {
$remove = [];
foreach($this->children as $child)
if($child === $target)
$remove[] = $child;
$this->children = array_diff($this->children, $remove);
}
public function setTextContent(string $textContent): void {
$this->children = [new HtmlText($textContent)];
}
public function getElementsByTagName(string $tagName): array {
$tagName = mb_strtolower($tagName);
$elements = [];
foreach($this->children as $child) {
if($child instanceof HtmlTag) {
$elements = array_merge($elements, $child->getElementsByTagName($tagName));
if(mb_strtolower($child->getTagName()) === $tagName)
$elements[] = $child;
}
}
return $elements;
}
public function getElementsByClassName(string $className): array {
$elements = [];
foreach($this->children as $child) {
if($child instanceof HtmlTag) {
$elements = array_merge($elements, $child->getElementsByClassName($className));
$classList = explode(' ', $child->getAttribute('class') ?? '');
if(in_array($className, $classList))
$elements[] = $child;
}
}
return $elements;
}
public function getElementById(string $idString): ?HtmlTag {
foreach($this->children as $child) {
if($child instanceof HtmlTag) {
if($child->getAttribute('id') == $idString)
return $child;
$element = $child->getElementById($idString);
if($element !== null)
return $element;
}
}
return null;
}
public function asHTML(): string {
$attrs = '';
$children = '';
foreach($this->attributes as $key => $val) {
$attrs .= sprintf(' %s', $key);
if($key !== $val)
$attrs .= sprintf('="%s"', htmlspecialchars($val));
}
if($this->selfClosing)
return sprintf('<%s%s/>', $this->getTagName(), $attrs);
foreach($this->children as $child)
$children .= $child->asHTML();
return sprintf('<%1$s%2$s>%3$s</%1$s>', $this->getTagName(), $attrs, $children);
}
}
<?php
namespace YTKNS\Html;
class HtmlTag implements HtmlTypeInterface {
private string $tagName = 'div';
private array $attributes = [];
private array $children = [];
private bool $selfClosing = false;
public function __construct(string $tagName, array $attributes = [], $children = null) {
$this->tagName = $tagName;
foreach($attributes as $key => $val)
$this->setAttribute($key, $val);
if(is_bool($children))
$this->selfClosing = $children;
elseif(is_array($children)) {
foreach($children as $child)
if($child !== null && $child instanceof HtmlTypeInterface)
$this->appendChild($child);
}
}
public function getTagName(): string {
return $this->tagName;
}
public function getAttribute(string $name): ?string {
return $this->attributes[$name] ?? null;
}
public function setAttribute(string $name, $value): void {
if($value === null) {
$this->removeAttribute($name);
return;
}
$this->attributes[$name] = $value;
}
public function removeAttribute(string $name): void {
unset($this->attributes[$name]);
}
public function appendChild(HtmlTypeInterface $child): HtmlTypeInterface {
return $this->children[] = $child;
}
public function removeChild(HtmlTypeInterface $target): void {
$remove = [];
foreach($this->children as $child)
if($child === $target)
$remove[] = $child;
$this->children = array_diff($this->children, $remove);
}
public function setTextContent(string $textContent): void {
$this->children = [new HtmlText($textContent)];
}
public function getElementsByTagName(string $tagName): array {
$tagName = mb_strtolower($tagName);
$elements = [];
foreach($this->children as $child) {
if($child instanceof HtmlTag) {
$elements = array_merge($elements, $child->getElementsByTagName($tagName));
if(mb_strtolower($child->getTagName()) === $tagName)
$elements[] = $child;
}
}
return $elements;
}
public function getElementsByClassName(string $className): array {
$elements = [];
foreach($this->children as $child) {
if($child instanceof HtmlTag) {
$elements = array_merge($elements, $child->getElementsByClassName($className));
$classList = explode(' ', $child->getAttribute('class') ?? '');
if(in_array($className, $classList))
$elements[] = $child;
}
}
return $elements;
}
public function getElementById(string $idString): ?HtmlTag {
foreach($this->children as $child) {
if($child instanceof HtmlTag) {
if($child->getAttribute('id') == $idString)
return $child;
$element = $child->getElementById($idString);
if($element !== null)
return $element;
}
}
return null;
}
public function asHTML(): string {
$attrs = '';
$children = '';
foreach($this->attributes as $key => $val) {
$attrs .= sprintf(' %s', $key);
if($key !== $val)
$attrs .= sprintf('="%s"', htmlspecialchars($val));
}
if($this->selfClosing)
return sprintf('<%s%s/>', $this->getTagName(), $attrs);
foreach($this->children as $child)
$children .= $child->asHTML();
return sprintf('<%1$s%2$s>%3$s</%1$s>', $this->getTagName(), $attrs, $children);
}
}

View file

@ -1,14 +1,14 @@
<?php
namespace YTKNS;
class HtmlText implements HtmlTypeInterface {
private string $text = '';
public function __construct(string $text) {
$this->text = $text;
}
public function asHTML(): string {
return htmlspecialchars($this->text, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5);
}
}
<?php
namespace YTKNS\Html;
class HtmlText implements HtmlTypeInterface {
private string $text = '';
public function __construct(string $text) {
$this->text = $text;
}
public function asHTML(): string {
return htmlspecialchars($this->text, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace YTKNS;
interface HtmlTypeInterface {
public function asHTML(): string;
}
<?php
namespace YTKNS\Html;
interface HtmlTypeInterface {
public function asHTML(): string;
}

View file

@ -1,6 +1,8 @@
<?php
namespace YTKNS;
use YTKNS\Html\HtmlTag;
final class PageBuilder {
private $tagHtml;
private $tagHead;
@ -8,9 +10,9 @@ final class PageBuilder {
private $tagTitle;
private $tagContainer;
public function __construct(string $pageTitle) {
$sharedCss = '//' . Config::get('domain.main') . '/assets/shared.css?v=' . hash_file('md5', YTKNS_PUB . '/assets/shared.css');
$sharedJs = '//' . Config::get('domain.main') . '/assets/shared.js?v=' . hash_file('md5', YTKNS_PUB . '/assets/shared.js');
public function __construct(string $domain, string $pageTitle) {
$sharedCss = '//' . $domain . '/assets/shared.css?v=' . hash_file('md5', YTKNS_PUB . '/assets/shared.css');
$sharedJs = '//' . $domain . '/assets/shared.js?v=' . hash_file('md5', YTKNS_PUB . '/assets/shared.js');
$this->tagHtml = new HtmlTag('html');
$this->tagHtml->appendChild($this->tagHead = new HtmlTag('head'));

View file

@ -1,14 +1,10 @@
<?php
namespace YTKNS;
use Exception;
class PageEffectException extends Exception {};
interface PageEffectInterface {
public function setEffectParams(array $vars, bool $quiet = false): void;
public function setEffectParams(YtknsContext $context, array $vars, bool $quiet = false): void;
public function getEffectParams(): array;
public function getEffectName(): string;
public function getEffectProperties(): array;
public function applyEffect(PageBuilder $builder): void;
public function applyEffect(YtknsContext $context, PageBuilder $builder): void;
}

View file

@ -1,230 +1,14 @@
<?php
namespace YTKNS;
use Exception;
class UploadNotFoundException extends Exception {};
class UploadCreationFailedException extends Exception {};
#[\AllowDynamicProperties]
final class Upload {
private const ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
public function getId(): string {
return $this->upload_id;
}
public function getPath(): string {
return YTKNS_UPLOADS . '/' . $this->getId();
}
public function getUrl(): string {
return 'https://' . Config::get('domain.main') . '/uploads/' . $this->getId();
}
public function getUserId(): int {
return $this->user_id;
}
public function setUserId(int $userId): void {
$this->user_id = $userId;
}
public function getType(): string {
return $this->upload_type ?? 'text/plain';
}
public function getName(): string {
return $this->upload_name ?? '';
}
public function getHash(): string {
return $this->upload_hash ?? str_pad('', 64, '0');
}
public function getUser(): User {
return User::byId($this->getUserId());
}
public function getUseCount(): int {
return $this->upload_use_count ?? 0;
}
public function getCreated(): int {
return $this->upload_created;
}
public function getlastUsed(): ?int {
return $this->upload_last_used;
}
public function getDeleted(): ?int {
return $this->upload_deleted;
}
public function getDMCA(): ?int {
return $this->upload_dmca;
}
public function delete(bool $hard): void {
if($hard) {
if(is_file($this->getPath()))
unlink($this->getPath());
if($this->getDMCA() < 1) {
$delete = DB::prepare('
DELETE FROM `ytkns_uploads`
WHERE `upload_id` = :id
');
$delete->bindValue('id', $this->getId());
$delete->execute();
}
} else {
$delete = DB::prepare('
UPDATE `ytkns_uploads`
SET `upload_deleted` = NOW()
WHERE `upload_id` = :id
');
$delete->bindValue('id', $this->getId());
$delete->execute();
}
}
public function toJson(bool $asString = false) {
$uploadInfo = [
'id' => $this->getId(),
'name' => $this->getName(),
'type' => $this->getType(),
'user' => $this->getUserId(),
'uses' => $this->getUseCount(),
'hash' => $this->getHash(),
'created' => $this->getCreated(),
'last_used' => $this->getLastUsed(),
'deleted' => $this->getDeleted(),
'dmca' => $this->getDMCA(),
];
if($asString)
$uploadInfo = json_encode($uploadInfo);
return $uploadInfo;
}
public static function generateId(int $length = 16): string {
$token = random_bytes($length);
$chars = strlen(self::ID_CHARS);
for($i = 0; $i < $length; $i++)
$token[$i] = self::ID_CHARS[ord($token[$i]) % $chars];
return $token;
}
public static function create(User $user, string $fileName, string $fileType, string $fileHash): self {
$id = self::generateId();
$create = DB::prepare('
INSERT INTO `ytkns_uploads` (
`upload_id`, `user_id`, `upload_ip`, `upload_name`, `upload_type`, `upload_hash`
) VALUES (
:id, :user, INET6_ATON(:ip), :name, :type, UNHEX(:hash)
)
');
$create->bindValue('id', $id);
$create->bindValue('user', $user->getId());
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
$create->bindValue('name', $fileName);
$create->bindValue('type', $fileType);
$create->bindValue('hash', $fileHash);
$create->execute();
try {
return self::byId($id);
} catch(UploadNotFoundException $ex) {
throw new UploadCreationFailedException;
}
}
public static function byId(string $id): self {
$getUpload = DB::prepare('
SELECT `upload_id`, `user_id`, `upload_use_count`, `upload_name`, `upload_type`,
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
UNIX_TIMESTAMP(`upload_last_used`) AS `upload_last_used`,
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
INET6_NTOA(`upload_ip`) AS `upload_ip`,
LOWER(HEX(`upload_hash`)) AS `upload_hash`
FROM `ytkns_uploads`
WHERE `upload_id` = :id
AND `upload_deleted` IS NULL
');
$getUpload->bindValue('id', $id);
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
if(!$upload)
throw new UploadNotFoundException;
return $upload;
}
public static function byHash(string $hash): ?self {
$getUpload = DB::prepare('
SELECT `upload_id`, `user_id`, `upload_use_count`, `upload_name`, `upload_type`,
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
UNIX_TIMESTAMP(`upload_last_used`) AS `upload_last_used`,
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
INET6_NTOA(`upload_ip`) AS `upload_ip`,
LOWER(HEX(`upload_hash`)) AS `upload_hash`
FROM `ytkns_uploads`
WHERE `upload_hash` = UNHEX(:hash)
AND `upload_deleted` IS NULL
');
$getUpload->bindValue('hash', $hash);
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
return $upload ? $upload : null;
}
public static function deleted(): array {
$getDeleted = DB::prepare('
SELECT `upload_id`, `user_id`, `upload_use_count`, `upload_name`, `upload_type`,
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
UNIX_TIMESTAMP(`upload_last_used`) AS `upload_last_used`,
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
INET6_NTOA(`upload_ip`) AS `upload_ip`,
LOWER(HEX(`upload_hash`)) AS `upload_hash`
FROM `ytkns_uploads`
WHERE `upload_deleted` IS NOT NULL
OR `upload_dmca` IS NOT NULL
');
if(!$getDeleted->execute())
return [];
$deleted = [];
while($upload = $getDeleted->fetchObject(self::class))
$deleted[] = $upload;
return $deleted;
}
public static function purgeOrphans(): void {
DB::exec('
UPDATE `ytkns_uploads`
SET `upload_deleted` = NOW()
WHERE `upload_use_count` < 1
AND (`upload_created` + INTERVAL 1 DAY) < NOW()
AND `upload_dmca` IS NULL
');
$orphans = self::deleted();
foreach($orphans as $orphan)
$orphan->delete(true);
}
public static function resync(array $uploadFields): void {
if(empty($uploadFields))
return;
// TODO: this needs splitting up and half of this should be in like ZoneEffectsData or w/e
// also use JSON_VALUE and JSON_EXISTS etc.
$effectNames = array_keys($uploadFields);
$fetchEffectsWhereIn = range(0, count($effectNames) - 1);
array_walk($fetchEffectsWhereIn, function(&$i, $k, $v) { $i = $v . $i; }, ':field_');

112
src/Uploads/UploadInfo.php Normal file
View file

@ -0,0 +1,112 @@
<?php
namespace YTKNS\Uploads;
use JsonSerializable;
use Index\Data\IDbResult;
class UploadInfo implements JsonSerializable {
public function __construct(
private string $id,
private ?string $userId,
private ?string $hash,
private string $name,
private string $type,
private int $useCount,
private string $remoteAddr,
private int $created,
private ?int $lastUsed,
private ?int $deleted,
private ?int $dmca
) {}
public static function fromResult(IDbResult $result): UploadInfo {
return new UploadInfo(
$result->getString(0),
$result->getStringOrNull(1),
$result->getStringOrNull(2),
$result->getString(3),
$result->getString(4),
$result->getInteger(5),
$result->getString(6),
$result->getInteger(7),
$result->getIntegerOrNull(8),
$result->getIntegerOrNull(9),
$result->getIntegerOrNull(10),
);
}
public function getId(): string {
return $this->id;
}
public function hasUserId(): bool {
return $this->userId !== null;
}
public function getUserId(): ?string {
return $this->userId;
}
public function getHashRaw(): string {
return $this->hash ?? str_repeat("\0", 32);
}
public function getHash(): ?string {
return bin2hex($this->getHashRaw());
}
public function getName(): string {
return $this->name;
}
public function getType(): string {
return $this->type;
}
public function getUseCount(): int {
return $this->useCount;
}
public function getRemoteAddr(): string {
return $this->remoteAddr;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getLastUsedTime(): ?int {
return $this->lastUsed;
}
public function isDeleted(): bool {
return $this->deleted !== null;
}
public function getDeletedTime(): ?int {
return $this->deleted;
}
public function isDmca(): bool {
return $this->dmca !== null;
}
public function getDmcaTime(): ?int {
return $this->dmca;
}
public function jsonSerialize(): mixed {
return [
'id' => $this->getId(),
'name' => $this->getName(),
'type' => $this->getType(),
'user' => $this->getUserId(),
'uses' => $this->getUseCount(),
'hash' => $this->getHash(),
'created' => $this->getCreatedTime(),
'last_used' => $this->getLastUsedTime(),
'deleted' => $this->getDeletedTime(),
'dmca' => $this->getDmcaTime(),
];
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace YTKNS\Uploads;
use Index\Data\IDbConnection;
class UploadsContext {
private UploadsRecords $records;
public function __construct(
IDbConnection $dbConn,
private string $mainDomain
) {
$this->records = new UploadsRecords($dbConn);
}
public function getRecords(): UploadsRecords {
return $this->records;
}
public function getLocalPath(UploadInfo $uploadInfo): string {
return YTKNS_UPLOADS . DIRECTORY_SEPARATOR . $uploadInfo->getId();
}
public function getRemotePath(UploadInfo $uploadInfo): string {
return sprintf('https://%s/uploads/%s', $this->mainDomain, $uploadInfo->getId());
}
public function nukeUpload(UploadInfo $uploadInfo): void {
$localPath = $this->getLocalPath($uploadInfo);
if(is_file($localPath))
unlink($localPath);
$this->records->nukeUpload($uploadInfo);
}
public function purgeOrphans(): void {
$this->records->markOrphansDeleted();
$orphans = $this->records->getUploadInfos(deleted: true);
foreach($orphans as $orphan)
$this->nukeUpload($orphan);
}
public function resyncUploads(): void {
//
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace YTKNS\Uploads;
use RuntimeException;
use InvalidArgumentException;
use YTKNS\Users\UserInfo;
use Index\XString;
use Index\Data\{DbStatementCache,IDbConnection};
class UploadsRecords {
private DbStatementCache $cache;
public function __construct(
private IDbConnection $dbConn,
) {
$this->cache = new DbStatementCache($dbConn);
}
public const BY_ID = 1;
public const BY_HASH = 2;
public const BY_ALL = self::BY_ID | self::BY_HASH;
public function getUploadInfo(string $value, int $select): UploadInfo {
$select &= self::BY_ALL;
if($select === 0)
throw new InvalidArgumentException('$select is not valid');
$args = 0;
$query = 'SELECT upload_id, user_id, upload_hash, upload_name, upload_type, upload_use_count, INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_last_used), UNIX_TIMESTAMP(upload_deleted), UNIX_TIMESTAMP(upload_dmca) FROM ytkns_uploads';
if($select & self::BY_ID) {
++$args;
$query .= sprintf(' WHERE upload_id = ?');
}
if($select & self::BY_HASH)
$query .= sprintf(' %s upload_hash = UNHEX(?)', ++$args > 1 ? 'AND' : 'WHERE');
$stmt = $this->cache->get($query);
while(--$args >= 0)
$stmt->nextParameter($value);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('upload not found');
return UploadInfo::fromResult($result);
}
public function getUploadInfos(
?bool $deleted = null
): iterable {
$query = 'SELECT upload_id, user_id, upload_hash, upload_name, upload_type, upload_use_count, INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_last_used), UNIX_TIMESTAMP(upload_deleted), UNIX_TIMESTAMP(upload_dmca) FROM ytkns_uploads';
if($deleted !== null)
$query .= sprintf(' WHERE upload_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
$stmt = $this->cache->get($query);
$stmt->execute();
return $stmt->getResult()->getIterator(UploadInfo::fromResult(...));
}
public function createUpload(
string $remoteAddr,
UserInfo|string $userInfo,
string $fileName,
string $fileType,
string $fileHash
): UploadInfo {
$uploadId = XString::random(16);
$stmt = $this->cache->get('INSERT INTO ytkns_uploads (upload_id, user_id, upload_ip, upload_name, upload_type, upload_hash) VALUES (?, ?, INET6_ATON(?), ?, ?, UNHEX(?))');
$stmt->nextParameter($uploadId);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->nextParameter($remoteAddr);
$stmt->nextParameter($fileName);
$stmt->nextParameter($fileType);
$stmt->nextParameter($fileHash);
$stmt->execute();
return $this->getUploadInfo($uploadId, self::BY_ID);
}
public function deleteUpload(UploadInfo|string $uploadInfo): void {
$stmt = $this->cache->get('UPDATE ytkns_uploads SET upload_deleted = COALESCE(upload_deleted, NOW()) WHERE upload_id = ?');
$stmt->nextParameter($uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo);
$stmt->execute();
}
public function restoreUpload(UploadInfo|string $uploadInfo): void {
$stmt = $this->cache->get('UPDATE ytkns_uploads SET upload_deleted = NULL WHERE upload_id = ?');
$stmt->nextParameter($uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo);
$stmt->execute();
}
public function nukeUpload(UploadInfo|string $uploadInfo): void {
$stmt = $this->cache->get('DELETE FROM ytkns_uploads WHERE upload_id = ?');
$stmt->nextParameter($uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo);
$stmt->execute();
}
public function markOrphansDeleted(): void {
$this->dbConn->execute('UPDATE ytkns_uploads SET upload_deleted = COALESCE(upload_deleted, NOW()) WHERE upload_use_count < 1 AND (upload_created + INTERVAL 1 DAY) < NOW() AND upload_dmca IS NULL');
}
}

View file

@ -1,95 +0,0 @@
<?php
namespace YTKNS;
use Exception;
class UserNotFoundException extends Exception {}
class UserCreationFailedException extends Exception {}
#[\AllowDynamicProperties]
class User {
public function __construct() {
}
public function getId(): int {
return $this->user_id ?? 0;
}
private function setId(int $userId): void {
$this->user_id = $userId;
}
public function getRemoteId(): string {
return (string)($this->remote_id ?? '');
}
public function getUsername(): string {
return $this->username ?? '';
}
public static function byId(int $userId): self {
$getUser = DB::prepare('
SELECT `user_id`, `remote_id`, `username`, `user_created`
FROM `ytkns_users`
WHERE `user_id` = :user
');
$getUser->bindValue('user', $userId);
$getUser->execute();
$user = $getUser->fetchObject(self::class);
if($user === false)
throw new UserNotFoundException;
return $user;
}
public static function byRemoteId(string $remoteId): self {
$getUser = DB::prepare('
SELECT `user_id`, `remote_id`, `username`, `user_created`
FROM `ytkns_users`
WHERE `remote_id` = :remote
');
$getUser->bindValue('remote', $remoteId);
$getUser->execute();
$user = $getUser->fetchObject(self::class);
if($user === false)
throw new UserNotFoundException;
return $user;
}
public static function forProfile(string $username): self {
$getUser = DB::prepare('
SELECT `user_id`, `remote_id`, `username`, `user_created`
FROM `ytkns_users`
WHERE LOWER(`username`) = LOWER(:username)
');
$getUser->bindValue('username', $username);
$getUser->execute();
$user = $getUser->fetchObject(self::class);
if($user === false)
throw new UserNotFoundException;
return $user;
}
public static function create(string $remoteId, string $userName): self {
$createUser = DB::prepare('
INSERT INTO `ytkns_users` (
`remote_id`, `username`
) VALUES (
:remote, :username
)
');
$createUser->bindValue('remote', $remoteId);
$createUser->bindValue('username', $userName);
$userId = $createUser->execute() ? (int)DB::lastInsertId() : 0;
try {
return self::byId($userId);
} catch(UserNotFoundException $ex) {
throw new UserCreationFailedException;
}
}
}

View file

@ -1,150 +0,0 @@
<?php
namespace YTKNS;
use Exception;
final class UserSessionNotFoundException extends Exception {};
final class UserSessionCreatedFailedException extends Exception {};
#[\AllowDynamicProperties]
class UserSession {
private const TOKEN_CHARS = 'abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private static $instance = null;
public static function instance(): ?self {
return self::$instance;
}
public function setInstance(): void {
self::$instance = $this;
}
public static function hasInstance(): bool {
return !empty(self::$instance->session_token);
}
public static function unsetInstance(): void {
self::$instance = null;
}
public function __construct() {
}
public function getUserId(): int {
return $this->user_id ?? 0;
}
public function getUser(): User {
return User::byId($this->getUserId());
}
public function getToken(): string {
return $this->session_token;
}
public function getSmallToken(int $rounds = 5, int $length = 8, int $offset = 0): string {
$token = $this->getToken();
$tokenLength = strlen($token) - $length;
for($i = 0; $i < $rounds; $i++)
$offset = ord($token[$offset]) % $tokenLength;
return str_rot13(substr($token, $offset, $length));
}
public function getCreated(): int {
return $this->session_created ?? 0;
}
public function getExpires(): int {
return $this->session_expires ?? 0;
}
public function getBump(): bool {
return $this->session_bump ?? false;
}
public function getFirstIp(): string {
return $this->session_ip_first ?? '';
}
public function getLastIp(): ?string {
return $this->session_ip_last ?? null;
}
public function update(): void {
$update = DB::prepare('
UPDATE `ytkns_users_sessions`
SET `session_expires` = IF(:bump, NOW() + INTERVAL 1 MONTH, `session_expires`),
`session_ip_last` = INET6_ATON(:ip),
`session_used` = NOW()
WHERE `session_token` = :token
');
$update->bindValue('bump', $this->getBump() ? 1 : 0);
$update->bindValue('ip', $_SERVER['REMOTE_ADDR']);
$update->bindValue('token', $this->getToken());
$update->execute();
}
public function destroy(): void {
$destroy = DB::prepare('
DELETE FROM `ytkns_users_sessions`
WHERE `session_token` = :token
');
$destroy->bindValue('token', $this->getToken());
$destroy->execute();
}
public static function generateToken(int $length = 64): string {
$token = random_bytes($length);
$chars = strlen(self::TOKEN_CHARS);
for($i = 0; $i < $length; $i++)
$token[$i] = self::TOKEN_CHARS[ord($token[$i]) % $chars];
return $token;
}
public static function create(User $user, bool $bump = false): self {
$token = self::generateToken();
$create = DB::prepare('
INSERT INTO `ytkns_users_sessions` (
`user_id`, `session_token`, `session_ip_first`, `session_bump`
) VALUES (
:user, :token, INET6_ATON(:ip), :bump
)
');
$create->bindValue('user', $user->getId());
$create->bindValue('token', $token);
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
$create->bindValue('bump', $bump ? 1 : 0);
$create->execute();
try {
return self::byToken($token);
} catch(UserSessionNotFoundException $ex) {
throw new UserSessionCreatedFailedException;
}
}
public static function byToken(string $token): self {
$getSession = DB::prepare('
SELECT `user_id`, `session_token`, `session_bump`,
UNIX_TIMESTAMP(`session_created`) AS `session_created`,
UNIX_TIMESTAMP(`session_expires`) AS `session_expires`,
INET6_NTOA(`session_ip_first`) AS `session_ip_first`,
INET6_NTOA(`session_ip_last`) AS `session_ip_last`
FROM `ytkns_users_sessions`
WHERE `session_token` = :token
');
$getSession->bindValue('token', $token);
$session = $getSession->execute() ? $getSession->fetchObject(self::class) : false;
if(!$session)
throw new UserSessionNotFoundException;
return $session;
}
public static function purge(): void {
DB::exec('
DELETE FROM `ytkns_users_sessions`
WHERE `session_expires` <= NOW()
');
}
}

42
src/Users/UserInfo.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace YTKNS\Users;
use Index\Data\IDbResult;
class UserInfo {
public function __construct(
private string $id,
private ?string $remoteId,
private string $name,
private int $created
) {}
public static function fromResult(IDbResult $result): UserInfo {
return new UserInfo(
$result->getString(0),
$result->getStringOrNull(1),
$result->getString(2),
$result->getInteger(3),
);
}
public function getId(): string {
return $this->id;
}
public function hasRemoteId(): bool {
return $this->remoteId;
}
public function getRemoteId(): ?string {
return $this->remoteId;
}
public function getName(): string {
return $this->name;
}
public function getCreatedTime(): int {
return $this->created;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace YTKNS\Users;
use Index\Data\IDbResult;
class UserSessionInfo {
public function __construct(
private string $userId,
private int $created,
private int $expires,
private ?int $lastUsed,
private string $token,
private bool $bump,
private string $firstRemoteAddr,
private ?string $lastRemoteAddr
) {}
public static function fromResult(IDbResult $result): UserSessionInfo {
return new UserSessionInfo(
$result->getString(0),
$result->getInteger(1),
$result->getInteger(2),
$result->getIntegerOrNull(3),
$result->getString(4),
$result->getBoolean(5),
$result->getString(6),
$result->getStringOrNull(7),
);
}
public function getUserId(): string {
return $this->userId;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getExpiresTime(): int {
return $this->expires;
}
public function getLastUsedTime(): ?int {
return $this->lastUsed;
}
public function getToken(): string {
return $this->token;
}
public function shouldBumpExpiry(): bool {
return $this->bump;
}
public function getFirstRemoteAddress(): string {
return $this->firstRemoteAddr;
}
public function getLastRemoteAddress(): ?string {
return $this->lastRemoteAddr;
}
public function createSmallToken(int $rounds = 5, int $length = 8, int $offset = 0): string {
$token = $this->getToken();
$tokenLength = strlen($token) - $length;
for($i = 0; $i < $rounds; $i++)
$offset = ord($token[$offset]) % $tokenLength;
return str_rot13(substr($token, $offset, $length));
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace YTKNS\Users;
use RuntimeException;
use InvalidArgumentException;
use Index\XString;
use Index\Data\{DbStatementCache,IDbConnection};
class UserSessionsData {
private DbStatementCache $cache;
public function __construct(
private IDbConnection $dbConn,
) {
$this->cache = new DbStatementCache($dbConn);
}
public function getSessionInfo(string $token, bool $includeExpired = false): UserSessionInfo {
$query = 'SELECT user_id, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_expires), UNIX_TIMESTAMP(session_used), session_token, session_bump, INET6_NTOA(session_ip_first), INET6_NTOA(session_ip_last) FROM ytkns_users_sessions WHERE session_token = ?';
if(!$includeExpired)
$query .= ' AND session_expires > NOW()';
$stmt = $this->cache->get($query);
$stmt->nextParameter($token);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('session not found');
return UserSessionInfo::fromResult($result);
}
public function purgeExpiredSessions(): void {
$this->dbConn->execute('DELETE FROM ytkns_users_sessions WHERE session_expires < NOW()');
}
public function createSession(string $remoteAddr, UserInfo|string $userInfo, bool $bump = false): UserSessionInfo {
$token = XString::random(64);
$stmt = $this->cache->get('INSERT INTO ytkns_users_sessions (user_id, session_token, session_ip_first, session_bump) VALUES (?, ?, INET6_ATON(?), ?)');
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->nextParameter($token);
$stmt->nextParameter($remoteAddr);
$stmt->nextParameter($bump ? 1 : 0);
$stmt->execute();
return $this->getSessionInfo($token);
}
public function destroySession(UserSessionInfo|string $sessionInfoOrToken): void {
$stmt = $this->cache->get('DELETE FROM ytkns_users_sessions WHERE session_token = ?');
$stmt->nextParameter($sessionInfoOrToken instanceof UserSessionInfo ? $sessionInfoOrToken->getToken() : $sessionInfoOrToken);
$stmt->execute();
}
public function updateSession(
UserSessionInfo|string $sessionInfoOrToken,
string $remoteAddr
): void {
$stmt = $this->cache->get('UPDATE ytkns_users_sessions SET session_used = NOW(), session_ip_last = INET6_ATON(?), session_expires = IF(session_bump, NOW() + INTERVAL 1 MONTH, session_expires) WHERE session_token = ?');
$stmt->nextParameter($remoteAddr);
$stmt->nextParameter($sessionInfoOrToken instanceof UserSessionInfo ? $sessionInfoOrToken->getToken() : $sessionInfoOrToken);
$stmt->execute();
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace YTKNS\Users;
use Index\Data\IDbConnection;
class UsersContext {
private UsersData $users;
private UserSessionsData $sessions;
public function __construct(IDbConnection $dbConn) {
$this->users = new UsersData($dbConn);
$this->sessions = new UserSessionsData($dbConn);
}
public function getUsers(): UsersData {
return $this->users;
}
public function getSessions(): UserSessionsData {
return $this->sessions;
}
public function getUserById(string $userId): UserInfo {
return $this->users->getUserInfo($userId, UsersData::BY_ID);
}
public function getUserByRemoteId(string $remoteId): UserInfo {
return $this->users->getUserInfo($remoteId, UsersData::BY_REMOTE_ID);
}
public function getUserByName(string $userName): UserInfo {
return $this->users->getUserInfo($userName, UsersData::BY_NAME);
}
}

61
src/Users/UsersData.php Normal file
View file

@ -0,0 +1,61 @@
<?php
namespace YTKNS\Users;
use RuntimeException;
use InvalidArgumentException;
use Index\Data\{DbStatementCache,IDbConnection};
class UsersData {
private DbStatementCache $cache;
public function __construct(
private IDbConnection $dbConn,
) {
$this->cache = new DbStatementCache($dbConn);
}
public const BY_ID = 1;
public const BY_REMOTE_ID = 2;
public const BY_NAME = 4;
public const BY_ALL = self::BY_ID | self::BY_REMOTE_ID | self::BY_NAME;
public function getUserInfo(string $value, int $select): UserInfo {
$select &= self::BY_ALL;
if($select === 0)
throw new InvalidArgumentException('$select is not valid');
$args = 0;
$query = 'SELECT user_id, remote_id, username, UNIX_TIMESTAMP(user_created) FROM ytkns_users';
if($select & self::BY_ID) {
++$args;
$query .= sprintf(' WHERE user_id = ?');
}
if($select & self::BY_REMOTE_ID)
$query .= sprintf(' %s remote_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
if($select & self::BY_NAME)
$query .= sprintf(' %s username = ?', ++$args > 1 ? 'AND' : 'WHERE');
$stmt = $this->cache->get($query);
while(--$args >= 0)
$stmt->nextParameter($value);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('user not found');
return UserInfo::fromResult($result);
}
public function createUser(string $remoteId, string $userName): UserInfo {
$stmt = $this->cache->get('INSERT INTO ytkns_users (remote_id, username) VALUES (?, ?)');
$stmt->nextParameter($remoteId);
$stmt->nextParameter($userName);
$stmt->execute();
return $this->getUserInfo(
(string)$this->dbConn->getLastInsertId(),
self::BY_ID
);
}
}

43
src/YtknsContext.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace YTKNS;
use YTKNS\Uploads\UploadsContext;
use YTKNS\Users\UsersContext;
use YTKNS\Zones\ZonesContext;
use Index\Data\IDbConnection;
use Syokuhou\IConfig;
class YtknsContext {
private UploadsContext $uploads;
private UsersContext $users;
private ZonesContext $zones;
public function __construct(
private IConfig $config,
private IDbConnection $dbConn
) {
$this->uploads = new UploadsContext($dbConn, $config->getString('domain:main'));
$this->users = new UsersContext($dbConn);
$this->zones = new ZonesContext($dbConn, $config->getString('domain:main'), $config->getString('domain:zone'));
}
public function getConfig(): IConfig {
return $this->config;
}
public function getDatabaseConnection(): IDbConnection {
return $this->dbConn;
}
public function getUploads(): UploadsContext {
return $this->uploads;
}
public function getUsers(): UsersContext {
return $this->users;
}
public function getZones(): ZonesContext {
return $this->zones;
}
}

View file

@ -1,13 +1,9 @@
<?php
namespace YTKNS;
use Exception;
class ZoneNotFoundException extends Exception {};
class ZoneCreationFailedException extends Exception {};
class ZoneInvalidIdException extends Exception {};
class ZoneInvalidNameException extends Exception {};
class ZoneInvalidTitleException extends Exception {};
use InvalidArgumentException;
use RuntimeException;
use YTKNS\Users\UserInfo;
#[\AllowDynamicProperties]
final class Zone {
@ -30,11 +26,8 @@ final class Zone {
public function hasId(): bool {
return isset($this->zone_id) && $this->zone_id > 0;
}
private function setId(int $id): void {
if($id < 1)
throw new ZoneInvalidIdException;
$this->zone_id = $id;
public function getIdStr(): string {
return (string)$this->getId();
}
public function getUserId(): int {
@ -44,57 +37,16 @@ final class Zone {
$this->user_id = $userId;
}
private $userObj = null;
public function getUser(): User {
if($this->userObj === null)
$this->userObj = User::byId($this->getUserId());
return $this->userObj;
}
public function getName(): string {
return $this->zone_name;
}
public function setName(string $name): void {
if(!self::validName($name))
throw new ZoneInvalidNameException;
$this->zone_name = $name;
}
public function getUrl(): string {
return 'https://' . sprintf(Config::get('domain.zone'), $this->getName());
}
public function getUrlForPreview(): string {
return $this->getUrl() . '?preview=1';
}
public function getScreenshotPath(): string {
return YTKNS_SCREENSHOTS . '/' . $this->getName() . '.jpg';
}
public function getScreenshotUrl(): string {
return 'https://' . Config::get('domain.main') . '/ss/' . $this->getName() . '.jpg';
}
public function takeScreenshot(): void {
$path = escapeshellarg($this->getScreenshotPath());
$url = escapeshellarg($this->getUrlForPreview());
system(sprintf('/usr/bin/firefox --window-size=800,600 --screenshot %s %s', $path, $url));
//system(sprintf('/usr/bin/convert %1$s 200x150 %1$s', $path));
}
public function removeScreenshot(): void {
$path = $this->getScreenshotPath();
if(is_file($path))
unlink($path);
}
public function getTitle(): string {
return $this->zone_title;
}
public function setTitle(string $title): void {
if(strlen($title) > 255)
throw new ZoneInvalidTitleException;
throw new InvalidArgumentException('Invalid title.');
$this->zone_title = $title;
}
@ -103,17 +55,6 @@ final class Zone {
return $this->zone_views ?? 0;
}
public function incrementViews(): void {
$updateViews = DB::prepare('
UPDATE `ytkns_zones`
SET `zone_views` = `zone_views` + 1
WHERE `zone_id` = :zone
');
$updateViews->bindValue('zone', $this->getId());
if($updateViews->execute())
$this->zone_views = $this->getViews() + 1;
}
public function getEffects(): array {
if(!is_array($this->effects))
$this->effects = $this->hasId() ? ZoneEffect::byZone($this) : [];
@ -121,13 +62,13 @@ final class Zone {
return $this->effects;
}
public function getPageBuilder(bool $quiet = false): PageBuilder {
$pageBuilder = new PageBuilder($this->getTitle());
public function getPageBuilder(YtknsContext $context, bool $quiet = false): PageBuilder {
$pageBuilder = new PageBuilder($context->getConfig()->getString('domain:main'), $this->getTitle());
$effects = $this->getEffects();
foreach($effects as $effect)
$effect->applyEffect($pageBuilder, $quiet);
$effect->applyEffect($context, $pageBuilder, $quiet);
return $pageBuilder;
}
@ -186,7 +127,7 @@ final class Zone {
$zone = $getZone->execute() ? $getZone->fetchObject(self::class) : false;
if(!$zone)
throw new ZoneNotFoundException;
throw new RuntimeException('Zone not found.');
return $zone;
}
@ -216,12 +157,12 @@ final class Zone {
$zone = $getZone->execute() ? $getZone->fetchObject(self::class) : false;
if(!$zone)
throw new ZoneNotFoundException;
throw new RuntimeException('Zone not found.');
return $zone;
}
public static function byUser(User $user, ?string $orderBy = null, bool $ascending = true, int $take = 0, int $offset = 0): array {
public static function byUser(UserInfo|string $user, ?string $orderBy = null, bool $ascending = true, int $take = 0, int $offset = 0): array {
$getZonesQuery = '
SELECT `zone_id`, `user_id`, `zone_name`, `zone_title`, `zone_views`,
UNIX_TIMESTAMP(`zone_created`) AS `zone_created`,
@ -236,7 +177,7 @@ final class Zone {
$getZonesQuery .= sprintf(' LIMIT %d OFFSET %d', $take, $offset);
$getZones = DB::prepare($getZonesQuery);
$getZones->bindValue('user', $user->getId());
$getZones->bindValue('user', $user instanceof UserInfo ? $user->getId() : $user);
$getZones->execute();
$zones = [];
@ -277,7 +218,7 @@ final class Zone {
return (int)($getZoneCount->execute() ? $getZoneCount->fetchColumn() : 0);
}
public static function create(User $user, string $name, string $title): self {
public static function create(UserInfo|string $user, string $name, string $title): self {
$create = DB::prepare('
INSERT INTO `ytkns_zones` (
`user_id`, `zone_name`, `zone_title`
@ -285,16 +226,12 @@ final class Zone {
:user, LOWER(:name), :title
)
');
$create->bindValue('user', $user->getId());
$create->bindValue('user', $user instanceof UserInfo ? $user->getId() : $user);
$create->bindValue('name', $name);
$create->bindValue('title', $title);
$create->execute();
try {
return self::byName($name);
} catch(ZoneNotFoundException $ex) {
throw new ZoneCreationFailedException;
}
return self::byName($name);
}
public function update(array $save = ['user_id', 'zone_name', 'zone_title']): void {
@ -368,8 +305,4 @@ final class Zone {
return $zoneInfo;
}
public function queueTask(string $task, ...$params): void {
ZoneTask::enqueue($this, $task, $params);
}
}

View file

@ -1,9 +1,7 @@
<?php
namespace YTKNS;
use Exception;
class ZoneEffectClassNotFoundException extends Exception {};
use RuntimeException;
#[\AllowDynamicProperties]
final class ZoneEffect {
@ -52,24 +50,24 @@ final class ZoneEffect {
$className = self::effectClassName($name);
if(!class_exists($className))
throw new ZoneEffectClassNotFoundException($name);
throw new RuntimeException(sprintf('Zone effect implementation could not be found: %s', $name));
return new $className;
}
public function getEffectClass(bool $quiet = false) {
public function getEffectClass(YtknsContext $context, bool $quiet = false) {
try {
$effect = self::effectClass($this->getEffectName());
$effect->setEffectParams($this->getEffectParams(), $quiet);
$effect->setEffectParams($context, $this->getEffectParams(), $quiet);
return $effect;
} catch(ZoneEffectClassNotFoundException $ex) {
} catch(RuntimeException $ex) {
return null;
}
}
public function applyEffect(PageBuilder $builder, bool $quiet = false): void {
$effect = $this->getEffectClass($quiet);
public function applyEffect(YtknsContext $context, PageBuilder $builder, bool $quiet = false): void {
$effect = $this->getEffectClass($context, $quiet);
if($effect !== null)
$effect->applyEffect($builder);
$effect->applyEffect($context, $builder);
}
public static function byZone(Zone $zone): array {

View file

@ -1,47 +0,0 @@
<?php
namespace YTKNS;
#[\AllowDynamicProperties]
final class ZoneRedirect {
public function __construct() {
}
public static function exists(string $subdomain): bool {
$check = DB::prepare('
SELECT COUNT(`redirect_name`) > 0
FROM `ytkns_redirects`
WHERE `redirect_name` = :subdomain
');
$check->bindValue('subdomain', $subdomain);
return $check->execute() ? (bool)$check->fetchColumn() : true;
}
public static function find(string $subdomain): ?ZoneRedirect {
$find = DB::prepare('
SELECT `redirect_name`, `redirect_target`
FROM `ytkns_redirects`
WHERE `redirect_name` = :subdomain
');
$find->bindValue('subdomain', $subdomain);
$redirect = $find->execute() ? $find->fetchObject(self::class) : false;
return $redirect ? $redirect : null;
}
public static function create(string $subdomain, string $target): void {
$create = DB::prepare('
REPLACE INTO `ytkns_redirects` (
`redirect_name`, `redirect_target`
) VALUES (
:name, :target
)
');
$create->bindValue('name', $subdomain);
$create->bindValue('target', $target);
$create->execute();
}
public function execute(): void {
http_response_code(301);
header(sprintf('Location: %s', $this->redirect_target));
}
}

View file

@ -1,61 +0,0 @@
<?php
namespace YTKNS;
#[\AllowDynamicProperties]
final class ZoneTask {
public function getZoneId(): int {
return $this->zone_id ?? 0;
}
public function getZone(): Zone {
return Zone::byId($this->getZoneId());
}
public function getName(): string {
return $this->task_name ?? '';
}
public function getParams(): array {
if(empty($this->task_params))
return [];
return unserialize($this->task_params);
}
public function delete(): void {
$deleteTask = DB::prepare('
DELETE FROM `ytkns_zones_tasks`
WHERE `zone_id` = :zone
AND `task_name` = :task
');
$deleteTask->bindValue('zone', $this->getZoneId());
$deleteTask->bindValue('task', $this->getName());
$deleteTask->execute();
}
public static function queue(): array {
$getTasks = DB::prepare('
SELECT `zone_id`, `task_name`, `task_params`
FROM `ytkns_zones_tasks`
');
$getTasks->execute();
$tasks = [];
while($task = $getTasks->fetchObject(self::class))
$tasks[] = $task;
return $tasks;
}
public static function enqueue(Zone $zone, string $name, array $params = []): void {
$enqueue = DB::prepare('
REPLACE INTO `ytkns_zones_tasks` (
`zone_id`, `task_name`, `task_params`
) VALUES (
:zone, :task, :params
)
');
$enqueue->bindValue('zone', $zone->getId());
$enqueue->bindValue('task', $name);
$enqueue->bindValue('params', serialize(array_values($params)));
$enqueue->execute();
}
}

View file

@ -1,45 +0,0 @@
<?php
namespace YTKNS;
#[\AllowDynamicProperties]
final class ZoneView {
private const THRESHOLD = 60 * 60 * 24;
public function getTime(): int {
return $this->view_time ?? 0;
}
public static function byZoneAddress(Zone $zone, string $ipAddress): ?self {
$getZoneView = DB::prepare('
SELECT `zone_id`,
UNIX_TIMESTAMP(`view_time`) AS `view_time`,
INET6_NTOA(`view_address`) AS `view_address`
FROM `ytkns_zones_views`
WHERE `zone_id` = :zone
AND `view_address` = INET6_ATON(:ip)
');
$getZoneView->bindValue('zone', $zone->getId());
$getZoneView->bindValue('ip', $ipAddress);
$zoneView = $getZoneView->execute() ? $getZoneView->fetchObject(self::class) : false;
return $zoneView ? $zoneView : null;
}
public static function increment(Zone $zone, string $ipAddress): void {
$zoneView = self::byZoneAddress($zone, $ipAddress);
if($zoneView !== null && ($zoneView->getTime() + self::THRESHOLD) > time())
return;
$updateZoneView = DB::prepare('
REPLACE INTO `ytkns_zones_views` (
`view_address`, `zone_id`, `view_time`
) VALUES (
INET6_ATON(:ip), :zone, NOW()
)
');
$updateZoneView->bindValue('ip', $ipAddress);
$updateZoneView->bindValue('zone', $zone->getId());
if($updateZoneView->execute())
$zone->incrementViews();
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace YTKNS\Uploads;
use Index\Data\IDbResult;
class ZoneEffectInfo {
public function __construct(
private string $zoneId,
private string $name,
private string $params
) {}
public static function fromResult(IDbResult $result): ZoneEffectInfo {
return new ZoneEffectInfo(
$result->getString(0),
$result->getString(1),
$result->getString(2),
);
}
public function getZoneId(): string {
return $this->zoneId;
}
public function getName(): string {
return $this->name;
}
public function getRawParams(): string {
return $this->params;
}
public function getParams(): mixed {
return json_decode($this->params);
}
}

60
src/Zones/ZoneInfo.php Normal file
View file

@ -0,0 +1,60 @@
<?php
namespace YTKNS\Uploads;
use Index\Data\IDbResult;
class ZoneInfo {
public function __construct(
private string $id,
private string $name,
private ?string $userId,
private string $title,
private int $views,
private int $created,
private int $updated
) {}
public static function fromResult(IDbResult $result): ZoneInfo {
return new ZoneInfo(
$result->getString(0),
$result->getString(1),
$result->getStringOrNull(2),
$result->getString(3),
$result->getInteger(4),
$result->getInteger(5),
$result->getInteger(6),
);
}
public function getId(): string {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function hasUserId(): bool {
return $this->userId !== null;
}
public function getUserId(): ?string {
return $this->userId;
}
public function getTitle(): string {
return $this->title;
}
public function getViews(): int {
return $this->views;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getUpdatedTime(): int {
return $this->updated;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace YTKNS\Zones;
use RuntimeException;
use InvalidArgumentException;
use Index\Data\{DbStatementCache,IDbConnection};
class ZoneRedirectsData {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function getRedirectTarget(string $subdomain): ?string {
$stmt = $this->cache->get('SELECT redirect_target FROM ytkns_redirects WHERE redirect_name = ?');
$stmt->nextParameter($subdomain);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
return null;
return $result->getStringOrNull(0);
}
public function createRedirect(string $subdomain, string $target): void {
$stmt = $this->cache->get('REPLACE INTO ytkns_redirects (redirect_name, redirect_target) VALUES (?, ?)');
$stmt->nextParameter($subdomain);
$stmt->nextParameter($target);
$stmt->execute();
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace YTKNS\Zones;
use Index\Data\IDbResult;
class ZoneTaskInfo {
public function __construct(
private string $zoneId,
private string $name,
private ?string $params
) {}
public static function fromResult(IDbResult $result): ZoneTaskInfo {
return new ZoneTaskInfo(
$result->getString(0),
$result->getString(1),
$result->getStringOrNull(2),
);
}
public function getZoneId(): string {
return $this->zoneId;
}
public function getName(): string {
return $this->name;
}
public function getRawParams(): ?string {
return $this->params;
}
public function getParams(): mixed {
return $this->params === null ? null : unserialize($this->params);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace YTKNS\Zones;
use Index\Data\{DbStatementCache,IDbConnection};
class ZoneTasksData {
private DbStatementCache $cache;
public function __construct(
private IDbConnection $dbConn,
) {
$this->cache = new DbStatementCache($dbConn);
}
public function queuedTasks(): iterable {
$stmt = $this->cache->get('SELECT zone_id, task_name, task_params FROM ytkns_zones_tasks');
$stmt->execute();
return $stmt->getResult()->getIterator(ZoneTaskInfo::fromResult(...));
}
public function enqueueTask(ZoneInfo|string $zoneInfo, string $name, array $params = []): void {
$stmt = $this->cache->get('REPLACE INTO ytkns_zones_tasks (zone_id, task_name, task_params) VALUES (?, ?, ?)');
$stmt->nextParameter($zoneInfo instanceof ZoneInfo ? $zoneInfo->getId() : $zoneInfo);
$stmt->nextParameter($name);
$stmt->nextParameter(serialize(array_values($params)));
$stmt->execute();
}
public function deleteTask(ZoneTaskInfo $taskInfo): void {
$stmt = $this->cache->get('DELETE FROM ytkns_zones_tasks WHERE zone_id = ? AND task_name = ?');
$stmt->nextParameter($taskInfo->getZoneId());
$stmt->nextParameter($taskInfo->getName());
$stmt->execute();
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace YTKNS\Zones;
use Index\Data\{DbStatementCache,IDbConnection};
class ZoneViewsData {
private DbStatementCache $cache;
public function __construct(
private IDbConnection $dbConn,
) {
$this->cache = new DbStatementCache($dbConn);
}
public function registerZoneView(ZoneInfo|string $zoneInfo, string $remoteAddr): bool {
$stmt = $this->cache->get('INSERT INTO ytkns_zones_views (view_address, zone_id) VALUES (INET6_ATON(?), ?) ON DUPLICATE KEY UPDATE view_time = IF(view_time < NOW() - INTERVAL 1 DAY, NOW(), view_time)');
$stmt->nextParameter($remoteAddr);
$stmt->nextParameter($zoneInfo instanceof ZoneInfo ? $zoneInfo->getId() : $zoneInfo);
return $stmt->execute() > 0;
}
}

104
src/Zones/ZonesContext.php Normal file
View file

@ -0,0 +1,104 @@
<?php
namespace YTKNS\Zones;
use Index\Data\IDbConnection;
class ZonesContext {
private ZonesData $zones;
private ZoneRedirectsData $redirects;
private ZoneTasksData $tasks;
private ZoneViewsData $views;
public function __construct(
IDbConnection $dbConn,
private string $mainDomain,
private string $zoneDomainFormat
) {
$this->zones = new ZonesData($dbConn);
$this->redirects = new ZoneRedirectsData($dbConn);
$this->tasks = new ZoneTasksData($dbConn);
$this->views = new ZoneViewsData($dbConn);
}
public function getZones(): ZonesData {
return $this->zones;
}
public function getRedirects(): ZoneRedirectsData {
return $this->redirects;
}
public function getTasks(): ZoneTasksData {
return $this->tasks;
}
public function getViews(): ZoneViewsData {
return $this->views;
}
public function getZoneRemotePath(\YTKNS\Zone|ZoneInfo|string $zoneInfo, bool $preview = false): string {
if($zoneInfo instanceof ZoneInfo)
$zoneInfo = $zoneInfo->getName();
elseif($zoneInfo instanceof \YTKNS\Zone)
$zoneInfo = $zoneInfo->getName();
$url = sprintf('https://%s', sprintf($this->zoneDomainFormat, $zoneInfo));
if($preview)
$url .= '?preview=1';
return $url;
}
public function getScreenshotLocalPath(\YTKNS\Zone|ZoneInfo|string $zoneInfo): string {
if($zoneInfo instanceof ZoneInfo)
$zoneInfo = $zoneInfo->getName();
elseif($zoneInfo instanceof \YTKNS\Zone)
$zoneInfo = $zoneInfo->getName();
return sprintf('%s/%s.jpg', YTKNS_SCREENSHOTS, $zoneInfo);
}
public function getScreenshotRemotePath(\YTKNS\Zone|ZoneInfo|string $zoneInfo): string {
if($zoneInfo instanceof ZoneInfo)
$zoneInfo = $zoneInfo->getName();
elseif($zoneInfo instanceof \YTKNS\Zone)
$zoneInfo = $zoneInfo->getName();
return sprintf('https://%s/ss/%s.jpg', $this->mainDomain, $zoneInfo);
}
public function registerZoneView(\YTKNS\Zone|ZoneInfo|string $zoneInfo, string $remoteAddr): void {
if($zoneInfo instanceof \YTKNS\Zone)
$zoneInfo = $zoneInfo->getIdStr();
if($this->views->registerZoneView($zoneInfo, $remoteAddr))
$this->zones->incrementZoneViews($zoneInfo);
}
public function queueTakeScreenshot(\YTKNS\Zone|ZoneInfo|string $zoneInfo): void {
if($zoneInfo instanceof \YTKNS\Zone)
$zoneInfo = $zoneInfo->getIdStr();
$this->tasks->enqueueTask($zoneInfo, 'screenshot');
}
public function takeScreenshot(\YTKNS\Zone|ZoneInfo|string $zoneInfo): void {
$path = escapeshellarg($this->getScreenshotLocalPath($zoneInfo));
$url = escapeshellarg($this->getZoneRemotePath($zoneInfo));
system(sprintf('/usr/bin/firefox --window-size=800,600 --screenshot %s %s', $path, $url));
//system(sprintf('/usr/bin/convert %1$s 200x150 %1$s', $path));
}
public function removeScreenshot(\YTKNS\Zone|ZoneInfo|string $zoneInfo): void {
if($zoneInfo instanceof ZoneInfo)
$zoneInfo = $zoneInfo->getName();
elseif($zoneInfo instanceof \YTKNS\Zone)
$zoneInfo = $zoneInfo->getName();
$path = $this->getScreenshotLocalPath($zoneInfo);
if(is_file($path))
unlink($path);
}
}

22
src/Zones/ZonesData.php Normal file
View file

@ -0,0 +1,22 @@
<?php
namespace YTKNS\Zones;
use RuntimeException;
use InvalidArgumentException;
use Index\Data\{DbStatementCache,IDbConnection};
class ZonesData {
private DbStatementCache $cache;
public function __construct(
private IDbConnection $dbConn,
) {
$this->cache = new DbStatementCache($dbConn);
}
public function incrementZoneViews(ZoneInfo|string $zoneInfo):void {
$stmt = $this->cache->get('UPDATE ytkns_zones SET zone_views = zone_views + 1 WHERE zone_id = ?');
$stmt->nextParameter($zoneInfo instanceof ZoneInfo ? $zoneInfo->getId() : $zoneInfo);
$stmt->execute();
}
}

View file

@ -1,7 +1,11 @@
<?php
namespace YTKNS;
use Index\Data\DbTools;
use Syokuhou\FileConfig;
define('YTKNS_STARTUP', microtime(true));
define('YTKNS_CLI', PHP_SAPI === 'cli');
define('YTKNS_ROOT', __DIR__);
define('YTKNS_SRC', YTKNS_ROOT . '/src');
define('YTKNS_TPL', YTKNS_ROOT . '/templates');
@ -9,8 +13,9 @@ define('YTKNS_UPLOADS', YTKNS_ROOT . '/uploads');
define('YTKNS_PUB', YTKNS_ROOT . '/public');
define('YTKNS_SCREENSHOTS', YTKNS_PUB . '/ss');
define('YTKNS_DEBUG', is_file(YTKNS_ROOT . '/.debug'));
define('YTKNS_MAINTENANCE', is_file(YTKNS_ROOT . '/.maintenance'));
require_once __DIR__ . '/config.php';
require_once YTKNS_ROOT . '/vendor/autoload.php';
define('ALLOWED_UPLOADS', [
'audio/mpeg', 'application/x-font-gdos', 'audio/ogg', 'application/ogg',
@ -30,13 +35,16 @@ define('EFFECT_UPLOADS', [
]);
error_reporting(YTKNS_DEBUG ? -1 : 0);
ini_set('display_errors', YTKNS_DEBUG ? 'On' : 'Off');
mb_internal_encoding('utf-8');
date_default_timezone_set('utc');
mb_internal_encoding('UTF-8');
date_default_timezone_set('GMT');
// Display exception report
set_exception_handler(function(\Throwable $ex) {
if(YTKNS_CLI) {
echo (string)$ex;
exit;
}
http_response_code(500);
$out = file_get_contents(YTKNS_TPL . (YTKNS_DEBUG ? '/debug/index.html' : '/errors/500.html'));
@ -45,7 +53,6 @@ set_exception_handler(function(\Throwable $ex) {
':type' => get_class($ex),
':msg' => $ex->getMessage(),
]);
exit;
});
@ -55,20 +62,16 @@ set_error_handler(function(int $errno, string $errstr, string $errfile, int $err
return true;
}, -1);
// Register class autoloader
spl_autoload_register(function(string $className) {
if(substr($className, 0, 6) !== 'YTKNS\\')
return;
$cfg = FileConfig::fromFile(YTKNS_ROOT . '/ytkns.cfg');
$classPath = YTKNS_SRC . str_replace('\\', '/', substr($className, 5)) . '.php';
$db = DbTools::create($cfg->getString('database:dsn', 'null:'));
$db->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\'');
if(is_file($classPath))
require_once $classPath;
});
$ctx = new YtknsContext($cfg, $db);
DB::init(PDO_DSN, PDO_USER, PDO_PASS, DB::FLAGS);
Config::init();
Config::setDefault('user.invite_only', true);
Config::setDefault('domain.main', 'ytkns.com');
Config::setDefault('domain.zone', '%s.ytkns.com');
DB::init(
$cfg->getString('pdo:dsn'),
$cfg->getString('pdo:user'),
$cfg->getString('pdo:pass'),
DB::FLAGS
);