Compare commits
1 commit
master
...
index-rewr
Author | SHA1 | Date | |
---|---|---|---|
108744cec1 |
42 changed files with 2237 additions and 1205 deletions
.gitignorecomposer.jsoncomposer.lockcron.php
public
src
Config.php
startup.phpEffects
BackgroundAudioEffect.phpBackgroundImageEffect.phpForegroundImageEffect.phpNewlyCreatedPageEffect.phpZoomTextEffect.php
Html
PageBuilder.phpPageEffectInterface.phpUpload.phpUploads
User.phpUserSession.phpUsers
YtknsContext.phpZone.phpZoneEffect.phpZoneRedirect.phpZoneTask.phpZoneView.phpZones
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,9 @@
|
|||
.DS_Store
|
||||
[Dd]esktop.ini
|
||||
/.debug
|
||||
/.maintenance
|
||||
/public/ss
|
||||
/uploads
|
||||
/config.php
|
||||
/ytkns.cfg
|
||||
/vendor
|
||||
|
|
12
composer.json
Normal file
12
composer.json
Normal 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
836
composer.lock
generated
Normal 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"
|
||||
}
|
18
cron.php
18
cron.php
|
@ -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 {
|
||||
|
|
311
public/index.php
311
public/index.php
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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')]),
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
interface HtmlTypeInterface {
|
||||
public function asHTML(): string;
|
||||
}
|
||||
<?php
|
||||
namespace YTKNS\Html;
|
||||
|
||||
interface HtmlTypeInterface {
|
||||
public function asHTML(): string;
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
222
src/Upload.php
222
src/Upload.php
|
@ -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
112
src/Uploads/UploadInfo.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
47
src/Uploads/UploadsContext.php
Normal file
47
src/Uploads/UploadsContext.php
Normal 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 {
|
||||
//
|
||||
}
|
||||
}
|
104
src/Uploads/UploadsRecords.php
Normal file
104
src/Uploads/UploadsRecords.php
Normal 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');
|
||||
}
|
||||
}
|
95
src/User.php
95
src/User.php
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
42
src/Users/UserInfo.php
Normal 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;
|
||||
}
|
||||
}
|
72
src/Users/UserSessionInfo.php
Normal file
72
src/Users/UserSessionInfo.php
Normal 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));
|
||||
}
|
||||
}
|
66
src/Users/UserSessionsData.php
Normal file
66
src/Users/UserSessionsData.php
Normal 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();
|
||||
}
|
||||
}
|
34
src/Users/UsersContext.php
Normal file
34
src/Users/UsersContext.php
Normal 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
61
src/Users/UsersData.php
Normal 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
43
src/YtknsContext.php
Normal 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;
|
||||
}
|
||||
}
|
99
src/Zone.php
99
src/Zone.php
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
36
src/Zones/ZoneEffectInfo.php
Normal file
36
src/Zones/ZoneEffectInfo.php
Normal 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
60
src/Zones/ZoneInfo.php
Normal 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;
|
||||
}
|
||||
}
|
33
src/Zones/ZoneRedirectsData.php
Normal file
33
src/Zones/ZoneRedirectsData.php
Normal 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();
|
||||
}
|
||||
}
|
36
src/Zones/ZoneTaskInfo.php
Normal file
36
src/Zones/ZoneTaskInfo.php
Normal 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);
|
||||
}
|
||||
}
|
36
src/Zones/ZoneTasksData.php
Normal file
36
src/Zones/ZoneTasksData.php
Normal 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();
|
||||
}
|
||||
}
|
21
src/Zones/ZoneViewsData.php
Normal file
21
src/Zones/ZoneViewsData.php
Normal 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
104
src/Zones/ZonesContext.php
Normal 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
22
src/Zones/ZonesData.php
Normal 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();
|
||||
}
|
||||
}
|
43
startup.php
43
startup.php
|
@ -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
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue